mangrovedigital/tide-engine-api
0
1"""Two-layer station selection (architect rework, Stage A/B).
2
3The engine used to collapse TIDE and WIND onto one nearest curated anchor. This
4module selects the two layers INDEPENDENTLY:
5
6 TIDE -> nearest NOAA CO-OPS tide-PREDICTION station from the FULL regional
7 network (data/noaa_tide_stations.json). Usually much closer than any
8 observed sensor. (Stage A — this module, tide half.)
9
10Runtime is stdlib-only: the station table is a bundled data file, read once.
11"""
12import json
13import os
14import math
15
16_TABLE = os.path.join(os.path.dirname(__file__), "..", "data", "noaa_tide_stations.json")
17_STATIONS = None
18
19
20def _load():
21 global _STATIONS
22 if _STATIONS is None:
23 with open(_TABLE, "r", encoding="utf-8") as f:
24 _STATIONS = json.load(f)["stations"]
25 return _STATIONS
26
27
28def _mi(a, b, c, d):
29 R = 6371.0
30 p = math.pi / 180
31 dla = (c - a) * p
32 dlo = (d - b) * p
33 h = math.sin(dla / 2) ** 2 + math.cos(a * p) * math.cos(c * p) * math.sin(dlo / 2) ** 2
34 return 2 * R * math.asin(math.sqrt(h)) * 0.621371
35
36
37def nearest_tide_station(lat, lon):
38 """Nearest NOAA CO-OPS tide-prediction station to (lat, lon) from the full
39 regional network. Returns dict {id, name, lat, lon, dist_mi} or None if the
40 table is empty. No fallback, no curated shortlist — the actual closest one."""
41 best = None
42 bd = float("inf")
43 for s in _load():
44 d = _mi(lat, lon, s["lat"], s["lon"])
45 if d < bd:
46 bd = d
47 best = s
48 if best is None:
49 return None
50 return {"id": best["id"], "name": best["name"], "lat": best["lat"],
51 "lon": best["lon"], "dist_mi": round(bd, 1)}
52
53
54# ---- Stage B: WIND layer — nearest real-time OBSERVED sensor, independent ----
55_SENSOR_TABLE = os.path.join(os.path.dirname(__file__), "..", "data", "observed_sensors.json")
56_SENSORS = None
57
58
59def _load_sensors():
60 global _SENSORS
61 if _SENSORS is None:
62 with open(_SENSOR_TABLE, "r", encoding="utf-8") as f:
63 _SENSORS = json.load(f)["sensors"]
64 return _SENSORS
65
66
67def observed_sensors_by_distance(lat, lon):
68 """All observed sensors sorted nearest-first (for the tidal-test walk)."""
69 return sorted(_load_sensors(), key=lambda s: _mi(lat, lon, s["lat"], s["lon"]))
70
71
72def dist_mi(lat, lon, s):
73 return round(_mi(lat, lon, s["lat"], s["lon"]), 1)
74
75
76def nearest_observed_sensor(lat, lon):
77 """Nearest real-time OBSERVED water-level sensor (NOAA CO-OPS observed or a
78 USGS gauge with a live IV record) to (lat, lon). Independent of the tide
79 station. Returns {id, name, lat, lon, source, dist_mi} or None."""
80 best = None
81 bd = float("inf")
82 for s in _load_sensors():
83 d = _mi(lat, lon, s["lat"], s["lon"])
84 if d < bd:
85 bd = d
86 best = s
87 if best is None:
88 return None
89 return {"id": best["id"], "name": best["name"], "lat": best["lat"],
90 "lon": best["lon"], "source": best["source"], "dist_mi": round(bd, 1)}
91 