mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
USGS rejects bBox coords with >7 decimals (raw Leaflet clicks have 14) —
round all coverage-derived coordinates to 6dp so USGS/others accept them.
TomTom flow 400 ("Point too far from nearest existing segment") on rural
grid cells is expected no-data, not an error — log debug and skip instead
of warning. Fix the fires log to not claim "in US-ID" under coverage mode.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
541 lines
17 KiB
Python
541 lines
17 KiB
Python
"""Tests for meshai/coverage.py — pure coverage-bbox derivation module.
|
|
|
|
Bbox convention: [west, south, east, north] = [min_lon, min_lat, max_lon, max_lat].
|
|
All functions are pure (no I/O) so no fixtures are needed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from meshai.coverage import (
|
|
AVALANCHE_CENTER_BBOXES,
|
|
US_STATE_BBOXES,
|
|
arcgis_envelope,
|
|
avalanche_centers_for_bbox,
|
|
bbox_intersects,
|
|
bbox_is_valid,
|
|
centroid,
|
|
grid_points,
|
|
point_in_bbox,
|
|
resolve_adapter_coverage,
|
|
states_for_bbox,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Reference boxes used in multiple tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Magic Valley / south-central Idaho
|
|
IDAHO_BOX = [-116.5, 42.0, -112.0, 44.0]
|
|
|
|
# Straddles the ID/OR border (western edge is inside Oregon)
|
|
ID_OR_BOX = [-118.0, 43.5, -115.0, 46.0]
|
|
|
|
# A box clearly out in the Pacific — no US states
|
|
PACIFIC_BOX = [-170.0, 20.0, -160.0, 25.0] # hits HI
|
|
|
|
# Very small Wyoming box (no avalanche centers overlap)
|
|
SMALL_WY_BOX = [-106.0, 41.5, -105.5, 42.0]
|
|
|
|
|
|
# ===========================================================================
|
|
# bbox_is_valid
|
|
# ===========================================================================
|
|
|
|
|
|
def test_valid_box_passes():
|
|
assert bbox_is_valid([-116.5, 42.0, -112.0, 44.0]) is True
|
|
|
|
|
|
def test_valid_box_tuple_passes():
|
|
assert bbox_is_valid((-116.5, 42.0, -112.0, 44.0)) is True
|
|
|
|
|
|
def test_invalid_wrong_length():
|
|
assert bbox_is_valid([-116.5, 42.0, -112.0]) is False
|
|
|
|
|
|
def test_invalid_empty():
|
|
assert bbox_is_valid([]) is False
|
|
|
|
|
|
def test_invalid_west_equals_east():
|
|
assert bbox_is_valid([-112.0, 42.0, -112.0, 44.0]) is False
|
|
|
|
|
|
def test_invalid_west_greater_than_east():
|
|
assert bbox_is_valid([-110.0, 42.0, -116.0, 44.0]) is False
|
|
|
|
|
|
def test_invalid_south_equals_north():
|
|
assert bbox_is_valid([-116.5, 44.0, -112.0, 44.0]) is False
|
|
|
|
|
|
def test_invalid_south_greater_than_north():
|
|
assert bbox_is_valid([-116.5, 46.0, -112.0, 42.0]) is False
|
|
|
|
|
|
def test_invalid_lat_out_of_range():
|
|
assert bbox_is_valid([-116.5, -91.0, -112.0, 44.0]) is False
|
|
assert bbox_is_valid([-116.5, 42.0, -112.0, 91.0]) is False
|
|
|
|
|
|
def test_invalid_lon_out_of_range():
|
|
assert bbox_is_valid([-181.0, 42.0, -112.0, 44.0]) is False
|
|
assert bbox_is_valid([-116.5, 42.0, 181.0, 44.0]) is False
|
|
|
|
|
|
def test_invalid_non_numeric():
|
|
assert bbox_is_valid([-116.5, "bad", -112.0, 44.0]) is False
|
|
|
|
|
|
def test_invalid_none():
|
|
assert bbox_is_valid(None) is False
|
|
|
|
|
|
# ===========================================================================
|
|
# point_in_bbox
|
|
# ===========================================================================
|
|
|
|
|
|
def test_point_inside_box():
|
|
assert point_in_bbox(43.0, -114.0, IDAHO_BOX) is True
|
|
|
|
|
|
def test_point_on_south_edge():
|
|
# On-edge counts as inside (inclusive)
|
|
assert point_in_bbox(42.0, -114.0, IDAHO_BOX) is True
|
|
|
|
|
|
def test_point_on_north_edge():
|
|
assert point_in_bbox(44.0, -114.0, IDAHO_BOX) is True
|
|
|
|
|
|
def test_point_outside_box_south():
|
|
assert point_in_bbox(41.9, -114.0, IDAHO_BOX) is False
|
|
|
|
|
|
def test_point_outside_box_north():
|
|
assert point_in_bbox(44.1, -114.0, IDAHO_BOX) is False
|
|
|
|
|
|
def test_point_outside_box_west():
|
|
assert point_in_bbox(43.0, -117.0, IDAHO_BOX) is False
|
|
|
|
|
|
def test_point_outside_box_east():
|
|
assert point_in_bbox(43.0, -111.0, IDAHO_BOX) is False
|
|
|
|
|
|
# ===========================================================================
|
|
# bbox_intersects
|
|
# ===========================================================================
|
|
|
|
|
|
def test_overlapping_boxes_intersect():
|
|
# Boxes share a large area
|
|
a = [-116.0, 42.0, -112.0, 44.0]
|
|
b = [-114.0, 43.0, -110.0, 45.0]
|
|
assert bbox_intersects(a, b) is True
|
|
assert bbox_intersects(b, a) is True # symmetric
|
|
|
|
|
|
def test_disjoint_boxes_do_not_intersect_east_west():
|
|
a = [-120.0, 42.0, -116.0, 44.0]
|
|
b = [-114.0, 42.0, -110.0, 44.0]
|
|
assert bbox_intersects(a, b) is False
|
|
|
|
|
|
def test_disjoint_boxes_do_not_intersect_north_south():
|
|
a = [-116.0, 42.0, -112.0, 44.0]
|
|
b = [-116.0, 45.0, -112.0, 47.0]
|
|
assert bbox_intersects(a, b) is False
|
|
|
|
|
|
def test_touching_edge_does_not_intersect():
|
|
# Boxes share only an edge — strict intersection → False
|
|
a = [-116.0, 42.0, -112.0, 44.0]
|
|
b = [-112.0, 42.0, -108.0, 44.0]
|
|
assert bbox_intersects(a, b) is False
|
|
|
|
|
|
def test_identical_boxes_intersect():
|
|
a = [-116.0, 42.0, -112.0, 44.0]
|
|
assert bbox_intersects(a, a) is True
|
|
|
|
|
|
def test_one_box_contained_in_other():
|
|
outer = [-120.0, 40.0, -110.0, 50.0]
|
|
inner = [-116.0, 42.0, -112.0, 44.0]
|
|
assert bbox_intersects(outer, inner) is True
|
|
assert bbox_intersects(inner, outer) is True
|
|
|
|
|
|
# ===========================================================================
|
|
# centroid
|
|
# ===========================================================================
|
|
|
|
|
|
def test_centroid_basic():
|
|
lat, lon = centroid([-116.0, 42.0, -112.0, 44.0])
|
|
assert lat == pytest.approx(43.0)
|
|
assert lon == pytest.approx(-114.0)
|
|
|
|
|
|
def test_centroid_asymmetric():
|
|
lat, lon = centroid([-116.5, 42.0, -112.0, 44.0])
|
|
assert lat == pytest.approx(43.0)
|
|
assert lon == pytest.approx(-114.25)
|
|
|
|
|
|
# ===========================================================================
|
|
# grid_points
|
|
# ===========================================================================
|
|
|
|
|
|
def test_grid_points_default_count():
|
|
pts = grid_points(IDAHO_BOX)
|
|
assert len(pts) == 9 # 3x3
|
|
|
|
|
|
def test_grid_points_custom_count():
|
|
pts = grid_points(IDAHO_BOX, cols=4, rows=2)
|
|
assert len(pts) == 8
|
|
|
|
|
|
def test_grid_points_all_inside_box():
|
|
"""All returned points must lie strictly inside the bbox (not on edges)."""
|
|
west, south, east, north = IDAHO_BOX
|
|
for lat, lon in grid_points(IDAHO_BOX, cols=3, rows=3):
|
|
assert south < lat < north, f"lat {lat} not strictly inside [{south}, {north}]"
|
|
assert west < lon < east, f"lon {lon} not strictly inside [{west}, {east}]"
|
|
|
|
|
|
def test_grid_points_1x1_is_centroid():
|
|
lat, lon = grid_points(IDAHO_BOX, cols=1, rows=1)[0]
|
|
clat, clon = centroid(IDAHO_BOX)
|
|
assert lat == pytest.approx(clat)
|
|
assert lon == pytest.approx(clon)
|
|
|
|
|
|
def test_grid_points_return_type():
|
|
pts = grid_points(IDAHO_BOX)
|
|
assert all(isinstance(p, tuple) and len(p) == 2 for p in pts)
|
|
|
|
|
|
# ===========================================================================
|
|
# arcgis_envelope
|
|
# ===========================================================================
|
|
|
|
|
|
def test_arcgis_envelope_keys():
|
|
result = arcgis_envelope(IDAHO_BOX)
|
|
assert set(result.keys()) == {"geometry", "geometryType", "spatialRel", "inSR"}
|
|
|
|
|
|
def test_arcgis_envelope_geometry_string():
|
|
result = arcgis_envelope([-116.5, 42.0, -112.0, 44.0])
|
|
assert result["geometry"] == "-116.5,42.0,-112.0,44.0"
|
|
|
|
|
|
def test_arcgis_envelope_static_fields():
|
|
result = arcgis_envelope(IDAHO_BOX)
|
|
assert result["geometryType"] == "esriGeometryEnvelope"
|
|
assert result["spatialRel"] == "esriSpatialRelIntersects"
|
|
assert result["inSR"] == "4326"
|
|
|
|
|
|
def test_arcgis_envelope_known_box():
|
|
bbox = [-117.0, 43.0, -113.0, 46.0]
|
|
env = arcgis_envelope(bbox)
|
|
assert env["geometry"] == "-117.0,43.0,-113.0,46.0"
|
|
|
|
|
|
# ===========================================================================
|
|
# states_for_bbox
|
|
# ===========================================================================
|
|
|
|
|
|
def test_idaho_box_returns_id():
|
|
states = states_for_bbox(IDAHO_BOX)
|
|
assert "ID" in states
|
|
|
|
|
|
def test_idaho_box_does_not_return_far_states():
|
|
"""A south-central Idaho box should not pull in distant states."""
|
|
states = states_for_bbox(IDAHO_BOX)
|
|
for far in ("FL", "ME", "TX", "NY"):
|
|
assert far not in states, f"unexpected far state {far} in {states}"
|
|
|
|
|
|
def test_id_or_straddling_box_returns_both():
|
|
states = states_for_bbox(ID_OR_BOX)
|
|
assert "ID" in states
|
|
assert "OR" in states
|
|
|
|
|
|
def test_result_is_sorted():
|
|
states = states_for_bbox(ID_OR_BOX)
|
|
assert states == sorted(states)
|
|
|
|
|
|
def test_result_is_deduped():
|
|
states = states_for_bbox(IDAHO_BOX)
|
|
assert len(states) == len(set(states))
|
|
|
|
|
|
def test_pacific_box_returns_only_hi():
|
|
states = states_for_bbox(PACIFIC_BOX)
|
|
# Hawaii bbox is [-160.3, 18.9, -154.8, 22.3]; PACIFIC_BOX overlaps it.
|
|
assert "HI" in states
|
|
# Continental states should not appear.
|
|
for continental in ("ID", "OR", "WA", "CA", "TX"):
|
|
assert continental not in states
|
|
|
|
|
|
def test_state_bboxes_table_has_all_50_plus_dc():
|
|
"""The table must have at least 51 entries (50 states + DC)."""
|
|
# We may also have territories; just require 50 states + DC minimum.
|
|
required = {
|
|
"AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "DC", "FL",
|
|
"GA", "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME",
|
|
"MD", "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH",
|
|
"NJ", "NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI",
|
|
"SC", "SD", "TN", "TX", "UT", "VT", "VA", "WA", "WV", "WI", "WY",
|
|
}
|
|
assert required.issubset(set(US_STATE_BBOXES.keys()))
|
|
|
|
|
|
# ===========================================================================
|
|
# avalanche_centers_for_bbox
|
|
# ===========================================================================
|
|
|
|
|
|
def test_idaho_box_includes_snfac():
|
|
centers = avalanche_centers_for_bbox(IDAHO_BOX)
|
|
assert "SNFAC" in centers
|
|
|
|
|
|
def test_result_is_sorted_deduped():
|
|
centers = avalanche_centers_for_bbox(IDAHO_BOX)
|
|
assert centers == sorted(centers)
|
|
assert len(centers) == len(set(centers))
|
|
|
|
|
|
def test_pacific_box_no_continental_centers():
|
|
centers = avalanche_centers_for_bbox(PACIFIC_BOX)
|
|
for c in ("SNFAC", "CAIC", "UAC"):
|
|
assert c not in centers
|
|
|
|
|
|
def test_avalanche_center_bboxes_includes_snfac_key():
|
|
assert "SNFAC" in AVALANCHE_CENTER_BBOXES
|
|
|
|
|
|
# ===========================================================================
|
|
# resolve_adapter_coverage
|
|
# ===========================================================================
|
|
|
|
|
|
# ---- None-returning cases ------------------------------------------------
|
|
|
|
|
|
def test_central_feed_returns_none():
|
|
assert resolve_adapter_coverage("nws", IDAHO_BOX, feed_source="central") is None
|
|
|
|
|
|
def test_empty_bbox_returns_none():
|
|
assert resolve_adapter_coverage("nws", [], feed_source="native") is None
|
|
|
|
|
|
def test_invalid_bbox_returns_none():
|
|
assert resolve_adapter_coverage("nws", [-116.5, 44.0, -112.0, 42.0]) is None
|
|
|
|
|
|
def test_swpc_returns_none():
|
|
"""SWPC is global — no geographic scope regardless of bbox."""
|
|
assert resolve_adapter_coverage("swpc", IDAHO_BOX) is None
|
|
|
|
|
|
def test_unknown_adapter_returns_none():
|
|
assert resolve_adapter_coverage("no_such_adapter", IDAHO_BOX) is None
|
|
|
|
|
|
# ---- fires adapter -------------------------------------------------------
|
|
|
|
|
|
def test_fires_returns_envelope_and_bbox():
|
|
result = resolve_adapter_coverage("fires", IDAHO_BOX)
|
|
assert result is not None
|
|
assert "envelope" in result
|
|
assert "bbox" in result
|
|
assert result["bbox"] == IDAHO_BOX
|
|
env = result["envelope"]
|
|
assert "geometry" in env
|
|
assert env["geometryType"] == "esriGeometryEnvelope"
|
|
|
|
|
|
# ---- nws adapter ---------------------------------------------------------
|
|
|
|
|
|
def test_nws_returns_areas_and_bbox():
|
|
result = resolve_adapter_coverage("nws", IDAHO_BOX)
|
|
assert result is not None
|
|
assert "areas" in result
|
|
assert "bbox" in result
|
|
assert "ID" in result["areas"]
|
|
assert result["bbox"] == IDAHO_BOX
|
|
|
|
|
|
def test_nws_areas_is_list():
|
|
result = resolve_adapter_coverage("nws", IDAHO_BOX)
|
|
assert isinstance(result["areas"], list)
|
|
|
|
|
|
# ---- wzdx adapter --------------------------------------------------------
|
|
|
|
|
|
def test_wzdx_returns_states_and_bbox():
|
|
result = resolve_adapter_coverage("wzdx", IDAHO_BOX)
|
|
assert result is not None
|
|
assert "states" in result
|
|
assert "bbox" in result
|
|
assert "ID" in result["states"]
|
|
|
|
|
|
# ---- bbox-only adapters --------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize("adapter", ["usgs_quake", "firms", "roads511", "usgs"])
|
|
def test_bbox_only_adapters(adapter):
|
|
result = resolve_adapter_coverage(adapter, IDAHO_BOX)
|
|
assert result is not None
|
|
assert list(result.keys()) == ["bbox"]
|
|
assert result["bbox"] == IDAHO_BOX
|
|
|
|
|
|
# ---- avalanche adapter ---------------------------------------------------
|
|
|
|
|
|
def test_avalanche_returns_center_ids():
|
|
result = resolve_adapter_coverage("avalanche", IDAHO_BOX)
|
|
assert result is not None
|
|
assert "center_ids" in result
|
|
assert "SNFAC" in result["center_ids"]
|
|
# Should NOT have a bbox key (avalanche uses center IDs, not a raw bbox)
|
|
assert "bbox" not in result
|
|
|
|
|
|
# ---- traffic adapter -----------------------------------------------------
|
|
|
|
|
|
def test_traffic_returns_points():
|
|
result = resolve_adapter_coverage("traffic", IDAHO_BOX)
|
|
assert result is not None
|
|
assert "points" in result
|
|
pts = result["points"]
|
|
assert isinstance(pts, list)
|
|
assert len(pts) == 9 # default 3x3 grid
|
|
|
|
|
|
def test_traffic_points_are_tuples():
|
|
result = resolve_adapter_coverage("traffic", IDAHO_BOX)
|
|
assert all(isinstance(p, tuple) and len(p) == 2 for p in result["points"])
|
|
|
|
|
|
# ---- satpass / ducting adapters ------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize("adapter", ["satpass", "ducting"])
|
|
def test_satpass_ducting_returns_centroid(adapter):
|
|
result = resolve_adapter_coverage(adapter, IDAHO_BOX)
|
|
assert result is not None
|
|
assert "centroid" in result
|
|
lat, lon = result["centroid"]
|
|
assert isinstance(lat, float)
|
|
assert isinstance(lon, float)
|
|
|
|
|
|
def test_satpass_centroid_is_correct():
|
|
result = resolve_adapter_coverage("satpass", [-116.0, 42.0, -112.0, 44.0])
|
|
lat, lon = result["centroid"]
|
|
assert lat == pytest.approx(43.0)
|
|
assert lon == pytest.approx(-114.0)
|
|
|
|
|
|
# ---- defensive copy of bbox ----------------------------------------------
|
|
|
|
|
|
def test_resolve_does_not_mutate_input_bbox():
|
|
"""The returned bbox should be a copy; mutating it must not affect the original."""
|
|
original = [-116.5, 42.0, -112.0, 44.0]
|
|
result = resolve_adapter_coverage("usgs_quake", original)
|
|
result["bbox"].append(999) # mutate return value
|
|
assert len(original) == 4 # original untouched
|
|
|
|
|
|
# ===========================================================================
|
|
# Coordinate precision — all outputs must be ≤6 decimal places (Fix 1)
|
|
# ===========================================================================
|
|
|
|
# Raw Leaflet map-click bbox: ~14 decimal places, triggers USGS HTTP 400.
|
|
HIGH_PREC_BOX = [
|
|
-116.99340820312501,
|
|
41.95949009892467,
|
|
-110.98388671875001,
|
|
44.09547572946637,
|
|
]
|
|
|
|
|
|
def _decimal_places(v) -> int:
|
|
"""Return significant decimal places in *v* (trailing zeros not counted)."""
|
|
s = f"{float(v):.10f}"
|
|
decimal_part = s.split(".")[1]
|
|
stripped = decimal_part.rstrip("0")
|
|
return len(stripped) if stripped else 0
|
|
|
|
|
|
def _assert_max_6dp(v, label: str = "") -> None:
|
|
"""Assert float *v* has at most 6 decimal places."""
|
|
n = _decimal_places(v)
|
|
assert n <= 6, f"{label}: {v!r} has {n} decimal places (max 6)"
|
|
|
|
|
|
def test_high_precision_bbox_coords_rounded():
|
|
"""bbox key in every adapter that returns one must have ≤6dp per coordinate."""
|
|
bbox_adapters = ("usgs_quake", "firms", "roads511", "usgs", "nws", "wzdx",
|
|
"fires", "wfigs", "nicf")
|
|
for adapter in bbox_adapters:
|
|
result = resolve_adapter_coverage(adapter, HIGH_PREC_BOX)
|
|
assert result is not None, f"{adapter} unexpectedly returned None"
|
|
if "bbox" in result:
|
|
for i, coord in enumerate(result["bbox"]):
|
|
_assert_max_6dp(coord, f"{adapter}.bbox[{i}]")
|
|
|
|
|
|
def test_high_precision_envelope_geometry_rounded():
|
|
"""ArcGIS envelope geometry string coords must have ≤6dp (fires adapter)."""
|
|
result = resolve_adapter_coverage("fires", HIGH_PREC_BOX)
|
|
assert result is not None
|
|
geom = result["envelope"]["geometry"]
|
|
for part in geom.split(","):
|
|
_assert_max_6dp(part.strip(), f"fires.envelope.geometry part {part!r}")
|
|
|
|
|
|
def test_high_precision_centroid_rounded():
|
|
"""centroid() computed from a high-precision bbox must have ≤6dp per coord."""
|
|
result = resolve_adapter_coverage("satpass", HIGH_PREC_BOX)
|
|
assert result is not None
|
|
lat, lon = result["centroid"]
|
|
_assert_max_6dp(lat, "satpass.centroid.lat")
|
|
_assert_max_6dp(lon, "satpass.centroid.lon")
|
|
|
|
|
|
def test_high_precision_grid_points_rounded():
|
|
"""All grid_points() coords from a high-precision bbox must have ≤6dp."""
|
|
result = resolve_adapter_coverage("traffic", HIGH_PREC_BOX)
|
|
assert result is not None
|
|
for i, (plat, plon) in enumerate(result["points"]):
|
|
_assert_max_6dp(plat, f"traffic.points[{i}].lat")
|
|
_assert_max_6dp(plon, f"traffic.points[{i}].lon")
|