magnetic_declination_for_nakarte.py (3517B)
1 #!/usr/bin/env python3 2 # coding: utf-8 3 import argparse 4 import base64 5 import csv 6 import datetime 7 import json 8 import math 9 import re 10 from urllib.parse import urlencode 11 from urllib.request import urlopen 12 13 MIN_LAT = -58 14 MAX_LAT = 78 15 GRID_STEP = 2 16 OUTPUT_VALUE_SCALE = 2 17 18 19 def fetch_magnetic_declination_grid(date: datetime.date) -> bytes: 20 # API documentation: https://www.ngdc.noaa.gov/geomag/CalcSurveyFin.shtml 21 timeout = 30 22 url = "https://www.ngdc.noaa.gov/geomag-web/calculators/calculateIgrfgrid" 23 request_params = { 24 "key": "gFE5W", 25 "lat1": MIN_LAT, 26 "lat2": MAX_LAT, 27 "latStepSize": GRID_STEP, 28 "lon1": "-180", 29 "lon2": "180", 30 "lonStepSize": GRID_STEP, 31 "magneticComponent": "d", 32 "model": "WMM", 33 "startYear": date.year, 34 "startMonth": date.month, 35 "startDay": date.day, 36 "endYear": date.year, 37 "endMonth": date.month, 38 "endDay": date.day, 39 "resultFormat": "csv", 40 } 41 resp = urlopen(url + "?" + urlencode(request_params), timeout=timeout) 42 assert resp.code == 200 43 return resp.read() 44 45 46 def parse_declination_csv(data: bytes) -> dict[tuple[float, float], float]: 47 declinations = {} 48 csv_lines = [ 49 line for line in data.decode().splitlines() if not line.startswith("#") 50 ] 51 for row in csv.reader(csv_lines): 52 assert len(row) == 7 53 lat = float(row[1]) 54 lon = float(row[2]) 55 declination = float(row[4]) 56 declinations[(lat, lon)] = declination 57 return declinations 58 59 60 def get_model_name(data: bytes) -> str: 61 m = re.search(r"Magnetic Model:\s*([^(+]+)", data.decode()) 62 return m.group(1).strip() 63 64 65 def write_output( 66 filename: str, 67 declinations: dict[tuple[float, float] : float], 68 model_name: str, 69 model_date: datetime.date, 70 ) -> None: 71 value_offset = -math.floor(min(declinations.values())) 72 packed_values = bytes( 73 int(round(declinations[(lat, lon)] + value_offset) * OUTPUT_VALUE_SCALE) 74 for lat in range(MIN_LAT, MAX_LAT + 1, GRID_STEP) 75 for lon in range(-180, 180, GRID_STEP) 76 ) 77 78 data = { 79 "array": base64.standard_b64encode(packed_values).decode(), 80 "minLat": MIN_LAT, 81 "maxLat": MAX_LAT, 82 "step": GRID_STEP, 83 "rowsCount": (MAX_LAT - MIN_LAT) // GRID_STEP + 1, 84 "colsCount": 360 // GRID_STEP, 85 "valueScale": OUTPUT_VALUE_SCALE, 86 "valueOffset": value_offset, 87 "model": model_name, 88 "dateYMD": [model_date.year, model_date.month, model_date.day], 89 } 90 with open(filename, "w") as f: 91 json.dump(data, f, indent=2) 92 93 94 def main() -> None: 95 parser = argparse.ArgumentParser() 96 parser.add_argument("output", help="Output filename") 97 parser.add_argument( 98 "--date", 99 help="Date for calculating magnetic declination. Format is YYYY-MM-DD. Default current date", 100 ) 101 conf = parser.parse_args() 102 if conf.date is not None: 103 date = datetime.datetime.strptime(conf.date, "%Y-%m-%d") 104 else: 105 date = datetime.date.today() 106 107 csv_data = fetch_magnetic_declination_grid(date) 108 109 # store/load file for debugging 110 # with open('data.csv', 'wb') as f: 111 # f.write(csv_data) 112 # with open("data.csv", "rb") as f: 113 # csv_data = f.read() 114 115 declinations = parse_declination_csv(csv_data) 116 model_name = get_model_name(csv_data) 117 write_output(conf.output, declinations, model_name, date) 118 119 120 if __name__ == "__main__": 121 main()