From 2acdc0f46487a03c7d00da2ce1e1680612d954ed Mon Sep 17 00:00:00 2001 From: apple Date: Sat, 7 Mar 2026 13:40:37 +0530 Subject: [PATCH 01/21] Fix landing page scroll glitch and layout: smooth scroll, Know your landscape visible - Make scroll container explicit (flex layout, main scrolls only) - Use scroll-snap proximity and GPU layer for smoother scroll - Replace backdrop-blur on sections with bg-white/10 to reduce jank - Fix layout: h-screen + navbar + footer so first section shows below navbar - Add landing-scroll-container CSS (scroll-padding, -webkit-overflow-scrolling) - Semantic structure: header, main, footer with shrink-0 for stable layout Made-with: Cursor --- src/index.css | 21 +++++++++++++++++++++ src/pages/LE_homepage.jsx | 30 ++++++++++++++++++------------ 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/index.css b/src/index.css index 172121e7..5adb48b3 100755 --- a/src/index.css +++ b/src/index.css @@ -88,6 +88,27 @@ background: #7F56D9E5; } +/* ---- Landing page: smooth scroll container (GPU-friendly, less jank) ---- */ +.landing-scroll-container { + scroll-behavior: auto; + -webkit-overflow-scrolling: touch; + scroll-snap-type: y proximity; + scroll-padding-top: 0.5rem; + scroll-padding-bottom: 0.5rem; +} +.landing-scroll-container .landing-scroll-content { + transform: translateZ(0); + backface-visibility: hidden; + min-height: 100%; +} +.landing-scroll-container section[class*="snap-start"] { + scroll-snap-align: start; + scroll-snap-stop: normal; +} +.landing-scroll-container #know-your-landscape { + scroll-margin-top: 0; +} + /* ---- Fix browser-default font-size inflation inside chart area ---- */ .chart-wrapper, .chart-wrapper * { diff --git a/src/pages/LE_homepage.jsx b/src/pages/LE_homepage.jsx index 5ed8d6eb..4eca5306 100644 --- a/src/pages/LE_homepage.jsx +++ b/src/pages/LE_homepage.jsx @@ -68,18 +68,22 @@ export default function KYLHomePage() { }; return ( -
- -
+
+ +
+
- {/* Know Section */} +
+ {/* Know Section - first section, fully visible below navbar */}
@@ -204,7 +208,7 @@ export default function KYLHomePage() {
{/* Plan Section */} -
+

@@ -329,7 +333,7 @@ export default function KYLHomePage() {

{/* Track Section */} -
+
{/* Narrow text container */}
@@ -429,9 +433,11 @@ export default function KYLHomePage() {
-
- -
+
+ +
+
+
); } From 1011a4292f6d635e99c19426da1e9cb6a7e42cde Mon Sep 17 00:00:00 2001 From: apple Date: Sat, 7 Mar 2026 17:26:45 +0530 Subject: [PATCH 02/21] Smooth landing page scroll more: scroll-padding, GPU layer, overscroll-behavior, scroll-margin Made-with: Cursor --- src/index.css | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/index.css b/src/index.css index 5adb48b3..cfe2c6bd 100755 --- a/src/index.css +++ b/src/index.css @@ -93,8 +93,13 @@ scroll-behavior: auto; -webkit-overflow-scrolling: touch; scroll-snap-type: y proximity; - scroll-padding-top: 0.5rem; - scroll-padding-bottom: 0.5rem; + scroll-padding-top: 1rem; + scroll-padding-bottom: 1rem; + overscroll-behavior-y: none; + scrollbar-gutter: stable; + transform: translateZ(0); + will-change: scroll-position; + isolation: isolate; } .landing-scroll-container .landing-scroll-content { transform: translateZ(0); @@ -104,6 +109,7 @@ .landing-scroll-container section[class*="snap-start"] { scroll-snap-align: start; scroll-snap-stop: normal; + scroll-margin-block: 0.5rem; } .landing-scroll-container #know-your-landscape { scroll-margin-top: 0; From a1f40f5ff7528e18bde22dcd57767aca24c9325c Mon Sep 17 00:00:00 2001 From: apple Date: Sun, 8 Mar 2026 20:26:22 +0530 Subject: [PATCH 03/21] Landing page: scroll-to-bottom button, Lenis sync tuning, button position Made-with: Cursor --- src/index.css | 38 ++++++++++-------- src/pages/LE_homepage.jsx | 84 ++++++++++++++++++++++++++++++++------- 2 files changed, 91 insertions(+), 31 deletions(-) diff --git a/src/index.css b/src/index.css index cfe2c6bd..f4bb2e43 100755 --- a/src/index.css +++ b/src/index.css @@ -88,31 +88,35 @@ background: #7F56D9E5; } -/* ---- Landing page: smooth scroll container (GPU-friendly, less jank) ---- */ +/* Landing page: smooth scroll (Lenis), GPU-friendly, trackpad-sync */ .landing-scroll-container { + overflow-y: scroll; + overflow-x: hidden; scroll-behavior: auto; - -webkit-overflow-scrolling: touch; - scroll-snap-type: y proximity; scroll-padding-top: 1rem; - scroll-padding-bottom: 1rem; - overscroll-behavior-y: none; - scrollbar-gutter: stable; - transform: translateZ(0); - will-change: scroll-position; - isolation: isolate; -} -.landing-scroll-container .landing-scroll-content { + scroll-padding-bottom: 1.5rem; + -webkit-overflow-scrolling: touch; + overscroll-behavior-y: contain; transform: translateZ(0); backface-visibility: hidden; - min-height: 100%; + touch-action: pan-y; + -webkit-font-smoothing: antialiased; + scrollbar-gutter: stable; } -.landing-scroll-container section[class*="snap-start"] { - scroll-snap-align: start; - scroll-snap-stop: normal; - scroll-margin-block: 0.5rem; +@media (prefers-reduced-motion: reduce) { + .landing-scroll-container { + scroll-behavior: auto; + } } -.landing-scroll-container #know-your-landscape { +.landing-scroll-content { scroll-margin-top: 0; + transform: translateZ(0); + backface-visibility: hidden; + -webkit-font-smoothing: antialiased; +} +.landing-scroll-content .landing-section { + transform: translateZ(0); + backface-visibility: hidden; } /* ---- Fix browser-default font-size inflation inside chart area ---- */ diff --git a/src/pages/LE_homepage.jsx b/src/pages/LE_homepage.jsx index 4eca5306..405a8951 100644 --- a/src/pages/LE_homepage.jsx +++ b/src/pages/LE_homepage.jsx @@ -1,6 +1,7 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router"; import { useRecoilState } from "recoil"; +import Lenis from "lenis"; import { stateDataAtom, stateAtom, @@ -23,12 +24,59 @@ import LandingNavbar from "../components/landing_navbar.jsx"; export default function KYLHomePage() { const navigate = useNavigate(); + const scrollRef = useRef(null); + const lenisRef = useRef(null); const [statesData, setStatesData] = useRecoilState(stateDataAtom); const [state, setState] = useRecoilState(stateAtom); const [district, setDistrict] = useRecoilState(districtAtom); const [block, setBlock] = useRecoilState(blockAtom); + useEffect(() => { + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; + + const el = scrollRef.current; + if (!el) return; + + const content = el.firstElementChild; + if (!content) return; + + let lenis; + const init = () => { + lenis = new Lenis({ + wrapper: el, + content, + eventsTarget: el, + smoothWheel: true, + syncTouch: true, + syncTouchLerp: 0.12, + lerp: 0.18, + wheelMultiplier: 1.1, + touchMultiplier: 1.15, + autoRaf: true, + duration: 1.15, + }); + lenisRef.current = lenis; + }; + + requestAnimationFrame(() => requestAnimationFrame(init)); + + return () => { + lenisRef.current = null; + if (lenis) lenis.destroy(); + }; + }, []); + + const scrollToBottom = () => { + if (lenisRef.current) { + const limit = lenisRef.current.limit; + lenisRef.current.scrollTo(limit?.max ?? 1e6, { duration: 1.2 }); + } else if (scrollRef.current) { + const el = scrollRef.current; + el.scrollTo({ top: el.scrollHeight - el.clientHeight, behavior: "smooth" }); + } + }; + useEffect(() => { initializeAnalytics(); trackPageView("/kyl_home"); @@ -68,22 +116,30 @@ export default function KYLHomePage() { }; return ( -
+
-
-
- {/* Know Section - first section, fully visible below navbar */} + + + + + +
+
+ {/* Know Section */}
@@ -208,7 +264,7 @@ export default function KYLHomePage() {
{/* Plan Section */} -
+

@@ -333,7 +389,7 @@ export default function KYLHomePage() {

{/* Track Section */} -
+
{/* Narrow text container */}
From 35f3e81cf67332185c1ab25e3aff6c6561027a80 Mon Sep 17 00:00:00 2001 From: apple Date: Mon, 9 Mar 2026 23:52:18 +0530 Subject: [PATCH 04/21] Merge feature/pattern-intensity-map changes into main Made-with: Cursor --- docs/ISSUE-marker-clustering.md | 45 +++++ package.json | 1 + src/App.jsx | 7 +- src/components/PatternIntensityMapModal.jsx | 164 ++++++++++++++++++ src/components/buttons/toggle_button_kyl.jsx | 7 +- src/components/dashboard_basemap.jsx | 106 ++++++++--- src/components/footer.jsx | 2 +- src/components/kyl_indicatorFilter.jsx | 1 + src/components/kyl_leftSidebar.jsx | 12 +- src/components/kyl_rightSidebar.jsx | 2 +- src/components/utils/layerStyle.jsx | 28 ++- src/components/utils/patternIntensityUtils.js | 88 ++++++++++ src/pages/AgroHorticulture.jsx | 85 +++++---- src/pages/RWBDashboard.jsx | 10 +- src/pages/kyl_dashboard.jsx | 146 +++++++++++----- 15 files changed, 587 insertions(+), 117 deletions(-) create mode 100644 docs/ISSUE-marker-clustering.md create mode 100644 src/components/PatternIntensityMapModal.jsx create mode 100644 src/components/utils/patternIntensityUtils.js diff --git a/docs/ISSUE-marker-clustering.md b/docs/ISSUE-marker-clustering.md new file mode 100644 index 00000000..ffb8cf45 --- /dev/null +++ b/docs/ISSUE-marker-clustering.md @@ -0,0 +1,45 @@ +# Issue: Improve map marker clustering for densely located plantation sites + +## Issue Title +**Improve map marker clustering for densely located plantation sites** + +--- + +## Description + +### Problem +Many green markers (plantation sites) are stacked on top of each other on the map, making it hard to distinguish and interact with individual sites. + +- **Overlapping markers:** In dense areas (e.g. under projects like *MBRDI Biodiversity Conservation - Kolar*), multiple plantation markers overlap heavily, forming large green blobs where individual sites cannot be identified. +- **Poor usability:** Users cannot easily click or select individual markers when several sites share the same or very close coordinates. +- **Scale:** With many sites (e.g. 117 sites over 114.39 hectares), overlapping is common and worsens user experience in concentrated regions. + +### Expected behavior +- Individual plantation sites are distinguishable or accessible even in dense areas. +- Users can click or expand markers to view details for a specific site. +- At lower zoom levels, markers are grouped (clustered); when zoomed in or when a cluster is expanded, individual markers become visible and clickable. + +### Suggested solution +- **Marker clustering:** Use a clustering approach (e.g. **Leaflet MarkerCluster**, **Supercluster**, or equivalent for the current map stack) so that: + - At zoomed-out levels, nearby markers are grouped into a single cluster icon with a count. + - Clusters show the number of sites they contain. +- **Expand on zoom:** When the user zooms in, clusters break apart into smaller clusters or individual markers. +- **Spiderfy (optional):** When a cluster is clicked before zooming, consider “spiderfy” behavior so markers spread out in a circle or fan, making each marker clickable without overlapping. + +### Acceptance criteria +- [ ] Dense plantation markers are clustered when they would otherwise overlap. +- [ ] Cluster icons display the number of sites in the cluster. +- [ ] Zooming in splits clusters and reveals individual markers. +- [ ] Users can click individual markers to see site details. +- [ ] Optionally: cluster click expands markers (e.g. spiderfy) for easier selection. + +### Labels (suggested) +`enhancement`, `map`, `ux`, `marker-clustering` + +### Context +- **Screen:** Map view showing plantation sites (e.g. green circular markers with plant/tree icon) under a project (e.g. MBRDI Biodiversity Conservation - Kolar). +- **Map library:** Confirm whether the app uses Leaflet, OpenLayers, or another library and choose a clustering solution that integrates with it. + +--- + +*This issue can be copied into GitHub/GitLab as a new issue.* diff --git a/package.json b/package.json index d9bb4799..5d4acf51 100755 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "date-fns": "^4.1.0", "dotenv": "^16.4.7", "file-saver": "^2.0.5", + "lenis": "^1.3.18", "lucide-react": "^0.468.0", "mathjs": "^14.8.2", "ol": "^9.0.0", diff --git a/src/App.jsx b/src/App.jsx index e4d876e9..1eb0f951 100755 --- a/src/App.jsx +++ b/src/App.jsx @@ -14,13 +14,10 @@ function App() { } /> } /> } /> - }/> - }/> + } /> + } /> } /> } /> - - - ); diff --git a/src/components/PatternIntensityMapModal.jsx b/src/components/PatternIntensityMapModal.jsx new file mode 100644 index 00000000..931572fa --- /dev/null +++ b/src/components/PatternIntensityMapModal.jsx @@ -0,0 +1,164 @@ +import React, { useEffect, useRef, useMemo } from "react"; +import Map from "ol/Map"; +import View from "ol/View"; +import TileLayer from "ol/layer/Tile"; +import XYZ from "ol/source/XYZ"; +import { Fill, Stroke, Style } from "ol/style"; +import getVectorLayers from "../actions/getVectorLayers"; +import { getPatternCountByMws, patternIntensityColor } from "./utils/patternIntensityUtils"; + +const PatternIntensityMapModal = ({ + open, + onClose, + district, + block, + dataJson, + patternSelections, +}) => { + const mapContainerRef = useRef(null); + const mapRef = useRef(null); + const mwsLayerRef = useRef(null); + + const countByMws = useMemo(() => { + if (!dataJson || !patternSelections?.selectedMWSPatterns) return {}; + return getPatternCountByMws(dataJson, patternSelections.selectedMWSPatterns); + }, [dataJson, patternSelections?.selectedMWSPatterns]); + + const maxCount = useMemo(() => { + const values = Object.values(countByMws); + return values.length ? Math.max(...values) : 0; + }, [countByMws]); + + useEffect(() => { + if (!open || !mapContainerRef.current || !district?.label || !block?.label) return; + + const baseLayer = new TileLayer({ + source: new XYZ({ + url: "https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}", + }), + zIndex: 0, + }); + + const map = new Map({ + target: mapContainerRef.current, + view: new View({ + projection: "EPSG:4326", + center: [80, 23.5], + zoom: 8, + }), + layers: [baseLayer], + }); + mapRef.current = map; + + const layerName = `deltaG_well_depth_${district.label + .toLowerCase() + .split(" ") + .join("_")}_${block.label + .toLowerCase() + .replace(/\s*\(\s*/g, "_") + .replace(/\s*\)\s*/g, "") + .replace(/\s+/g, "_")}`; + + getVectorLayers("mws_layers", layerName, true, true).then((mwsLayer) => { + if (!mapRef.current) return; + mwsLayerRef.current = mwsLayer; + mwsLayer.setStyle((feature) => { + const uid = feature.get?.("uid") ?? feature.values_?.uid; + const count = countByMws[uid] ?? 0; + const fillColor = patternIntensityColor(count, maxCount); + return new Style({ + stroke: new Stroke({ color: "#374151", width: 1 }), + fill: new Fill({ color: fillColor }), + }); + }); + mapRef.current.addLayer(mwsLayer); + const source = mwsLayer.getSource(); + const tryFit = () => { + const ext = source.getExtent(); + if (ext && ext[0] !== Infinity && !Number.isNaN(ext[0]) && source.getFeatures().length > 0) { + mapRef.current?.getView().fit(ext, { padding: [40, 40, 80, 40], maxZoom: 14 }); + source.un("change", tryFit); + } + }; + source.on("change", tryFit); + }); + + return () => { + if (mwsLayerRef.current && mapRef.current) { + mapRef.current.removeLayer(mwsLayerRef.current); + mwsLayerRef.current = null; + } + mapRef.current?.setTarget(undefined); + mapRef.current = null; + }; + }, [open, district?.label, block?.label]); + + // Update style when countByMws/maxCount change (e.g. pattern selection changed while modal open) + useEffect(() => { + if (!open || !mwsLayerRef.current) return; + mwsLayerRef.current.setStyle((feature) => { + const uid = feature.get?.("uid") ?? feature.values_?.uid; + const count = countByMws[uid] ?? 0; + const fillColor = patternIntensityColor(count, maxCount); + return new Style({ + stroke: new Stroke({ color: "#374151", width: 1 }), + fill: new Fill({ color: fillColor }), + }); + }); + }, [open, countByMws, maxCount]); + + if (!open) return null; + + return ( +
+
+
+

+ Map View (Pattern intensity) +

+ +
+
+
+ Pattern intensity: +
+ Safe +
+ 0 +
+
+
+
+
+
+
+ High stress +
+
+
+
+ ); +}; + +export default PatternIntensityMapModal; diff --git a/src/components/buttons/toggle_button_kyl.jsx b/src/components/buttons/toggle_button_kyl.jsx index ebe8cdf5..7d840da6 100644 --- a/src/components/buttons/toggle_button_kyl.jsx +++ b/src/components/buttons/toggle_button_kyl.jsx @@ -1,12 +1,13 @@ -const ToggleButton = ({isOn, toggleSwitch}) => { +const ToggleButton = ({ isOn, toggleSwitch, disabled = false }) => { return ( -
+
Visualize
diff --git a/src/components/kyl_leftSidebar.jsx b/src/components/kyl_leftSidebar.jsx index 65cfb170..01393c35 100644 --- a/src/components/kyl_leftSidebar.jsx +++ b/src/components/kyl_leftSidebar.jsx @@ -25,7 +25,8 @@ const KYLLeftSidebar = ({ getPatternsForSubcategory, patternSelections, handlePatternSelection, - isPatternSelected + isPatternSelected, + onOpenPatternIntensityMap }) => { // State to track active tab (Patterns or Filters) const [activeTab, setActiveTab] = useState('Filters'); @@ -204,6 +205,15 @@ const KYLLeftSidebar = ({ Click to Know More About Patterns + + {/* Main Category Buttons (Agriculture, Livelihood) */}
{getAllPatternTypes && getAllPatternTypes().map((category) => ( diff --git a/src/components/kyl_rightSidebar.jsx b/src/components/kyl_rightSidebar.jsx index a3998413..671e6b50 100644 --- a/src/components/kyl_rightSidebar.jsx +++ b/src/components/kyl_rightSidebar.jsx @@ -325,7 +325,7 @@ const toggleConnectivity = () => {
- + {process.env.NODE_ENV === "development" && organizationOptions.length === 0 && ( +

+ Dropdowns empty? Set REACT_APP_API_URL, REACT_APP_WATERBODYREJ_USERNAME and REACT_APP_WATERBODYREJ_PASSWORD in .env (same as production), then restart npm start. +

+ )}
diff --git a/src/pages/RWBDashboard.jsx b/src/pages/RWBDashboard.jsx index c7fc115c..39b7d356 100644 --- a/src/pages/RWBDashboard.jsx +++ b/src/pages/RWBDashboard.jsx @@ -92,11 +92,11 @@ const RWBDashboard =()=>{ }, [location.search]); const handleNavigate =()=>{ - if(!organization && !project) return; + if (!project?.value) return; const params = new URLSearchParams(location.search); params.set("type", "project"); params.set("projectId", project.value); - params.set("project_name", project.label); + params.set("project_name", project.label ?? ""); navigate( { pathname:location.pathname, @@ -178,8 +178,10 @@ const RWBDashboard =()=>{ handleItemSelect={(setState, e) => setState(e)} />
-
diff --git a/src/pages/kyl_dashboard.jsx b/src/pages/kyl_dashboard.jsx index 3fe11056..09b657b1 100644 --- a/src/pages/kyl_dashboard.jsx +++ b/src/pages/kyl_dashboard.jsx @@ -32,6 +32,7 @@ import PatternsData from '../components/data/Patterns.json'; import KYLLeftSidebar from "../components/kyl_leftSidebar"; import KYLRightSidebar from "../components/kyl_rightSidebar.jsx"; import KYLMapContainer from "../components/kyl_mapContainer.jsx"; +import PatternIntensityMapModal from "../components/PatternIntensityMapModal"; import layerStyle from "../components/utils/layerStyle.jsx"; import { getAllPatternTypes, getSubcategoriesForCategory, getPatternsForSubcategory } from '../components/utils/patternsHelper.js'; import { handlePatternSelection as handlePatternSelectionLogic, isPatternSelected } from '../components/utils/patternSelectionLogic.js'; @@ -87,11 +88,12 @@ const KYLDashboardPage = () => { const [indicatorType, setIndicatorType] = useState(null); const [showMWS, setShowMWS] = useState(true); const [showVillages, setShowVillages] = useState(true); - const [filtersEnabled, setFiltersEnabled] = useState(false); + const [filtersEnabled, setFiltersEnabled] = useState(true); const [toastId, setToastId] = useState(null); const [selectedMWSProfile, setSelectedMWSProfile] = useState(null); const [searchLatLong, setSearchLatLong] = useState(null); + const [showPatternIntensityModal, setShowPatternIntensityModal] = useState(false); // * Triggers const [filterTrigger, setFilterTrigger] = useState(0) @@ -667,27 +669,37 @@ const KYLDashboardPage = () => { return; } - // UID → coordinate map + // ------------------------- + // Create UID → coordinate map + // ------------------------- const uidToCoord = {}; + centroidFeatures.forEach((feature) => { - const uid = feature.get("uid"); + const uid = feature.get("uid") || feature.get("UID"); + if (!uid) return; + const coord = feature.getGeometry().getCoordinates(); - uidToCoord[uid] = coord; + uidToCoord[uid.toString().trim()] = coord; }); const arrowFeatures = []; + // ------------------------- + // Create arrows + // ------------------------- connectivityFeatures.forEach((feature) => { const uid = feature.get("uid"); const downstream = feature.get("downstream"); + if (!uid || !downstream) return; - const start = uidToCoord[uid]; - const end = uidToCoord[downstream]; + const start = uidToCoord[uid.toString().trim()]; + const end = uidToCoord[downstream.toString().trim()]; - if (!start || !end) return; + if (!start || !end) return; const line = new LineString([start, end]); + const arrowFeature = new Feature({ geometry: line, upstream: uid, @@ -704,40 +716,55 @@ const KYLDashboardPage = () => { const arrowLayer = new VectorLayer({ source: arrowSource, style: (feature) => { + const styles = []; + const geometry = feature.getGeometry(); const coords = geometry.getCoordinates(); - const start = coords[0]; - const end = coords[1]; + // Need at least 2 points + if (!coords || coords.length < 2) return styles; + + const start = coords[coords.length - 2]; + const end = coords[coords.length - 1]; const dx = end[0] - start[0]; const dy = end[1] - start[1]; - - // Fix: subtract Math.PI/2 to align with OL's rotation (from top, clockwise) - const rotation = -Math.atan2(dy, dx) + Math.PI / 2; - const isDownstream = end[1] < start[1]; - - const arrowColor = isDownstream ? "#39FF14" : "#FF1493"; //green down pink up - return [ - new Style({ - stroke: new Stroke({ - color: arrowColor, - width: 2.2, - lineCap: "round", - }), - }), + const len = Math.sqrt(dx * dx + dy * dy); + + // Skip zero-length or near-zero edges + if (len < 1e-6) return styles; + + const angle = Math.atan2(dy, dx); + const color = "#FF1493"; + + // Main line + styles.push( new Style({ - geometry: new Point(end), - image: new RegularShape({ - points: 3, - radius: 7, - fill: new Fill({ color: arrowColor }), - rotation: rotation, - rotateWithView: true, - angle: 0, - }), - }), + stroke: new Stroke({ color, width: 1.5 }), + }) + ); + + // Arrowhead size proportional to edge length, capped + const arrowLen = Math.min(len * 0.08, 0.006); + const arrowAngle = Math.PI / 6; + + const left = [ + end[0] - arrowLen * Math.cos(angle - arrowAngle), + end[1] - arrowLen * Math.sin(angle - arrowAngle), + ]; + const right = [ + end[0] - arrowLen * Math.cos(angle + arrowAngle), + end[1] - arrowLen * Math.sin(angle + arrowAngle), ]; + + styles.push( + new Style({ + geometry: new LineString([left, end, right]), + stroke: new Stroke({ color, width: 1.5 }), + }) + ); + + return styles; }, }); @@ -1038,10 +1065,11 @@ const KYLDashboardPage = () => { setDataJson(result); setIsLoading(false); - setFiltersEnabled(true) + setFiltersEnabled(true); } catch (e) { console.log(e); setIsLoading(false); + setFiltersEnabled(true); } }; @@ -1076,19 +1104,25 @@ const KYLDashboardPage = () => { }; const handleLayerSelection = async (filter) => { + if (!mapRef.current) return; + if (!district?.label || !block?.label) { + toast.error("Please select State, District and Tehsil first."); + return; + } let checkIfPresent = currentLayer.find((f) => f.name === filter.name); let checkIfInMap = mapRef.current.getLayers().getArray(); - let existingLayer = checkIfInMap.find((layer) => { - return layer.ol_uid === boundaryLayerRef.current.ol_uid; - }); + const boundaryLayer = boundaryLayerRef.current; + let existingLayer = boundaryLayer + ? checkIfInMap.find((layer) => layer && layer.ol_uid === boundaryLayer.ol_uid) + : false; let tempArr = currentLayer; let len = filter.layer_store.length; if (checkIfPresent) { - checkIfPresent.layerRef.map((item) => { - mapRef.current.removeLayer(item); + checkIfPresent.layerRef.forEach((item) => { + if (item) mapRef.current.removeLayer(item); }); - if (!existingLayer) { - mapRef.current.addLayer(boundaryLayerRef.current); + if (!existingLayer && boundaryLayer) { + mapRef.current.addLayer(boundaryLayer); } mwsLayerRef.current.setStyle((feature) => { if ( @@ -1124,8 +1158,8 @@ const KYLDashboardPage = () => { //setFiltersEnabled(true); } else if (currentLayer.length === 0) { let layerRef = []; - mapRef.current.removeLayer(mwsLayerRef.current); - mapRef.current.removeLayer(boundaryLayerRef.current); + if (mwsLayerRef.current) mapRef.current.removeLayer(mwsLayerRef.current); + if (boundaryLayerRef.current) mapRef.current.removeLayer(boundaryLayerRef.current); for (let i = 0; i < len; ++i) { let tempLayer; if (filter.layer_store[i] === "terrain") { @@ -1271,11 +1305,23 @@ const KYLDashboardPage = () => { color: "#254871", width: 2.0, }), + fill: new Fill({ + color: "rgba(255, 75, 75, 0.25)", + }), }); } + return new Style({ + stroke: new Stroke({ + color: "#0a1628", + width: 2, + }), + fill: new Fill({ + color: "rgba(74, 144, 226, 0.15)", + }), + }); }); - mapRef.current.addLayer(mwsLayerRef.current); - mapRef.current.addLayer(boundaryLayerRef.current); + if (mwsLayerRef.current) mapRef.current.addLayer(mwsLayerRef.current); + if (boundaryLayerRef.current) mapRef.current.addLayer(boundaryLayerRef.current); let tempObj = { name: filter.name, layerRef: layerRef, @@ -2221,6 +2267,16 @@ const KYLDashboardPage = () => { patternSelections={patternSelections} handlePatternSelection={handlePatternSelection} isPatternSelected={isPatternSelected} + onOpenPatternIntensityMap={() => setShowPatternIntensityModal(true)} + /> + + setShowPatternIntensityModal(false)} + district={district} + block={block} + dataJson={dataJson} + patternSelections={patternSelections} /> {/* Map Container */} From c4e7ba52bd005057e26f5e3df7f802c23c491d2f Mon Sep 17 00:00:00 2001 From: apple Date: Mon, 9 Mar 2026 23:53:30 +0530 Subject: [PATCH 05/21] chore: profile contribution 1/4 Made-with: Cursor From 1f11713dc0634249ab0d7c371b0ff7c779506cde Mon Sep 17 00:00:00 2001 From: apple Date: Mon, 9 Mar 2026 23:53:30 +0530 Subject: [PATCH 06/21] chore: profile contribution 2/4 Made-with: Cursor From a8bfcbe66e29dd0501189196063961187b4acb14 Mon Sep 17 00:00:00 2001 From: apple Date: Mon, 9 Mar 2026 23:53:30 +0530 Subject: [PATCH 07/21] chore: profile contribution 3/4 Made-with: Cursor From 4845daa9e64303170cc9db5bd9340c8dbc1f0152 Mon Sep 17 00:00:00 2001 From: apple Date: Mon, 9 Mar 2026 23:53:30 +0530 Subject: [PATCH 08/21] chore: profile contribution 4/4 Made-with: Cursor From 711364275b8a620dacd46578c4ef2581cfde6cc1 Mon Sep 17 00:00:00 2001 From: apple Date: Mon, 9 Mar 2026 23:55:49 +0530 Subject: [PATCH 09/21] chore: profile contribution 5/8 Made-with: Cursor From 82af22e17811f5ff6b654c3becb03ba5b049012a Mon Sep 17 00:00:00 2001 From: apple Date: Mon, 9 Mar 2026 23:55:49 +0530 Subject: [PATCH 10/21] chore: profile contribution 6/8 Made-with: Cursor From 40d8306067da2baab749db4b0b19a9bcb32fa8b0 Mon Sep 17 00:00:00 2001 From: apple Date: Mon, 9 Mar 2026 23:55:49 +0530 Subject: [PATCH 11/21] chore: profile contribution 7/8 Made-with: Cursor From e4e6cf081cd9b81ac2e5efa4c76dc84dc1892c4e Mon Sep 17 00:00:00 2001 From: apple Date: Mon, 9 Mar 2026 23:55:49 +0530 Subject: [PATCH 12/21] chore: profile contribution 8/8 Made-with: Cursor From 9f0dcbcc075f76f1a4ca7c5c771cd786e9771407 Mon Sep 17 00:00:00 2001 From: apple Date: Tue, 10 Mar 2026 14:32:41 +0530 Subject: [PATCH 13/21] chore: empty contribution commit Made-with: Cursor From 663b872cf4901b7c3401ecc7a45beef7a6a83db1 Mon Sep 17 00:00:00 2001 From: apple Date: Tue, 10 Mar 2026 14:32:41 +0530 Subject: [PATCH 14/21] chore: empty contribution commit Made-with: Cursor From bea885de66d1a928663192127b502db79ea2608b Mon Sep 17 00:00:00 2001 From: apple Date: Tue, 10 Mar 2026 14:32:41 +0530 Subject: [PATCH 15/21] chore: empty contribution commit Made-with: Cursor From 2e9ede639dc466bc021e447652eb72f4c3a57a55 Mon Sep 17 00:00:00 2001 From: apple Date: Tue, 10 Mar 2026 14:32:41 +0530 Subject: [PATCH 16/21] chore: empty contribution commit Made-with: Cursor From 775ff716268a7d58c5fabef208cb37170df84a37 Mon Sep 17 00:00:00 2001 From: apple Date: Tue, 10 Mar 2026 14:32:41 +0530 Subject: [PATCH 17/21] chore: empty contribution commit Made-with: Cursor From 784ad829ee910e8c57a267ec2ce4e27121a93bd1 Mon Sep 17 00:00:00 2001 From: apple Date: Tue, 10 Mar 2026 14:32:41 +0530 Subject: [PATCH 18/21] chore: empty contribution commit Made-with: Cursor From 9dd174ae37823b37f385ef68ad2245c17451cbfc Mon Sep 17 00:00:00 2001 From: apple Date: Tue, 10 Mar 2026 16:15:07 +0530 Subject: [PATCH 19/21] feat: add smooth scrolling navigation on KYL home Made-with: Cursor --- src/index.css | 6 +++++- src/pages/LE_homepage.jsx | 45 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/index.css b/src/index.css index f4bb2e43..7d294455 100755 --- a/src/index.css +++ b/src/index.css @@ -22,6 +22,10 @@ --inv-zoom: calc(1 / var(--browser-zoom)); } +html { + scroll-behavior: smooth; +} + .legend-container { transform: scale(var(--inv-zoom)); transform-origin: top left; @@ -92,7 +96,7 @@ .landing-scroll-container { overflow-y: scroll; overflow-x: hidden; - scroll-behavior: auto; + scroll-behavior: smooth; scroll-padding-top: 1rem; scroll-padding-bottom: 1.5rem; -webkit-overflow-scrolling: touch; diff --git a/src/pages/LE_homepage.jsx b/src/pages/LE_homepage.jsx index 405a8951..dded2507 100644 --- a/src/pages/LE_homepage.jsx +++ b/src/pages/LE_homepage.jsx @@ -77,6 +77,18 @@ export default function KYLHomePage() { } }; + const scrollToSection = (sectionId) => { + const selector = `#${sectionId}`; + if (lenisRef.current) { + lenisRef.current.scrollTo(selector, { duration: 1.1, offset: -80 }); + } else { + const el = document.querySelector(selector); + if (el) { + el.scrollIntoView({ behavior: "smooth", block: "start" }); + } + } + }; + useEffect(() => { initializeAnalytics(); trackPageView("/kyl_home"); @@ -121,6 +133,33 @@ export default function KYLHomePage() { + {/* In-page navigation for smooth scrolling between sections */} +
+
+ + + +
+
+ ))}
From 4cce3c1fd3205bf22bb71dfbc878e5e43d31c561 Mon Sep 17 00:00:00 2001 From: apple Date: Thu, 12 Mar 2026 12:48:04 +0530 Subject: [PATCH 21/21] feat: improve plantation cluster selection Made-with: Cursor --- src/components/dashboard_basemap.jsx | 140 +++++++++++++++++++++++++-- src/pages/AgroHorticulture.jsx | 28 ++++-- 2 files changed, 153 insertions(+), 15 deletions(-) diff --git a/src/components/dashboard_basemap.jsx b/src/components/dashboard_basemap.jsx index 822743e7..5cc2a345 100644 --- a/src/components/dashboard_basemap.jsx +++ b/src/components/dashboard_basemap.jsx @@ -57,6 +57,8 @@ const pendingPlantationRef = useRef(null); const pendingWaterbodyRef = useRef(null); const hasUserZoomedRef = useRef(false); + const spiderLayerRef = useRef(null); + const spiderAnchorRef = useRef(null); // coordinate of current spiderfy anchor const geojsonReaderRef = useRef(new GeoJSON()); const lulcLoadedRef = useRef(false); @@ -267,6 +269,82 @@ }); }; + const clearSpiderfy = () => { + spiderAnchorRef.current = null; + spiderLayerRef.current = null; + removeLayersById(["plantation_spider_layer"]); + }; + + const getPlantationAnchorCoord = (olFeature) => { + const geom = olFeature?.getGeometry?.(); + if (!geom) return null; + try { + if (geom.getType() === "Polygon" && geom.getInteriorPoint) { + return geom.getInteriorPoint().getCoordinates(); + } + if (geom.getType() === "MultiPolygon" && geom.getInteriorPoint) { + return geom.getInteriorPoint().getCoordinates(); + } + } catch (_) { + // fallthrough + } + const ex = geom.getExtent(); + return [(ex[0] + ex[2]) / 2, (ex[1] + ex[3]) / 2]; + }; + + const spiderfyPlantations = (clusteredFeatures, centerCoord) => { + const map = mapRef.current; + if (!map || !centerCoord || !clusteredFeatures?.length) return; + + // Toggle: clicking same cluster center again closes spiderfy + const prev = spiderAnchorRef.current; + if (prev && prev[0] === centerCoord[0] && prev[1] === centerCoord[1]) { + clearSpiderfy(); + return; + } + + clearSpiderfy(); + spiderAnchorRef.current = centerCoord; + + // Convert pixel radius to map units (EPSG:4326 degrees) + const view = map.getView(); + const resolution = view.getResolution?.() ?? 1; + const pixelRadius = Math.max(22, Math.min(48, 14 + clusteredFeatures.length * 0.35)); + const radius = pixelRadius * resolution; + + const max = Math.min(clusteredFeatures.length, 24); // cap spiderfy for performance/clarity + const slice = clusteredFeatures.slice(0, max); + const angleStep = (Math.PI * 2) / Math.max(1, slice.length); + + const spiderFeatures = slice.map((orig, i) => { + const a = i * angleStep; + const dx = radius * Math.cos(a); + const dy = radius * Math.sin(a); + const f = new Feature({ + geometry: new Point([centerCoord[0] + dx, centerCoord[1] + dy]), + }); + f.set("layerType", "plantation"); + f.set("spiderOriginal", orig); + return f; + }); + + const spiderLayer = new VectorLayer({ + source: new VectorSource({ features: spiderFeatures }), + style: new Style({ + image: new Icon({ + src: plantationIcon, + anchor: [0.5, 1], + scale: 1.1, + }), + }), + }); + spiderLayer.set("id", "plantation_spider_layer"); + spiderLayer.setZIndex(50); + + map.addLayer(spiderLayer); + spiderLayerRef.current = spiderLayer; + }; + const removeLayersBySourceParam = (needle) => { const m = mapRef.current; if (!m) return; @@ -375,21 +453,65 @@ if (!popupEl) return; if (!feature) { + clearSpiderfy(); popupEl.style.display = "none"; return; } - // Unwrap cluster: if this is a cluster feature, zoom in to expand or use single for popup + // If this is a spiderfy marker, unwrap to original plantation feature + const spiderOriginal = feature.get?.("spiderOriginal"); + if (spiderOriginal) { + feature = spiderOriginal; + clearSpiderfy(); + } + + // Unwrap cluster: if this is a cluster feature, zoom in or spiderfy to access individuals const clusterFeatures = feature.get("features"); if (clusterFeatures && clusterFeatures.length > 1) { - const view = m.getView(); - const extent = feature.getGeometry().getExtent(); - view.fit(extent, { - padding: [60, 60, 60, 60], - maxZoom: view.getZoom() + 2, - duration: 300, - }); - return; + const isPlantationCluster = + clusterFeatures?.[0]?.get?.("layerType") === "plantation"; + + // Only handle plantation clusters here; other modes fall through + if (isPlantationCluster) { + const view = m.getView(); + const zoom = view.getZoom?.() ?? 0; + const center = + feature.getGeometry?.().getCoordinates?.() ?? event.coordinate; + + // Compute spread extent from underlying features (their centroid anchors) + const coords = clusterFeatures + .map((f) => getPlantationAnchorCoord(f)) + .filter(Boolean); + + if (coords.length) { + let ex = [coords[0][0], coords[0][1], coords[0][0], coords[0][1]]; + coords.forEach(([x, y]) => { + ex[0] = Math.min(ex[0], x); + ex[1] = Math.min(ex[1], y); + ex[2] = Math.max(ex[2], x); + ex[3] = Math.max(ex[3], y); + }); + + const span = Math.max(Math.abs(ex[2] - ex[0]), Math.abs(ex[3] - ex[1])); + + // If we’re already zoomed in OR all points are basically identical → spiderfy + if (zoom >= 17 || span < 1e-6) { + spiderfyPlantations(clusterFeatures, center); + } else { + clearSpiderfy(); + view.fit(ex, { + padding: [70, 70, 70, 70], + maxZoom: 17, + duration: 300, + }); + } + return; + } + + // Fallback: if no coords, just spiderfy at click point + spiderfyPlantations(clusterFeatures, center); + return; + } } if (clusterFeatures && clusterFeatures.length === 1) { feature = clusterFeatures[0]; diff --git a/src/pages/AgroHorticulture.jsx b/src/pages/AgroHorticulture.jsx index f6c16436..45404a3b 100644 --- a/src/pages/AgroHorticulture.jsx +++ b/src/pages/AgroHorticulture.jsx @@ -12,6 +12,7 @@ const AgroHorticulture =()=>{ const [projectOptions, setProjectOptions] = useState([]); const [project, setProject] = useState(null); const [showPlantationSites, setShowPlantationSites] = useState(false); + const [accessToken, setAccessToken] = useState(() => sessionStorage.getItem("accessToken")); const navigate = useNavigate(); const location = useLocation(); @@ -55,11 +56,19 @@ const AgroHorticulture =()=>{ }, ) const data = await respone.json(); + if (!respone.ok || !data?.access) { + console.warn("Login failed:", respone.status, data); + setAccessToken(null); + sessionStorage.removeItem("accessToken"); + return null; + } sessionStorage.setItem("accessToken",data.access); + setAccessToken(data.access); return data.access; } catch(error){ console.warn("Error in fetching token",error) + setAccessToken(null); } }; @@ -67,10 +76,11 @@ const AgroHorticulture =()=>{ if(!organization?.value){ setProject(null); setProjectOptions([]); + return; }; setProject(null); loadProjects(organization?.value); - },[organization]); + },[organization, accessToken]); const handleNavigate =()=>{ if (!project?.value) return; @@ -90,11 +100,14 @@ const AgroHorticulture =()=>{ setProjectOptions([]); return; } - const token = sessionStorage.getItem("accessToken"); + let token = accessToken || sessionStorage.getItem("accessToken"); if (!token) { - console.warn("No access token: set REACT_APP_WATERBODYREJ_USERNAME and REACT_APP_WATERBODYREJ_PASSWORD in .env and restart the dev server."); - setProjectOptions([]); - return; + token = await fetchToken(); + if (!token) { + console.warn("No access token: set REACT_APP_WATERBODYREJ_USERNAME and REACT_APP_WATERBODYREJ_PASSWORD in .env and restart the dev server."); + setProjectOptions([]); + return; + } } try { const projects = await fetch (`${process.env.REACT_APP_API_URL}/projects`, { @@ -113,7 +126,10 @@ const AgroHorticulture =()=>{ setProjectOptions([]); return; } - const options = projectResult.filter((p)=>p.organization==orgId).map((p)=>({ + const orgIdNum = Number(orgId); + const options = projectResult + .filter((p)=> Number(p.organization) === orgIdNum) + .map((p)=>({ label: p.name, value: p.id, }));