Skip to content
Open
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
4 changes: 2 additions & 2 deletions src/components/dashboard_basemap.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1188,7 +1188,7 @@

const [terrainLayer, drainageLayer] = await Promise.all([
getImageLayer("terrain", terrainKey, true, "Terrain_Style_11_Classes").catch(() => null),
getVectorLayers("drainage", drainageKey, true, "drainage").catch(() => null),
getVectorLayers("drainage", drainageKey, true, true).catch(() => null),
]);

if (terrainLayer) {
Expand Down Expand Up @@ -1332,7 +1332,7 @@ if (isTehsil) {

const [terrainLayer, drainageLayer] = await Promise.all([
getImageLayer("terrain", terrainKey, true, "Terrain_Style_11_Classes").catch(() => null),
getVectorLayers("drainage", drainageKey, true, "drainage").catch(() => null),
getVectorLayers("drainage", drainageKey, true, true).catch(() => null),
]);

// Clip terrain to MultiPolygon
Expand Down
53 changes: 51 additions & 2 deletions src/components/landscape-explorer/map/Map.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -819,12 +819,18 @@ const Map = forwardRef(({
setShowVillages,
lulcYear1,
lulcYear2,
lulcYear3
lulcYear3,
onViewChange
}, ref) => {
const mapElement = useRef(null);
const mapRef = useRef(null);
const baseLayerRef = useRef(null);
const markersLayer = useRef(null);
const onViewChangeRef = useRef(onViewChange);

useEffect(() => {
onViewChangeRef.current = onViewChange;
}, [onViewChange]);

// Added flag to prevent recursion
const handlingExternalToggle = useRef(false);
Expand Down Expand Up @@ -931,6 +937,7 @@ const Map = forwardRef(({
'degradation': 'Change Detection Degradation',
'urbanization': 'Change Detection Urbanization',
'cropIntensity': 'Change Detection Crop-Intensity',
'cropintensity': 'Change Detection Crop-Intensity',
'restoration': 'Change Detection Restoration',
'soge': 'SOGE',
'aquifer': 'Aquifer',
Expand Down Expand Up @@ -963,7 +970,25 @@ const Map = forwardRef(({
});
}
},
getMap: () => mapRef.current
getMap: () => mapRef.current,
getViewSnapshot: () => {
if (!mapRef.current) return null;
const view = mapRef.current.getView();
const center = view.getCenter();
const zoom = view.getZoom();
if (!center || zoom == null) return null;
return { center, zoom };
},
applyView: ({ center, zoom }) => {
if (!mapRef.current) return;
const view = mapRef.current.getView();
if (center && Array.isArray(center) && center.length >= 2) {
view.setCenter(center);
}
if (typeof zoom === "number" && !Number.isNaN(zoom)) {
view.setZoom(zoom);
}
}
}));

// Get block features (copied from original implementation)
Expand Down Expand Up @@ -2307,6 +2332,29 @@ const Map = forwardRef(({
};
}, []);

// Report map viewport for shareable URL (debounced moveend)
useEffect(() => {
if (!isInitialized || !mapRef.current || !onViewChange) return;
const map = mapRef.current;
let timeoutId;
const handler = () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
const view = map.getView();
const center = view.getCenter();
const zoom = view.getZoom();
if (center && zoom != null) {
onViewChangeRef.current?.({ center, zoom });
}
}, 400);
};
map.on("moveend", handler);
return () => {
if (timeoutId) clearTimeout(timeoutId);
map.un("moveend", handler);
};
}, [isInitialized, onViewChange]);

