mirror of
https://github.com/zvx-echo6/recon.git
synced 2026-05-20 06:34:40 +02:00
Add PAD-US public land classification lookup
Integrates USGS PAD-US 4.0 (651k features) into a local PostGIS database for point-in-polygon land ownership queries. Adds /api/landclass endpoint returning classifications, public/private status, and management hierarchy. - lib/landclass.py: connection pool, lookup_landclass(), domain label maps - lib/api.py: GET /api/landclass?lat=&lon= (feature-flag gated) - home.yaml: enable has_landclass flag Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3280e34718
commit
9c5b0520f9
3 changed files with 287 additions and 1 deletions
|
|
@ -37,7 +37,7 @@ features:
|
|||
has_hillshade: true
|
||||
has_3d_terrain: false
|
||||
has_traffic_overlay: true
|
||||
has_landclass: false
|
||||
has_landclass: true
|
||||
has_address_book_write: false
|
||||
has_overture_enrichment: true
|
||||
has_google_places_enrichment: true
|
||||
|
|
|
|||
34
lib/api.py
34
lib/api.py
|
|
@ -26,6 +26,7 @@ from .utils import get_config, content_hash, clean_filename_to_title, derive_sou
|
|||
from .status import StatusDB
|
||||
from .deployment_config import get_deployment_config
|
||||
from .place_detail import get_place_detail
|
||||
from .landclass import lookup_landclass, format_summary
|
||||
|
||||
logger = setup_logging('recon.api')
|
||||
|
||||
|
|
@ -1234,6 +1235,39 @@ def api_place_detail(osm_type, osm_id):
|
|||
return jsonify(result), status
|
||||
|
||||
|
||||
|
||||
@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/config')
|
||||
def api_config():
|
||||
"""Return deployment profile config for frontend consumption."""
|
||||
|
|
|
|||
252
lib/landclass.py
Normal file
252
lib/landclass.py
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
"""
|
||||
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))
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue