-
Notifications
You must be signed in to change notification settings - Fork 125
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* VORs updated * Amend changelog * ENR4.1 parser * Tools README * Added AIRAC parser from @chssn * Update ENR4.1 parser Co-authored-by: Peter Mooney <[email protected]> * Update AIRAC class Co-authored-by: Peter Mooney <[email protected]> * Update DME naming * Completely update ENR4.1 parser - Update parser - Create backbone for further parsers - Add some testing (more to follow) * Update testing * Made some changes @chssn suggested --------- Co-authored-by: Alice <[email protected]> Co-authored-by: Peter Mooney <[email protected]>
- Loading branch information
1 parent
6ab0ef6
commit f050bf3
Showing
10 changed files
with
341 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
from src.util import airac, util | ||
|
||
import requests | ||
from bs4 import BeautifulSoup | ||
from loguru import logger | ||
import re | ||
|
||
class AipAPI: | ||
def __init__(self): | ||
self.airac = airac.Airac() | ||
self.cycle = self.airac.cycle() | ||
self.rootUrl = self.airac.url() | ||
|
||
def parseENR4_1(self) -> dict[str,list]: | ||
"""Parse the AIP ENR4.1 page | ||
Returns: | ||
dict[str,list]: 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 | ||
... | ||
} | ||
""" | ||
|
||
url = self.rootUrl + "EG-ENR-4.1-en-GB.html" | ||
text = requests.get(url).text | ||
soup = BeautifulSoup(text, "html.parser") | ||
|
||
# get table rows from heading | ||
|
||
ad22 = soup.find("div", attrs={"id": "ENR-4.1"}) | ||
rows = list(list(ad22.children)[1].children)[1].children | ||
|
||
outputs = {} | ||
|
||
for row in rows: | ||
name = list(list(list(row.children)[0].children)[1].children)[1].string | ||
name = util.capitalise(name) | ||
|
||
vorDmeNdb = str(list(list(row.children)[0].children)[3]) | ||
if re.search(r"NDB", vorDmeNdb): | ||
continue # skip NDBs | ||
elif re.search(r"DME", vorDmeNdb) and not re.search(r"VOR", vorDmeNdb): | ||
name += " (DME)" # add DME to the name if it's only a DME | ||
|
||
identifier = list(list(row.children)[1].children)[1].string | ||
|
||
freq = list(list(list(row.children)[2].children)[1].children)[1].string | ||
try: | ||
float(freq) | ||
except ValueError: | ||
if identifier == "LON": # LON's frequency isn't on the AIP | ||
freq = "113.600" | ||
else: | ||
freq = list(list(list(row.children)[2].children)[3].children)[1].string | ||
|
||
coordA = list(list(list(row.children)[4].children)[0].children)[1].string | ||
coordB = list(list(list(row.children)[4].children)[1].children)[1].string | ||
coords = util.ukCoordsToSectorFile(coordA, coordB) | ||
|
||
# logger.debug(f"{identifier} {freq} {' '.join(coords)} ; {name}") | ||
|
||
outputs[identifier] = [name, freq, coords] | ||
|
||
return outputs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import api | ||
|
||
import argparse | ||
|
||
class Runner: | ||
def __init__(self, args): | ||
self.args = args | ||
self.aipApi = api.AipAPI() | ||
|
||
def readCurrentData(self, page): | ||
with open(f"./../../{page}", "r") as f: | ||
return f.read().split("\n") | ||
|
||
def writeLines(self, page, data): | ||
with open(f"./../../{page}", "w") as f: | ||
f.write("\n".join(data)) | ||
|
||
def run(self): | ||
if self.args["page"] == "ENR4.1" or self.args["page"] == "all": # ENR4.1 | ||
# get current data | ||
currentData = self.readCurrentData("Navaids/VOR_UK.txt") | ||
|
||
# get new data | ||
newData = self.aipApi.parseENR4_1() | ||
|
||
# compare | ||
for i, line in enumerate(currentData): | ||
vorID = line.split(" ")[0] | ||
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]}" | ||
|
||
self.writeLines("Navaids/VOR_UK.txt", currentData) | ||
|
||
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"]) | ||
|
||
args = vars(parser.parse_args()) | ||
|
||
runner = Runner(args) | ||
|
||
runner.run() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
""" | ||
eAIP Parser | ||
Chris Parkinson (@chssn) | ||
""" | ||
|
||
# Standard Libraries | ||
import math | ||
from datetime import date, timedelta | ||
|
||
# Third Party Libraries | ||
from loguru import logger | ||
|
||
# Local Libraries | ||
|
||
class Airac: | ||
"""Class for general functions relating to AIRAC""" | ||
|
||
def __init__(self): | ||
# First AIRAC date following the last cycle length modification | ||
start_date = "2020-01-02" # 2001 | ||
self.base_date = date.fromisoformat(str(start_date)) | ||
# Length of one AIRAC cycle | ||
self.cycle_days = 28 | ||
|
||
self.cycles = -1 | ||
|
||
def initialise(self, date_in=0) -> int: | ||
"""Calculate the number of AIRAC cycles between any given date and the start date""" | ||
|
||
if date_in: | ||
input_date = date.fromisoformat(str(date_in)) | ||
else: | ||
input_date = date.today() | ||
|
||
# How many AIRAC cycles have occured since the start date | ||
diff_cycles = (input_date - self.base_date) / timedelta(days=1) | ||
# Round that number down to the nearest whole integer | ||
self.cycles = math.floor(diff_cycles / self.cycle_days) | ||
|
||
return self.cycles | ||
|
||
def cycle(self, next_cycle:bool=False) -> str: | ||
"""Return the date of the current AIRAC cycle""" | ||
|
||
if self.cycles == -1: # only initialise if not already done | ||
self.cycles = self.initialise() | ||
|
||
if next_cycle: | ||
number_of_days = (self.cycles + 1) * self.cycle_days | ||
else: | ||
number_of_days = self.cycles * self.cycle_days | ||
current_cycle = self.base_date + timedelta(days=number_of_days) | ||
logger.debug("Current AIRAC Cycle is: {}", current_cycle) | ||
|
||
return current_cycle | ||
|
||
def url(self, next_cycle:bool=False) -> str: | ||
"""Return a generated URL based on the AIRAC cycle start date""" | ||
|
||
base_url = "https://www.aurora.nats.co.uk/htmlAIP/Publications/" | ||
if next_cycle: | ||
# if the 'next_cycle' variable is passed, generate a URL for the next AIRAC cycle | ||
base_date = self.cycle(next_cycle=True) | ||
else: | ||
base_date = self.cycle() | ||
|
||
base_post_string = "-AIRAC/html/eAIP/" | ||
|
||
formatted_url = base_url + str(base_date) + base_post_string | ||
logger.debug(formatted_url) | ||
|
||
return formatted_url |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import re | ||
|
||
def capitalise(inp): | ||
# capitalise the first letter of each word in a string | ||
out = [] | ||
for word in inp.split(" "): | ||
out.append(word[0].upper() + word[1:].lower()) | ||
|
||
return " ".join(out) | ||
|
||
def ukCoordsToSectorFile(inA, inB): | ||
# convert from XXXXXX.XXN to NXXX.XX.XX.XXX etc | ||
if re.match(r"[0-9]{6}(?:\.[0-9]{2}|)[N|S]", inA) is None or re.match(r"[0-9]{7}(?:\.[0-9]{2}|)[E|W]", inB) is None: | ||
raise ValueError(f"Invalid coordinates provided: {inA}, {inB}") | ||
outA = inA[-1] + "0" + inA[:2] + "." + inA[2:4] + "." + inA[4:6] + "." + inA[7:9].ljust(3, '0') | ||
outB = inB[-1] + inB[:3] + "." + inB[3:5] + "." + inB[5:7] + "." + inB[8:10].ljust(3, '0') | ||
return (outA, outB) # probably should be a tuple |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import unittest | ||
from datetime import date | ||
from src.util import airac | ||
|
||
class TestAirac(unittest.TestCase): | ||
def setUp(self): | ||
self.airac = airac.Airac() | ||
|
||
def test_initialise(self): | ||
self.assertEqual(self.airac.initialise("2020-01-02"), 0) # 0 date | ||
self.assertEqual(self.airac.initialise("2023-07-12"), 45) # random date | ||
self.assertEqual(self.airac.initialise("2023-07-19"), 46) # random date | ||
self.assertEqual(self.airac.initialise("2023-08-09"), 46) # edge case | ||
self.assertEqual(self.airac.initialise("2023-08-10"), 47) # edge case | ||
self.assertEqual(self.airac.initialise("2028-12-20"), 116) # far edge case | ||
self.assertEqual(self.airac.initialise("2028-12-21"), 117) # far edge case | ||
|
||
def test_cycle(self): | ||
# next_cycle = False | ||
self.airac.initialise("2020-01-02") | ||
self.assertEqual(self.airac.cycle(), date(2020, 1, 2)) # 0 date | ||
self.airac.initialise("2021-05-16") | ||
self.assertEqual(self.airac.cycle(), date(2021, 4, 22)) # random date | ||
self.airac.initialise("2023-12-27") | ||
self.assertEqual(self.airac.cycle(), date(2023, 11, 30)) # edge case | ||
self.airac.initialise("2023-12-28") | ||
self.assertEqual(self.airac.cycle(), date(2023, 12, 28)) # edge case | ||
|
||
# next_cycle = True | ||
self.airac.initialise("2020-01-02") | ||
self.assertEqual(self.airac.cycle(next_cycle=True), date(2020, 1, 30)) # 0 date | ||
self.airac.initialise("2021-05-16") | ||
self.assertEqual(self.airac.cycle(next_cycle=True), date(2021, 5, 20)) # random date | ||
self.airac.initialise("2023-12-27") | ||
self.assertEqual(self.airac.cycle(next_cycle=True), date(2023, 12, 28)) # edge case | ||
self.airac.initialise("2023-12-28") | ||
self.assertEqual(self.airac.cycle(next_cycle=True), date(2024, 1, 25)) # edge case | ||
|
||
def test_url(self): | ||
# next_cycle = False | ||
self.airac.initialise("2020-01-02") | ||
self.assertEqual(self.airac.url(next_cycle=False), "https://www.aurora.nats.co.uk/htmlAIP/Publications/2020-01-02-AIRAC/html/eAIP/") # 0 date | ||
self.airac.initialise("2021-05-16") | ||
self.assertEqual(self.airac.url(next_cycle=False), "https://www.aurora.nats.co.uk/htmlAIP/Publications/2021-04-22-AIRAC/html/eAIP/") # random date | ||
self.airac.initialise("2023-12-27") | ||
self.assertEqual(self.airac.url(next_cycle=False), "https://www.aurora.nats.co.uk/htmlAIP/Publications/2023-11-30-AIRAC/html/eAIP/") # edge case | ||
self.airac.initialise("2023-12-28") | ||
self.assertEqual(self.airac.url(next_cycle=False), "https://www.aurora.nats.co.uk/htmlAIP/Publications/2023-12-28-AIRAC/html/eAIP/") # edge case | ||
|
||
# next_cycle = True | ||
self.airac.initialise("2020-01-02") | ||
self.assertEqual(self.airac.url(next_cycle=True), "https://www.aurora.nats.co.uk/htmlAIP/Publications/2020-01-30-AIRAC/html/eAIP/") # 0 date | ||
self.airac.initialise("2021-05-16") | ||
self.assertEqual(self.airac.url(next_cycle=True), "https://www.aurora.nats.co.uk/htmlAIP/Publications/2021-05-20-AIRAC/html/eAIP/") # random date | ||
self.airac.initialise("2023-12-27") | ||
self.assertEqual(self.airac.url(next_cycle=True), "https://www.aurora.nats.co.uk/htmlAIP/Publications/2023-12-28-AIRAC/html/eAIP/") # edge case | ||
self.airac.initialise("2023-12-28") | ||
self.assertEqual(self.airac.url(next_cycle=True), "https://www.aurora.nats.co.uk/htmlAIP/Publications/2024-01-25-AIRAC/html/eAIP/") # edge case | ||
|
||
|
||
if __name__ == '__main__': | ||
unittest.main() |
Oops, something went wrong.