From a55cd63c23a51a47d1e9d1faab011ee56659728e Mon Sep 17 00:00:00 2001 From: manvi Date: Wed, 25 Feb 2026 14:25:00 +0530 Subject: [PATCH 1/7] MWS connecity arrow width and direction improvement --- src/components/kyl_rightSidebar.jsx | 2 +- src/pages/kyl_dashboard.jsx | 89 ++++++++++++++++++----------- 2 files changed, 58 insertions(+), 33 deletions(-) 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 = () => { + + +
+
+ {loading && ( +
+ Loading map… +
+ )} + {error && ( +
+

{error}

+
+ )} +
+ +
+ Intensity (stress patterns): +
+ Safe +
+ High stress +
+

+ Green = no stress patterns; yellow/orange = moderate; red = high number of patterns matched. +

+
+
+
+ ); +}; + +export default PatternIntensityMapModal; diff --git a/src/components/kyl_leftSidebar.jsx b/src/components/kyl_leftSidebar.jsx index 65cfb170..2dc51212 100644 --- a/src/components/kyl_leftSidebar.jsx +++ b/src/components/kyl_leftSidebar.jsx @@ -25,7 +25,9 @@ const KYLLeftSidebar = ({ getPatternsForSubcategory, patternSelections, handlePatternSelection, - isPatternSelected + isPatternSelected, + onOpenPatternIntensityMap, + hasLocationSelected, }) => { // State to track active tab (Patterns or Filters) const [activeTab, setActiveTab] = useState('Filters'); @@ -204,6 +206,25 @@ const KYLLeftSidebar = ({ Click to Know More About Patterns + {/* Pattern intensity map view */} + + {!hasLocationSelected && ( +

Select State, District & Tehsil to enable

+ )} + {/* Main Category Buttons (Agriculture, Livelihood) */}
{getAllPatternTypes && getAllPatternTypes().map((category) => ( diff --git a/src/components/utils/patternIntensityUtils.js b/src/components/utils/patternIntensityUtils.js new file mode 100644 index 00000000..d1089286 --- /dev/null +++ b/src/components/utils/patternIntensityUtils.js @@ -0,0 +1,63 @@ +/** + * For each MWS, counts how many selected patterns it matches (OR within pattern). + * Used for pattern intensity map: green (0) → yellow/orange → red (max). + * @param {Array} dataJson - MWS data rows (each with mws_id and indicator keys) + * @param {Object} selectedMWSPatterns - { patternName: { conditions: [{ type, key, value }] } } + * @returns {Object} { mws_id: number } pattern count per MWS + */ +export function getPatternCountByMws(dataJson, selectedMWSPatterns) { + const counts = {}; + if (!dataJson || !Array.isArray(dataJson) || !selectedMWSPatterns) return counts; + + const patternNames = Object.keys(selectedMWSPatterns); + if (patternNames.length === 0) { + dataJson.forEach((item) => { + if (item && item.mws_id != null) counts[item.mws_id] = 0; + }); + return counts; + } + + dataJson.forEach((item) => { + if (!item || item.mws_id == null) return; + let count = 0; + patternNames.forEach((patternName) => { + const pattern = selectedMWSPatterns[patternName]; + if (!pattern || !Array.isArray(pattern.conditions)) return; + const matchesPattern = pattern.conditions.some((condition) => { + if (condition.type === 1) return item[condition.key] === condition.value; + if (condition.type === 2) + return ( + item[condition.key] >= condition.value.lower && + item[condition.key] <= condition.value.upper + ); + if (condition.type === 3) return item[condition.key] != condition.value; + return false; + }); + if (matchesPattern) count += 1; + }); + counts[item.mws_id] = count; + }); + return counts; +} + +/** + * Interpolate color from green (0) → yellow/orange (0.5) → red (1). + * @param {number} t - 0 to 1 + * @returns {string} rgba color + */ +export function patternIntensityColor(t) { + if (t <= 0) return "rgba(34, 197, 94, 0.75)"; // green - safe + if (t >= 1) return "rgba(220, 38, 38, 0.75)"; // red - high stress + if (t <= 0.5) { + const s = t * 2; // 0..1 over first half + const r = Math.round(34 + (234 - 34) * s); + const g = Math.round(197 + (179 - 197) * s); + const b = Math.round(94 + (44 - 94) * s); + return `rgba(${r}, ${g}, ${b}, 0.75)`; + } + const s = (t - 0.5) * 2; // 0..1 over second half + const r = Math.round(234 + (220 - 234) * s); + const g = Math.round(179 + (38 - 179) * s); + const b = Math.round(44 + (38 - 44) * s); + return `rgba(${r}, ${g}, ${b}, 0.75)`; +} diff --git a/src/pages/kyl_dashboard.jsx b/src/pages/kyl_dashboard.jsx index a75a6b5c..477ec00f 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.jsx"; 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'; @@ -92,6 +93,7 @@ const KYLDashboardPage = () => { 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) @@ -2246,6 +2248,8 @@ const KYLDashboardPage = () => { patternSelections={patternSelections} handlePatternSelection={handlePatternSelection} isPatternSelected={isPatternSelected} + onOpenPatternIntensityMap={() => setShowPatternIntensityModal(true)} + hasLocationSelected={!!(district && block)} /> {/* Map Container */} @@ -2306,6 +2310,15 @@ const KYLDashboardPage = () => { />
+ + setShowPatternIntensityModal(false)} + district={district} + block={block} + dataJson={dataJson} + patternSelections={patternSelections} + /> ); }; From c702068870b0e7fb7cc57110c4cab1ffe69a9b35 Mon Sep 17 00:00:00 2001 From: apple Date: Sun, 8 Mar 2026 20:26:22 +0530 Subject: [PATCH 6/7] 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 6a789245fca469ca80eded122ce4f021532cfd38 Mon Sep 17 00:00:00 2001 From: apple Date: Mon, 9 Mar 2026 23:45:42 +0530 Subject: [PATCH 7/7] Update KYL UI and docs Made-with: Cursor --- docs/ISSUE-marker-clustering.md | 45 ++++ package.json | 1 + src/App.jsx | 7 +- src/components/PatternIntensityMapModal.jsx | 235 ++++++++---------- 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 | 17 +- src/components/utils/layerStyle.jsx | 28 ++- src/components/utils/patternIntensityUtils.js | 115 +++++---- src/pages/AgroHorticulture.jsx | 85 ++++--- src/pages/RWBDashboard.jsx | 10 +- src/pages/kyl_dashboard.jsx | 66 +++-- 14 files changed, 447 insertions(+), 278 deletions(-) create mode 100644 docs/ISSUE-marker-clustering.md 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 index 0c349318..931572fa 100644 --- a/src/components/PatternIntensityMapModal.jsx +++ b/src/components/PatternIntensityMapModal.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef, useMemo } from "react"; import Map from "ol/Map"; import View from "ol/View"; import TileLayer from "ol/layer/Tile"; @@ -18,158 +18,143 @@ const PatternIntensityMapModal = ({ const mapContainerRef = useRef(null); const mapRef = useRef(null); const mwsLayerRef = useRef(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - useEffect(() => { - if (!open || !mapContainerRef.current || !district?.label || !block?.label) return; - - let mounted = true; - const container = mapContainerRef.current; - - const initMap = async () => { - setLoading(true); - setError(null); - try { - const baseLayer = new TileLayer({ - source: new XYZ({ - url: "https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}", - maxZoom: 30, - }), - }); - - const map = new Map({ - target: container, - layers: [baseLayer], - view: new View({ - center: [0, 0], - zoom: 4, - }), - }); - mapRef.current = map; + const countByMws = useMemo(() => { + if (!dataJson || !patternSelections?.selectedMWSPatterns) return {}; + return getPatternCountByMws(dataJson, patternSelections.selectedMWSPatterns); + }, [dataJson, patternSelections?.selectedMWSPatterns]); - const layerName = `deltaG_well_depth_${district.label - .toLowerCase() - .split(" ") - .join("_")}_${block.label - .toLowerCase() - .replace(/\s*\(\s*/g, "_") - .replace(/\s*\)\s*/g, "") - .replace(/\s+/g, "_")}`; + const maxCount = useMemo(() => { + const values = Object.values(countByMws); + return values.length ? Math.max(...values) : 0; + }, [countByMws]); - const mwsLayer = await getVectorLayers("mws_layers", layerName, true, true); - if (!mounted) return; - - mwsLayerRef.current = mwsLayer; - map.addLayer(mwsLayer); - - const countByMws = getPatternCountByMws( - dataJson || [], - patternSelections?.selectedMWSPatterns || {} - ); - const maxCount = Math.max(1, ...Object.values(countByMws)); + useEffect(() => { + if (!open || !mapContainerRef.current || !district?.label || !block?.label) return; - mwsLayer.setStyle((feature) => { - const uid = feature.get("uid"); - const count = uid != null ? countByMws[uid] ?? 0 : 0; - const t = maxCount > 0 ? count / maxCount : 0; - const color = patternIntensityColor(t); - return new Style({ - stroke: new Stroke({ color: "#1f2937", width: 1 }), - fill: new Fill({ color }), - }); + 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); + }); - const source = mwsLayer.getSource(); - const fitView = () => { - const features = source.getFeatures(); - if (features.length > 0) { - try { - map.getView().fit(source.getExtent(), { - padding: [40, 40, 40, 40], - maxZoom: 14, - duration: 300, - }); - } catch (_) {} - } - }; - source.on("change", fitView); - setTimeout(fitView, 800); - } catch (err) { - if (mounted) setError(err?.message || "Failed to load map"); - } finally { - if (mounted) setLoading(false); - } - }; - - initMap(); return () => { - mounted = false; - if (mapRef.current) { - mapRef.current.setTarget(undefined); - mapRef.current = null; + if (mwsLayerRef.current && mapRef.current) { + mapRef.current.removeLayer(mwsLayerRef.current); + mwsLayerRef.current = null; } - mwsLayerRef.current = null; + mapRef.current?.setTarget(undefined); + mapRef.current = null; }; - }, [open, district?.label, block?.label, dataJson, patternSelections?.selectedMWSPatterns]); + }, [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 ( -
e.target === e.currentTarget && onClose()} - role="dialog" - aria-modal="true" - aria-labelledby="pattern-intensity-map-title" - > -
e.stopPropagation()} - > +
+
-

Pattern intensity across MWS

+

+ Map View (Pattern intensity) +

- -
-
- {loading && ( -
- Loading map… -
- )} - {error && ( -
-

{error}

-
- )} -
- -
- Intensity (stress patterns): +
+
+ Pattern intensity:
Safe
+ 0 +
+
+
+
+
+
+
High stress
-

- Green = no stress patterns; yellow/orange = moderate; red = high number of patterns matched. -

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 2dc51212..01393c35 100644 --- a/src/components/kyl_leftSidebar.jsx +++ b/src/components/kyl_leftSidebar.jsx @@ -26,8 +26,7 @@ const KYLLeftSidebar = ({ patternSelections, handlePatternSelection, isPatternSelected, - onOpenPatternIntensityMap, - hasLocationSelected, + onOpenPatternIntensityMap }) => { // State to track active tab (Patterns or Filters) const [activeTab, setActiveTab] = useState('Filters'); @@ -206,24 +205,14 @@ const KYLLeftSidebar = ({ Click to Know More About Patterns - {/* Pattern intensity map view */} - {!hasLocationSelected && ( -

Select State, District & Tehsil to enable

- )} {/* Main Category Buttons (Agriculture, Livelihood) */}
diff --git a/src/components/utils/layerStyle.jsx b/src/components/utils/layerStyle.jsx index 7aa56156..adf1c0d9 100644 --- a/src/components/utils/layerStyle.jsx +++ b/src/components/utils/layerStyle.jsx @@ -7,15 +7,24 @@ const layerStyles = (feature, vectorStyle, idx = 0, villageJson, dataJson) => { let tempIdx = 0; let avg_Res = 0; + // High-visibility boundaries: strong contrast, thick stroke, dashed for distinct adjacent polygons + const defaultStroke = "rgba(0, 12, 40, 1)"; + const defaultFill = "rgba(200, 230, 255, 0.45)"; + const strokeWidth = 6; + const lineDash = [16, 8]; + + const safeVectorStyle = Array.isArray(vectorStyle) && vectorStyle.length > 0 ? vectorStyle : [{ stroke: defaultStroke, fill: defaultFill }]; + switch (idx) { case 0: return new Style({ stroke: new Stroke({ - color: vectorStyle[0].stroke !== undefined ? vectorStyle[0].stroke : "#006400", - width: 1.0, + color: safeVectorStyle[0].stroke !== undefined ? safeVectorStyle[0].stroke : defaultStroke, + width: strokeWidth, + lineDash, }), fill: new Fill({ - color: vectorStyle[0].fill !== undefined ? vectorStyle[0].fill : "rgba(144, 238, 144, 0.3)", + color: safeVectorStyle[0].fill !== undefined ? safeVectorStyle[0].fill : defaultFill, }) }) case 1: @@ -156,18 +165,21 @@ const layerStyles = (feature, vectorStyle, idx = 0, villageJson, dataJson) => { break; } - for(tempIdx = 0; tempIdx < vectorStyle.length; ++tempIdx){ - if(avg_Res >= vectorStyle[tempIdx].lower && avg_Res <= vectorStyle[tempIdx].upper){ + for(tempIdx = 0; tempIdx < safeVectorStyle.length; ++tempIdx){ + if(avg_Res >= safeVectorStyle[tempIdx].lower && avg_Res <= safeVectorStyle[tempIdx].upper){ break; } } + if (tempIdx >= safeVectorStyle.length) tempIdx = 0; + const styleEntry = safeVectorStyle[tempIdx]; return new Style({ stroke: new Stroke({ - color: vectorStyle[tempIdx].stroke !== undefined ? vectorStyle[tempIdx].stroke : "#006400", - width: 1.0, + color: styleEntry && styleEntry.stroke !== undefined ? styleEntry.stroke : defaultStroke, + width: strokeWidth, + lineDash, }), fill: new Fill({ - color: vectorStyle[tempIdx].fill !== undefined ? vectorStyle[tempIdx].fill : "rgba(144, 238, 144, 0.3)", + color: styleEntry && styleEntry.fill !== undefined ? styleEntry.fill : defaultFill, }) }) } diff --git a/src/components/utils/patternIntensityUtils.js b/src/components/utils/patternIntensityUtils.js index d1089286..e7b4f0e8 100644 --- a/src/components/utils/patternIntensityUtils.js +++ b/src/components/utils/patternIntensityUtils.js @@ -1,63 +1,88 @@ /** - * For each MWS, counts how many selected patterns it matches (OR within pattern). - * Used for pattern intensity map: green (0) → yellow/orange → red (max). - * @param {Array} dataJson - MWS data rows (each with mws_id and indicator keys) - * @param {Object} selectedMWSPatterns - { patternName: { conditions: [{ type, key, value }] } } + * Pattern intensity: per-MWS count of how many selected patterns each MWS matches. + * Used to style MWS polygons (green = safe, red = high stress). + */ + +/** + * Check if a single data row matches a single condition. + * @param {Object} item - dataJson row (has mws_id and filter keys) + * @param {Object} condition - { key, type, value } (value may be { lower, upper } for type 2) + */ +function itemMatchesCondition(item, condition) { + if (!item || !condition || item[condition.key] === undefined) return false; + if (condition.type === 1) return item[condition.key] === condition.value; + if (condition.type === 2) { + const v = Number(item[condition.key]); + if (isNaN(v)) return false; + return v >= condition.value.lower && v <= condition.value.upper; + } + if (condition.type === 3) return item[condition.key] != condition.value; + return false; +} + +/** + * Check if a data row matches any condition of a pattern (OR within pattern). + */ +function itemMatchesPattern(item, pattern) { + if (!pattern?.conditions?.length) return false; + return pattern.conditions.some((c) => itemMatchesCondition(item, c)); +} + +/** + * For each MWS, count how many selected patterns it matches (any row for that MWS). + * @param {Array} dataJson - list of { mws_id, ... } + * @param {Object} selectedMWSPatterns - { patternName: { conditions: [...] } } * @returns {Object} { mws_id: number } pattern count per MWS */ export function getPatternCountByMws(dataJson, selectedMWSPatterns) { const counts = {}; - if (!dataJson || !Array.isArray(dataJson) || !selectedMWSPatterns) return counts; + if (!Array.isArray(dataJson) || !selectedMWSPatterns) return counts; - const patternNames = Object.keys(selectedMWSPatterns); - if (patternNames.length === 0) { + const entries = Object.entries(selectedMWSPatterns).filter(([, p]) => p); + if (entries.length === 0) return counts; + + // Per pattern: set of mws_id that match (OR over conditions, any row) + const mwsIdsByPattern = entries.map(([, pattern]) => { + const set = new Set(); dataJson.forEach((item) => { - if (item && item.mws_id != null) counts[item.mws_id] = 0; + if (!item?.mws_id) return; + if (itemMatchesPattern(item, pattern)) set.add(item.mws_id); }); - return counts; - } + return set; + }); - dataJson.forEach((item) => { - if (!item || item.mws_id == null) return; - let count = 0; - patternNames.forEach((patternName) => { - const pattern = selectedMWSPatterns[patternName]; - if (!pattern || !Array.isArray(pattern.conditions)) return; - const matchesPattern = pattern.conditions.some((condition) => { - if (condition.type === 1) return item[condition.key] === condition.value; - if (condition.type === 2) - return ( - item[condition.key] >= condition.value.lower && - item[condition.key] <= condition.value.upper - ); - if (condition.type === 3) return item[condition.key] != condition.value; - return false; - }); - if (matchesPattern) count += 1; - }); - counts[item.mws_id] = count; + // All MWS ids that appear in data or in any pattern set + const allMwsIds = new Set(); + dataJson.forEach((item) => item?.mws_id && allMwsIds.add(item.mws_id)); + + allMwsIds.forEach((mwsId) => { + counts[mwsId] = mwsIdsByPattern.filter((set) => set.has(mwsId)).length; }); + return counts; } /** - * Interpolate color from green (0) → yellow/orange (0.5) → red (1). - * @param {number} t - 0 to 1 - * @returns {string} rgba color + * Interpolate color from green (safe) -> yellow/orange -> red (high stress). + * @param {number} count - pattern count for this MWS + * @param {number} maxCount - max count in current set (for normalization) + * @returns {string} rgba fill color */ -export function patternIntensityColor(t) { - if (t <= 0) return "rgba(34, 197, 94, 0.75)"; // green - safe - if (t >= 1) return "rgba(220, 38, 38, 0.75)"; // red - high stress +export function patternIntensityColor(count, maxCount) { + if (maxCount <= 0) return "rgba(34, 197, 94, 0.5)"; // green default + const t = maxCount === 0 ? 0 : Math.min(1, count / maxCount); + // Green (34,197,94) -> Yellow (234,179,8) -> Red (220,38,38) + let r, g, b; if (t <= 0.5) { - const s = t * 2; // 0..1 over first half - const r = Math.round(34 + (234 - 34) * s); - const g = Math.round(197 + (179 - 197) * s); - const b = Math.round(94 + (44 - 94) * s); - return `rgba(${r}, ${g}, ${b}, 0.75)`; + const s = t * 2; // 0..1 + r = Math.round(34 + (234 - 34) * s); + g = Math.round(197 + (179 - 197) * s); + b = Math.round(94 + (8 - 94) * s); + } else { + const s = (t - 0.5) * 2; + r = Math.round(234 + (220 - 234) * s); + g = Math.round(179 + (38 - 179) * s); + b = Math.round(8 + (38 - 8) * s); } - const s = (t - 0.5) * 2; // 0..1 over second half - const r = Math.round(234 + (220 - 234) * s); - const g = Math.round(179 + (38 - 179) * s); - const b = Math.round(44 + (38 - 44) * s); - return `rgba(${r}, ${g}, ${b}, 0.75)`; + return `rgba(${r},${g},${b},0.55)`; } diff --git a/src/pages/AgroHorticulture.jsx b/src/pages/AgroHorticulture.jsx index 2957ec19..f6c16436 100644 --- a/src/pages/AgroHorticulture.jsx +++ b/src/pages/AgroHorticulture.jsx @@ -19,15 +19,19 @@ const AgroHorticulture =()=>{ const loadOrganization = async()=>{ try{ const orgData = await fetch (`${process.env.REACT_APP_API_URL}/auth/register/available_organizations/?app_type=plantation`); - const orgResult = await orgData.json(); - const options = orgResult.filter(org=>org.name && org.id).map((org=>({ - label: org.name, - value: org.id, - }))) - setOrganizationOptions(options) + const orgResult = await orgData.json(); + if (!orgData.ok || !Array.isArray(orgResult)) { + console.warn("Organizations API error or non-array:", orgData.status, orgResult); + return; + } + const options = orgResult.filter(org=>org.name && org.id).map((org=>({ + label: org.name, + value: org.id, + }))); + setOrganizationOptions(options); } catch(error){ - console.warn("Not loading org",error) + console.warn("Not loading org (check REACT_APP_API_URL and network/CORS):", error); } } loadOrganization(); @@ -69,9 +73,9 @@ const AgroHorticulture =()=>{ },[organization]); const handleNavigate =()=>{ - if(!organization && !project) return; + if (!project?.value) return; const params = new URLSearchParams(location.search); - params.set("projectId",project.value) + params.set("projectId", project.value); navigate( { pathname:location.pathname, @@ -82,26 +86,42 @@ const AgroHorticulture =()=>{ }; const loadProjects = async(orgId)=>{ - let token = sessionStorage.getItem("accessToken") - try{ - const projects = await fetch (`${process.env.REACT_APP_API_URL}/projects`, - { - headers:{ - Authorization:`Bearer ${token}`, - "Content-Type": "application/json", - } - } - ); - const projectResult = await projects.json(); - const options = projectResult.filter((p)=>p.organization==orgId).map((p)=>({ - label:p.name, - value:p.id - })); - setProjectOptions(options); + if (!orgId) { + setProjectOptions([]); + return; + } + const token = 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; + } + try { + const projects = await fetch (`${process.env.REACT_APP_API_URL}/projects`, { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + const projectResult = await projects.json(); + if (!projects.ok) { + console.warn("Projects API error:", projects.status, projectResult); + setProjectOptions([]); + return; } - catch(error){ - console.warn("Error while loading projects",error); + if (!Array.isArray(projectResult)) { + setProjectOptions([]); + return; } + const options = projectResult.filter((p)=>p.organization==orgId).map((p)=>({ + label: p.name, + value: p.id, + })); + setProjectOptions(options); + } catch(error) { + console.warn("Error while loading projects (check API and network):", error); + setProjectOptions([]); + } }; useEffect(() => { @@ -162,10 +182,17 @@ const AgroHorticulture =()=>{ handleItemSelect={(setState, e) => setState(e)} />
- + {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 477ec00f..09b657b1 100644 --- a/src/pages/kyl_dashboard.jsx +++ b/src/pages/kyl_dashboard.jsx @@ -32,7 +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.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'; @@ -88,7 +88,7 @@ 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); @@ -1065,10 +1065,11 @@ const KYLDashboardPage = () => { setDataJson(result); setIsLoading(false); - setFiltersEnabled(true) + setFiltersEnabled(true); } catch (e) { console.log(e); setIsLoading(false); + setFiltersEnabled(true); } }; @@ -1103,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 ( @@ -1151,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") { @@ -1298,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, @@ -2249,7 +2268,15 @@ const KYLDashboardPage = () => { handlePatternSelection={handlePatternSelection} isPatternSelected={isPatternSelected} onOpenPatternIntensityMap={() => setShowPatternIntensityModal(true)} - hasLocationSelected={!!(district && block)} + /> + + setShowPatternIntensityModal(false)} + district={district} + block={block} + dataJson={dataJson} + patternSelections={patternSelections} /> {/* Map Container */} @@ -2310,15 +2337,6 @@ const KYLDashboardPage = () => { />
- - setShowPatternIntensityModal(false)} - district={district} - block={block} - dataJson={dataJson} - patternSelections={patternSelections} - />
); };