// When state changes, update district markers
useEffect(() => {
if (mapRef.current && state && !district) {
Expand Down Expand Up @@ -2371,6 +2419,7 @@ const Map = forwardRef(({
'degradation': 'Change Detection Degradation',
'urbanization': 'Change Detection Urbanization',
'cropintensity': 'Change Detection Crop-Intensity',
'cropIntensity': 'Change Detection Crop-Intensity',
'restoration': 'Change Detection Restoration',
'soge': 'SOGE',
'aquifer': 'Aquifer',
Expand Down
154 changes: 154 additions & 0 deletions src/pages/LandscapeExplorer.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef, useCallback } from "react";
import { useSearchParams } from "react-router-dom";
import Map from "../components/landscape-explorer/map/Map.jsx";
import LeftSidebar from "../components/landscape-explorer/sidebar/LeftSidebar.jsx";
import RightSidebar from "../components/landscape-explorer/sidebar/RightSidebar.jsx";
Expand All @@ -19,8 +20,17 @@ import {
initializeAnalytics,
} from "../services/analytics";
import LandingNavbar from "../components/landing_navbar.jsx";
import {
buildSearchParams,
findLocationInStatesData,
hasUrlMapParams,
lulcYearFromParam,
parseViewParams,
toggledLayersFromLayersParam,
} from "../utils/landscapeExplorerUrlState";

const LandscapeExplorer = () => {
const [, setSearchParams] = useSearchParams();
const [showLeftSidebar, setShowLeftSidebar] = useState(false);
const [showRightSidebar, setShowRightSidebar] = useState(true);
const [isLoading, setIsLoading] = useState(false);
Expand All @@ -39,6 +49,13 @@ const LandscapeExplorer = () => {
// Map ref for accessing map instance from other components
const mapRef = useRef(null);

/** Apply map center/zoom from URL after block layers load (OpenLayers fits extent first). */
const pendingViewFromUrlRef = useRef(null);
const hydratedUrlRef = useRef(false);
const [urlSyncReady, setUrlSyncReady] = useState(false);
const [mapView, setMapView] = useState(null);
const [linkCopied, setLinkCopied] = useState(false);

// Add flag to prevent infinite recursion
const isUpdatingFromMap = useRef(false);

Expand Down Expand Up @@ -343,6 +360,132 @@ const LandscapeExplorer = () => {
}
}, [statesData, setStatesData]);

// Hydrate location, layers, LULC, and view from URL (once when states load)
useEffect(() => {
if (!statesData || hydratedUrlRef.current) return;
const params = new URLSearchParams(window.location.search);
if (!hasUrlMapParams(params)) {
hydratedUrlRef.current = true;
setUrlSyncReady(true);
return;
}
hydratedUrlRef.current = true;

const stateLabel = params.get("state");
const districtLabel = params.get("district");
const blockLabel = params.get("block");
const found = findLocationInStatesData(
statesData,
stateLabel,
districtLabel,
blockLabel
);
if (found?.state) setState(found.state);
if (found?.district) setDistrict(found.district);
if (found?.block) {
setBlock(found.block);
setCanFetchLayers(true);
setLayersReady(true);
}

const mergedLayers = toggledLayersFromLayersParam(params.get("layers"));
if (mergedLayers) setToggledLayers(mergedLayers);

const l1 = lulcYearFromParam(params.get("lulc1"));
const l2 = lulcYearFromParam(params.get("lulc2"));
const l3 = lulcYearFromParam(params.get("lulc3"));
if (l1) setLulcYear1(l1);
if (l2) setLulcYear2(l2);
if (l3) setLulcYear3(l3);

const parsedView = parseViewParams(params);
if (parsedView) {
pendingViewFromUrlRef.current = parsedView;
setMapView({
center: parsedView.center,
zoom: parsedView.zoom,
});
}
setUrlSyncReady(true);
}, [statesData, setState, setDistrict, setBlock]);

// Keep the URL in sync with app state (shareable links, refresh)
useEffect(() => {
if (!urlSyncReady) return;
const id = setTimeout(() => {
const params = buildSearchParams({
state,
district,
block,
toggledLayers,
lulcYear1,
lulcYear2,
lulcYear3,
mapView,
});
setSearchParams(params, { replace: true });
}, 450);
return () => clearTimeout(id);
}, [
urlSyncReady,
state,
district,
block,
toggledLayers,
lulcYear1,
lulcYear2,
lulcYear3,
mapView,
setSearchParams,
]);

// After URL-driven location is set, re-apply saved bbox view (map fits block first)
useEffect(() => {
if (!block || !pendingViewFromUrlRef.current) return;
const view = pendingViewFromUrlRef.current;
const timer = setTimeout(() => {
mapRef.current?.applyView?.(view);
pendingViewFromUrlRef.current = null;
}, 2000);
return () => clearTimeout(timer);
}, [block, state, district]);

const handleMapViewChange = useCallback(({ center, zoom }) => {
setMapView({ center, zoom });
}, []);

const copyShareableLink = useCallback(() => {
const liveView = mapRef.current?.getViewSnapshot?.() || mapView;
const params = buildSearchParams({
state,
district,
block,
toggledLayers,
lulcYear1,
lulcYear2,
lulcYear3,
mapView: liveView || undefined,
});
const url = `${window.location.origin}${window.location.pathname}?${params.toString()}`;
if (navigator.clipboard?.writeText) {
navigator.clipboard.writeText(url).then(() => {
setLinkCopied(true);
setTimeout(() => setLinkCopied(false), 2000);
});
} else {
window.prompt("Copy this link:", url);
}
}, [
state,
district,
block,
toggledLayers,
lulcYear1,
lulcYear2,
lulcYear3,
mapView,
]);

// Handle map-initiated layer toggle updates
const handleMapToggle = (layerName, isVisible) => {
// Set the recursion prevention flag
Expand Down Expand Up @@ -417,7 +560,18 @@ const LandscapeExplorer = () => {
lulcYear1={lulcYear1}
lulcYear2={lulcYear2}
lulcYear3={lulcYear3}
onViewChange={handleMapViewChange}
/>

<div className="absolute bottom-4 left-4 z-20 flex items-center gap-2">
<button
type="button"
onClick={copyShareableLink}
className="rounded-md bg-white/95 px-3 py-2 text-sm font-medium text-gray-800 shadow-md ring-1 ring-gray-200 hover:bg-gray-50"
>
{linkCopied ? "Link copied" : "Copy link to this view"}
</button>
</div>
</div>

{showRightSidebar && (
Expand Down
Loading