mirror of
https://github.com/zvx-echo6/recon.git
synced 2026-06-10 08:54:34 +02:00
cleanup: remove /api/landclass handler + lib/landclass.py (extraction #4 shadow)
/api/landclass is edge-shadowed since extraction #4 — navi-landclass :8424 serves the route via nginx. Cleanup #4 removed the last in-process consumer (netsyms_api._reverse_landclass), so lib/landclass.py is now fully orphaned. - api.py: drop the @app.route('/api/landclass') handler + the `from .landclass import lookup_landclass, format_summary` import. - DELETE lib/landclass.py (only consumer was the deleted handler). - DELETE lib/landclass_test.py (SUT gone). PADUS_DB_* vars in /opt/recon/.env are now dead in recon — flagged for an out-of-band post-merge cleanup, not touched here (data, not code). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d7292c4cc7
commit
1f05d4b4d6
3 changed files with 0 additions and 332 deletions
33
lib/api.py
33
lib/api.py
|
|
@ -25,7 +25,6 @@ from werkzeug.utils import secure_filename
|
|||
from .utils import get_config, content_hash, clean_filename_to_title, derive_source_and_category, generate_download_url, setup_logging
|
||||
from .status import StatusDB
|
||||
from .deployment_config import get_deployment_config
|
||||
from .landclass import lookup_landclass, format_summary
|
||||
|
||||
logger = setup_logging('recon.api')
|
||||
|
||||
|
|
@ -1170,38 +1169,6 @@ def api_knowledge_stats():
|
|||
return jsonify(_cache['knowledge_stats'])
|
||||
|
||||
|
||||
@app.route('/api/landclass')
|
||||
def api_landclass():
|
||||
"""PAD-US land classification lookup for a point."""
|
||||
config = get_deployment_config()
|
||||
if not config.get('features', {}).get('has_landclass'):
|
||||
return jsonify({'error': 'Land classification not available'}), 404
|
||||
|
||||
try:
|
||||
lat = float(request.args.get('lat', ''))
|
||||
lon = float(request.args.get('lon', ''))
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({'error': 'lat and lon required as numbers'}), 400
|
||||
|
||||
if not (-90 <= lat <= 90) or not (-180 <= lon <= 180):
|
||||
return jsonify({'error': 'lat must be -90..90, lon must be -180..180'}), 400
|
||||
|
||||
classifications = lookup_landclass(lat, lon)
|
||||
is_public = len(classifications) > 0
|
||||
is_private = len(classifications) == 0
|
||||
summary = format_summary(classifications)
|
||||
|
||||
return jsonify({
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'classifications': classifications,
|
||||
'count': len(classifications),
|
||||
'is_public': is_public,
|
||||
'is_private': is_private,
|
||||
'summary': summary,
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/health')
|
||||
def api_health():
|
||||
"""Health check endpoint for monitoring."""
|
||||
|
|
|
|||
255
lib/landclass.py
255
lib/landclass.py
|
|
@ -1,255 +0,0 @@
|
|||
"""
|
||||
PAD-US land classification lookup.
|
||||
|
||||
Provides point-in-polygon queries against the USGS Protected Areas Database
|
||||
(PAD-US) stored in a local PostGIS database. Returns land ownership,
|
||||
management, and public access information for any lat/lon coordinate.
|
||||
|
||||
Connection pool is lazy-initialized on first call. If PostgreSQL is unreachable,
|
||||
functions return empty results gracefully (feature degrades, doesn't crash).
|
||||
"""
|
||||
import os
|
||||
|
||||
import psycopg2
|
||||
import psycopg2.pool
|
||||
|
||||
from .utils import setup_logging
|
||||
|
||||
logger = setup_logging('recon.landclass')
|
||||
|
||||
_pool = None
|
||||
_pool_failed = False
|
||||
|
||||
# ── Label mappings from PAD-US domain tables ────────────────────────────
|
||||
# Extracted from PADUS4_0_Geodatabase.gdb domain lookup layers.
|
||||
# ogr2ogr lowercases all column names.
|
||||
|
||||
AGENCY_NAME_MAP = {
|
||||
'TVA': 'Tennessee Valley Authority',
|
||||
'BLM': 'Bureau of Land Management',
|
||||
'BOEM': 'Bureau of Ocean Energy Management',
|
||||
'USBR': 'Bureau of Reclamation',
|
||||
'FWS': 'U.S. Fish and Wildlife Service',
|
||||
'USFS': 'Forest Service',
|
||||
'DOD': 'Department of Defense',
|
||||
'USACE': 'Army Corps of Engineers',
|
||||
'DOE': 'Department of Energy',
|
||||
'NPS': 'National Park Service',
|
||||
'NRCS': 'Natural Resources Conservation Service',
|
||||
'ARS': 'Agricultural Research Service',
|
||||
'BIA': 'Bureau of Indian Affairs',
|
||||
'NOAA': 'National Oceanic and Atmospheric Administration',
|
||||
'BPA': 'Bonneville Power Administration',
|
||||
'OTHF': 'Other or Unknown Federal Land',
|
||||
'TRIB': 'American Indian Lands',
|
||||
'SPR': 'State Park and Recreation',
|
||||
'SDC': 'State Department of Conservation',
|
||||
'SLB': 'State Land Board',
|
||||
}
|
||||
|
||||
AGENCY_TYPE_MAP = {
|
||||
'FED': 'Federal',
|
||||
'TRIB': 'American Indian Lands',
|
||||
'STAT': 'State',
|
||||
'DIST': 'Regional Agency Special District',
|
||||
'LOC': 'Local Government',
|
||||
'NGO': 'Non-Governmental Organization',
|
||||
'PVT': 'Private',
|
||||
'JNT': 'Joint',
|
||||
'UNK': 'Unknown',
|
||||
'TERR': 'Territorial',
|
||||
'DESG': 'Designation',
|
||||
}
|
||||
|
||||
DESIGNATION_TYPE_MAP = {
|
||||
'NP': 'National Park',
|
||||
'NM': 'National Monument',
|
||||
'NCA': 'Conservation Area',
|
||||
'NF': 'National Forest',
|
||||
'NG': 'National Grassland',
|
||||
'PUB': 'National Public Lands',
|
||||
'NT': 'National Scenic or Historic Trail',
|
||||
'NWR': 'National Wildlife Refuge',
|
||||
'WA': 'Wilderness Area',
|
||||
'WSR': 'Wild and Scenic River',
|
||||
'WSA': 'Wilderness Study Area',
|
||||
'MPA': 'Marine Protected Area',
|
||||
'NRA': 'National Recreation Area',
|
||||
'NSBV': 'National Scenic, Botanical or Volcanic Area',
|
||||
'NLS': 'National Lakeshore or Seashore',
|
||||
'IRA': 'Inventoried Roadless Area',
|
||||
'ACEC': 'Area of Critical Environmental Concern',
|
||||
'RNA': 'Research Natural Area',
|
||||
'REC': 'Recreation Management Area',
|
||||
'RMA': 'Resource Management Area',
|
||||
'WPA': 'Watershed Protection Area',
|
||||
'REA': 'Research or Educational Area',
|
||||
'HCA': 'Historic or Cultural Area',
|
||||
'MIT': 'Mitigation Land or Bank',
|
||||
'MIL': 'Military Land',
|
||||
'ACC': 'Access Area',
|
||||
'SDA': 'Special Designation Area',
|
||||
'PROC': 'Approved or Proclamation Boundary',
|
||||
'FOTH': 'Federal Other or Unknown',
|
||||
'ND': 'Not Designated',
|
||||
}
|
||||
|
||||
PUBLIC_ACCESS_MAP = {
|
||||
'OA': 'Open Access',
|
||||
'RA': 'Restricted Access',
|
||||
'XA': 'Closed',
|
||||
'UK': 'Unknown',
|
||||
}
|
||||
|
||||
GAP_STATUS_MAP = {
|
||||
'1': 'Managed for biodiversity (disturbance events proceed)',
|
||||
'2': 'Managed for biodiversity (disturbance suppressed)',
|
||||
'3': 'Multiple uses (extractive/OHV)',
|
||||
'4': 'No known mandate for biodiversity protection',
|
||||
}
|
||||
|
||||
CATEGORY_MAP = {
|
||||
'Fee': 'Fee',
|
||||
'Easement': 'Easement',
|
||||
'Other': 'Other',
|
||||
'Unknown': 'Unknown',
|
||||
'Designation': 'Designation',
|
||||
'Marine': 'Marine Area',
|
||||
'Proclamation': 'Approved, Proclamation or Extent Boundary',
|
||||
}
|
||||
|
||||
STATE_MAP = {
|
||||
'AL': 'Alabama', 'AK': 'Alaska', 'AZ': 'Arizona', 'AR': 'Arkansas',
|
||||
'CA': 'California', 'CO': 'Colorado', 'CT': 'Connecticut', 'DE': 'Delaware',
|
||||
'DC': 'District of Columbia', 'FL': 'Florida', 'GA': 'Georgia', 'HI': 'Hawaii',
|
||||
'ID': 'Idaho', 'IL': 'Illinois', 'IN': 'Indiana', 'IA': 'Iowa',
|
||||
'KS': 'Kansas', 'KY': 'Kentucky', 'LA': 'Louisiana', 'ME': 'Maine',
|
||||
'MD': 'Maryland', 'MA': 'Massachusetts', 'MI': 'Michigan', 'MN': 'Minnesota',
|
||||
'MS': 'Mississippi', 'MO': 'Missouri', 'MT': 'Montana', 'NE': 'Nebraska',
|
||||
'NV': 'Nevada', 'NH': 'New Hampshire', 'NJ': 'New Jersey', 'NM': 'New Mexico',
|
||||
'NY': 'New York', 'NC': 'North Carolina', 'ND': 'North Dakota', 'OH': 'Ohio',
|
||||
'OK': 'Oklahoma', 'OR': 'Oregon', 'PA': 'Pennsylvania', 'RI': 'Rhode Island',
|
||||
'SC': 'South Carolina', 'SD': 'South Dakota', 'TN': 'Tennessee', 'TX': 'Texas',
|
||||
'UT': 'Utah', 'VT': 'Vermont', 'VA': 'Virginia', 'WA': 'Washington',
|
||||
'WV': 'West Virginia', 'WI': 'Wisconsin', 'WY': 'Wyoming',
|
||||
}
|
||||
|
||||
|
||||
def _decode(code, label_map):
|
||||
"""Decode a PAD-US code using a label map. Returns decoded label or the raw code."""
|
||||
if not code:
|
||||
return ''
|
||||
code = str(code).strip()
|
||||
return label_map.get(code, code)
|
||||
|
||||
|
||||
def _get_pool():
|
||||
"""Lazy-init the connection pool. Returns None if Postgres is unreachable."""
|
||||
global _pool, _pool_failed
|
||||
if _pool is not None:
|
||||
return _pool
|
||||
if _pool_failed:
|
||||
return None
|
||||
|
||||
try:
|
||||
_pool = psycopg2.pool.SimpleConnectionPool(
|
||||
minconn=1,
|
||||
maxconn=3,
|
||||
host=os.environ.get('PADUS_DB_HOST', 'localhost'),
|
||||
port=int(os.environ.get('PADUS_DB_PORT', '5432')),
|
||||
dbname=os.environ.get('PADUS_DB_NAME', 'padus'),
|
||||
user=os.environ.get('PADUS_DB_USER', 'overture'),
|
||||
password=os.environ.get('PADUS_DB_PASSWORD', ''),
|
||||
connect_timeout=5,
|
||||
)
|
||||
logger.info("PAD-US PostgreSQL connection pool initialized")
|
||||
return _pool
|
||||
except Exception as e:
|
||||
_pool_failed = True
|
||||
logger.warning(f"PAD-US PostgreSQL unavailable, land classification disabled: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _query_all(sql, params):
|
||||
"""Execute a query and return all rows as a list of dicts, or empty list."""
|
||||
pool = _get_pool()
|
||||
if pool is None:
|
||||
return []
|
||||
|
||||
conn = None
|
||||
try:
|
||||
conn = pool.getconn()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql, params)
|
||||
rows = cur.fetchall()
|
||||
if not rows:
|
||||
return []
|
||||
cols = [desc[0] for desc in cur.description]
|
||||
return [dict(zip(cols, row)) for row in rows]
|
||||
except Exception as e:
|
||||
logger.warning(f"PAD-US query error: {e}")
|
||||
if conn:
|
||||
try:
|
||||
conn.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
finally:
|
||||
if conn:
|
||||
try:
|
||||
pool.putconn(conn)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def lookup_landclass(lat, lon):
|
||||
"""
|
||||
Look up PAD-US land classifications for a point.
|
||||
|
||||
Returns a list of classification dicts, ordered by area ascending
|
||||
(smallest/most specific first). Empty list on error or no results.
|
||||
"""
|
||||
rows = _query_all(
|
||||
"""SELECT unit_nm, mang_name, mang_type, own_name, own_type,
|
||||
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)
|
||||
)
|
||||
|
||||
results = []
|
||||
for row in rows:
|
||||
pa_code = str(row.get('pub_access', '')).strip()
|
||||
|
||||
results.append({
|
||||
'unit_name': (row.get('unit_nm') or '').strip(),
|
||||
'manager_name': _decode(row.get('mang_name'), AGENCY_NAME_MAP),
|
||||
'manager_type': _decode(row.get('mang_type'), AGENCY_TYPE_MAP),
|
||||
'owner_type': _decode(row.get('own_type'), AGENCY_TYPE_MAP),
|
||||
'designation_type': _decode(row.get('des_tp'), DESIGNATION_TYPE_MAP),
|
||||
'gap_status': str(row.get('gap_sts', '')).strip(),
|
||||
'public_access': _decode(pa_code, PUBLIC_ACCESS_MAP),
|
||||
'public_access_code': pa_code,
|
||||
'category': _decode(row.get('category'), CATEGORY_MAP),
|
||||
'acres': row.get('gis_acres'),
|
||||
'state': _decode(row.get('state_nm'), STATE_MAP),
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def format_summary(classifications):
|
||||
"""
|
||||
Format a human-readable summary from classification results.
|
||||
|
||||
Returns the most specific unit name, or None if no results.
|
||||
"""
|
||||
if not classifications:
|
||||
return None
|
||||
# First result is smallest/most specific (ordered by acres ASC)
|
||||
return classifications[0].get('unit_name') or None
|
||||
|
|
@ -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.")
|
||||
Loading…
Add table
Add a link
Reference in a new issue