mirror of
https://github.com/zvx-echo6/recon.git
synced 2026-05-21 23:24:47 +02:00
Compare commits
No commits in common. "dc7591b101838c19035c646512f8fa1af85d57e2" and "a80bb6e848d6667ef147be157f5742ce2b6fc1e0" have entirely different histories.
dc7591b101
...
a80bb6e848
5 changed files with 17 additions and 124 deletions
|
|
@ -214,9 +214,6 @@ def lookup_landclass(lat, lon):
|
|||
des_tp, gap_sts, pub_access, category, gis_acres, state_nm
|
||||
FROM pad_units
|
||||
WHERE ST_Intersects(geom, ST_SetSRID(ST_MakePoint(%s, %s), 4326))
|
||||
-- exclude antimeridian-wrapping polygons: 47 BOEM marine artifacts
|
||||
-- span ~360 deg longitude and false-match non-US points at their lat band
|
||||
AND (ST_XMax(geom) - ST_XMin(geom)) < 60
|
||||
ORDER BY gis_acres ASC
|
||||
LIMIT 10""",
|
||||
(lon, lat)
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Tests for lib.landclass PAD-US lookups.
|
||||
|
||||
Live-PostgreSQL regression test using the skip-if-not-available pattern
|
||||
(matching test_real_timezone_db in reverse_bundle_test.py). Plain asserts +
|
||||
a __main__ runner, matching the rest of lib/*_test.py.
|
||||
|
||||
Note: lookup_landclass swallows DB errors and returns [] (it never raises),
|
||||
so PG availability is probed via a known US point (Boise) rather than by
|
||||
catching an exception.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from lib import landclass
|
||||
|
||||
|
||||
def test_landclass_no_antimeridian_false_match():
|
||||
# Yosemite doubles as the liveness probe: a point on real US public land.
|
||||
# (lookup_landclass returns [] when PG is unreachable AND when the point is
|
||||
# off public land, so the probe must be a known-public-land point — e.g.
|
||||
# downtown Boise is private and would yield [] even with PG up.)
|
||||
yosemite = landclass.lookup_landclass(37.85, -119.55)
|
||||
if not yosemite:
|
||||
print(" SKIP: live PG not available (Yosemite returned no rows)")
|
||||
return
|
||||
# Filter must NOT drop legitimate (non-wrapping) US units.
|
||||
assert len(yosemite) >= 1, f"Yosemite should match >=1 PAD-US unit, got {len(yosemite)}"
|
||||
|
||||
# London (51.5074 N) previously false-matched the antimeridian-wrapping
|
||||
# 'Rat Islands' record (ogc_fid 3974, ~360 deg lon span). The < 60 deg
|
||||
# filter must now drop it -> empty result.
|
||||
london = landclass.lookup_landclass(51.5074, -0.1278)
|
||||
assert london == [], f"London should match no PAD-US unit, got {[r.get('unit_name') for r in london]}"
|
||||
print(" PASS: antimeridian filter drops London false-match, keeps Yosemite coverage")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Running landclass tests...")
|
||||
test_landclass_no_antimeridian_false_match()
|
||||
print("All tests passed.")
|
||||
|
|
@ -17,7 +17,6 @@ from . import netsyms
|
|||
from . import address_book
|
||||
from . import nav_tools
|
||||
from .geocode import PHOTON_URL
|
||||
from .offroute.dem import DEMReader
|
||||
from .utils import setup_logging
|
||||
|
||||
logger = setup_logging('recon.netsyms_api')
|
||||
|
|
@ -138,11 +137,12 @@ def api_reverse():
|
|||
#
|
||||
# Sibling to the query-string /api/reverse above; that route is unchanged.
|
||||
# Every component is sourced from localhost only (Photon, timezones.sqlite,
|
||||
# in-process landclass/PostGIS, planet-DEM PMTiles). Each lookup is
|
||||
# independent: a component failure logs a warning and yields null — never 5xx.
|
||||
# in-process landclass/PostGIS, Valhalla). Each lookup is independent: a
|
||||
# component failure logs a warning and yields null — never a 5xx.
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
_TZ_DB_PATH = "/mnt/nav/sources/timezones.sqlite"
|
||||
_VALHALLA_HEIGHT_URL = "http://localhost:8002/height"
|
||||
|
||||
# Full bundle cache: key=(round(lat,4), round(lon,4)) -> dict. ~10k entries, 24h TTL.
|
||||
_REVERSE_BUNDLE_CACHE = TTLCache(maxsize=10_000, ttl=86_400)
|
||||
|
|
@ -151,14 +151,6 @@ _REVERSE_BUNDLE_LOCK = threading.Lock()
|
|||
_BUNDLE_KEYS = ('name', 'city', 'county', 'state', 'country',
|
||||
'postal_code', 'timezone', 'landclass', 'elevation_m')
|
||||
|
||||
# planet-DEM elevation source (single PMTiles, replaces Valhalla /height).
|
||||
# Instantiated once at import; the underlying mmap is lazy. None if unavailable.
|
||||
try:
|
||||
_DEM = DEMReader()
|
||||
except Exception as e: # pragma: no cover - depends on PMTiles availability
|
||||
logger.warning("DEMReader unavailable, elevation will be null: %s", e)
|
||||
_DEM = None
|
||||
|
||||
|
||||
def _spatialite_blob_to_wkb(blob):
|
||||
"""Recover standard WKB from a SpatiaLite geometry BLOB.
|
||||
|
|
@ -232,12 +224,16 @@ def _reverse_landclass(lat, lon):
|
|||
|
||||
|
||||
def _reverse_elevation(lat, lon):
|
||||
"""Elevation in metres from the planet-DEM PMTiles — the single elevation
|
||||
source per OFFROUTE-ARCHITECTURE.md §9. None on failure, on untiled points
|
||||
(e.g. true ocean), or if DEMReader could not be initialized at startup."""
|
||||
if _DEM is None:
|
||||
return None
|
||||
return _DEM.sample_point(lat, lon)
|
||||
"""Elevation in metres from local Valhalla /height. None on failure."""
|
||||
import requests as http_requests
|
||||
resp = http_requests.post(
|
||||
_VALHALLA_HEIGHT_URL,
|
||||
json={"shape": [{"lat": lat, "lon": lon}]},
|
||||
timeout=10,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
heights = resp.json().get("height", [])
|
||||
return heights[0] if heights else None
|
||||
|
||||
|
||||
@geocode_bp.route('/api/reverse/<lat>/<lon>')
|
||||
|
|
|
|||
|
|
@ -158,28 +158,7 @@ class DEMReader:
|
|||
}
|
||||
|
||||
return elevation, metadata
|
||||
|
||||
def sample_point(self, lat: float, lon: float) -> Optional[float]:
|
||||
"""Return elevation in meters at a single point, or None if untiled.
|
||||
|
||||
Reads one z12 Terrarium tile (LRU-cached) and indexes the matching
|
||||
pixel. Sub-ms warm, ~15 ms cold per tile via NFS. Returns None when the
|
||||
tile is absent (e.g. true ocean nodata) or lat is outside the
|
||||
Web-Mercator pole cap (~+/-85.05 deg).
|
||||
"""
|
||||
if not -85.05112878 <= lat <= 85.05112878:
|
||||
return None
|
||||
n = 2 ** ZOOM_LEVEL
|
||||
fx = (lon + 180.0) / 360.0 * n
|
||||
fy = (1.0 - math.asinh(math.tan(math.radians(lat))) / math.pi) / 2.0 * n
|
||||
tx, ty = int(fx), int(fy)
|
||||
tile = self._decode_tile(ZOOM_LEVEL, tx, ty)
|
||||
if tile is None:
|
||||
return None
|
||||
row = min(TILE_SIZE - 1, int((fy - ty) * TILE_SIZE))
|
||||
col = min(TILE_SIZE - 1, int((fx - tx) * TILE_SIZE))
|
||||
return float(tile[row, col])
|
||||
|
||||
|
||||
def pixel_to_latlon(self, row: int, col: int, metadata: dict) -> Tuple[float, float]:
|
||||
"""Convert pixel coordinates to lat/lon."""
|
||||
lat = metadata["origin_lat"] + row * metadata["pixel_size_lat"]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Tests for the /api/reverse/<lat>/<lon> enrichment bundle (lib.netsyms_api).
|
||||
|
||||
Photon/DEM/landclass are mocked so the suite runs without live services;
|
||||
Photon/Valhalla/landclass are mocked so the suite runs without live services;
|
||||
one timezone test exercises the real SpatiaLite DB when it is present. Plain
|
||||
asserts + a __main__ runner, matching the rest of lib/*_test.py.
|
||||
"""
|
||||
|
|
@ -15,7 +15,8 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|||
from flask import Flask
|
||||
from lib import netsyms_api
|
||||
|
||||
EXPECTED_KEYS = set(netsyms_api._BUNDLE_KEYS)
|
||||
EXPECTED_KEYS = {'name', 'city', 'county', 'state', 'country',
|
||||
'postal_code', 'timezone', 'landclass', 'elevation_m'}
|
||||
|
||||
|
||||
def _client():
|
||||
|
|
@ -123,40 +124,6 @@ def test_real_timezone_db():
|
|||
print(" PASS: real timezones.sqlite point-in-polygon")
|
||||
|
||||
|
||||
def test_elevation_from_dem_reader_mock():
|
||||
# elevation_m comes from DEMReader.sample_point (not Valhalla); other
|
||||
# components stubbed to null so the bundle is hermetic.
|
||||
_clear_cache()
|
||||
fake_dem = mock.Mock()
|
||||
fake_dem.sample_point.return_value = 824
|
||||
with mock.patch.object(netsyms_api, '_DEM', fake_dem), \
|
||||
mock.patch.object(netsyms_api, '_reverse_photon', return_value={}), \
|
||||
mock.patch.object(netsyms_api, '_reverse_timezone', return_value=None), \
|
||||
mock.patch.object(netsyms_api, '_reverse_landclass', return_value=None):
|
||||
resp = _client().get('/api/reverse/43.6150/-116.2023')
|
||||
assert resp.status_code == 200, resp.status_code
|
||||
data = resp.get_json()
|
||||
assert set(data.keys()) == EXPECTED_KEYS
|
||||
assert data['elevation_m'] == 824, data['elevation_m']
|
||||
fake_dem.sample_point.assert_called_once()
|
||||
print(" PASS: elevation_m sourced from DEMReader.sample_point")
|
||||
|
||||
|
||||
def test_elevation_dem_unavailable():
|
||||
# DEMReader failed to init at startup (_DEM is None) -> elevation_m null, 200.
|
||||
_clear_cache()
|
||||
with mock.patch.object(netsyms_api, '_DEM', None), \
|
||||
mock.patch.object(netsyms_api, '_reverse_photon', return_value={}), \
|
||||
mock.patch.object(netsyms_api, '_reverse_timezone', return_value=None), \
|
||||
mock.patch.object(netsyms_api, '_reverse_landclass', return_value=None):
|
||||
resp = _client().get('/api/reverse/43.6150/-116.2023')
|
||||
assert resp.status_code == 200, resp.status_code
|
||||
data = resp.get_json()
|
||||
assert set(data.keys()) == EXPECTED_KEYS
|
||||
assert data['elevation_m'] is None
|
||||
print(" PASS: DEMReader unavailable -> elevation_m null, still 200")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Running reverse-bundle tests...")
|
||||
test_happy_path()
|
||||
|
|
@ -166,6 +133,4 @@ if __name__ == '__main__':
|
|||
test_invalid_input_400()
|
||||
test_cache_hit_serves_without_recompute()
|
||||
test_real_timezone_db()
|
||||
test_elevation_from_dem_reader_mock()
|
||||
test_elevation_dem_unavailable()
|
||||
print("All tests passed.")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue