# Copyright (C) 2017 Apple Inc. All rights reserved. # Copyright (C) 2016-2017 Michael Saboff. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # This parses the legacy FAA NFDC text files APT.txt, AWY.txt, NAV.txt # and FIX.txt into a JavaScript format that he Flight Planner can use. # These text files can be downloaded from the FAA at # https://nfdc.faa.gov/xwiki/bin/view/NFDC/28+Day+NASR+Subscription # This script generates two JavaScript object literal files, one for # waypoints and the other for airways. Both files create maps, where # the key is the waypoint or airway identifier and the value is the # corresponding data. # # Run this file in a directory where APT.txt, AWY.txt, NAV.txt and FIX.txt # are downloaded. import re latLongRE = re.compile(r'\s*(\d+)-(\d{2})-(\d{2}.\d{3,8})([NS])\s*(\d+)-(\d{2})-(\d{2}.\d{3,8})([EW])') statesAbbreviationsToExclude = ['AK', 'HI'] navaidsToInclude = ['VOR', 'NDB'] airwayFixTypes = frozenset(['CN', 'NDB/DME', 'NDB', 'MIL-REP-PT', 'REP-PT', 'RNAV', 'VOR', 'VOR/DME', 'VORTAC']) airwayTypes = frozenset(['J', 'T', 'Q', 'V']) class WaypointData: def __init__(self): self.waypoints = [] self.allNames = set() def parseLatLongDMS(self, latLongString): match = latLongRE.match(latLongString) latitude = float(match.group(1)) + (float(match.group(2)) * 60 + float(match.group(3))) / 3600 if match.group(4) == 'S': latitude = -latitude longitude = float(match.group(5)) + (float(match.group(6)) * 60 + float(match.group(7))) / 3600 if match.group(8) == 'W': longitude = -longitude return (latitude, longitude) def parseAirportData(self, airportFile): file = open(airportFile, 'r') for line in file: if line[0:3] != 'APT': continue if line[48:50] in statesAbbreviationsToExclude: continue facilityType = line[14:27].rstrip().title() latLong = self.parseLatLongDMS(line[523:538] + line[550:565]) description = line[133:183].rstrip().title() + ' ' + facilityType + ', ' + line[93:133].rstrip().title() + ', ' + line[48:50] description = description.replace('"', '\\"'); name = line[1210:1217].rstrip() if not len(name): name = line[27:31].rstrip() if name in self.allNames: print('Duplicate airport waypoint name {0}'.format(name)) continue self.allNames.add(name) self.waypoints.append((name, facilityType, description, latLong[0], latLong[1])) file.close() def parseNavAidData(self, navaidFile): file = open(navaidFile, 'r') self.ndbs = [] for line in file: if line[0:4] != 'NAV1': continue if line[144:147] == 'AIN': continue if not line[8:11] in navaidsToInclude: continue facilityType = line[8:28].rstrip() latLong = self.parseLatLongDMS(line[371:385] + line[396:410]) name = line[4:8].rstrip() description = line[42:72].rstrip().title() + ' ' + line[8:28].rstrip() # Since NDBs can have the same name as a VOR, we keep track of them separately # and add NDBs below if the NDB has a unique name. if line[8:11] == 'NDB': self.ndbs.append((name, facilityType, description, latLong[0], latLong[1])) else: self.allNames.add(name) self.waypoints.append((name, facilityType, description, latLong[0], latLong[1])) for ndb in self.ndbs: if ndb[0] not in self.allNames: self.allNames.add(ndb[0]) self.waypoints.append(ndb) file.close() def parseFixData(self, fixFile): file = open(fixFile, 'r') for line in file: if line[0:4] != 'FIX1': continue if line[4] < 'A' or line[4] > 'Z': continue facilityType = 'Intersection' latLong = self.parseLatLongDMS(line[66:80] + line[80:94]) name = line[4:34].rstrip() description = name + ' Intersection' if name in self.allNames: print('Duplicate fix name {0}'.format(name)) continue self.allNames.add(name) self.waypoints.append((name, facilityType, description, latLong[0], latLong[1], '')) file.close() def outputWaypointFile(self, outputFilename): sortedWaypoints = sorted(self.waypoints, key=lambda waypoint: waypoint[0]) outputFile = open(outputFilename, 'w+') outputFile.write('var _faaWaypoints = {\n'); isFirst = True for waypoint in sortedWaypoints: if isFirst: isFirst = False else: outputFile.write(',\n'); outputFile.write(' "{0}":{{ "name":"{0}", "type":"{1}", "description":"{2}", "latitude":{3}, "longitude":{4}}}'.format(waypoint[0], waypoint[1], waypoint[2], waypoint[3], waypoint[4])) outputFile.write('\n};\n'); class AirwayData: def __init__(self): self.airways = [] def parseAirwayData(self, airwayFile): file = open(airwayFile, 'r') self.currentAirway = '' self.airwayPoints = [] for line in file: airway = line[4:8].rstrip() if airway != self.currentAirway: if self.currentAirway != '': self.airways.append((self.currentAirway, self.airwayPoints)) self.currentAirway = '' self.airwayPoints = [] if line[0:4] != 'AWY2': continue if line[4] not in airwayTypes: continue if line[9] != ' ': continue if line[45:64].rstrip() not in airwayFixTypes: continue if self.currentAirway == '': self.currentAirway = airway if line[64:79].rstrip() == 'FIX': self.airwayPoints.append(line[15:45].rstrip()) else: self.airwayPoints.append(line[116:120].rstrip()) if self.currentAirway != '': self.airways.append((self.currentAirway, self.airwayPoints)) file.close() def outputAirwayFile(self, outputFilename): sortedAirways = sorted(self.airways, key=lambda airways: airways[0]) outputFile = open(outputFilename, 'w+') outputFile.write('var _faaAirways = {\n'); isFirst = True for airway in sortedAirways: if isFirst: isFirst = False else: outputFile.write(',\n'); outputFile.write(' "{0}":{{ "name":"{0}", "fixes":['.format(airway[0])) isFirstFix = True for fix in airway[1]: if isFirstFix: isFirstFix = False else: outputFile.write(', '); outputFile.write('"{0}"'.format(fix)) outputFile.write(']}') outputFile.write('\n};\n'); def main(): waypoints = WaypointData() waypoints.parseAirportData('APT.txt') waypoints.parseNavAidData('NAV.txt') waypoints.parseFixData('FIX.txt') waypoints.outputWaypointFile('waypoints.js') airways = AirwayData() airways.parseAirwayData('AWY.txt') airways.outputAirwayFile('airways.js') if __name__ == "__main__": main()