Skip to content

Commit

Permalink
Fixes VATSIM-UK#4885 - Create UK AIP Parser Tooling (VATSIM-UK#4886)
Browse files Browse the repository at this point in the history
* Slightly change ENR4.1 API

- change api output to be a dictionary, better for expansion later on
- change runner to match
- change test to match

* ENR4.4 parser

* ENR4.4 testing

* ENR3.2

* oof this is all bad

* Fixed ENR3.1

* update to usable state

* fix nats being nats

* ATS Routes

---------

Co-authored-by: Peter Mooney <[email protected]>
  • Loading branch information
AliceFord and PLM1995 authored Apr 13, 2024
1 parent c888417 commit ab2a2a8
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 11 deletions.
3 changes: 3 additions & 0 deletions _data/Tools/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
src/__pycache__
src/util/__pycache__
tests/__pycache__
127 changes: 122 additions & 5 deletions _data/Tools/src/api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from src.util import airac, util
from util import airac, util

import requests
from bs4 import BeautifulSoup
Expand All @@ -11,13 +11,99 @@ def __init__(self):
self.cycle = self.airac.cycle()
self.rootUrl = self.airac.url()

def parseENR4_1(self) -> dict[str,list]:
def parseENR3_2(self) -> dict[str,dict]:
"""Parse the AIP ENR3.2 page
Returns:
dict[str,dict]: A dictionary containing the airway identifier and some information about it (see example below)
{
"L6": {"waypoints": [{"name": "DVR", "lowerlimit": 85, "upperlimit": 460}, {"name": "DET"}]},
...
}
"""
url = self.rootUrl + "EG-ENR-3.2-en-GB.html"
text = requests.get(url).text
soup = BeautifulSoup(text, "html.parser")

enr32 = soup.find("div", attrs={"id": "ENR-3.2"})
airways = list(enr32.children)[1:]

outputs = {}

for airway in airways:
if "AmdtInsertedAIRAC" in str(airway): # skip amendments
continue
tbody = list(airway.children)[2]
wpts = list(tbody.children)
routeTitleHTML = wpts[0]
airwayName = list(list(list(routeTitleHTML.children)[0].children)[0].children)[1].string

if airwayName == "N84": # badly formatted airway
continue

outputs[airwayName] = {"waypoints": []}

wpts.pop(0) # remove name

# deal with first wpt
wptName = list(list(wpts[0].children)[1].children)
if len(wptName) > 4: # VOR/DME
wptName = wptName[5].string
else: # FIX
wptName = wptName[1].string

wpts.pop(0)

outputs[airwayName]["waypoints"].append({"name": wptName})

# deal with rest of wpts

for i in range(0, len(wpts), 2): # pair waypoint names with their data
wptHTML = wpts[i + 1]
wptName = list(list(wptHTML.children)[1].children)
if len(wptName) > 4: # VOR/DME
wptName = wptName[5].string
else: # FIX
wptName = wptName[1].string
# print(wptName)

try:
wptDataHTML = wpts[i]

upperLowerBox = list(list(wptDataHTML.children)[3].children)[0]

if airwayName == "N22" and wptName == "BHD": # for some reason only this waypoint is in a different format :facepalm:
upperLimit = 245
lowerLimit = 85
else:
upperLimit = list(list(list(list(list(list(upperLowerBox.children)[0].children)[0].children)[0].children)[0].children)[0].children)[4].string
lowerLimit = list(list(list(list(list(list(upperLowerBox.children)[0].children)[0].children)[1].children)[0].children)[0].children)
if "FT" in lowerLimit[4].string:
lowerLimit = lowerLimit[1].string[:2]
else:
lowerLimit = lowerLimit[4].string

outputs[airwayName]["waypoints"].append({"name": wptName, "lowerlimit": int(lowerLimit), "upperlimit": int(upperLimit)})
except IndexError:
outputs[airwayName]["waypoints"].append({"name": wptName})
except AttributeError:
print(airwayName, wptName)
raise ValueError # NATS broke something if this gets run :(

# wptLowerLimit = list(list(wpt.children)[1].children)[0].string
# wptUpperLimit = list(list(wpt.children)[1].children)[2].string
# print(f"{wptName} {wptLowerLimit} {wptUpperLimit}")

return outputs


def parseENR4_1(self) -> dict[str,dict]:
"""Parse the AIP ENR4.1 page
Returns:
dict[str,list]: A dictionary containing the VOR identifier and some information about it (see example below)
dict[str,dict]: A dictionary containing the VOR identifier and some information about it (see example below)
{
"ADN": ["Aberdeen", "114.300", ("N057.18.37.620", "W002.16.01.950")], # name, frequency, coords
"ADN": {"name": "Aberdeen", "frequency": "114.300", "coordinates": ("N057.18.37.620", "W002.16.01.950")}, # name, frequency, coordinates
...
}
"""
Expand Down Expand Up @@ -60,6 +146,37 @@ def parseENR4_1(self) -> dict[str,list]:

# logger.debug(f"{identifier} {freq} {' '.join(coords)} ; {name}")

outputs[identifier] = [name, freq, coords]
outputs[identifier] = {"name": name, "frequency": freq, "coordinates": coords}

return outputs

def parseENR4_4(self) -> dict[str,dict]:
"""Parse the AIP ENR4.4 page
Returns:
dict[str,dict]: A dictionary containing the FIX identifier and some information about it (see example below)
{
"ABBEW": {"coordinates": ("N050.30.11.880", "W003.28.33.640")}, # coordinates
...
}
"""
url = self.rootUrl + "EG-ENR-4.4-en-GB.html"
text = requests.get(url).text
soup = BeautifulSoup(text, "html.parser")

