From 771d7eb3b3c9ee127915f14a9f118521e08f243a Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 22 Apr 2026 15:36:44 +0000 Subject: [PATCH] Add public land classification to PlaceDetail Shows PAD-US land ownership data (unit name, agency hierarchy, access status) when clicking on map areas. Upgrades "Dropped pin" name to land unit summary on public land. Private land gets a muted indicator. - api.js: fetchLandclass() for /api/landclass endpoint - PlaceDetail.jsx: LandclassSection, PrivateLandIndicator components Co-Authored-By: Claude Opus 4.6 --- src/api.js | 18 +++++++++ src/components/PlaceDetail.jsx | 72 +++++++++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/api.js b/src/api.js index 83a4af2..35d724b 100644 --- a/src/api.js +++ b/src/api.js @@ -242,3 +242,21 @@ export async function fetchNearbyContacts(lat, lon, radiusM, signal) { return [] } } + +/** + * Fetch PAD-US land classification for a point. + * @param {number} lat + * @param {number} lon + * @param {AbortSignal} signal + * @returns {Promise} Classification data or null on error + */ +export async function fetchLandclass(lat, lon, signal) { + try { + const params = new URLSearchParams({ lat: String(lat), lon: String(lon) }) + const resp = await fetch(`/api/landclass?${params}`, { signal }) + if (!resp.ok) return null + return resp.json() + } catch { + return null + } +} diff --git a/src/components/PlaceDetail.jsx b/src/components/PlaceDetail.jsx index 18760a8..5fb89bb 100644 --- a/src/components/PlaceDetail.jsx +++ b/src/components/PlaceDetail.jsx @@ -1,12 +1,12 @@ import { useEffect, useState, useRef, useCallback } from 'react' import { X, Navigation, Plus, Bookmark, ChevronDown, ChevronUp, Copy, - Clock, Phone, Globe, Mail, BookOpen, Info, + Clock, Phone, Globe, Mail, BookOpen, Info, Trees, } from 'lucide-react' import OpeningHours from 'opening_hours' import toast from 'react-hot-toast' import { useStore } from '../store' -import { fetchElevation, fetchPlaceDetails, fetchDriveTime, fetchNearbyContacts } from '../api' +import { fetchElevation, fetchPlaceDetails, fetchDriveTime, fetchNearbyContacts, fetchLandclass } from '../api' import { hasFeature } from '../config' import { buildAddress } from '../utils/place' @@ -354,6 +354,46 @@ function EnrichmentSections({ details }) { // ── Skeleton loader ──────────────────────────────────────────────────── + +// ── Land classification display ────────────────────────────────────────────────────────────────────── + +function LandclassSection({ data }) { + if (!data || data.is_public !== true || !data.classifications?.length) return null + + return ( + +
+ {data.classifications.map((c, i) => ( +
+ + {c.unit_name} + + {(c.owner_type || c.manager_name || c.designation_type) && ( + + {[c.owner_type, c.manager_name, c.designation_type].filter(Boolean).join(' \u203a ')} + + )} + {c.public_access && c.public_access !== 'Unknown' && ( + + {c.public_access} + + )} +
+ ))} +
+
+ ) +} + +function PrivateLandIndicator({ data }) { + if (!data || data.is_private !== true) return null + return ( +

+ Private land +

+ ) +} + function EnrichmentSkeleton() { return (
@@ -383,6 +423,7 @@ export default function PlaceDetail() { const [placeDetails, setPlaceDetails] = useState(null) const [driveTime, setDriveTime] = useState(null) const [nearbyLabel, setNearbyLabel] = useState(null) + const [landclass, setLandclass] = useState(null) const closeCopy = useCallback(() => setCopyOpen(false), []) @@ -481,6 +522,28 @@ export default function PlaceDetail() { return () => controller.abort() }, [placeLat, placeLon]) + // Fetch land classification when place changes (if feature enabled) + useEffect(() => { + if (!hasFeature('has_landclass') || placeLat == null || placeLon == null) { + setLandclass(null) + return + } + const controller = new AbortController() + fetchLandclass(placeLat, placeLon, controller.signal).then((data) => { + if (!controller.signal.aborted && data) { + setLandclass(data) + // Upgrade "Dropped pin" name to land summary if reverse geocode didn't resolve + if (data.summary && useStore.getState().selectedPlace?.name === 'Dropped pin') { + const current = useStore.getState().selectedPlace + useStore.getState().setSelectedPlace({ ...current, name: data.summary }) + } + } else if (!controller.signal.aborted) { + setLandclass(null) + } + }) + return () => controller.abort() + }, [placeLat, placeLon]) + // Derive elevation/loading from comparing result to current place const elevLoading = placeLat != null && (elevResult.lat !== placeLat || elevResult.lon !== placeLon) const elevation = !elevLoading ? elevResult.value : null @@ -598,6 +661,11 @@ export default function PlaceDetail() {
+ {/* OSM enrichment sections */} + {/* Land classification (PAD-US) */} + + + {/* OSM enrichment sections */} {placeDetails === 'loading' && } {placeDetails && placeDetails !== 'loading' && }