From 516a5cf016a7d504b09aaac2e0e66c7e9efd42b2 Mon Sep 17 00:00:00 2001 From: Florent Rougon Date: Mon, 23 May 2016 00:43:03 +0200 Subject: [PATCH] Support merging of arbitrary apt.dat[.gz] files It is now allowed to have the same airport appear in several apt.dat files ($scenery_path/NavData/apt/*.dat[.gz] for each scenery path, plus the default $FG_ROOT/Airports/apt.dat.gz, coming last). Airports found in earlier files(*) take precedence over those found later, in case several apt.dat files define the same airports. Airports that are skipped due to this mechanism are logged with SG_LOG(SG_GENERAL, SG_INFO, ...). (*) using 1) FG_SCENERY order (followed by $FG_ROOT/Airports/apt.dat.gz) and 2) lexicographic order inside each $scenery_path/NavData/apt folder With this commit, APTLoader::parseAPT() is replaced by two methods: readAptDatFile() and loadAirports(): - APTLoader::readAptDatFile() reads airport definitions from an apt.dat file into APTLoader's 'airportInfoMap' member variable, discarding duplicate definitions due to overlapping apt.dat files ('airportInfoMap' is an std::unordered_map instance in C++11 and later, an std::map otherwise); - APTLoader::loadAirports() reads each airport definition from 'airportInfoMap' and loads it into the NavCache, the same way as APTLoader::parseAPT() used to do. The airportDBLoad() function is not useful anymore, and is thus removed (in NavDataCache::doRebuild(), APTLoader::readAptDatFile() is now called once per apt.dat file, but APTLoader::loadAirports() is only called once at the end, after duplicate airports have been discarded; the class interface is much better suited to this scheme, because it can cleanly retain the state between these calls). By the way, this commit fixes an old bug: APTLoader's member variable 'last_apt_id' was used in several places but never assigned to, except in APTLoader::APTLoader() as the empty string. Thanks to Alan Teeder for his feedback and testing. --- src/Airports/apt_loader.cxx | 211 +++++++++++++++++++++++------------ src/Airports/apt_loader.hxx | 59 ++++++++-- src/Navaids/NavDataCache.cxx | 4 +- 3 files changed, 192 insertions(+), 82 deletions(-) diff --git a/src/Airports/apt_loader.cxx b/src/Airports/apt_loader.cxx index 8ee3fab87..6943320a1 100644 --- a/src/Airports/apt_loader.cxx +++ b/src/Airports/apt_loader.cxx @@ -44,6 +44,7 @@ #include #include +#include // std::pair, std::move() #include "airport.hxx" #include "runways.hxx" @@ -85,7 +86,7 @@ APTLoader::APTLoader() APTLoader::~APTLoader() { } -void APTLoader::parseAPT(const SGPath &aptdb_file) +void APTLoader::readAptDatFile(const SGPath &aptdb_file) { string apt_dat = aptdb_file.utf8Str(); // full path to the file being parsed sg_gzifstream in(aptdb_file); @@ -103,6 +104,15 @@ void APTLoader::parseAPT(const SGPath &aptdb_file) unsigned int line_id = 0; unsigned int line_num = 0; + // "airport identifier": terminology used in the apt.dat format spec. It is + // often an ICAO code, but not always. + string currentAirportId; + // Boolean used to make sure we don't try to load the same airport several + // times. Defaults to true only to ensure we don't add garbage to + // 'airportInfoMap' under the key "" (empty airport identifier) in case the + // apt.dat file doesn't have a start-of-airport row code (1, 16 or 17) after + // its header---which would be invalid, anyway. + bool skipAirport = true; // Read the apt.dat header (two lines) while ( line_num < 2 && std::getline(in, line) ) { @@ -150,65 +160,136 @@ void APTLoader::parseAPT(const SGPath &aptdb_file) if ( line_id == 1 /* Airport */ || line_id == 16 /* Seaplane base */ || line_id == 17 /* Heliport */ ) { - parseAirportLine(apt_dat, simgear::strutils::split(line)); - } else if ( line_id == 10 ) { // Runway v810 - parseRunwayLine810(simgear::strutils::split(line)); - } else if ( line_id == 100 ) { // Runway v850 - parseRunwayLine850(simgear::strutils::split(line)); - } else if ( line_id == 101 ) { // Water Runway v850 - parseWaterRunwayLine850(simgear::strutils::split(line)); - } else if ( line_id == 102 ) { // Helipad v850 - parseHelipadLine850(simgear::strutils::split(line)); - } else if ( line_id == 18 ) { - // beacon entry (ignore) - } else if ( line_id == 14 ) { - // control tower entry - vector token(simgear::strutils::split(line)); + vector tokens(simgear::strutils::split(line)); + if (tokens.size() < 6) { + SG_LOG( SG_GENERAL, SG_WARN, + apt_dat << ":" << line_num << ": invalid airport header " + "(at least 6 fields are required)" ); + skipAirport = true; // discard everything until the next airport header + continue; + } - double lat = atof( token[1].c_str() ); - double lon = atof( token[2].c_str() ); - double elev = atof( token[3].c_str() ); - tower = SGGeod::fromDegFt(lon, lat, elev + last_apt_elev); - cache->insertTower(currentAirportID, tower); - } else if ( line_id == 19 ) { - // windsock entry (ignore) - } else if ( line_id == 20 ) { - // Taxiway sign (ignore) - } else if ( line_id == 21 ) { - // lighting objects (ignore) - } else if ( line_id == 15 ) { - // custom startup locations (ignore) - } else if ( line_id == 0 ) { - // ?? - } else if ( line_id >= 50 && line_id <= 56) { - parseCommLine(apt_dat, line_id, simgear::strutils::split(line)); - } else if ( line_id == 110 ) { - pavement = true; - parsePavementLine850(simgear::strutils::split(line, 0, 4)); - } else if ( line_id >= 111 && line_id <= 114 ) { - if ( pavement ) - parsePavementNodeLine850(line_id, simgear::strutils::split(line)); - } else if ( line_id >= 115 && line_id <= 116 ) { - // other pavement nodes (ignore) - } else if ( line_id == 120 ) { - pavement = false; - } else if ( line_id == 130 ) { - pavement = false; - } else if ( line_id >= 1000 ) { - // airport traffic flow (ignore) + currentAirportId = tokens[4]; // often an ICAO, but not always + // Check if the airport is already in 'airportInfoMap'; get the + // existing entry, if any, otherwise insert a new one. + std::pair + insertRetval = airportInfoMap.insert( + AirportInfoMapType::value_type(currentAirportId, RawAirportInfo())); + skipAirport = !insertRetval.second; + + if ( skipAirport ) { + SG_LOG( SG_GENERAL, SG_INFO, + apt_dat << ":" << line_num << ": skipping airport " << + currentAirportId << " (already defined earlier)" ); + } else { + // We haven't seen this airport yet in any apt.dat file + RawAirportInfo& airportInfo = insertRetval.first->second; + airportInfo.file = aptdb_file; + airportInfo.rowCode = line_id; + airportInfo.firstLineNum = line_num; + airportInfo.firstLineTokens = +#if __cplusplus >= 201103L + std::move(tokens); // requires C++11, untested +#else + tokens; +#endif + } } else if ( line_id == 99 ) { SG_LOG( SG_GENERAL, SG_DEBUG, - apt_dat << ": code 99 found (normally at end of file)" ); - } else { - std::ostringstream oss; - oss << apt_dat << ":" << line_num << ": unknown row code " << line_id; - SG_LOG( SG_GENERAL, SG_ALERT, oss.str() << " (" << line << ")" ); - throw sg_format_exception(oss.str(), line); + apt_dat << ":" << line_num << ": code 99 found " + "(normally at end of file)" ); + } else if ( !skipAirport ) { + // Line belonging to an already started, and not skipped airport entry; + // just append it. + airportInfoMap[currentAirportId].otherLines. +#if __cplusplus >= 201103L + emplace_back(line_num, line_id, line); // requires C++11, untested +#else + push_back(Line(line_num, line_id, line)); +#endif } - } + } // of file reading loop throwExceptionIfStreamError(in, aptdb_file); - finishAirport(apt_dat); +} + +void APTLoader::loadAirports() +{ + // Loop over all airports found in all apt.dat files + for (AirportInfoMapType::const_iterator it = airportInfoMap.begin(); + it != airportInfoMap.end(); it++) { + // Full path to the apt.dat file this airport info comes from + const string aptDat = it->second.file.utf8Str(); + last_apt_id = it->first; // this is just the current airport identifier + // The first line for this airport was already split over whitespace, but + // remains to be parsed for the most part. + parseAirportLine(it->second.rowCode, it->second.firstLineTokens); + const LinesList& lines = it->second.otherLines; + + // Loop over the second and subsequent lines + for (LinesList::const_iterator linesIt = lines.begin(); + linesIt != lines.end(); linesIt++) { + // Beware that linesIt->str may end with an '\r' character, see above! + unsigned int line_id = linesIt->rowCode; + + if ( line_id == 10 ) { // Runway v810 + parseRunwayLine810(simgear::strutils::split(linesIt->str)); + } else if ( line_id == 100 ) { // Runway v850 + parseRunwayLine850(simgear::strutils::split(linesIt->str)); + } else if ( line_id == 101 ) { // Water Runway v850 + parseWaterRunwayLine850(simgear::strutils::split(linesIt->str)); + } else if ( line_id == 102 ) { // Helipad v850 + parseHelipadLine850(simgear::strutils::split(linesIt->str)); + } else if ( line_id == 18 ) { + // beacon entry (ignore) + } else if ( line_id == 14 ) { + // control tower entry + vector token(simgear::strutils::split(linesIt->str)); + + double lat = atof( token[1].c_str() ); + double lon = atof( token[2].c_str() ); + double elev = atof( token[3].c_str() ); + tower = SGGeod::fromDegFt(lon, lat, elev + last_apt_elev); + cache->insertTower(currentAirportID, tower); + } else if ( line_id == 19 ) { + // windsock entry (ignore) + } else if ( line_id == 20 ) { + // Taxiway sign (ignore) + } else if ( line_id == 21 ) { + // lighting objects (ignore) + } else if ( line_id == 15 ) { + // custom startup locations (ignore) + } else if ( line_id == 0 ) { + // ?? + } else if ( line_id >= 50 && line_id <= 56) { + parseCommLine(aptDat, line_id, simgear::strutils::split(linesIt->str)); + } else if ( line_id == 110 ) { + pavement = true; + parsePavementLine850(simgear::strutils::split(linesIt->str, 0, 4)); + } else if ( line_id >= 111 && line_id <= 114 ) { + if ( pavement ) + parsePavementNodeLine850(line_id, + simgear::strutils::split(linesIt->str)); + } else if ( line_id >= 115 && line_id <= 116 ) { + // other pavement nodes (ignore) + } else if ( line_id == 120 ) { + pavement = false; + } else if ( line_id == 130 ) { + pavement = false; + } else if ( line_id >= 1000 ) { + // airport traffic flow (ignore) + } else { + std::ostringstream oss; + oss << aptDat << ":" << linesIt->number << ": unknown row code " << + line_id; + SG_LOG( SG_GENERAL, SG_ALERT, + oss.str() << " (" << linesIt->str << ")" ); + throw sg_format_exception(oss.str(), linesIt->str); + } + } // of loop over the second and subsequent apt.dat lines for the airport + + finishAirport(aptDat); + } // of loop over 'airportInfoMap' } // Tell whether an apt.dat line is blank or a comment line @@ -253,15 +334,13 @@ void APTLoader::finishAirport(const string& aptDat) currentAirportID = 0; } -void APTLoader::parseAirportLine(const string& aptDat, +// 'rowCode' is passed to avoid decoding it twice, since that work was already +// done in order to detect the start of the new airport. +void APTLoader::parseAirportLine(unsigned int rowCode, const vector& token) { const string& id(token[4]); double elev = atof( token[1].c_str() ); - - // finish the previous airport - finishAirport(aptDat); - last_apt_elev = elev; string name; @@ -276,8 +355,7 @@ void APTLoader::parseAirportLine(const string& aptDat, rwy_lat_accum = 0.0; rwy_count = 0; - int robinType = atoi(token[0].c_str()); - currentAirportID = cache->insertAirport(fptypeFromRobinType(robinType), + currentAirportID = cache->insertAirport(fptypeFromRobinType(rowCode), id, name); } @@ -553,16 +631,7 @@ void APTLoader::parseCommLine(const string& aptDat, int lineId, "), skipping" ); } -// Load the airport data base from the specified aptdb file. The -// metar file is used to mark the airports as having metar available -// or not. -bool airportDBLoad( const SGPath &aptdb_file ) -{ - APTLoader ld; - ld.parseAPT(aptdb_file); - return true; -} - +// The 'metar.dat' file lists the airports that have METAR available. bool metarDataLoad(const SGPath& metar_file) { sg_gzifstream metar_in( metar_file ); diff --git a/src/Airports/apt_loader.hxx b/src/Airports/apt_loader.hxx index 9193d7271..c4959b27f 100644 --- a/src/Airports/apt_loader.hxx +++ b/src/Airports/apt_loader.hxx @@ -28,13 +28,18 @@ #include #include +#if __cplusplus >= 201103L + #include +#else + #include +#endif + #include #include #include +#include #include -// Forward declarations -class SGPath; class NavDataCache; class sg_gzifstream; class FGPavement; @@ -48,9 +53,47 @@ public: APTLoader(); ~APTLoader(); - void parseAPT(const SGPath &aptdb_file); + // Read the specified apt.dat file into 'airportInfoMap' + void readAptDatFile(const SGPath& aptdb_file); + // Read all airports gathered in 'airportInfoMap' and load them into the + // navdata cache (even in case of overlapping apt.dat files, + // 'airportInfoMap' has only one entry per airport). + void loadAirports(); private: + struct Line + { + Line(unsigned int number_, unsigned int rowCode_, std::string str_) + : number(number_), rowCode(rowCode_), str(str_) { } + + unsigned int number; + unsigned int rowCode; // Terminology of the apt.dat spec + std::string str; + }; + + typedef std::vector LinesList; + + struct RawAirportInfo + { + // apt.dat file where the airport was defined + SGPath file; + // Row code for the airport (1, 16 or 17) + unsigned int rowCode; + // Line number in the apt.dat file where the airport definition starts + unsigned int firstLineNum; + // The whitespace-separated strings comprising the first line of the airport + // definition + std::vector firstLineTokens; + // Subsequent lines of the airport definition (one element per line) + LinesList otherLines; + }; + +#if __cplusplus >= 201103L + typedef std::unordered_map AirportInfoMapType; +#else + typedef std::map AirportInfoMapType; +#endif + typedef SGSharedPtr FGPavementPtr; APTLoader(const APTLoader&); // disable copy constructor @@ -60,7 +103,7 @@ private: bool isBlankOrCommentLine(const std::string& line); void throwExceptionIfStreamError(const sg_gzifstream& input_stream, const SGPath& path); - void parseAirportLine(const std::string& aptDat, + void parseAirportLine(unsigned int rowCode, const std::vector& token); void finishAirport(const std::string& aptDat); void parseRunwayLine810(const std::vector& token); @@ -73,6 +116,8 @@ private: void parseCommLine(const std::string& aptDat, int lineId, const std::vector& token); + std::vector token; + AirportInfoMapType airportInfoMap; double rwy_lat_accum; double rwy_lon_accum; double last_rwy_heading; @@ -90,12 +135,6 @@ private: PositionedID currentAirportID; }; -// Load the airport data base from the specified aptdb file. The -// metar file is used to mark the airports as having metar available -// or not. - -bool airportDBLoad(const SGPath& path); - bool metarDataLoad(const SGPath& path); } // of namespace flighgear diff --git a/src/Navaids/NavDataCache.cxx b/src/Navaids/NavDataCache.cxx index 3f60ff9f6..412fd8ce4 100644 --- a/src/Navaids/NavDataCache.cxx +++ b/src/Navaids/NavDataCache.cxx @@ -1339,16 +1339,18 @@ void NavDataCache::doRebuild() SGTimeStamp st; { Transaction txn(this); + APTLoader aptLoader; string_list aptDatFiles; st.stamp(); for (PathList::const_iterator it = d->aptDatPaths.begin(); it != d->aptDatPaths.end(); it++) { aptDatFiles.push_back(it->realpath().utf8Str()); - airportDBLoad(*it); + aptLoader.readAptDatFile(*it); stampCacheFile(*it); // this uses the realpath() of the file } + aptLoader.loadAirports(); // load airport data into the NavCache // Store the list of apt.dat files we have loaded writeOrderedStringListProperty( datTypeStr[DATFILETYPE_APT] + ".dat files", aptDatFiles,