# get table rows from heading

table = soup.find("table", attrs={"class": "ENR-table"})
rows = list(list(table.children)[1].children)

outputs = {}

for row in rows:
fixName = list(list(row.children)[0].children)[1].string
coordA = list(list(list(row.children)[1].children)[0].children)[1].string
coordB = list(list(list(row.children)[1].children)[1].children)[1].string
coords = util.ukCoordsToSectorFile(coordA, coordB)

outputs[fixName] = {"coordinates": coords}

return outputs
118 changes: 114 additions & 4 deletions _data/Tools/src/runner.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import api

import os
import argparse

import api

class Runner:
def __init__(self, args):
self.args = args
Expand Down Expand Up @@ -29,13 +30,122 @@ def run(self):
if vorID in newData.keys(): # only rewrite if the VOR/DME is in both the old data and the new data, otherwise existing data is kept
# if the VOR/DME is in both the old data and the new data, write the new data onto the old data (if the data is the same we still write, just no change will be visible because the written data is the same as the stored data)
dataAboutVORDME = newData[vorID]
currentData[i] = f"{vorID} {dataAboutVORDME[1]} {' '.join(dataAboutVORDME[2])} ; {dataAboutVORDME[0]}"
currentData[i] = f"{vorID} {dataAboutVORDME['frequency']} {' '.join(dataAboutVORDME['coordinates'])} ; {dataAboutVORDME['name']}"

self.writeLines("Navaids/VOR_UK.txt", currentData)

elif self.args["page"] == "ENR4.4" or self.args["page"] == "all":
currentData = self.readCurrentData("Navaids/FIXES_UK.txt")

newData = self.aipApi.parseENR4_4()

for i, line in enumerate(currentData):
fixID = line.split(" ")[0]
if fixID in newData.keys():
dataAboutFix = newData[fixID]
currentData[i] = f"{fixID} {' '.join(dataAboutFix['coordinates'])}"

self.writeLines("Navaids/FIXES_UK.txt", currentData)

elif self.args["page"] == "ENR3.2" or self.args["page"] == "all":
newData = self.aipApi.parseENR3_2()

lowerAirways = os.listdir("../../ATS Routes/RNAV/Lower")
upperAirways = os.listdir("../../ATS Routes/RNAV/Upper")

for airway in newData.keys():
prevLowerIndex = None
prevUpperIndex = None
firstLower = True
firstUpper = True
lowerLines = []
upperLines = []
for i, waypoint in enumerate(newData[airway]["waypoints"]):
try:
lowerLimit = waypoint["lowerlimit"]
except KeyError:
lowerLimit = 0

try:
upperLimit = waypoint["upperlimit"]
except KeyError:
upperLimit = 0

lb = False

if i == 0: # special logic for first wpt: only include in lower if next wpt is also in lower
if newData[airway]["waypoints"][1]["lowerlimit"] < 245:
lowerLines.append(waypoint["name"])
lb = True

if i == 0:
if newData[airway]["waypoints"][1]["upperlimit"] > 245:
upperLines.append(waypoint["name"])
continue

if lb:
continue

# two VERY annoying exceptions
if airway == "M40" and waypoint["name"] == "IDESI":
lowerLines.append("XXXXX")
lowerLines.append(waypoint["name"])
prevLowerIndex = i
elif airway == "L620" and waypoint["name"] == "CLN":
lowerLines.append("XXXXX")
lowerLines.append(waypoint["name"])
prevLowerIndex = i

elif lowerLimit < 245:
if firstLower:
lowerLines.append(waypoint["name"])
prevLowerIndex = i
firstLower = False
elif prevLowerIndex == i - 1:
lowerLines.append(waypoint["name"])
prevLowerIndex = i
else: # add in spacing line with a filler wpt of `XXXXX`
lowerLines.append("XXXXX")
lowerLines.append(waypoint["name"])
prevLowerIndex = i
if upperLimit > 245: # same logic as above
if firstUpper:
upperLines.append(waypoint["name"])
prevUpperIndex = i
firstUpper = False
elif prevUpperIndex == i - 1:
upperLines.append(waypoint["name"])
prevUpperIndex = i
else:
upperLines.append("XXXXX")
upperLines.append(waypoint["name"])
prevUpperIndex = i

# put into file format
lowerOutput = []
for i in range(len(lowerLines) - 1):
if lowerLines[i] == "XXXXX":
lowerOutput.append(";Non-contiguous")
elif lowerLines[i + 1] == "XXXXX":
pass
else:
lowerOutput.append(lowerLines[i].ljust(5, " ") + " " + lowerLines[i + 1].ljust(5, " "))

upperOutput = []
for i in range(len(upperLines) - 1):
if upperLines[i] == "XXXXX":
upperOutput.append(";Non-contiguous")
elif upperLines[i + 1] == "XXXXX":
pass
else:
upperOutput.append(upperLines[i].ljust(5, " ") + " " + upperLines[i + 1].ljust(5, " "))

print(airway, lowerOutput, upperOutput)


if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Parse parts of the UK eAIP, using our AIP API")
parser.add_argument("page", help="The part of the AIP to parse", choices=["all", "ENR4.1"])
parser.add_argument("page", help="The part of the AIP to parse", choices=["all", "ENR3.2", "ENR4.1", "ENR4.4"])

args = vars(parser.parse_args())

Expand Down
Loading

0 comments on commit ab2a2a8

Please sign in to comment.