From 699a5dda64e21be2e34a710b8c2b97322a6cad34 Mon Sep 17 00:00:00 2001 From: Sujit Karki Date: Tue, 7 Oct 2025 17:37:35 +0545 Subject: [PATCH 1/5] Combine all base layers source and layer on single object --- frontend/src/config/index.js | 46 ++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/frontend/src/config/index.js b/frontend/src/config/index.js index e6139d4b56..ce0b525535 100644 --- a/frontend/src/config/index.js +++ b/frontend/src/config/index.js @@ -192,3 +192,49 @@ export const DROPZONE_SETTINGS = { // TM_DEFAULT_CHANGESET_COMMENT without '#' export const defaultChangesetComment = TM_DEFAULT_CHANGESET_COMMENT.replace('#', ''); + +export const DEFAULT_MAP_STYLE = { + version: 8, + glyphs: 'https://fonts.openmaptiles.org/{fontstack}/{range}.pbf', + sources: {}, + layers: [], +}; + +// base layers list on single object along with visibility +export const baseLayers = { + OSM: { + source: { + type: 'raster', + tiles: ['https://a.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png'], + tileSize: 256, + attribution: + '© OpenStreetMap contributors', + }, + layer: { + id: 'OSM-layer', + type: 'raster', + source: 'OSM-source', + layout: { visibility: 'visible' }, + }, + }, + bing: { + source: { + type: 'raster', + tiles: [ + 'https://ecn.t0.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=587&mkt=en-gb&n=z', + 'https://ecn.t1.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=587&mkt=en-gb&n=z', + 'https://ecn.t2.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=587&mkt=en-gb&n=z', + 'https://ecn.t3.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=587&mkt=en-gb&n=z', + ], + tileSize: 256, + attribution: + '© Microsoft Corporation', + }, + layer: { + id: 'bing-layer', + type: 'raster', + source: 'bing-source', + layout: { visibility: 'none' }, + }, + }, +}; From 54cbdf0c7fe6846277262d7430c3dd2045010983 Mon Sep 17 00:00:00 2001 From: Sujit Karki Date: Tue, 7 Oct 2025 17:40:01 +0545 Subject: [PATCH 2/5] Toggle base layer visibility insted of updating overall style which prevents reloading the entire style and re-adding overalays --- frontend/src/components/basemapMenu.js | 36 ++++++++++++-------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/basemapMenu.js b/frontend/src/components/basemapMenu.js index d9ce7ae545..11b01e3f48 100644 --- a/frontend/src/components/basemapMenu.js +++ b/frontend/src/components/basemapMenu.js @@ -1,38 +1,34 @@ import { useState } from 'react'; -import { MAPBOX_TOKEN, BASEMAP_OPTIONS } from '../config'; +import { baseLayers } from '../config'; export const BasemapMenu = ({ map }) => { - // Remove elements that require mapbox token; - let styles = BASEMAP_OPTIONS; - if (!MAPBOX_TOKEN) { - styles = BASEMAP_OPTIONS.filter((s) => typeof s.value === 'object'); - } + const [basemap, setBasemap] = useState('OSM'); - const [basemap, setBasemap] = useState(styles[0].label); - - const handleClick = (style) => { - let styleValue = style.value; - - if (typeof style.value === 'string') { - styleValue = 'mapbox://styles/mapbox/' + style.value; - } - map.setStyle(styleValue); - setBasemap(style.label); + const handleClick = (activeLayer) => { + // toggle visibiity as per active base layer + Object.keys(baseLayers).forEach((layer) => { + map.setLayoutProperty( + `${layer}-layer`, + 'visibility', + `${activeLayer}-layer` === `${layer}-layer` ? 'visible' : 'none', + ); + }); + setBasemap(activeLayer); }; return (
- {styles.map((style, k) => { + {Object.keys(baseLayers).map((baseLayer, k) => { return (
handleClick(style)} + onClick={() => handleClick(baseLayer)} className={`ttc pv2 ph3 pointer link + ${ - basemap === style.label ? 'bg-grey-light fw6' : '' + basemap === baseLayer ? 'bg-grey-light fw6' : '' }`} > - {style.label} + {k === 0 ? `Default (${baseLayer})` : baseLayer}
); })} From c07247afec521c65b6d674aadd194db096a9d5f4 Mon Sep 17 00:00:00 2001 From: Sujit Karki Date: Tue, 7 Oct 2025 17:44:10 +0545 Subject: [PATCH 3/5] Add only basic styles on initialization and add baselayer on `on.load` event which allows to load all base layers and toggle switch using its visibility property It helps to avoid the re-adding map style entirely so it prevents all custom layers, sources, controls, and event bindings lost --- .../projectCreate/projectCreationMap.js | 21 ++++++++++++++++--- .../projectEdit/priorityAreasForm.js | 12 +++++++++-- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/projectCreate/projectCreationMap.js b/frontend/src/components/projectCreate/projectCreationMap.js index fdba548eda..2e003fc20c 100644 --- a/frontend/src/components/projectCreate/projectCreationMap.js +++ b/frontend/src/components/projectCreate/projectCreationMap.js @@ -11,7 +11,13 @@ import { useDropzone } from 'react-dropzone'; import { maplibreLayerDefn } from '../projects/projectsMap'; import useMapboxSupportedLanguage from '../../hooks/UseMapboxSupportedLanguage'; -import { MAPBOX_TOKEN, MAP_STYLE, CHART_COLOURS, TASK_COLOURS } from '../../config'; +import { + MAPBOX_TOKEN, + CHART_COLOURS, + TASK_COLOURS, + baseLayers, + DEFAULT_MAP_STYLE, +} from '../../config'; import { fetchLocalJSONAPI } from '../../network/genericJSONRequest'; import { useDebouncedCallback } from '../../hooks/UseThrottle'; import isWebglSupported from '../../utils/isWebglSupported'; @@ -50,7 +56,7 @@ const ProjectCreationMap = ({ mapObj, setMapObj, metadata, updateMetadata, step, let bounds = mapObj.map.getBounds(); let bbox = `${bounds._sw.lng},${bounds._sw.lat},${bounds._ne.lng},${bounds._ne.lat}`; fetchLocalJSONAPI(`projects/queries/bbox/?bbox=${bbox}&srid=4326`, token).then((res) => { - mapObj.map.getSource('otherProjects').setData(res); + mapObj.map.getSource('otherProjects')?.setData(res); setIsAoiLoading(false); }); } @@ -74,7 +80,7 @@ const ProjectCreationMap = ({ mapObj, setMapObj, metadata, updateMetadata, step, if (!isWebglSupported()) return; const map = new maplibregl.Map({ container: mapRef.current, - style: MAP_STYLE, + style: DEFAULT_MAP_STYLE, center: [0, 0], zoom: 1.3, attributionControl: false, @@ -103,6 +109,14 @@ const ProjectCreationMap = ({ mapObj, setMapObj, metadata, updateMetadata, step, }, []); const addMapLayers = (map) => { + // load all base layer and toggle visibility + Object.entries(baseLayers).forEach(([key, value]) => { + if (mapObj.map.getSource(`${key}-source`) === undefined) { + mapObj.map.addSource(`${key}-source`, value.source); + mapObj.map.addLayer(value.layer); + } + }); + if (map.getSource('aoi') === undefined) { map.addSource('aoi', { type: 'geojson', @@ -273,6 +287,7 @@ const ProjectCreationMap = ({ mapObj, setMapObj, metadata, updateMetadata, step, } }); } + // eslint-disable-next-line }, [mapObj, metadata, updateMetadata, step]); if (!isWebglSupported()) { diff --git a/frontend/src/components/projectEdit/priorityAreasForm.js b/frontend/src/components/projectEdit/priorityAreasForm.js index a438400d66..c5075787a8 100644 --- a/frontend/src/components/projectEdit/priorityAreasForm.js +++ b/frontend/src/components/projectEdit/priorityAreasForm.js @@ -13,7 +13,7 @@ import messages from './messages'; import { StateContext, styleClasses } from '../../views/projectEdit'; import { CustomButton } from '../button'; import { MappedIcon, WasteIcon, MappedSquareIcon, FileImportIcon } from '../svgIcons'; -import { MAPBOX_TOKEN, MAP_STYLE, CHART_COLOURS } from '../../config'; +import { MAPBOX_TOKEN, CHART_COLOURS, baseLayers, DEFAULT_MAP_STYLE } from '../../config'; import { BasemapMenu } from '../basemapMenu'; import { verifyGeometry, @@ -122,7 +122,7 @@ export const PriorityAreasForm = () => { isWebglSupported() && new maplibregl.Map({ container: mapRef.current, - style: MAP_STYLE, + style: DEFAULT_MAP_STYLE, center: [0, 0], zoom: 1, attributionControl: false, @@ -138,6 +138,14 @@ export const PriorityAreasForm = () => { }, []); const addMapLayers = (map) => { + // load all base layer and toggle visibility + Object.entries(baseLayers).forEach(([key, value]) => { + if (mapObj.map.getSource(`${key}-source`) === undefined) { + mapObj.map.addSource(`${key}-source`, value.source); + mapObj.map.addLayer(value.layer); + } + }); + if (map.getSource('aoi') === undefined) { map.addSource('aoi', { type: 'geojson', From 13ded51e916543c2feb0a06e0b76a569c91bbbdf Mon Sep 17 00:00:00 2001 From: Sujit Karki Date: Wed, 29 Oct 2025 09:53:34 +0545 Subject: [PATCH 4/5] refactor: update foreach loop to for of and add Object as a proptype on components --- frontend/src/components/basemapMenu.js | 7 +++---- .../components/projectCreate/projectCreationMap.js | 13 ++++++++++--- .../src/components/projectEdit/priorityAreasForm.js | 4 ++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/basemapMenu.js b/frontend/src/components/basemapMenu.js index 11b01e3f48..f50ee19691 100644 --- a/frontend/src/components/basemapMenu.js +++ b/frontend/src/components/basemapMenu.js @@ -1,19 +1,18 @@ import { useState } from 'react'; - import { baseLayers } from '../config'; -export const BasemapMenu = ({ map }) => { +export const BasemapMenu = ({ map }: Object) => { const [basemap, setBasemap] = useState('OSM'); const handleClick = (activeLayer) => { // toggle visibiity as per active base layer - Object.keys(baseLayers).forEach((layer) => { + for (const layer of Object.keys(baseLayers)) { map.setLayoutProperty( `${layer}-layer`, 'visibility', `${activeLayer}-layer` === `${layer}-layer` ? 'visible' : 'none', ); - }); + } setBasemap(activeLayer); }; diff --git a/frontend/src/components/projectCreate/projectCreationMap.js b/frontend/src/components/projectCreate/projectCreationMap.js index 2e003fc20c..e0d5b2dcf8 100644 --- a/frontend/src/components/projectCreate/projectCreationMap.js +++ b/frontend/src/components/projectCreate/projectCreationMap.js @@ -28,7 +28,14 @@ import WebglUnsupported from '../webglUnsupported'; maplibregl.accessToken = MAPBOX_TOKEN; -const ProjectCreationMap = ({ mapObj, setMapObj, metadata, updateMetadata, step, uploadFile }) => { +const ProjectCreationMap = ({ + mapObj, + setMapObj, + metadata, + updateMetadata, + step, + uploadFile, +}: Object) => { const mapRef = createRef(); const mapboxSupportedLanguage = useMapboxSupportedLanguage(); const token = useSelector((state) => state.auth.token); @@ -110,12 +117,12 @@ const ProjectCreationMap = ({ mapObj, setMapObj, metadata, updateMetadata, step, const addMapLayers = (map) => { // load all base layer and toggle visibility - Object.entries(baseLayers).forEach(([key, value]) => { + for (const [key, value] of Object.entries(baseLayers)) { if (mapObj.map.getSource(`${key}-source`) === undefined) { mapObj.map.addSource(`${key}-source`, value.source); mapObj.map.addLayer(value.layer); } - }); + } if (map.getSource('aoi') === undefined) { map.addSource('aoi', { diff --git a/frontend/src/components/projectEdit/priorityAreasForm.js b/frontend/src/components/projectEdit/priorityAreasForm.js index c5075787a8..795aa94dbf 100644 --- a/frontend/src/components/projectEdit/priorityAreasForm.js +++ b/frontend/src/components/projectEdit/priorityAreasForm.js @@ -139,12 +139,12 @@ export const PriorityAreasForm = () => { const addMapLayers = (map) => { // load all base layer and toggle visibility - Object.entries(baseLayers).forEach(([key, value]) => { + for (const [key, value] of Object.entries(baseLayers)) { if (mapObj.map.getSource(`${key}-source`) === undefined) { mapObj.map.addSource(`${key}-source`, value.source); mapObj.map.addLayer(value.layer); } - }); + } if (map.getSource('aoi') === undefined) { map.addSource('aoi', { From 85f7c8178e29f4f61c8c48ce9a7150715b3996e8 Mon Sep 17 00:00:00 2001 From: Sujit Karki Date: Mon, 3 Nov 2025 14:23:54 +0545 Subject: [PATCH 5/5] re-order the map layers add base layers before draw --- frontend/src/components/projectCreate/projectCreationMap.js | 2 +- frontend/src/components/projectEdit/priorityAreasForm.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/projectCreate/projectCreationMap.js b/frontend/src/components/projectCreate/projectCreationMap.js index e0d5b2dcf8..e2ffeabc58 100644 --- a/frontend/src/components/projectCreate/projectCreationMap.js +++ b/frontend/src/components/projectCreate/projectCreationMap.js @@ -259,8 +259,8 @@ const ProjectCreationMap = ({ if (mapObj.map !== null && isWebglSupported()) { mapObj.map.on('load', () => { mapObj.map.addControl(new maplibregl.NavigationControl()); - mapObj.map.addControl(mapObj.draw); addMapLayers(mapObj.map); + mapObj.map.addControl(mapObj.draw); }); // Remove area and geometry when aoi is deleted. diff --git a/frontend/src/components/projectEdit/priorityAreasForm.js b/frontend/src/components/projectEdit/priorityAreasForm.js index 795aa94dbf..de201e99bf 100644 --- a/frontend/src/components/projectEdit/priorityAreasForm.js +++ b/frontend/src/components/projectEdit/priorityAreasForm.js @@ -236,8 +236,8 @@ export const PriorityAreasForm = () => { useLayoutEffect(() => { if (mapObj.map !== null && isWebglSupported()) { mapObj.map.on('load', () => { - mapObj.map.addControl(mapObj.draw); addMapLayers(mapObj.map); + mapObj.map.addControl(mapObj.draw); mapObj.map.fitBounds(projectInfo.aoiBBOX, { duration: 0, padding: 100 }); });