Move fgtools module into it's own subfolder, add scripts to fix ICAO's of apt.dat files (mainly for those pulled from the XPlane gateway) and to generate apt.dat files from OurAirports and OSM data

This commit is contained in:
TheFGFSEagle
2022-09-15 20:33:25 +02:00
parent eae92402bc
commit 875799b048
20 changed files with 1295 additions and 110 deletions
+41 -39
View File
@@ -8,7 +8,7 @@ import statistics
from fgtools.utils.files import find_input_files
from fgtools import utils
from fgtools.utils import geo
from fgtools.geo import coord
from fgtools.utils import unit_convert
def format_coord(coord, lonlat):
@@ -19,10 +19,7 @@ def format_coord(coord, lonlat):
return f"{prefix}{i} {f * 60:.8f}"
def get_icao_xml_path(icao, what):
if len(icao) == 3:
return f"{icao[0]}/{icao[1]}/{icao}.{what}.xml"
else:
return f"{icao[0]}/{icao[1]}/{icao[2]}/{icao}.{what}.xml"
return f"{icao[0]}/{icao[1]}/{icao[2]}/{icao}.{what}.xml"
class Parking:
def __init__(self, index, type, name, lon, lat, hdg, radius=7.5, pushback_route=-1, airline_codes=[]):
@@ -42,7 +39,7 @@ class Parking:
def __repr__(self):
s = (f' <Parking index="{self.index}" type="{self.type}" name="{self.name}"' +
f' lon="{format_coord(self.lon, "lon")}" lat="{format_coord(self.lat, "lat")}"' +
f' lat="{format_coord(self.lat, "lat")}" lon="{format_coord(self.lon, "lon")}"' +
f' heading="{self.hdg}" radius="{self.radius}"')
if self.pushback_route > -1:
s += f' pushBackRoute="{self.pushback_route}"'
@@ -63,66 +60,69 @@ class TaxiNode:
return self.on_runway != None
def __repr__(self):
return (f' <node index="{self.index}" lon="{format_coord(self.lon, "lon")}" ' +
f'lat="{format_coord(self.lat, "lat")}" isOnRunway="{int(self.on_runway)}" ' +
f'holdPointType="{self.holdPointType}"/>\n')
return (f' <node index="{self.index}" lat="{format_coord(self.lat, "lat")}"' +
f' lon="{format_coord(self.lon, "lon")}" isOnRunway="{int(self.on_runway)}"' +
f' holdPointType="{self.holdPointType}"/>\n')
class TaxiEdge:
def __init__(self, begin, end, is_on_runway, name):
def __init__(self, begin, end, bidirectional, is_on_runway, name):
self.begin = begin
self.end = end
self.name = name
self.bidirectional = bidirectional
self.is_on_runway = is_on_runway
self.is_pushback_route = False
def __bool__(self):
return self.is_pushback_route != None
def __contains__(self, node):
return node.index in (self.begin, self.end)
def __repr__(self):
return (f' <arc begin="{self.begin}" end="{self.end}" ' +
s = (f' <arc begin="{self.begin}" end="{self.end}" ' +
f'isPushBackRoute="{int(self.is_pushback_route)}" name="{self.name}"/>\n')
if self.bidirectional:
s += (f' <arc begin="{self.end}" end="{self.begin}" ' +
f'isPushBackRoute="{int(self.is_pushback_route)}" name="{self.name}"/>\n')
return s
class Runway:
def __init__(self, id1, lon1, lat1, displ1, stopway1, id2, lon2, lat2, displ2, stopway2):
self.lon1 = lon1
self.lat1 = lat1
self.coord1 = coord.Coord(lon1, lat1)
self.id1 = id1
self.displ1 = displ1
self.stopway1 = stopway1
self.lon2 = lon2
self.lat2 = lat2
self.coord2 = coord.Coord(lon2, lat2)
self.id2 = id2
self.displ2 = displ2
self.stopway2 = stopway2
def get_length_m(self):
return geo.great_circle_distance_m(self.lon1, self.lat1, self.lon2, self.lat2)
return self.coord1.distance_m(self.coord2)
def get_length_ft(self):
return unit_convert.m2ft(self.get_length_m())
def get_heading1_deg(self):
return geo.get_bearing_deg(self.lon1, self.lat1, self.lon2, self.lat2)
return self.coord2.angle(self.coord1)
def get_heading2_deg(self):
brg = self.get_heading1_deg() + 180
while brg >= 360:
brg -= 360
return brg
return self.coord1.angle(self.coord2)
def __repr__(self):
return f""" <runway>
<threshold>
<lon>{self.lon1}</lon>
<lat>{self.lat1}</lat>
<lat>{self.coord1.lat}</lat>
<lon>{self.coord1.lon}</lon>
<rwy>{self.id1}</rwy>
<hdg-deg>{self.get_heading1_deg():.2f}</hdg-deg>
<displ-m>{self.displ1}</displ-m>
<stopw-m>{self.stopway1}</stopw-m>
</threshold>
<threshold>
<lon>{self.lon2}</lon>
<lat>{self.lat2}</lat>
<lat>{self.coord2.lat}</lat>
<lon>{self.coord2.lon}</lon>
<rwy>{self.id2}</rwy>
<hdg-deg>{self.get_heading2_deg():.2f}</hdg-deg>
<displ-m>{self.displ2}</displ-m>
@@ -144,8 +144,8 @@ class Tower:
def __repr__(self):
return f""" <tower>
<twr>
<lon>{self.lon}</lon>
<lat>{self.lat}</lat>
<lon>{self.lon}</lon>
<elev-m>{self.agl}</elev-m>
</twr>
</tower>
@@ -185,8 +185,8 @@ class ILS:
if None not in (self.lon1, self.lat1, self.rwy1, self.hdg1, self.elev1, self.ident1):
s += f""" <ils>
<lon>{self.lon1}</lon>
<lat>{self.lat1}</lat>
<lon>{self.lon1}</lon>
<rwy>{self.rwy1}</rwy>
<hdg-deg>{self.hdg1:.2f}</hdg-deg>
<elev-m>{self.elev1}</elev-m>
@@ -195,8 +195,8 @@ class ILS:
"""
if None not in (self.lon2, self.lat2, self.rwy2, self.hdg2, self.elev2, self.ident2):
s += """ <ils>
<lon>{self.lon2}</lon>
<lat>{self.lat2}</lat>
<lon>{self.lon2}</lon>
<rwy>{self.rwy2}</rwy>
<hdg-deg>{self.hdg2:.2f}</hdg-deg>
<elev-m>{self.elev2}</elev-m>
@@ -283,25 +283,26 @@ def parse_aptdat_files(files, nav_dat, print_runway_lengths):
elif line[0] == 14:
towers[icao] = Tower(float(line[2]), float(line[1]), float(line[3]))
elif line[0] == 1201: # taxi node
taxi_nodes[icao].append(TaxiNode(float(line[2]), float(line[1]), len(taxi_nodes[icao])))
taxi_nodes[icao].append(TaxiNode(float(line[2]), float(line[1]), int(line[4]) + len(parkings[icao])))
elif line[0] == 1202: # taxi edge
if len(line) == 6:
taxi_edges[icao].append(TaxiEdge(int(line[1]), int(line[2]), line[4] == "runway", line[5]))
edge = TaxiEdge(int(line[1]) + len(parkings[icao]), int(line[2]) + len(parkings[icao]), line[3] == "twoway", line[4] == "runway", line[5])
if edge.begin != edge.end:
taxi_edges[icao].append(edge)
if not icao in towers and len(runways[icao]) > 0:
runway_lons = []
runway_lats = []
runway_hdgs = []
for runway in runways[icao]:
runway_lons += [runway.lon1, runway.lon2]
runway_lats += [runway.lat1, runway.lat2]
runway_lons += [runway.coord1.lon, runway.coord2.lon]
runway_lats += [runway.coord1.lat, runway.coord2.lat]
runway_hdgs.append(runway.get_heading1_deg())
tower_lon, tower_lat = geo.apply_heading_distance(statistics.median(runway_lons),
statistics.median(runway_lats),
statistics.median(runway_hdgs) + 90, 50)
towers[icao] = Tower(tower_lon, tower_lat, 15)
tower_pos = (coord.Coord(statistics.median(runway_lons), statistics.median(runway_lats))
.apply_angle_distance_m(statistics.median(runway_hdgs) + 90, 200))
towers[icao] = Tower(tower_pos.lon, tower_pos.lat, 15)
if not parkings[icao]:
del parkings[icao]
@@ -352,10 +353,11 @@ def write_groundnet_files(parkings, taxi_nodes, taxi_edges, output, overwrite):
if len(taxi_nodes[icao]) > 0 and len(taxi_edges[icao]) > 0:
f.write(" <TaxiNodes>\n")
for edge in taxi_edges[icao]:
taxi_nodes[icao][edge.begin].is_on_runway = edge.is_on_runway
taxi_nodes[icao][edge.end].is_on_runway = edge.is_on_runway
taxi_nodes[icao][edge.begin - len(parkings[icao])].is_on_runway = edge.is_on_runway
taxi_nodes[icao][edge.end - len(parkings[icao])].is_on_runway = edge.is_on_runway
for node in taxi_nodes[icao]:
f.write(repr(node))
if any(node in edge for edge in taxi_edges[icao]):
f.write(repr(node))
f.write(" </TaxiNodes>\n")
f.write(" <TaxiWaySegments>\n")
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import os
import sys
import csv
import requests
import argparse
import shutil
from fgtools.utils.files import find_input_files
from fgtools.geo.coord import Coord
from fgtools.utils import constants
def _get_ourairports_csv(what):
path = os.path.join(constants.CACHEDIR, what + ".csv")
if not os.path.isfile(path):
with open(path, "w") as f:
f.write(requests.get(f"https://davidmegginson.github.io/ourairports-data/{what}.csv").content.decode())
f = open(path, "r", newline="")
return list(csv.DictReader(f))[1:]
def get_ourairports_icao(airport, csv):
c = Coord(airport["lon"], airport["lat"])
matches = []
for line in csv:
d = c.distance_km(Coord(float(line["longitude_deg"]), float(line["latitude_deg"])))
if d <= 10:
matches.append({"distance": d, "icao": line["gps_code"] or line["local_code"] or line["ident"]})
matches.sort(key=lambda m: m["distance"])
if len(matches) and matches[0]["icao"]:
if len(airport["icao"]) != 4:
airport["newicao"] = matches[0]["icao"]
else:
print(f"No matching airport found for {airport['icao']} - skipping", end=" " * 100 + "\n")
return airport
def process(files, output):
csv = _get_ourairports_csv("airports")
i = 0
n = 0
skipped = 0
total = len(files)
files_d = {}
for p in files:
print(f"Parsing files … {i / total * 100:.1f}% ({i} of {total} done, found {n} airports)", end="\r")
i += 1
file_d = {"lines": [], "airports": {}}
with open(p, "r") as f:
file_d["lines"] = list(map(lambda l: list(filter(None, l)), map(lambda s: s.split(" "), filter(None, map(str.strip, f.readlines())))))
curicao = ""
for line in file_d["lines"]:
if line[0] in ("1", "16", "17"):
curicao = line[4]
skip = False
file_d["airports"][curicao] = {"icao": curicao}
n += 1
elif line[0] == "1302":
if line[1] == "datum_lon":
if len(line) < 3:
continue
file_d["airports"][curicao]["lon"] = float(line[2])
if line[1] == "datum_lat":
if len(line) < 3:
continue
file_d["airports"][curicao]["lat"] = float(line[2])
elif line[0] in ("100", "101", "102") and not ("lon" in file_d["airports"][curicao] and "lat" in file_d["airports"][curicao]):
# no datum_lon / datum_lat found, approximate airport position from first runway / helipad found
if line[0] == "100": # land runway
lon = (float(line[10]) + float(line[19])) / 2
lat = (float(line[9]) + float(line[18])) / 2
elif line[0] == "101": # water runway
lon = (float(line[5]) + float(line[8])) / 2
lat = (float(line[4]) + float(line[7])) / 2
else: # helipad
lon = float(line[3])
lat = float(line[2])
file_d["airports"][curicao]["lon"] = lon
file_d["airports"][curicao]["lat"] = lat
for icao in list(file_d["airports"].keys()):
if not ("lon" in file_d["airports"][icao] and "lat" in file_d["airports"][icao]):
print(f"Unable to get longitude / latitude of airport {curicao} in file {p} - skipping", end=" " * 100 + "\n")
del file_d["airports"][icao]
n -= 1
skipped += 1
files_d[p] = file_d
print(f"Parsing files … {i / total * 100:.1f}% ({i} of {total} done, found {n} airports, skipped {skipped})", end=" " * 100 + "\n")
i = 0
total = n
for p in files_d:
for icao in files_d[p]["airports"]:
print(f"Getting ICAOs for airports … {i / total * 100:.1f}% ({i} of {total} done)", end="\r")
i += 1
files_d[p]["airports"][icao] = get_ourairports_icao(files_d[p]["airports"][icao], csv)
print(f"Getting ICAOs for airports … {i / total * 100:.1f}% ({i} of {total} done)", end=" " * 100 + "\n")
i = 0
total = len(files_d)
for p in files_d:
print(f"Writing new apt.dat files … {i / total * 100:.1f}% ({i} of {total} done)", end="\r")
i += 1
if output == None:
outp = p
else:
outp = os.path.join(output, os.path.split(p)[-1])
parts = os.path.split(outp)
prefix, newname = os.path.join(*parts[:-1]), parts[-1]
if len(files_d[p]["airports"]) > 0 and newname != "apt.dat":
firsticao = list(files_d[p]["airports"].keys())[0]
if "newicao" in files_d[p]["airports"][firsticao]:
newname = files_d[p]["airports"][firsticao]["newicao"] + ".dat"
newoutp = os.path.join(prefix, newname)
with open(outp, "w") as f:
for line in files_d[p]["lines"]:
if line[0] in ("1", "16", "17") and line[4] in files_d[p]["airports"] and "newicao" in files_d[p]["airports"][line[4]]:
line[4] = files_d[p]["airports"][line[4]]["newicao"]
f.write(" ".join(line) + "\n")
if outp != newoutp:
print(f"Renaming file: {outp} -> {newoutp}", end=" " * 100 + "\n")
shutil.move(outp, newoutp)
print(f"Writing new apt.dat files … {i / total * 100:.1f}% ({i} of {total} done)", end=" " * 100 + "\n")
if __name__ == "__main__":
argp = argparse.ArgumentParser(description="Fix apt.dat ICAO's - some apt.dat files from the XPlane gateway have them of the form XAY0016 - this script gets the right ICAO from OurAirports data (if the airport is found there)")
argp.add_argument(
"-i", "--input",
help="Input apt.dat file(s) / folder(s) containing apt.dat files",
required=True,
nargs="+"
)
argp.add_argument(
"-o", "--output",
help="Output folder for the modified apt.dat files - omit to edit the files in-place",
default=None
)
args = argp.parse_args()
infiles = find_input_files(args.input)
process(infiles, args.output)
+490
View File
@@ -0,0 +1,490 @@
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import os
import sys
import argparse
import requests
import csv
import re
import logging
import math
from OSMPythonTools import overpass
from fgtools.geo import coord, rectangle
from fgtools.utils import files
from fgtools import aptdat
from fgtools.utils import constants
from fgtools.utils import unit_convert
osmapi = overpass.Overpass()
def parse_runway_id(id):
which, heading = "", 0
if id[-1] in ("L", "C", "R"):
which = id[-1]
try:
if which:
heading = int(id[:-1])
else:
heading = int(id)
except ValueError:
heading = 0
return heading * 10, which
def parse_surface_type(surface):
if re.search("pem|mac|sealed|bit|asp(h)?(alt)?|tarmac", surface) or surface in ("b"):
surface = "Asphalt"
elif re.search("wood|cement|bri(ck)?|hard|paved|pad|psp|met|c[o0]n(c)?", surface):
surface = "Concrete"
elif re.search("rock|gvl|grvl|gravel|pi(ç|c)", surface):
surface = "Gravel"
elif re.search("tr(ea)?t(e)?d|san(d)?|ter|none|cor|so(ft|d|il)|earth|cop|com|per|ground|silt|cla(y)?|dirt|turf", surface):
surface = "Dirt"
elif re.search("gr(a*)?s|gre", surface) or surface in ("g"):
surface = "Grass"
elif re.search("wat(er)?", surface):
surface = "Water"
elif re.search("sno|ice", surface):
surface = "SnowIce"
else:
surface = "Unknown"
return surface
def _get_ourairports_csv(what):
path = os.path.join(constants.CACHEDIR, what + ".csv")
if not os.path.isfile(path):
with open(path, "w") as f:
f.write(requests.get(f"https://davidmegginson.github.io/ourairports-data/{what}.csv").content.decode())
f = open(path, "r", newline="")
return list(csv.DictReader(f))[1:]
def get_ourairports_airports(bbox=None, icaos=[]):
if not (bbox or icaos):
raise TypeError("both bbox and icaos are None")
csv = _get_ourairports_csv("airports")
airports = []
print("Creating airports from OurAirports data … ", end="")
for line in csv:
type = aptdat.AirportType.Land
if "sea" in line["type"]:
type = aptdat.AirportType.Sea
elif "heli" in line["type"]:
type = aptdat.AirportType.Heli
code = line["gps_code"] or line["local_code"] or line["ident"]
if not code:
continue
airport = {"ident": line["ident"], "airport": aptdat.Airport(int(line["elevation_ft"] or 0), code,
line["name"], float(line["longitude_deg"]),
float(line["latitude_deg"]), type=type)}
if code in icaos:
icaos.remove(code)
airports.append(airport)
elif bbox and bbox.is_inside(coord.Coord(airport["airport"].lon, airport["airport"].lat)):
airports.append(airport)
print(f"done - found {len(airports)} airports for the given bounding box / ICAO")
return airports
def get_osm_elements_near_airport(airport, what, query, element_type, radius=10000, max_retries=10):
left = coord.Coord(airport.lon, 0).apply_angle_distance_m(-90, radius).lon
right = coord.Coord(airport.lon, 0).apply_angle_distance_m(90, radius).lon
upper = coord.Coord(0, airport.lat).apply_angle_distance_m(0, radius).lat
lower = coord.Coord(0, airport.lat).apply_angle_distance_m(180, radius).lat
query = overpass.overpassQueryBuilder(bbox=[lower, left, upper, right], elementType=element_type,
selector=query, out="center")
result = -1
retries = 0
while result == -1 and retries < max_retries:
try:
if element_type == "node":
result = osmapi.query(query, timeout=60).nodes()
elif element_type == "way":
result = osmapi.query(query, timeout=60).ways()
elif element_type == "relations":
result = osmapi.query(query, timeout=60).relations()
else:
result = []
qresult = osmapi.query(query, timeout=100)
if not qresult:
result = None
break
for etype in element_type:
result += getattr(qresult, etype + "s")() or []
except Exception as e:
if "timeout" in str(e.args).lower():
result = -1
else:
raise e
retries += 1
if result == -1:
print(f"API query for OSM {what} data for airport {airport.icao} timed out {retries} times - won't retry", end=" " * 100 + "\n")
result = []
if result == None:
result = []
print(f"No OSM {what} data found for airport {airport.icao}", end=" " * 100 + "\n")
return result
def add_osm_runways(airport):
result = get_osm_elements_near_airport(airport["airport"], "runway", '"aeroway"="runway"', "way")
osmways = []
for way in result:
first, last = way.nodes()[0], way.nodes()[-1]
if first.id() == last.id():
print("Got a runway mapped as area from OSM - not supported yet", end=" " * 100 + "\n")
continue
first = coord.Coord(first.lon(), first.lat())
last = coord.Coord(last.lon(), last.lat())
heading = first.angle(last)
if heading > 180:
first, last = last, first
heading -= 180
center = rectangle.Rectangle(last, first).midpoint()
distance = coord.Coord(airport["airport"].lon, airport["airport"].lat).distance_m(center)
osmways.append({"distance": distance, "heading": heading, "way": way, "first": first, "last": last})
osmways.sort(key=lambda r: r["distance"])
for i, runway in enumerate(airport["runways"]):
osmways_filtered = []
for osmway in osmways:
if "ref" in osmway["way"].tags():
# water runways somtimes have N, NE, etc. as identifier instead of 36, 04, etc.
mapping = {"N": "36", "NE": "04", "E": "09", "SE": "13", "S": "18", "SW": "22", "W": "27", "NW": "31"}
if runway["le_ident"] in mapping:
runway["le_ident"] = mapping[runway["le_ident"]]
if runway["he_ident"] in mapping:
runway["he_ident"] = mapping[runway["he_ident"]]
if osmway["way"].tags()["ref"] == runway["le_ident"] + "/" + runway["he_ident"]:
osmways_filtered = [osmway]
break
else:
heading, which = parse_runway_id(runway["le_ident"])
if heading <= osmway["heading"] < heading + 10:
if not which:
osmways_filtered = [osmway]
break
else:
osmways_filtered.append(osmway)
if len(osmways_filtered) == 3:
break
if len(osmways_filtered) == 0:
print("No OSM data found for runway", runway["le_ident"], "at airport", airport["airport"].icao, end=" " * 100 + "\n")
elif len(osmways_filtered) == 1: # just one matching runway - nothing left to do
runway["osmway"] = osmways_filtered[0]
elif len(osmways_filtered) == 2: # two parallel runways - sort from left to right and pick the right one
center1 = coord.Coord(osmways_filtered[0].centerLon(), osmways_filtered[0].centerLat())
center2 = coord.Coord(osmways_filtered[1].centerLon(), osmways_filtered[1].centerLat())
heading, which = parse_runway_id(runway["le_ident"])
rel_bearing = center1.angle(center2) - heading
index = 0
if rel_bearing > 0:
index = 0 if which == "L" else 1
else:
index = 1 if which == "L" else 0
runway["osmway"] = osmways_filtered[index]
else: # three or more parallel runways - sort the first three from left to right and pick the right one
center1 = coord.Coord(osmways_filtered[0].centerLon(), osmways_filtered[0].centerLat())
center2 = coord.Coord(osmways_filtered[1].centerLon(), osmways_filtered[1].centerLat())
center3 = coord.Coord(osmways_filtered[2].centerLon(), osmways_filtered[2].centerLat())
heading, which = parse_runway_id(runway["le_ident"])
rel_bearing1 = center1.angle(center2) - heading
rel_bearing2 = center2.angle(center3) - heading
index = 0
if rel_bearing1 > 0 and rel_bearing2 > 0:
index = "LCR".find(which)
elif rel_bearing1 <= 0 and rel_bearing2 > 0:
index = "CLR".find(which)
elif rel_bearing1 > 0 and rel_bearing2 <= 0:
index = "LRC".find(which)
else:
index = "RCL".find(which)
runway["osmway"] = osmways_filtered[index]
if not runway["le_longitude_deg"] or not runway["he_longitude_deg"]:
if "osmway" in runway:
runway["le_heading_degT"] = runway["osmway"]["heading"]
runway["he_heading_degT"] = runway["osmway"]["heading"] + 180
runway["le_longitude_deg"] = runway["osmway"]["first"].lon
runway["le_latitude_deg"] = runway["osmway"]["first"].lat
runway["he_longitude_deg"] = runway["osmway"]["last"].lon
runway["he_latitude_deg"] = runway["osmway"]["last"].lat
else:
print("No threshold information found for runway", runway["le_ident"], "at", airport["airport"].icao, "- removing !", end=" " * 100 + "\n")
airport["runways"][i] = None
airport["runways"] = list(filter(None, airport["runways"]))
def add_osm_helipads(airport):
result = get_osm_elements_near_airport(airport["airport"], "helipad", '"aeroway"="helipad"', ["node", "way"])
osmhelipads = []
counter = 0
for element in result:
if element.type() == "node":
c = coord.Coord(element.lon(), element.lat())
radius = 0
else:
lon_sum = lat_sum = 0
divider = 0
for node in element.nodes():
lon_sum += node.lon()
lat_sum += node.lat()
divider += 1
c = coord.Coord(lon_sum / divider, lat_sum / divider)
dist_sum = 0
for node in element.nodes():
dist_sum += c.distance_m(coord.Coord(node.lon(), node.lat()))
radius = dist_sum / divider
surface = ""
if "surface" in element.tags():
surface = element.tags()["surface"]
lit = None
if "lit" in element.tags():
lit = element.tags()["lit"] == "yes"
id = f"H{counter}"
counter += 1
osmhelipads.append({"coord": c, "radius": radius, "surface": surface, "id": id, "lit": None})
with_lon_lat = []
without_lon_lat = []
while len(airport["helipads"]):
helipad = airport["helipads"].pop()
if helipad["le_longitude_deg"] and helipad["le_latitude_deg"]:
with_lon_lat.append(helipad)
else:
without_lon_lat.append(helipad)
for helipad in with_lon_lat:
if len(osmhelipads) > 0:
for osmhelipad in osmhelipads:
osmhelipad["distance"] = osmhelipad["coord"].distance_m(coord.Coord(float(helipad["le_longitude_deg"]),
float(helipad["le_latitude_deg"])))
osmhelipads.sort(key=lambda d: d["distance"])
helipad["osmhelipad"] = osmhelipads.pop(0)
else:
helipad["osmhelipad"] = {}
for osmhelipad in osmhelipads:
osmhelipad["distance"] = osmhelipad["coord"].distance_m(coord.Coord(airport["airport"].lon, airport["airport"].lat))
osmhelipads.sort(key=lambda d: d["distance"])
for i, helipad in enumerate(without_lon_lat):
if len(osmhelipads) > 0:
helipad["osmhelipad"] = osmhelipads.pop(0)
helipad["le_longitude_deg"] = helipad["osmhelipad"]["coord"].lon
helipad["le_latitude_deg"] = helipad["osmhelipad"]["coord"].lat
else:
without_lon_lat[i] = None
if None in without_lon_lat:
print(f"No position information found for {without_lon_lat.count(None)} helipad(s) at {airport['airport'].icao} - removing", end=" " * 100 + "\n")
without_lon_lat = list(filter(None, without_lon_lat))
airport["helipads"] = with_lon_lat + without_lon_lat
for osmhelipad in osmhelipads:
helipad = {"airport_ident": airport["airport"].icao, "le_longitude_deg": osmhelipad["coord"].lon,
"le_latitude_deg": osmhelipad["coord"].lat, "lighted": osmhelipad["lit"], "surface": osmhelipad["surface"],
"length_ft": 0, "width_ft": 0, "osmhelipad": osmhelipad}
def add_ourairports_runways(airports):
csv = _get_ourairports_csv("runways")
i = 0
total = len(airports)
for airport in airports:
print(f"Extracting runways from OurAirports data … {i / total * 100:.1f}% ({i} of {total} airports done)", end="\r")
i += 1
runways = []
helipads = []
for line in csv:
if line["airport_ident"] == airport["ident"]:
if re.match(line["le_ident"], "H[0-9]*"):
helipads.append(line)
else:
runways.append(line)
else:
if runways or helipads:
break
airport["runways"] = runways
airport["helipads"] = helipads
add_osm_runways(airport)
add_osm_helipads(airport)
if len(airport["runways"]) == 0 and len(airport["helipads"]) == 0:
print(f"Removing airport {airport['airport'].icao} since it has no runways / helipads !", end=" " * 100 + "\n")
airports[i - 1] = None
continue
for runway in airport["runways"]:
surface = parse_surface_type(runway["surface"].lower())
if surface == "Unknown":
if "osmway" in runway:
if "surface" in runway["osmway"]["way"].tags():
surface = parse_surface_type(runway["osmway"]["way"].tags()["surface"])
if surface == "Unknown":
if (int(runway["length_ft"] or 0) > 1500 and int(runway["width_ft"] or 0) > 30) or int(runway["lighted"]):
surface = "Asphalt"
else:
surface = "Dirt"
print("Unknown surface type:", runway["surface"], "for runway", runway["le_ident"], "at airport", airport["airport"].icao, "- falling back to", surface, end=" " * 100 + "\n")
runway["surface"] = surface
print(f"Extracting runways from OurAirports data … {i / total * 100:.1f}% ({i} of {total} airports done)", end="\r\n")
airports = list(filter(None, airports))
i = 0
total = len(airports)
for airport in airports:
print(f"Creating runways … {i / total * 100}% ({i} of {total} airports done)", end="\r")
i += 1
for runway in airport["runways"]:
width = runway["width_ft"]
if width != "":
width = float(width)
elif "osmway" in runway and "width" in runway["osmway"]["way"].tags():
width = round(float(runway["osmway"]["way"].tags()["width"]), 2)
else:
print(f"No width found for runway {runway['le_ident']} at airport {airport['airport'].icao} - guessing from length", end=" " * 100 + "\n")
width = math.sqrt(int(runway["length_ft"] or 0))
if runway["surface"] == "water":
runway = aptdat.WaterRunway(unit_convert.ft2m(width),
runway["le_ident"], float(runway["le_longitude_deg"]), float(runway["le_latitude_deg"]),
runway["he_ident"], float(runway["he_longitude_deg"]), float(runway["he_latitude_deg"]),
perimeter_buoys=True)
else:
center_lights = edge_lights = bool(runway["lighted"])
if center_lights and surface not in ("Asphalt", "Concrete"):
center_lights = edge_lights = False
distance_signs = int(runway["length_ft"] or 0) > 4000
tdz_lights = runway["surface"] in ("Asphalt", "Concrete")
markings = aptdat.RunwayMarkingCode.Visual
if runway["surface"] not in ("Asphalt", "Concrete"):
markings = aptdat.RunwayMarkingCode.NoMarkings
elif 4000 < int(runway["length_ft"] or 0) < 6000:
markings = aptdat.RunwayMarkingCode.NonPrecision
elif int(runway["length_ft"] or 0) >= 6000:
markings = aptdat.RunwayMarkingCode.Precision
reil_type = aptdat.REILCode.NoREIL
if markings == aptdat.RunwayMarkingCode.NonPrecision:
reil_type = aptdat.REILCode.UnidirREIL
runway = aptdat.LandRunway(unit_convert.ft2m(width), getattr(aptdat.SurfaceCode, runway["surface"]),
runway["le_ident"], float(runway["le_longitude_deg"]), float(runway["le_latitude_deg"]),
runway["he_ident"], float(runway["he_longitude_deg"]), float(runway["he_latitude_deg"]),
center_lights=center_lights, edge_lights=edge_lights, distance_signs=distance_signs,
displ_thresh1=float(runway["le_displaced_threshold_ft"] or 0), tdz_lights1=tdz_lights,
markings1=markings, reil_type1=reil_type,
displ_thresh2=float(runway["he_displaced_threshold_ft"] or 0), tdz_lights2=tdz_lights,
markings2=markings, reil_type2=reil_type)
airport["airport"].add_runway(runway)
print(f"Creating runways … {i / total * 100}% ({i} of {total} airports done)")
i = 0
for airport in airports:
print("Creating helipads … {i / total * 100}% ({i} of {total} airports done)", end="\r")
i += 1
for helipad in airport["helipads"]:
if helipad["width_ft"]:
width = round(float(helipad["width_ft"]), 2)
elif "radius" in helipad["osmhelipad"]:
width = radius * 2
else:
width = 50
print((f"Unable to get width for for helipad {helipad['le_ident']} at airport {airport['airport'].icao}" +
f" - setting to {width} ft"), end=" " * 100 + "\n")
if helipad["length_ft"]:
length = round(float(helipad["length_ft"]), 2)
elif "radius" in helipad["osmhelipad"]:
length = radius * 2
else:
length = 50
print((f"Unable to get length for for helipad {helipad['le_ident']} at airport {airport['airport'].icao}" +
f" - setting to {length} ft"), end=" " * 100 + "\n")
lighted = bool(int(helipad["lighted"]))
surface = parse_surface_type(helipad["surface"])
if surface == "Unknown" and "surface" in helipad["osmhelipad"]:
surface = parse_surface_type(helipad["osmhelipad"]["surface"])
if surface == "Unknown":
if lighted:
surface = "Asphalt"
else:
surface = "Grass"
print((f"Unknown surface type {helipad['surface']} for helipad {helipad['le_ident']} at" +
f" airport {airport['airport'].icao} - setting to {surface}"), end=" " * 100 + "\n")
if surface not in ("Concrete", "Asphalt"):
lighted = False
print(helipad)
helipad = aptdat.Helipad(helipad["id"], float(helipad["le_longitude_deg"]), float(helipad["le_latitude_deg"]), 0,
unit_convert.ft2m(length), unit_convert.ft2m(width), surface, edge_lights=lighted)
airport["airport"].add_helipad(helipad)
airports[i - 1] = airport["airport"]
return airports
def query_airports_by_icaos(icaos):
# remove doubles
icaos = list(set(icaos))
ourairports = get_ourairports_airports(icaos=icaos)
ourairports = add_ourairports_runways(ourairports)
return ourairports
def query_airports_by_bbox(left, lower, right, upper):
ourairports = get_ourairports_airports(bbox=rectangle.Rectangle(coord.Coord(left, lower), coord.Coord(right, upper)))
ourairports = add_ourairports_runways(ourairports)
return ourairports
def check_aptdat_written_by_this(path):
with open(path, "r") as f:
i = 0
while i < 2:
fl = f.readline()
if fl:
i += 1
if "osm2aptdat.py" in fl:
return True
return False
def write_aptdat_files(output, airports, merge=False):
writer = aptdat.ReaderWriterAptDat(file_header="Generated from OSM and OurAirports data by fgtools.osm2aptdat.py")
writer.add_airports(airports)
writer.write(output, merge=merge, overwrite_func=check_aptdat_written_by_this)
if __name__ == "__main__":
logging.getLogger("OSMPythonTools").setLevel(logging.FATAL)
argp = argparse.ArgumentParser(description="query airports from OSM and convert the results to apt.dat files")
bbox_icao_group = argp.add_mutually_exclusive_group(required=True)
bbox_icao_group.add_argument(
"-b", "--bbox",
help="GPS coordinates of the lower left and upper right corners of the bounding box within which all airports should be processed",
nargs=4,
metavar=("LL_LON", "LL_LAT", "UR_LON", "UR_LAT"),
type=float,
)
bbox_icao_group.add_argument(
"-i", "--icao",
help="ICAO code(s) of the airport(s) to process",
nargs="+"
)
argp.add_argument(
"-m", "--merge",
help="Merge all airports into one big apt.dat file instead of writing one file for each airport",
action="store_true"
)
argp.add_argument(
"-o", "--output",
help="directory to put apt.dat files into",
required=True
)
args = argp.parse_args()
if args.icao:
airports = query_airports_by_icaos(args.icao)
else:
airports = query_airports_by_bbox(left=args.bbox[0], lower=args.bbox[1], right=args.bbox[2], upper=args.bbox[3])
write_aptdat_files(args.output, airports, merge=args.merge)