import argparse
import hashlib
import os
import shutil
import sys
import urllib.request
import zipfile
URL = "https://www.ngdc.noaa.gov/geomag/data/EMAG2/EMAG2_V3_20170530.zip"
ZIP_NAME = "EMAG2_V3_20170530.zip"
ZIP_SHA256 = "502958066dec31d1d24b9d1b95d58e5ff630af14b02ba180b0a27321c39208c5"
OUT_DIR = "tools/emag2"
CHUNK = 1 << 20
def sha256_file(path: str) -> str:
h = hashlib.sha256()
with open(path, "rb") as f:
for block in iter(lambda: f.read(CHUNK), b""):
h.update(block)
return h.hexdigest()
def download(url: str, dest: str) -> None:
tmp = dest + ".part"
print(f"fetching {url} -> {dest}")
with urllib.request.urlopen(url) as resp, open(tmp, "wb") as f: shutil.copyfileobj(resp, f, CHUNK)
os.replace(tmp, dest)
def main() -> int:
ap = argparse.ArgumentParser(description="Crop a lat/lon box out of EMAG2v3.")
ap.add_argument("south", type=float)
ap.add_argument("north", type=float)
ap.add_argument("west", type=float, help="longitude, -180..180")
ap.add_argument("east", type=float, help="longitude, -180..180")
ap.add_argument("--zip", dest="zip_path", default=os.path.join(OUT_DIR, ZIP_NAME))
ap.add_argument("--out", dest="out_path", default=None)
ap.add_argument("--no-verify", action="store_true", help="skip the zip SHA-256 check")
a = ap.parse_args()
if not (a.south <= a.north and a.west <= a.east):
print("need south <= north and west <= east", file=sys.stderr)
return 2
west, east = a.west % 360.0, a.east % 360.0
if a.east - a.west >= 360.0 or west > east:
print("the box may not cross the 0/360 meridian", file=sys.stderr)
return 2
os.makedirs(os.path.dirname(a.zip_path) or ".", exist_ok=True)
if not os.path.exists(a.zip_path):
download(URL, a.zip_path)
zip_sha = sha256_file(a.zip_path)
if not a.no_verify and zip_sha != ZIP_SHA256:
print(f"zip SHA-256 mismatch: got {zip_sha}, expected {ZIP_SHA256}", file=sys.stderr)
return 1
with zipfile.ZipFile(a.zip_path) as zf:
members = [n for n in zf.namelist() if n.lower().endswith(".csv")]
if len(members) != 1:
print(f"expected one CSV member, found {members}", file=sys.stderr)
return 1
member = members[0]
rows = []
with zf.open(member) as f:
for line in f: cols = line.split(b",")
try:
lon = float(cols[2])
lat = float(cols[3])
except (IndexError, ValueError):
continue if a.south <= lat <= a.north and west <= lon % 360.0 <= east:
rows.append(line)
box = f"{a.south}_{a.north}_{a.west}_{a.east}"
out = a.out_path or os.path.join(OUT_DIR, f"EMAG2_V3_20170530_{box}.csv")
os.makedirs(os.path.dirname(out) or ".", exist_ok=True)
header = [
"# EMAG2v3 crop written by tools/fetch_emag2_tile.py",
f"# source: {URL}",
f"# zip sha256: {zip_sha}",
f"# member: {member}",
f"# box: lat {a.south}..{a.north}, lon {west}..{east} east (inclusive)",
f"# rows: {len(rows)}",
"# columns unchanged, rows verbatim: i,j,LON,LAT,SeaLevel(nT),UpCont(nT, 4 km),Code,Error",
"# void: 99999 nT / Code 999 / Error -999; ambiguous: Code 888 / Error -888",
"# cite: Meyer, Saltus, Chulliat (2017) EMAG2v3, NOAA NCEI, doi:10.7289/V5H70CVX",
]
with open(out, "wb") as f:
f.write(("\n".join(header) + "\n").encode("ascii"))
f.writelines(rows)
print(f"wrote {out}: {len(rows)} rows")
print(f"sha256 {sha256_file(out)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())