Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #4816 - Fix incorrect VOR/NDB Coordinates #4817

Merged
merged 12 commits into from
Jul 26, 2023
3 changes: 3 additions & 0 deletions .github/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Changes from release 2023/07 to 2023/08
1. Bug - Fixed incorrect VOR/NDB Coordinates - thanks to @TechieHelper (Alice Ford)

# Changes from release 2023/06 to 2023/07
1. AIRAC (2307) - Add Farnborough (EGLF) Apron B - Thanks to @rishab-alt
2. AIRAC (2307) - Updated Compton Abbas (EGHA) ARP - thanks to @stu612 (Stuart Duncan)
Expand Down
82 changes: 41 additions & 41 deletions Navaids/VOR_UK.txt
Original file line number Diff line number Diff line change
@@ -1,54 +1,54 @@
ADN 114.300 N057.18.37.620 W002.16.01.950 ; Aberdeen
BKY 116.250 N051.59.23.000 E000.03.43.000 ; Barkway
BEL 117.200 N054.39.40.120 W006.13.47.660 ; Belfast
BKY 116.250 N051.59.23.170 E000.03.42.870 ; Barkway (DME)
BEL 117.200 N054.39.40.270 W006.13.47.660 ; Belfast
BEN 113.950 N057.28.40.570 W007.21.55.080 ; Benbecula (DME)
BCL 108.100 N057.28.30.310 W007.22.13.160 ; Benbecula (DME)
BHD 112.050 N050.23.55.000 W003.29.37.000 ; Berry Head
BIG 115.100 N051.19.51.150 E000.02.05.320 ; Biggin
BNN 113.750 N051.43.34.000 W000.32.59.000 ; Bovingdon
BCN 117.450 N051.43.32.000 W003.15.47.000 ; Brecon (DME)
BPK 117.500 N051.44.59.000 W000.06.24.000 ; Brookmans Park
CLN 114.550 N051.50.55.000 E001.08.51.000 ; Clacton
CPT 114.350 N051.29.30.000 W001.13.11.000 ; Compton
DTY 116.400 N052.10.48.510 W001.06.49.640 ; Daventry
DCS 115.200 N054.43.19.000 W003.20.26.000 ; Dean Cross
DET 117.300 N051.18.14.000 E000.35.50.000 ; Detling
BHD 112.050 N050.23.54.980 W003.29.37.460 ; Berry Head
BIG 115.100 N051.19.51.150 E000.02.05.320 ; Biggin (DME)
BNN 113.750 N051.43.34.190 W000.32.59.100 ; Bovingdon (DME)
BCN 117.450 N051.43.31.890 W003.15.46.920 ; Brecon (DME)
BPK 117.500 N051.44.59.050 W000.06.24.250 ; Brookmans Park (DME)
CLN 114.550 N051.50.54.500 E001.08.51.320 ; Clacton
CPT 114.350 N051.29.29.660 W001.13.10.890 ; Compton
DTY 116.400 N052.10.48.510 W001.06.49.640 ; Daventry (DME)
DCS 115.200 N054.43.18.880 W003.20.26.300 ; Dean Cross (DME)
DET 117.300 N051.18.14.410 E000.35.50.190 ; Detling (DME)
DVR 114.950 N051.09.45.440 E001.21.32.720 ; Dover (DME)
GAM 112.800 N053.16.53.280 W000.56.49.790 ; Gamston (DME)
GOW 115.400 N055.52.13.810 W004.26.44.600 ; Glasgow (DME)
GWC 114.750 N050.51.18.790 W000.45.24.250 ; Goodwood (DME)
DUF 115.250 N054.41.00.600 W002.27.03.660 ; Great Dun Fell (DME)
DVR 114.950 N051.09.45.000 E001.21.33.000 ; Dover
GAM 112.800 N053.16.53.000 W000.56.50.000 ; Gamston
GOW 115.400 N055.52.13.810 W004.26.44.610 ; Glasgow
GWC 114.750 N050.51.18.790 W000.45.24.250 ; Goodwood
GUR 109.400 N049.26.14.000 W002.36.14.000 ; Guernsey
HON 113.650 N052.21.24.000 W001.39.49.000 ; Honiley
HON 113.650 N052.21.24.040 W001.39.49.410 ; Honiley
INS 109.200 N057.32.33.490 W004.02.27.990 ; Inverness
IOM 112.200 N054.04.00.720 W004.45.48.510 ; Isle of Man
IOM 112.200 N054.04.00.720 W004.45.48.510 ; Isle Of Man
IWL 110.150 N054.07.37.800 W003.15.52.620 ; Barrow (DME)
JSY 112.200 N049.13.16.000 W002.02.46.000 ; Jersey
LAM 115.600 N051.38.46.000 E000.09.06.000 ; Lambourne
LND 114.200 N050.08.11.000 W005.38.13.000 ; Land's End
LON 113.600 N051.29.14.000 W000.28.00.000 ; London
LYD 114.050 N050.59.59.000 E000.52.43.000 ; Lydd
JSY 112.200 N049.13.15.970 W002.02.46.150 ; Jersey
LAM 115.600 N051.38.45.690 E000.09.06.130 ; Lambourne (DME)
LND 114.200 N050.08.10.620 W005.38.12.960 ; Land's End
LON 113.600 N051.29.14.090 W000.27.59.540 ; London (DME)
LYD 114.050 N050.59.58.870 E000.52.43.180 ; Lydd (DME)
MAC 116.000 N055.25.48.080 W005.39.01.490 ; Machrihanish (DME)
MCT 113.550 N053.21.25.290 W002.15.44.240 ; Manchester
MAY 117.900 N051.01.02.000 E000.06.58.000 ; Mayfield
MID 114.000 N051.03.14.000 W000.37.30.000 ; Midhurst
NEW 114.250 N055.02.18.410 W001.41.54.130 ; Newcastle (DME)
OCK 115.300 N051.18.18.000 W000.26.50.000 ; Ockham
OTR 113.900 N053.41.54.000 W000.06.13.000 ; Ottringham
PTH 110.400 N056.26.33.000 W003.22.07.000 ; Perth
POL 112.100 N053.44.38.000 W002.06.12.000 ; Pole Hill
SAB 112.500 N055.54.27.000 W002.12.23.000 ; Saint Abbs
SFD 117.000 N050.45.38.000 E000.07.19.000 ; Seaford
SAM 113.350 N050.57.18.900 W001.20.42.200 ; Southampton
STN 115.100 N058.12.25.010 W006.10.58.970 ; Stornoway
STU 113.100 N051.59.41.000 W005.02.25.000 ; Strumble
MCT 113.550 N053.21.25.290 W002.15.44.240 ; Manchester (DME)
MAY 117.900 N051.01.01.860 E000.06.58.040 ; Mayfield (DME)
MID 114.000 N051.03.14.230 W000.37.30.010 ; Midhurst (DME)
NEW 114.250 N055.02.18.410 W001.41.54.140 ; Newcastle (DME)
OCK 115.300 N051.18.18.170 W000.26.49.860 ; Ockham (DME)
OTR 113.900 N053.41.53.570 W000.06.13.450 ; Ottringham
PTH 110.400 N056.26.32.630 W003.22.06.960 ; Perth
POL 112.100 N053.44.37.560 W002.06.11.980 ; Pole Hill
SAB 112.500 N055.54.27.040 W002.12.22.810 ; Saint Abbs
SFD 117.000 N050.45.38.490 E000.07.18.890 ; Seaford
SAM 113.350 N050.57.18.900 W001.20.42.200 ; Southampton (DME)
STN 115.100 N058.12.25.020 W006.10.58.970 ; Stornoway
STU 113.100 N051.59.40.880 W005.02.24.690 ; Strumble
SUM 117.350 N059.52.43.340 W001.17.11.490 ; Sumburgh
SWB 116.800 N052.47.52.830 W002.39.44.350 ; Shawbury
TLA 113.800 N055.29.57.000 W003.21.10.000 ; Talla
TLA 113.800 N055.29.56.920 W003.21.10.200 ; Talla
TIR 117.700 N056.29.35.570 W006.52.32.120 ; Tiree
TNT 115.700 N053.03.14.230 W001.40.11.890 ; Trent
TRN 117.500 N055.18.48.000 W004.47.02.000 ; Turnberry
WAL 114.100 N053.23.31.000 W003.08.04.000 ; Wallasey
TNT 115.700 N053.03.14.230 W001.40.11.890 ; Trent (DME)
TRN 117.500 N055.18.48.280 W004.47.01.890 ; Turnberry (DME)
WAL 114.100 N053.23.30.970 W003.08.04.060 ; Wallasey
WIK 113.600 N058.27.31.740 W003.06.01.340 ; Wick
WIN 115.750 N051.47.26.300 W002.03.00.770 ; Winstone (DME)

