From 7682d7d00ecbb9f30dd20dd4a0ac861a64cf147e Mon Sep 17 00:00:00 2001 From: Colin Geniet Date: Fri, 24 Feb 2023 20:58:59 +0100 Subject: [PATCH] Load apt.dat files from scenery directories Search scenery directories for apt.dat files. Files of the form NavData/apt/*.dat[.gz] are recognised (same as FG). These files are then used for pavement overlap checks. Add parameter OVERLAP_CHECK_APT_DAT_SCENERY_LIST to allow control of which scenery directories are scanned. --- build_tiles.py | 4 +- doc/manual/how_it_works.rst | 4 +- doc/manual/parameters.rst | 7 ++ osm2city/parameters.py | 1 + osm2city/utils/aptdat_io.py | 156 ++++++++++++++++++++++-------------- 5 files changed, 109 insertions(+), 63 deletions(-) diff --git a/build_tiles.py b/build_tiles.py index 9eec55a..ae021a3 100755 --- a/build_tiles.py +++ b/build_tiles.py @@ -296,8 +296,8 @@ if __name__ == '__main__': # get airports from apt_dat. Transformation to blocked areas can only be done in sub-process due to local # coordinate system - airports = aio.read_apt_dat_gz_file(boundary_west, boundary_south, - boundary_east, boundary_north) + airports = aio.read_apt_dat_files(boundary_west, boundary_south, + boundary_east, boundary_north) # Reset the progress file in write mode with open(FILE_NAME_PROGRESS_LOG, 'w') as f: diff --git a/doc/manual/how_it_works.rst b/doc/manual/how_it_works.rst index badf694..29a14be 100644 --- a/doc/manual/how_it_works.rst +++ b/doc/manual/how_it_works.rst @@ -152,6 +152,6 @@ There are three situations, where a gabled roof around an edge is done. See also Aerodromes ---------- -Data for runways and helipads are depending on parameters read from the ``apt.dat file`` in ``$FG_ROOT/Airports/apt.dat.gz`` or directly from btg-files. This information is used to avoid having crossing OSM roads to be visible and potentially creating a considerable bump when a plane rolls over. +Data for runways and helipads are depending on parameters read from the ``apt.dat files`` in ``$FG_ROOT/Airports/apt.dat.gz`` and under ``NavData/apt/`` in the scenery directories, or directly from btg-files. This information is used to avoid having crossing OSM roads to be visible and potentially creating a considerable bump when a plane rolls over. -Data for airport boundaries are also read from the ``apt.dat`` file (not all airports have information about boundaries). This data is then merged with OSM data for ``aeroway=aerodrome`` (again not all airports might be modelled with a zone). The resulting land-use is making sure that buildings within these zones will look more like airport buildings: using flat roofs (unless the roof type is explicitly modelled in OSM) and using a modern facade texture. Also: no buildings are generated inside zones for aerodromes. +Data for airport boundaries are also read from the ``apt.dat`` files (not all airports have information about boundaries). This data is then merged with OSM data for ``aeroway=aerodrome`` (again not all airports might be modelled with a zone). The resulting land-use is making sure that buildings within these zones will look more like airport buildings: using flat roofs (unless the roof type is explicitly modelled in OSM) and using a modern facade texture. Also: no buildings are generated inside zones for aerodromes. diff --git a/doc/manual/parameters.rst b/doc/manual/parameters.rst index 6fee41a..7b377ea 100644 --- a/doc/manual/parameters.rst +++ b/doc/manual/parameters.rst @@ -399,6 +399,13 @@ OVERLAP_CHECK_CONSIDER_SHARED Bool True Whether onl models reused in different places like a church model). For this to work ``PATH_TO_SCENERY`` must point to the TerraSync directory. +OVERLAP_CHECK_APT_DAT_SCENERY_LIST List None Search for apt.dat files within these scenery directories. Files of the form + ``NavData/apt/*.dat[.gz]`` in these directories will be loaded in addition + to ``${FG_ROOT}/Airport/apt.dat.gz`` for the purpose of pavement overlap checks. + If None (default), PATH_TO_SCENERY and PATH_TO_SCENERY_OPT are used. + If information for the same airport is found in several files, only the last is used. + Set to ``[]`` to prevent loading any additional apt.dat files. + OVERLAP_CHECK_APT_PAVEMENT_BUILDINGS_INCLUDE List None At airports in list include overlap check with pavement for buildings. Pavements are e.g. APRON and taxiways. If the list is empty, then checks at all airports are done. diff --git a/osm2city/parameters.py b/osm2city/parameters.py index 2323639..2eb689e 100755 --- a/osm2city/parameters.py +++ b/osm2city/parameters.py @@ -112,6 +112,7 @@ OVERLAP_CHECK_CH_BUFFER_SHARED = 0.0 OVERLAP_CHECK_CONSIDER_SHARED = True +OVERLAP_CHECK_APT_DAT_SCENERY_LIST = None # Scenery directories in which to look for apt.dat files of the form NavData/apt/*.dat[.gz]. If None (default), use PATH_TO_SCENERY and PATH_TO_SCENERY_OPT. OVERLAP_CHECK_APT_PAVEMENT_BUILDINGS_INCLUDE = None # At apts in list include overlap check with pavement for buildings OVERLAP_CHECK_APT_PAVEMENT_ROADS_INCLUDE = None # At airports in list include overlap check with pavement for roads OVERLAP_CHECK_APT_USE_BTG_ROADS = [] diff --git a/osm2city/utils/aptdat_io.py b/osm2city/utils/aptdat_io.py index 8cabb00..0ba2e66 100644 --- a/osm2city/utils/aptdat_io.py +++ b/osm2city/utils/aptdat_io.py @@ -1,10 +1,8 @@ """Handles reading from apt.dat airport files and read/write to pickle file for minimized representation. See http://developer.x-plane.com/?article=airport-data-apt-dat-file-format-specification for the specification. -FlightGear 2016.4 can read multiple apt.data files - see e.g. http://wiki.flightgear.org/FFGo -and https://sourceforge.net/p/flightgear/flightgear/ci/516a5cf016a7d504b09aaac2e0e66c7e9efd42b2/. -However this module does only support reading from the apt.dat.gz in $FG_ROOT/Airports/apt.dat.gz). - +Files in NavData/apt/ in scenery directories are used in addition to $FG_ROOT/Airports/apt.dat.gz, +see https://sourceforge.net/p/flightgear/flightgear/ci/516a5cf016a7d504b09aaac2e0e66c7e9efd42b2/. """ from abc import ABCMeta, abstractmethod @@ -184,65 +182,105 @@ class Airport(object): return self.airport_boundary.create_polygons(coords_transform) -def read_apt_dat_gz_file(min_lon: float, min_lat: float, - max_lon: float, max_lat: float, read_water_runways: bool = False) -> List[Airport]: - apt_dat_gz_file = os.path.join(utilities.get_fg_root(), 'Airports', 'apt.dat.gz') +def get_scenery_apt_dat_files() -> List[str]: + """Return a list of paths to .dat files found in the scenery directories.""" + apt_dat_list = [] + + if parameters.OVERLAP_CHECK_APT_DAT_SCENERY_LIST is not None: + scenery_list = parameters.OVERLAP_CHECK_APT_DAT_SCENERY_LIST + else: + scenery_list = [parameters.PATH_TO_SCENERY] + if parameters.PATH_TO_SCENERY_OPT is not None: + scenery_list += parameters.PATH_TO_SCENERY_OPT + + for apt_dat_dir in [os.path.join(d, "NavData", "apt") for d in scenery_list]: + for file in os.listdir(apt_dat_dir): + if file.endswith(".dat") or file.endswith(".dat.gz"): + apt_dat_list.append(os.path.join(apt_dat_dir, file)) + + return apt_dat_list + + +def _read_apt_dat_file(file, airports: dict[Airport], + min_lon: float, min_lat: float, max_lon: float, max_lat: float, + read_water_runways: bool = False) -> int: + """Parse apt.dat files, extending the dict of airports from the input parameter.""" + total_airports = 0 + my_airport = None + boundary = None + current_boundary_nodes = list() + in_boundary = False + for line in file: + parts = line.split() + if not parts: + continue + if in_boundary: + if parts[0] not in ['111', '112', '113', '114', '115', '116']: + in_boundary = False + else: + current_boundary_nodes.append((float(parts[2]), float(parts[1]))) + if parts[0] in ['113', '114']: # closed loop + boundary.append_nodes_list(current_boundary_nodes) + current_boundary_nodes = list() + if parts[0] in ['1', '16', '17', '99']: + # first actually append the previously read airport data to the collection if within bounds + if (my_airport is not None) and (my_airport.within_boundary(min_lon, min_lat, max_lon, max_lat)): + # will overwrite previous airport with same ICAO if present + airports[my_airport.code] = my_airport + # and then create a new empty airport + if not parts[0] == '99': + my_airport = Airport(parts[4], parts[5], int(parts[0])) + total_airports += 1 + elif parts[0] == '100': + my_runway = LandRunway(float(parts[1]), co.Vec2d(float(parts[10]), float(parts[9])), + co.Vec2d(float(parts[19]), float(parts[18]))) + my_airport.append_runway(my_runway) + elif parts[0] == '101': + if read_water_runways: + my_runway = WaterRunway(float(parts[1]), co.Vec2d(float(parts[5]), float(parts[4])), + co.Vec2d(float(parts[8]), float(parts[7]))) + my_airport.append_runway(my_runway) + elif parts[0] == '102': + my_helipad = Helipad(float(parts[5]), float(parts[6]), co.Vec2d(float(parts[3]), float(parts[2])), + float(parts[4])) + my_airport.append_runway(my_helipad) + elif parts[0] == '110': # Pavement + name = 'no name' + if len(parts) == 5: + name = parts[4] + boundary = Boundary(name) + in_boundary = True + my_airport.append_pavement(boundary) + elif parts[0] == '130': # Airport boundary header + boundary = Boundary(parts[1]) + in_boundary = True + my_airport.append_airport_boundary(boundary) + + return total_airports + + +def read_apt_dat_files(min_lon: float, min_lat: float, max_lon: float, max_lat: float, + read_water_runways: bool = False) -> List[Airport]: + """Return a list Airports read from the apt.dat files in FGData and in the scenery directories.""" start_time = time.time() - airports = list() + airports = dict() total_airports = 0 + + apt_dat_gz_file = os.path.join(utilities.get_fg_root(), 'Airports', 'apt.dat.gz') with gzip.open(apt_dat_gz_file, 'rt', encoding="latin-1") as f: - my_airport = None - boundary = None - current_boundary_nodes = list() - in_boundary = False - for line in f: - parts = line.split() - if not parts: - continue - if in_boundary: - if parts[0] not in ['111', '112', '113', '114', '115', '116']: - in_boundary = False - else: - current_boundary_nodes.append((float(parts[2]), float(parts[1]))) - if parts[0] in ['113', '114']: # closed loop - boundary.append_nodes_list(current_boundary_nodes) - current_boundary_nodes = list() - if parts[0] in ['1', '16', '17', '99']: - # first actually append the previously read airport data to the collection if within bounds - if (my_airport is not None) and (my_airport.within_boundary(min_lon, min_lat, max_lon, max_lat)): - airports.append(my_airport) - # and then create a new empty airport - if not parts[0] == '99': - my_airport = Airport(parts[4], parts[5], int(parts[0])) - total_airports += 1 - elif parts[0] == '100': - my_runway = LandRunway(float(parts[1]), co.Vec2d(float(parts[10]), float(parts[9])), - co.Vec2d(float(parts[19]), float(parts[18]))) - my_airport.append_runway(my_runway) - elif parts[0] == '101': - if read_water_runways: - my_runway = WaterRunway(float(parts[1]), co.Vec2d(float(parts[5]), float(parts[4])), - co.Vec2d(float(parts[8]), float(parts[7]))) - my_airport.append_runway(my_runway) - elif parts[0] == '102': - my_helipad = Helipad(float(parts[5]), float(parts[6]), co.Vec2d(float(parts[3]), float(parts[2])), - float(parts[4])) - my_airport.append_runway(my_helipad) - elif parts[0] == '110': # Pavement - name = 'no name' - if len(parts) == 5: - name = parts[4] - boundary = Boundary(name) - in_boundary = True - my_airport.append_pavement(boundary) - elif parts[0] == '130': # Airport boundary header - boundary = Boundary(parts[1]) - in_boundary = True - my_airport.append_airport_boundary(boundary) - - logging.info("Read %d airports, %d having runways/helipads within the boundary", total_airports, len(airports)) + total_airports += _read_apt_dat_file(f, airports, min_lon, min_lat, max_lon, max_lat, read_water_runways) + + total_files = 1 + for path in get_scenery_apt_dat_files(): + total_files += 1 + open_fun = gzip.open if path.endswith(".dat.gz") else open + with open_fun(path, 'rt', encoding="latin-1") as f: + total_airports += _read_apt_dat_file(f, airports, min_lon, min_lat, max_lon, max_lat, read_water_runways) + + logging.info("Read %d airports from %d files, %d having runways/helipads within the boundary", + total_airports, total_files, len(airports)) utilities.time_logging("Execution time", start_time) - return airports + return list(airports.values()) def get_apt_dat_blocked_areas_from_airports(coords_transform: co.Transformation, -- GitLab