Coverage for mapology/icao.py: 19.57%
394 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-04 20:27:38 +00:00
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-04 20:27:38 +00:00
1#! /usr/bin/env python
2"""Transform the R airport script data portion into a leaflet geojson file.
3Constructing the City hints:
4$ grep airport_name ????/index.json | tr "a-z" "A-Z" | \
5 sed "s/$ESC$/INDEX$ESC$.JSON://g; s/AIRPORT_NAME//g;" | tr -d " " | tr -s '"' | sed s/^/'"'/g
6Valid Google Maps query GET URLs (official with map and pin - but no satellite):
7https://www.google.com/maps/search/?api=1&query={lat}%2c{lon}
8Old unofficial but as of 2020-11-04 still working satellite and pin:
9https://maps.google.com/maps?t=k&q=loc:{lat}+{lon}
11Create index entries like:
12[
13 {
14 "title": "LOXT (Tulln-Langenlebarn, Austria)",
15 "url": "./loxt/",
16 "body": "Airport LOXT (Tulln-Langenlebarn, Austria), LOX, LO, L, AUT, EUR"
17 },
18...
19]
20"""
21import collections
22import copy
23import datetime as dti
24import functools
25import json
26import os
27import pathlib
28import sys
29from typing import Any, Callable, Collection, Dict, Iterator, List, Mapping, Optional, Tuple, Union, no_type_check
31import mapology.country as cc
32import mapology.db as db
33import mapology.template_loader as template
34from mapology import BASE_URL, DEBUG, ENCODING, FOOTER_HTML, FS_PREFIX_PATH, LIB_PATH, PATH_NAV, country_blurb, log
36FeatureDict = Dict[str, Collection[str]]
37PHeaderDict = Dict[str, Collection[str]]
38PFeatureDict = Dict[str, Collection[str]]
40THIS_YY_INT = int(dti.datetime.now(dti.UTC).strftime('%y'))
42HTML_TEMPLATE = os.getenv('GEO_PAGE_HTML_TEMPLATE', '')
43HTML_TEMPLATE_IS_EXTERNAL = bool(HTML_TEMPLATE)
44if not HTML_TEMPLATE: 44 ↛ 47line 44 didn't jump to line 47, because the condition on line 44 was never false
45 HTML_TEMPLATE = 'airport_page_template.html'
47REC_SEP = ','
48STDIN_TOKEN = '-' # nosec B105
49TRIGGER_START_OF_DATA = "csv <- 'marker_label,lat,lon"
50TRIGGER_END_OF_DATA = "'"
52AIRP = 'airport'
53RUNW = 'runways'
54FREQ = 'frequencies'
55LOCA = 'localizers'
56GLID = 'glideslopes'
58CC_HINT = 'CC_HINT'
59City = 'City'
60CITY = City.upper()
61ICAO = 'ICAO'
62IC_PREFIX = 'IC_PREFIX'
63IC_PREFIX_ICAO = f'{IC_PREFIX}_{ICAO}'
64ITEM = 'ITEM'
65KIND = 'KIND'
66PATH = '/PATH'
67BASE_URL_TARGET = 'BASE_URL'
68ANCHOR = 'ANCHOR'
69TEXT = 'TEXT'
70URL = 'URL'
71ZOOM = 'ZOOM'
72DEFAULT_ZOOM = 16
73FOOTER_HTML_KEY = 'FOOTER_HTML'
74LIB_PATH_KEY = 'LIB_PATH'
76icao = 'icao_lower'
77LAT_LON = 'LAT_LON'
78cc_page = 'cc_page'
79Cc_page = 'Cc_page'
81ATTRIBUTION = f'{KIND} {ITEM} of '
83AIRPORT_NAME = {} # Example: {"GCFV": "FUERTEVENTURA",}
85Point = collections.namedtuple('Point', ['label', 'lat', 'lon'])
87# GOOGLE_MAPS_URL = f'https://www.google.com/maps/search/?api=1&query={{lat}}%2c{{lon}}' # Map + pin Documented
88GOOGLE_MAPS_URL = 'https://maps.google.com/maps?t=k&q=loc:{lat}+{lon}' # Sat + pin Undocumented
90APT_SEARCH_DATA = {
91 'title': '',
92 'url': '',
93 'body': '',
94}
96GEO_JSON_HEADER = {
97 'type': 'FeatureCollection',
98 'name': f'Airport - {ICAO} ({City}, {CC_HINT})',
99 'crs': {
100 'type': 'name',
101 'properties': {
102 'name': 'urn:ogc:def:crs:OGC:1.3:CRS84',
103 },
104 },
105 'features': [],
106}
107GEO_JSON_FEATURE: FeatureDict = {
108 'type': 'Feature',
109 'properties': {
110 'name': (
111 f"<a href='{URL}' class='nd' target='_blank'"
112 f" title='{KIND} {ITEM} of {ICAO}({CITY}, {CC_HINT})'>{TEXT}</a>"
113 ),
114 },
115 'geometry': {
116 'type': 'Point',
117 'coordinates': [], # Note: lon, lat
118 },
119}
121GEO_JSON_APT_FEATURE: FeatureDict = {
122 'type': 'Feature',
123 'properties': {
124 'name': f"<a href='{URL}' class='apnd' title='{KIND} {ITEM} of {ICAO}({CITY}, {CC_HINT})'>{TEXT}</a>",
125 },
126 'geometry': {
127 'type': 'Point',
128 'coordinates': [], # Note: lon, lat
129 },
130}
132GEO_JSON_PREFIX_FEATURE: PFeatureDict = {
133 'type': 'Feature',
134 'properties': {
135 'name': f"<a href='{URL}' class='apnd' title='{KIND} {ITEM} of {ICAO}({CITY}, {CC_HINT})'>{TEXT}</a>",
136 },
137 'geometry': {
138 'type': 'Point',
139 'coordinates': [], # Note: lon, lat
140 },
141}
143JSON_PREFIX_TABLE_ROW = {
144 'area_code': '',
145 'prefix': '',
146 'icao': '',
147 'latitude': '',
148 'longitude': '',
149 'elevation': '',
150 'updated': '',
151 'record_number': '',
152 'airport_name': '',
153}
156def icao_from_key_path(text: str) -> str:
157 """HACK A DID ACK"""
158 return text.rstrip('/:').rsplit('/', 1)[1]
161def derive_base_facts_path(folder: pathlib.Path, icao_identifier: str) -> pathlib.Path:
162 """DRY."""
163 return pathlib.Path(folder, f'airport-{icao_identifier.upper()}.json')
166def derive_geojson_in_path(folder: pathlib.Path, icao_identifier: str) -> pathlib.Path:
167 """DRY."""
168 return pathlib.Path(folder, f'airport-{icao_identifier.upper()}.geojson')
171def parse_cycle_date(cycle_date_code: str) -> Tuple[int, int]:
172 """Parse string encoded cycle date into full (YYYY, cycle number)- pair."""
173 dt, cy = int(cycle_date_code[:2]), int(cycle_date_code[2:])
174 dt = 1900 + dt if dt > THIS_YY_INT else 2000 + dt
175 return dt, cy
178def parse_int_or_empty(decimals: str) -> Union[int, None]:
179 """Parse string encoded decimal and yield integer or None."""
180 return None if not decimals.strip() else int(decimals.strip())
183@no_type_check
184def parse_base_facts(folder: pathlib.Path, icao_identifier: str) -> dict[str, Union[str, float]]:
185 """Some additional attributes for the airport from database parsing.
187 If available will populate the table of the page:
188 <th>Cust.Region</th><th>Prefix</th><th>ICAO</th><th>Latitude</th><th>Longitude</th><th>Elevation</th>
189 <th>Updated</th><th>Rec#</th><th>Airport Name</th>
191 and may correct the prefix semantics in the page for prefix to country mapping.
192 """
193 with open(derive_base_facts_path(folder, icao_identifier), 'rt', encoding=ENCODING) as raw_handle:
194 data = json.load(raw_handle)
195 conv = data['airport_converted']
196 raw = data['airport_raw']
197 return {
198 'airport_name': raw['airport_name'].strip(),
199 'customer_area_code': raw['customer_area_code'].strip().upper(), # 'USA'
200 'icao_code': raw['icao_code'].strip().upper(), # 'K2'
201 'icao_identifier': raw['icao_identifier'].strip().upper(), # '04CA'
202 'ifr_capability': raw['ifr_capability'].strip(), # 'N'
203 'longest_runway': raw['longest_runway'].strip(), # '080'
204 'longest_runway_surface_code': raw['longest_runway_surface_code'].strip(), # ' ' -> ''
205 'magnetic_true_indicator': raw['magnetic_true_indicator'].strip(), # 'M'
206 'magnetic_variation': raw['magnetic_variation'].strip(), # 'E0140'
207 'public_military_indicator': raw['public_military_indicator'].strip(), # 'P'
208 'recommended_navaid': raw['recommended_navaid'].strip(), # ' ' -> ''
209 'speed_limit': parse_int_or_empty(raw['speed_limit']), # in km/h
210 'speed_limit_altitude': raw['speed_limit_altitude'].strip(), # in feet for speed limit
211 'time_zone': raw['time_zone'].strip(), # 'U00' or similarly
212 'transition_level': parse_int_or_empty(raw['transition_level']), # in feet
213 'transitions_altitude': parse_int_or_empty(raw['transitions_altitude']), # in feet
214 'latitude': conv['latitude'], # signed degrees as float
215 'longitude': conv['longitude'], # signed degrees as float
216 'elevation': conv['elevation'], # meters above mean sea level as float
217 'record_type': raw['record_type'].strip().upper(),
218 'section_code': raw['section_code'].strip().upper(),
219 'subsection_code': raw['subsection_code'].strip().upper(),
220 'cycle_date': parse_cycle_date(raw['cycle_date']), # Code '1913' -> [2019, 13]
221 'file_record_number': int(raw['file_record_number'].strip()), # Five digits wrap around counter within db
222 }
225def read_stdin() -> Iterator[str]:
226 """A simple stdin line based reader (generator)."""
227 readline = sys.stdin.readline()
228 while readline:
229 yield readline
230 readline = sys.stdin.readline()
233def read_file(path: str) -> Iterator[str]:
234 """A simple file line based reader (generator)."""
235 with open(path, 'rt', encoding=ENCODING) as r_handle:
236 for line in r_handle:
237 if DEBUG:
238 log.debug(line.strip())
239 yield line.strip()
242def is_runway(label: str) -> bool:
243 """Detect if label is a runway label"""
244 return label.startswith('RW') and len(label) < 7
247def is_frequency(label: str) -> bool:
248 """Detect if label is a frequency label"""
249 return '_' in label and label.index('_') == 2 and len(label) == 10
252def maybe_localizer(label: str) -> bool:
253 """Detect if label is maybe a localizer label"""
254 return '_' not in label and len(label) < 7
257def maybe_glideslope(label: str) -> bool:
258 """Detect if label is maybe a glideslope label"""
259 return '_' in label and len(label) > 5 and any([label.endswith(t) for t in ('DME', 'ILS', 'TAC')])
262def parse(record: str, seen: Dict[str, bool], data: Dict[str, List[Point]]) -> bool:
263 """Parse the record in a context sensitive manner (seen) into data."""
265 def update(aspect: str, new_point: Point) -> bool:
266 """DRY."""
267 if aspect not in data:
268 data[aspect] = []
269 data[aspect].append(new_point)
270 seen[aspect] = True
271 # log.debbug(data[aspect][-1])
272 return True
274 try:
275 label, lat, lon = record.split(REC_SEP)
276 except ValueError as err:
277 log.warning('<<<%s>>>' % record)
278 log.error(err)
279 return False
281 point = Point(label, lat, lon)
282 if not seen[AIRP]:
283 return update(AIRP, point)
285 if is_frequency(label): # ARINC424 source may provide airports without runways but with frequencies
286 return update(FREQ, point)
288 if is_runway(label):
289 return update(RUNW, point)
291 if seen[RUNW] and maybe_localizer(label):
292 return update(LOCA, point)
293 if seen[RUNW] and maybe_glideslope(label):
294 return update(GLID, point)
296 return False
299def parse_data(reader: Callable[[], Iterator[str]]) -> Tuple[Dict[str, bool], Dict[str, List[Point]], List[str]]:
300 """Parse the R language level data and return the entries and categories seen."""
301 on = False # The data start is within the file - not at the beginning
302 seen = {k: False for k in (AIRP, RUNW, FREQ, LOCA, GLID)}
303 data: Dict[str, List[Point]] = {}
304 lines = []
305 for line in reader():
306 lines.append(line.strip())
307 # log.debug('Read: %s' % line.strip())
308 if on:
309 record = line.strip().strip(TRIGGER_END_OF_DATA)
310 found = parse(record, seen, data)
311 if not found:
312 log.warning('Unhandled ->>>>>>%s' % record)
313 if not on:
314 on = line.startswith(TRIGGER_START_OF_DATA)
315 else:
316 if line.strip().endswith(TRIGGER_END_OF_DATA):
317 break
318 return seen, data, lines
321@no_type_check
322def collect_glideslopes(feature_data: List[Point]) -> dict[tuple[Any, Any], dict[str, Optional[list[Any]]]]:
323 """DRY."""
324 glideslopes = {}
325 for triplet in feature_data:
326 label = triplet.label
327 lat_str = triplet.lat
328 lon_str = triplet.lon
329 pair = (lat_str, lon_str)
330 if pair not in glideslopes:
331 glideslopes[pair] = {'local_id': None, 'kinds': []}
332 if label[-4:] in ('_DME', '_ILS', '_TAC'):
333 glideslopes[pair]['local_id'] = label[:-4]
334 glideslopes[pair]['kinds'].append(label[-3:])
335 glideslopes[pair]['kinds'].sort()
337 return glideslopes
340def make_feature(
341 coord_stack: Dict[Tuple[str, str], int], feature_data: List[Point], kind: str, cc: str, icao: str, apn: str
342) -> List[FeatureDict]:
343 """DRY."""
344 glideslopes = collect_glideslopes(feature_data)
346 local_features = []
347 for triplet in feature_data:
348 feature = copy.deepcopy(GEO_JSON_FEATURE)
350 label = triplet.label
351 lat_str = triplet.lat
352 lon_str = triplet.lon
353 pair = (lat_str, lon_str)
354 if kind == 'Frequency':
355 name = None
356 elif kind == 'Glideslope':
357 if pair in glideslopes and glideslopes[pair]['local_id'] is not None:
358 the_first = f'{glideslopes[pair]["local_id"]}_{glideslopes[pair]["kinds"][0]}'
359 if label == the_first:
360 label_display = f'{glideslopes[pair]["local_id"]} ({", ".join(glideslopes[pair]["kinds"])})'
361 name = feature['properties']['name'] # type: ignore
362 name = name.replace(ICAO, icao).replace(KIND, kind)
363 name = name.replace(ITEM, label_display).replace(TEXT, label_display)
364 name = name.replace(CITY, apn)
365 name = name.replace(CC_HINT, cc)
366 name = (
367 '<small>'
368 + name.replace(URL, GOOGLE_MAPS_URL.format(lat=float(lat_str), lon=float(lon_str)))
369 + '</small>'
370 )
371 else:
372 name = None
373 else:
374 name = feature['properties']['name'] # type: ignore
375 name = name.replace(ICAO, icao).replace(KIND, kind)
376 name = name.replace(ITEM, label).replace(TEXT, label)
377 name = name.replace(CITY, apn)
378 name = name.replace(CC_HINT, cc)
379 name = name.replace(URL, GOOGLE_MAPS_URL.format(lat=float(lat_str), lon=float(lon_str)))
380 if label.endswith('_DME'):
381 name = f'{name}'
382 elif label.endswith('_ILS'):
383 name = f'<br>{name}'
384 else: # ... ends with _TAC
385 name = f'<br><br>{name}'
386 elif kind == 'Runway':
387 label_display = f'{label.replace("RW", "")}'
388 name = feature['properties']['name'] # type: ignore
389 name = name.replace(ICAO, icao).replace(KIND, kind)
390 name = name.replace(ITEM, label_display).replace(TEXT, label_display)
391 name = name.replace(CITY, apn)
392 name = name.replace(CC_HINT, cc)
393 name = name.replace(URL, GOOGLE_MAPS_URL.format(lat=float(lat_str), lon=float(lon_str)))
394 name = name.replace("class='nd'", "class='rwnd'")
395 elif kind == 'Localizer':
396 label_display = f'{label} (Loc)'
397 name = feature['properties']['name'] # type: ignore
398 name = name.replace(ICAO, icao).replace(KIND, kind)
399 name = name.replace(ITEM, label_display).replace(TEXT, label_display)
400 name = name.replace(CITY, apn)
401 name = name.replace(CC_HINT, cc)
402 name = (
403 '<small>'
404 + name.replace(URL, GOOGLE_MAPS_URL.format(lat=float(lat_str), lon=float(lon_str)))
405 + '</small>'
406 )
407 else:
408 name = feature['properties']['name'] # type: ignore
409 name = name.replace(ICAO, icao).replace(KIND, kind)
410 name = name.replace(ITEM, label).replace(TEXT, label)
411 name = name.replace(CITY, apn)
412 name = name.replace(CC_HINT, cc)
413 name = name.replace(URL, GOOGLE_MAPS_URL.format(lat=float(lat_str), lon=float(lon_str)))
415 feature['properties']['name'] = name # type: ignore
416 feature['geometry']['coordinates'].append(float(lon_str)) # type: ignore
417 feature['geometry']['coordinates'].append(float(lat_str)) # type: ignore
419 local_features.append(feature)
421 return local_features
424def make_airport(coord_stack: Dict[Tuple[str, str], int], point: Point, cc: str, icao: str, apn: str) -> FeatureDict:
425 """DRY."""
426 geojson = copy.deepcopy(GEO_JSON_HEADER)
427 name = geojson['name']
428 name = name.replace(ICAO, icao).replace(City, apn.title()) # type: ignore
429 name = name.replace(CC_HINT, cc) # type: ignore
430 geojson['name'] = name
432 airport = copy.deepcopy(GEO_JSON_APT_FEATURE)
433 name = airport['properties']['name'] # type: ignore
434 name = name.replace(ICAO, icao).replace(TEXT, icao).replace(ATTRIBUTION, '') # type: ignore
435 name = name.replace(CITY, apn.title()) # type: ignore
436 name = name.replace(URL, './') # type: ignore
437 name = name.replace(CC_HINT, cc) # type: ignore
439 pair = (str(point.lat), str(point.lon))
440 if pair in coord_stack:
441 coord_stack[pair] += 2
442 else:
443 coord_stack[pair] = 0
444 if coord_stack[pair]:
445 spread = '<br>' * coord_stack[pair]
446 name = f'{spread}{name}'
448 airport['properties']['name'] = name # type: ignore
449 airport['geometry']['coordinates'].append(float(point.lon)) # type: ignore
450 airport['geometry']['coordinates'].append(float(point.lat)) # type: ignore
452 geojson['features'] = [airport] # type: ignore
453 return geojson
456@no_type_check
457def make_table_row(facts):
458 row = copy.deepcopy(JSON_PREFIX_TABLE_ROW)
459 row['area_code'] = facts['customer_area_code']
460 row['prefix'] = facts['icao_code']
461 row['icao'] = facts['icao_identifier']
462 row['latitude'] = facts['latitude']
463 row['longitude'] = facts['longitude']
464 row['elevation'] = facts['elevation']
465 row['updated'] = f'{"/".join(str(f) for f in facts["cycle_date"])}'
466 row['record_number'] = facts['file_record_number']
467 row['airport_name'] = facts['airport_name']
468 return row
471def add_airport(point: Point, cc: str, icao: str, apn: str) -> PFeatureDict:
472 """DRY."""
473 airport = copy.deepcopy(GEO_JSON_PREFIX_FEATURE)
474 name = airport['properties']['name'] # type: ignore
475 name = name.replace(ICAO, icao).replace(TEXT, icao).replace(ATTRIBUTION, '')
476 name = name.replace(CITY, apn.title())
477 name = name.replace(URL, f'{icao}/')
478 name = name.replace(CC_HINT, cc)
479 airport['properties']['name'] = name # type: ignore
480 airport['geometry']['coordinates'].append(float(point.lon)) # type: ignore
481 airport['geometry']['coordinates'].append(float(point.lat)) # type: ignore
483 return airport
486def expand_tasks(text_path: str, path_sep: str, magic_token: str) -> List[str]:
487 """DRY."""
488 bootstrap = text_path.rstrip(path_sep) # ensure it does not end with a slash
489 # is now either where/ever/r or where/ever/r/CC or where/ever/r/CC/ICAO
490 full = bootstrap.endswith(magic_token.rstrip(path_sep))
491 cc_only = not full and path_sep not in bootstrap.rsplit(magic_token, 1)[1]
492 tasks = []
493 if full:
494 for path in pathlib.Path(bootstrap).iterdir():
495 if path.is_dir():
496 for sub_path in path.iterdir():
497 if sub_path.is_dir():
498 tasks.append(str(sub_path))
499 elif cc_only:
500 for path in pathlib.Path(bootstrap).iterdir():
501 if path.is_dir():
502 tasks.append(str(path))
503 else:
504 tasks.append(bootstrap)
506 return tasks
509def write_json_store(at: pathlib.Path, what: Mapping[str, object]) -> None:
510 """DRY."""
511 with open(at, 'wt', encoding=ENCODING) as handle:
512 json.dump(what, handle, indent=2)
515def main(argv: Union[List[str], None] = None) -> int:
516 """Drive the derivation."""
517 argv = sys.argv[1:] if argv is None else argv
518 if len(argv) != 1: 518 ↛ 522line 518 didn't jump to line 522, because the condition on line 518 was never false
519 print('usage: mapology icao base/r/[IC/[ICAO]]')
520 return 2
522 db.ensure_fs_tree()
523 store_index = db.load_index('store')
524 table_index = db.load_index('table')
525 apt_search_index = db.load_index('apt_search')
527 slash, magic = '/', '/r/'
528 tasks = expand_tasks(argv[0], slash, magic)
530 num_tasks = len(tasks)
531 many = num_tasks > 4200 # hundredfold magic
532 for current, task in enumerate(sorted(tasks), start=1):
533 booticao = task.rstrip(slash).rsplit(slash, 1)[1]
534 r_path = f'{task}/airport-with-runways-{booticao}.r'
535 r_file_name = pathlib.Path(r_path).name
536 g_folder = pathlib.Path(str(pathlib.Path(r_path).parent).replace('/r/', '/geojson/')) # HACK
537 s_folder = pathlib.Path(str(pathlib.Path(r_path).parent).replace('/r/', '/json/')) # HACK
539 reader = functools.partial(read_file, r_path)
540 seen, data, r_lines = parse_data(reader)
542 full_r_source = open(r_path, 'rt', encoding=ENCODING).read()
543 runway_count = 0
544 if data and AIRP in data:
545 triplet = data[AIRP][0]
546 root_icao, root_lat, root_lon = triplet.label.strip(), float(triplet.lat), float(triplet.lon)
547 facts = parse_base_facts(s_folder, root_icao)
548 s_name = facts['airport_name']
549 s_area_code = facts['customer_area_code']
550 s_prefix = facts['icao_code']
551 ic_prefix = s_prefix # HACK A DID ACK
553 message = f'processing {current :>5d}/{num_tasks} {ic_prefix}/{root_icao} --> ({s_name}) ...'
554 if not many or not current % 100 or current == num_tasks:
555 log.info(message)
557 s_identifier = facts['icao_identifier']
558 s_lat = facts['latitude']
559 s_lon = facts['longitude']
560 s_elev = facts['elevation']
561 s_updated = f'{"/".join(str(f) for f in facts["cycle_date"])}'
562 s_rec_num = facts['file_record_number']
563 geojson_path = derive_geojson_in_path(g_folder, s_identifier)
565 if s_identifier not in AIRPORT_NAME:
566 AIRPORT_NAME[s_identifier] = s_name
568 data_rows = [] # noqa
569 # monkey patching
570 # ensure cycles are state with two digits zero left padded
571 year, cyc = s_updated.split(slash)
572 s_updated_padded = f'{year}/{int(cyc) :02d}'
573 # Make the ICAO cell entry a link to the google page
574 href = GOOGLE_MAPS_URL.format(lat=root_lat, lon=root_lon)
575 s_identifier_link = f'<a href="{href}" class="nd" target="_blank" title="{s_name}">{s_identifier}</a>'
576 data_rows.append(
577 f'<tr><td>{s_area_code}</td><td>{s_prefix}</td><td>{s_identifier_link}</td>'
578 f'<td class="ra">{round(s_lat, 3) :7.03f}</td><td class="ra">{round(s_lon, 3) :7.03f}</td>'
579 f'<td class="ra">{round(s_elev, 3) :7.03f}</td>'
580 f'<td class="la">{s_updated_padded}</td><td class="ra">{s_rec_num}</td>'
581 f'<td class="la">{s_name}</td></tr>'
582 )
584 cc_hint = cc.FROM_ICAO_PREFIX.get(facts.get('icao_code', 'ZZ'), 'No Country Entry Present')
585 my_prefix_path = f'/prefix/{ic_prefix}/{root_icao}/'
587 markers = cc_hint, root_icao, s_name
589 coord_stack: Dict[Tuple[str, str], int] = {}
590 geojson = make_airport(coord_stack, triplet, *markers)
592 if RUNW in data:
593 geojson['features'].extend(make_feature(coord_stack, data[RUNW], 'Runway', *markers)) # type: ignore
594 runway_count = len(data[RUNW]) # HACK A DID ACK for zoom heuristics
596 if FREQ in data:
597 geojson['features'].extend(make_feature(coord_stack, data[FREQ], 'Frequency', *markers)) # type: ignore
599 if LOCA in data:
600 geojson['features'].extend(make_feature(coord_stack, data[LOCA], 'Localizer', *markers)) # type: ignore
602 if GLID in data:
603 geojson['features'].extend( # type: ignore
604 make_feature(coord_stack, data[GLID], 'Glideslope', *markers)
605 )
607 # Process kinds' index and ensure kinds' prefix db is present
608 prefix_store = db.update_aspect(store_index, ic_prefix, cc_hint, 'store')
609 table_store = db.update_aspect(table_index, ic_prefix, cc_hint, 'table')
610 apt_search_index[root_icao] = str(db.DB_FOLDER_PATHS['apt_search'] / f'{root_icao}.json')
612 ic_airport_names = set(airp['properties']['name'] for airp in prefix_store['features']) # noqa
613 ic_airport = add_airport(triplet, *markers)
614 if ic_airport['properties']['name'] not in ic_airport_names: # type: ignore
615 prefix_store['features'].append(ic_airport) # noqa
616 table_store['airports'].append(make_table_row(facts)) # noqa
618 prefix_root = pathlib.Path(FS_PREFIX_PATH)
619 map_folder = pathlib.Path(prefix_root, ic_prefix, root_icao)
620 map_folder.mkdir(parents=True, exist_ok=True)
621 write_json_store(geojson_path, geojson)
622 log.debug('Wrote geojson to %s' % str(geojson_path))
623 geojson_path = pathlib.Path(map_folder, f'{root_icao.lower()}-geo.json')
624 geo_json_name = geojson_path.name
625 write_json_store(geojson_path, geojson)
626 log.debug('Wrote geojson to %s' % str(geojson_path))
627 r_source_path = pathlib.Path(map_folder, f'airport-with-runways-{root_icao}.r')
628 with open(r_source_path, 'wt', encoding=ENCODING) as handle:
629 handle.write(full_r_source)
630 log.debug('Wrote R Source to %s' % str(r_source_path))
632 search_data = copy.deepcopy(APT_SEARCH_DATA)
633 search_data['title'] = f'{root_icao} ({AIRPORT_NAME[root_icao].title()})'
634 search_data['url'] = f'{BASE_URL}/{FS_PREFIX_PATH}/{ic_prefix}/{root_icao}/'
635 search_data['body'] = f'Airport {AIRPORT_NAME[root_icao]}, {cc_hint}, {root_icao}, {ic_prefix}'
636 with open(apt_search_index[root_icao], 'wt', encoding=ENCODING) as handle:
637 json.dump(search_data, handle, indent=2)
638 log.debug(str(search_data))
640 html_dict = {
641 f'{ANCHOR}/{IC_PREFIX_ICAO}': my_prefix_path,
642 f'{ANCHOR}/{IC_PREFIX}': f'prefix/{ic_prefix}/',
643 ICAO: root_icao,
644 icao: root_icao.lower(),
645 City: AIRPORT_NAME[root_icao].title(),
646 CITY: AIRPORT_NAME[root_icao],
647 CC_HINT: cc_hint,
648 cc_page: country_blurb(cc_hint),
649 Cc_page: country_blurb(cc_hint).title(),
650 LAT_LON: f'{root_lat},{root_lon}',
651 LIB_PATH_KEY: LIB_PATH,
652 PATH: PATH_NAV,
653 BASE_URL_TARGET: BASE_URL,
654 URL: GOOGLE_MAPS_URL.format(lat=root_lat, lon=root_lon),
655 ZOOM: str(max(DEFAULT_ZOOM - runway_count + 1, 9)),
656 'IrealCAO': ICAO,
657 'index.json': geo_json_name,
658 'index.r.txt': r_file_name,
659 'index.txt': f'airport-{root_icao}.json',
660 IC_PREFIX: ic_prefix,
661 FOOTER_HTML_KEY: FOOTER_HTML,
662 'DATA_ROWS': '\n'.join(data_rows) + '\n',
663 }
664 html_page = template.load_html(HTML_TEMPLATE, HTML_TEMPLATE_IS_EXTERNAL)
665 for key, replacement in html_dict.items():
666 html_page = html_page.replace(key, replacement)
668 html_path = pathlib.Path(map_folder, 'index.html')
669 with open(html_path, 'wt', encoding=ENCODING) as html_handle:
670 html_handle.write(html_page)
672 write_json_store(pathlib.Path(table_index[ic_prefix]), table_store)
673 write_json_store(pathlib.Path(store_index[ic_prefix]), prefix_store)
675 else:
676 log.warning('no airport found with R sources.')
678 db.dump_index('table', table_index)
679 db.dump_index('store', store_index)
680 db.dump_index('apt_search', apt_search_index)
682 return 0
685if __name__ == '__main__': 685 ↛ 686line 685 didn't jump to line 686, because the condition on line 685 was never true
686 sys.exit(main(sys.argv[1:]))