Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 60 additions & 4 deletions src/components/Map/ClickPopover.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// components/Map/ClickPopover.jsx
import React, { useMemo } from 'react';
import { area as turfArea } from '@turf/turf';

function computeQuantileEdges(sortedVals, bins) {
const n = sortedVals.length;
Expand Down Expand Up @@ -82,13 +83,49 @@ const MiniHistogram = ({ values = [], currentValue = null, bins = 10, barWidth =
);
};

const ClickPopover = ({ tooltip, stats, distributions, onFocus, onClose }) => {
const ClickPopover = ({ tooltip, stats, distributions, geometry, dimensionUnits = 'ft', onFocus, onClose }) => {
if (!tooltip || !tooltip.visible || !tooltip.content) return null;
const avgValues = distributions?.avg || [];
const totalValues = distributions?.total || [];
const hasAvg = stats && typeof stats.a === 'number' && isFinite(stats.a);
const hasTotal = stats && typeof stats.t === 'number' && isFinite(stats.t);

// Calculate area using turf.js
const areaDisplay = useMemo(() => {
if (!geometry) return null;
try {
const areaMetersSquared = turfArea({ type: 'Feature', geometry });
if (!isFinite(areaMetersSquared) || areaMetersSquared <= 0) return null;

if (dimensionUnits === 'ft') {
// Convert square meters to square feet (1 m² = 10.7639 ft²)
const areaFtSquared = areaMetersSquared * 10.7639;
if (areaFtSquared >= 43560) {
// Convert to acres (1 acre = 43,560 ft²)
const acres = areaFtSquared / 43560;
return { value: acres, unit: 'ac', label: 'Area', fullText: `${acres.toFixed(2)} ac` };
} else if (areaFtSquared >= 1) {
return { value: areaFtSquared, unit: 'ft²', label: 'Area', fullText: `${Math.round(areaFtSquared).toLocaleString()} ft²` };
} else {
return { value: areaFtSquared * 144, unit: 'in²', label: 'Area', fullText: `${Math.round(areaFtSquared * 144).toLocaleString()} in²` };
}
} else {
// Metric units
if (areaMetersSquared >= 1000000) {
// Convert to square kilometers (1 km² = 1,000,000 m²)
const areaKmSquared = areaMetersSquared / 1000000;
return { value: areaKmSquared, unit: 'km²', label: 'Area', fullText: `${areaKmSquared.toFixed(3)} km²` };
} else if (areaMetersSquared >= 1) {
return { value: areaMetersSquared, unit: 'm²', label: 'Area', fullText: `${Math.round(areaMetersSquared).toLocaleString()} m²` };
} else {
return { value: areaMetersSquared * 10000, unit: 'cm²', label: 'Area', fullText: `${Math.round(areaMetersSquared * 10000).toLocaleString()} cm²` };
}
}
} catch (_) {
return null;
}
}, [geometry, dimensionUnits]);

return (
<div
className="absolute z-50 bg-white dark:bg-gray-800 p-2 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 max-w-xs transition-all duration-200 ease-out"
Expand Down Expand Up @@ -131,13 +168,32 @@ const ClickPopover = ({ tooltip, stats, distributions, onFocus, onClose }) => {
<MiniHistogram values={totalValues} currentValue={stats.t} />
</div>
)}
{areaDisplay && (
<div>
<div className="flex items-center justify-between text-[11px] text-gray-600 dark:text-gray-300 mb-1">
<span>{areaDisplay.label}</span>
<span className="font-medium text-gray-700 dark:text-gray-200">{areaDisplay.fullText}</span>
</div>
</div>
)}
</div>
)}

{/* Data availability notice when no stats */}
{/* Area display when no stats */}
{(!hasAvg && !hasTotal) && (
<div className="mt-2 text-[11px] text-gray-600 dark:text-gray-300">
Park usage stats are not available for this zone.
<div className="mt-2 space-y-2">
{areaDisplay ? (
<div>
<div className="flex items-center justify-between text-[11px] text-gray-600 dark:text-gray-300 mb-1">
<span>{areaDisplay.label}</span>
<span className="font-medium text-gray-700 dark:text-gray-200">{areaDisplay.fullText}</span>
</div>
</div>
) : (
<div className="text-[11px] text-gray-600 dark:text-gray-300">
Park usage stats are not available for this zone.
</div>
)}
</div>
)}

Expand Down
5 changes: 4 additions & 1 deletion src/components/Map/MapContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ const MapContainer = forwardRef(({
isLoading,
responsive,
isSitePlanMode = false,
isRightSidebarOpen = false
isRightSidebarOpen = false,
exportOptions
}, ref) => {
const safeResponsive = responsive || { sidebarMode: 'expanded' };
const {
Expand Down Expand Up @@ -1635,6 +1636,8 @@ const MapContainer = forwardRef(({
tooltip={permitAreas.clickedTooltip}
stats={permitAreas.clickedTooltip.stats}
distributions={permitAreas.clickedTooltip.distributions}
geometry={permitAreas.clickedTooltip.geometry}
dimensionUnits={exportOptions?.dimensionUnits || 'ft'}
onClose={permitAreas.dismissClickedTooltip}
onFocus={permitAreas.focusClickedTooltipArea}
/>
Expand Down
1 change: 1 addition & 0 deletions src/components/SpaceStager.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,7 @@ const SpaceStager = () => {
responsive={responsive}
isSitePlanMode={isSitePlanMode}
isRightSidebarOpen={responsive.sidebarMode !== 'icon-rail' || isRightDrawerOpen}
exportOptions={exportOptions}
/>

{/* Center-bottom contextual nudges */}
Expand Down
22 changes: 15 additions & 7 deletions src/hooks/usePermitAreas.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export const usePermitAreas = (map, mapLoaded, options = {}) => {
const [showFocusInfo, setShowFocusInfo] = useState(false);
const [tooltip, setTooltip] = useState({ visible: false, x: 0, y: 0, content: null });
// Persistent click popover for parks mode (single at a time)
const [clickedTooltip, setClickedTooltip] = useState({ visible: false, x: 0, y: 0, lngLat: null, content: null, featureId: null });
const [clickedTooltip, setClickedTooltip] = useState({ visible: false, x: 0, y: 0, lngLat: null, content: null, featureId: null, geometry: null });
const [overlappingAreas, setOverlappingAreas] = useState([]);
const [selectedOverlapIndex, setSelectedOverlapIndex] = useState(0);
const [showOverlapSelector, setShowOverlapSelector] = useState(false);
Expand Down Expand Up @@ -268,7 +268,7 @@ export const usePermitAreas = (map, mapLoaded, options = {}) => {
clearGeoFeatureState(map, prevIdPrefix);
} catch (_) {}
// Dismiss any open click popover when mode changes
setClickedTooltip({ visible: false, x: 0, y: 0, lngLat: null, content: null, featureId: null });
setClickedTooltip({ visible: false, x: 0, y: 0, lngLat: null, content: null, featureId: null, geometry: null });
try {
// Abort any in-flight fetches
if (abortControllerRef.current) { try { abortControllerRef.current.abort(); } catch (_) {} }
Expand Down Expand Up @@ -1196,14 +1196,22 @@ export const usePermitAreas = (map, mapLoaded, options = {}) => {
try { console.debug('PERMIT: handleClickPermitFill start', { prevented: !!e?.defaultPrevented, feats: e?.features?.length, x: e?.point?.x, y: e?.point?.y }); } catch (_) {}
if (e?.defaultPrevented) { try { console.debug('PERMIT: bail defaultPrevented'); } catch (_) {} return; }
if (e.features.length === 0) return;

// Lock popover: if a popover is already visible, ignore clicks on other park zones
// User must dismiss it first before clicking another park
const activeMode = options.mode || mode;
if (activeMode === 'parks' && clickedTooltipVisibleRef.current) {
try { console.debug('PERMIT: bail popover already visible, ignoring click'); } catch (_) {}
return;
}

// Ignore clicks that intersect annotation layers to avoid clashing with annotation popup
try {
const pt = [e.point.x, e.point.y];
const layers = ['annotation-text', 'annotation-arrows', 'annotation-arrowheads'];
const annHits = map.queryRenderedFeatures && map.queryRenderedFeatures(pt, { layers });
if (annHits && annHits.length) { try { console.debug('PERMIT: bail annotation hit', { hits: annHits.length }); } catch (_) {} return; }
} catch (_) {}
const activeMode = options.mode || mode;
if (activeMode === 'intersections') return;
const drawControl = map.getControl && map.getControl('MapboxDraw');
if (drawControl && drawControl.getMode && drawControl.getMode() !== 'simple_select') { try { console.debug('PERMIT: bail drawing active'); } catch (_) {} return; }
Expand All @@ -1221,7 +1229,7 @@ export const usePermitAreas = (map, mapLoaded, options = {}) => {
try {
const lngLat = e.lngLat || map.unproject([e.point.x, e.point.y]);
const content = buildTooltipContent(top.properties, { includeStats: false });
setClickedTooltip({ visible: !!content, x: e.point.x, y: e.point.y, lngLat: lngLat ? { lng: lngLat.lng, lat: lngLat.lat } : null, content, featureId: (top.properties?.system ?? null), stats: (() => { const id = (top.properties?.CEMSID || top.properties?.cemsid || top.properties?.CEMS_ID || top.properties?.cems_id || '').toString(); const dict = eventsByCemsidRef.current || {}; return id && dict[id] ? dict[id] : null; })(), distributions: eventsDistributionsRef.current });
setClickedTooltip({ visible: !!content, x: e.point.x, y: e.point.y, lngLat: lngLat ? { lng: lngLat.lng, lat: lngLat.lat } : null, content, featureId: (top.properties?.system ?? null), geometry: top.geometry || null, stats: (() => { const id = (top.properties?.CEMSID || top.properties?.cemsid || top.properties?.CEMS_ID || top.properties?.cems_id || '').toString(); const dict = eventsByCemsidRef.current || {}; return id && dict[id] ? dict[id] : null; })(), distributions: eventsDistributionsRef.current });
setTooltip(prev => ({ ...prev, visible: false }));
} catch (_) {}
setShowOverlapSelector(false);
Expand Down Expand Up @@ -1252,7 +1260,7 @@ export const usePermitAreas = (map, mapLoaded, options = {}) => {
focusOnPermitArea(feature);
setShowOverlapSelector(false);
if (activeMode === 'parks') clearOverlapHighlights(map);
setClickedTooltip({ visible: false, x: 0, y: 0, lngLat: null, content: null, featureId: null });
setClickedTooltip({ visible: false, x: 0, y: 0, lngLat: null, content: null, featureId: null, geometry: null });
}, [map, mode, options.mode, focusOnPermitArea]);

const handleClickGeneral = useCallback((e) => {
Expand All @@ -1274,7 +1282,7 @@ export const usePermitAreas = (map, mapLoaded, options = {}) => {
setShowOverlapSelector(false);
if (mode === 'parks') {
clearOverlapHighlights(map);
setClickedTooltip({ visible: false, x: 0, y: 0, lngLat: null, content: null, featureId: null });
setClickedTooltip({ visible: false, x: 0, y: 0, lngLat: null, content: null, featureId: null, geometry: null });
}
}
}, [map, mode]);
Expand Down Expand Up @@ -1902,7 +1910,7 @@ export const usePermitAreas = (map, mapLoaded, options = {}) => {

// Expose helpers for popover UX
const dismissClickedTooltip = useCallback(() => {
setClickedTooltip({ visible: false, x: 0, y: 0, lngLat: null, content: null, featureId: null });
setClickedTooltip({ visible: false, x: 0, y: 0, lngLat: null, content: null, featureId: null, geometry: null });
}, []);

const focusClickedTooltipArea = useCallback(() => {
Expand Down
Loading