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:
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user