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

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} 

10 

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 

30 

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 

35 

36FeatureDict = Dict[str, Collection[str]] 

37PHeaderDict = Dict[str, Collection[str]] 

38PFeatureDict = Dict[str, Collection[str]] 

39 

40THIS_YY_INT = int(dti.datetime.now(dti.UTC).strftime('%y')) 

41 

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' 

46 

47REC_SEP = ',' 

48STDIN_TOKEN = '-' # nosec B105 

49TRIGGER_START_OF_DATA = "csv <- 'marker_label,lat,lon" 

50TRIGGER_END_OF_DATA = "'" 

51 

52AIRP = 'airport' 

53RUNW = 'runways' 

54FREQ = 'frequencies' 

55LOCA = 'localizers' 

56GLID = 'glideslopes' 

57 

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' 

75 

76icao = 'icao_lower' 

77LAT_LON = 'LAT_LON' 

78cc_page = 'cc_page' 

79Cc_page = 'Cc_page' 

80 

81ATTRIBUTION = f'{KIND} {ITEM} of ' 

82 

83AIRPORT_NAME = {} # Example: {"GCFV": "FUERTEVENTURA",} 

84 

85Point = collections.namedtuple('Point', ['label', 'lat', 'lon']) 

86 

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 

89 

90APT_SEARCH_DATA = { 

91 'title': '', 

92 'url': '', 

93 'body': '', 

94} 

95 

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} 

120 

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} 

131 

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} 

142 

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} 

154 

155 

156def icao_from_key_path(text: str) -> str: 

157 """HACK A DID ACK""" 

158 return text.rstrip('/:').rsplit('/', 1)[1] 

159 

160 

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') 

164 

165 

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') 

169 

170 

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 

176 

177 

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()) 

181 

182 

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. 

186 

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> 

190 

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 } 

223 

224 

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() 

231 

232 

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() 

240 

241 

242def is_runway(label: str) -> bool: 

243 """Detect if label is a runway label""" 

244 return label.startswith('RW') and len(label) < 7 

245 

246 

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 

250 

251 

252def maybe_localizer(label: str) -> bool: 

253 """Detect if label is maybe a localizer label""" 

254 return '_' not in label and len(label) < 7 

255 

256 

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')]) 

260 

261 

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.""" 

264 

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 

273 

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 

280 

281 point = Point(label, lat, lon) 

282 if not seen[AIRP]: 

283 return update(AIRP, point) 

284 

285 if is_frequency(label): # ARINC424 source may provide airports without runways but with frequencies 

286 return update(FREQ, point) 

287 

288 if is_runway(label): 

289 return update(RUNW, point) 

290 

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) 

295 

296 return False 

297 

298 

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 

319 

320 

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() 

336 

337 return glideslopes 

338 

339 

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) 

345 

346 local_features = [] 

347 for triplet in feature_data: 

348 feature = copy.deepcopy(GEO_JSON_FEATURE) 

349 

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))) 

414 

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 

418 

419 local_features.append(feature) 

420 

421 return local_features 

422 

423 

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 

431 

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 

438 

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}' 

447 

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 

451 

452 geojson['features'] = [airport] # type: ignore 

453 return geojson 

454 

455 

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 

469 

470 

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 

482 

483 return airport 

484 

485 

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) 

505 

506 return tasks 

507 

508 

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) 

513 

514 

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 

521 

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') 

526 

527 slash, magic = '/', '/r/' 

528 tasks = expand_tasks(argv[0], slash, magic) 

529 

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 

538 

539 reader = functools.partial(read_file, r_path) 

540 seen, data, r_lines = parse_data(reader) 

541 

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 

552 

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) 

556 

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) 

564 

565 if s_identifier not in AIRPORT_NAME: 

566 AIRPORT_NAME[s_identifier] = s_name 

567 

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 ) 

583 

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}/' 

586 

587 markers = cc_hint, root_icao, s_name 

588 

589 coord_stack: Dict[Tuple[str, str], int] = {} 

590 geojson = make_airport(coord_stack, triplet, *markers) 

591 

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 

595 

596 if FREQ in data: 

597 geojson['features'].extend(make_feature(coord_stack, data[FREQ], 'Frequency', *markers)) # type: ignore 

598 

599 if LOCA in data: 

600 geojson['features'].extend(make_feature(coord_stack, data[LOCA], 'Localizer', *markers)) # type: ignore 

601 

602 if GLID in data: 

603 geojson['features'].extend( # type: ignore 

604 make_feature(coord_stack, data[GLID], 'Glideslope', *markers) 

605 ) 

606 

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') 

611 

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 

617 

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)) 

631 

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)) 

639 

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) 

667 

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) 

671 

672 write_json_store(pathlib.Path(table_index[ic_prefix]), table_store) 

673 write_json_store(pathlib.Path(store_index[ic_prefix]), prefix_store) 

674 

675 else: 

676 log.warning('no airport found with R sources.') 

677 

678 db.dump_index('table', table_index) 

679 db.dump_index('store', store_index) 

680 db.dump_index('apt_search', apt_search_index) 

681 

682 return 0 

683 

684 

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:]))