Expand All @@ -70,4 +70,4 @@ WAD 117.100 N053.09.55.290 W000.31.36.160 ; Waddington
WTN 113.200 N053.44.30.290 W002.53.06.560 ; Warton
WTZ 109.300 N052.07.18.960 E000.56.25.700 ; Wattisham
WIT 117.600 N052.36.28.460 W000.29.55.420 ; Wittering
VLN 111.000 N051.00.18.040 W002.38.19.460 ; Yeovilton
VLN 111.000 N051.00.18.040 W002.38.19.460 ; Yeovilton
Binary file added _data/Tools/resources/airac-dates.pdf
Binary file not shown.
65 changes: 65 additions & 0 deletions _data/Tools/src/api.py
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
44 changes: 44 additions & 0 deletions _data/Tools/src/runner.py
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()
72 changes: 72 additions & 0 deletions _data/Tools/src/util/airac.py
AliceFord marked this conversation as resolved.
Show resolved Hide resolved
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
AliceFord marked this conversation as resolved.
Show resolved Hide resolved

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
AliceFord marked this conversation as resolved.
Show resolved Hide resolved

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
AliceFord marked this conversation as resolved.
Show resolved Hide resolved
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
17 changes: 17 additions & 0 deletions _data/Tools/src/util/util.py
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
62 changes: 62 additions & 0 deletions _data/Tools/tests/test_airac.py
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()
Loading