From 2494c77640374e7258f83731cca4ba3ba78ab952 Mon Sep 17 00:00:00 2001 From: luisrodriguezgalvez <77997772+luisrodriguezgalvez@users.noreply.github.com> Date: Thu, 28 May 2026 15:16:20 +0200 Subject: [PATCH 1/6] tree locations on map and legend --- src/App.vue | 42 ++++++++++++--- src/components/MapComponent.vue | 78 ++++++++++++++++++++++++--- src/components/MapLayer.vue | 15 +----- src/lib/get-bomen-locations-data.js | 21 ++++++++ src/stores/app.js | 4 ++ src/stores/bomenLocations.js | 83 +++++++++++++++++++++++++++++ src/stores/map.js | 7 +++ 7 files changed, 223 insertions(+), 27 deletions(-) create mode 100644 src/lib/get-bomen-locations-data.js create mode 100644 src/stores/bomenLocations.js diff --git a/src/App.vue b/src/App.vue index e029940..129c294 100644 --- a/src/App.vue +++ b/src/App.vue @@ -28,16 +28,17 @@
+ {{ item.dataleverancier }}
@@ -144,6 +151,7 @@ import { useAppStore } from "@/stores/app"; import { useDepthInfoStore } from "@/stores/depthInfo"; import { useLocationsStore } from "@/stores/locations"; + import { useBomenLocationsStore } from "@/stores/bomenLocations"; import { useChartTimeseriesStore } from "@/stores/chartTimeseries"; import { usePeilfilterDataStore } from "@/stores/peilfilterData"; @@ -151,6 +159,7 @@ const chartTimeseriesStore = useChartTimeseriesStore(); const depthInfoStore = useDepthInfoStore(); const locationsStore = useLocationsStore(); + const bomenLocationsStore = useBomenLocationsStore(); const peilfilterDataStore = usePeilfilterDataStore(); const panelIsCollapsed = computed(() => appStore.panelIsCollapsed); @@ -178,6 +187,7 @@ if (bronId && dataleverancier && !uniqueProviders.has(bronId)) { uniqueProviders.set(bronId, { + key: `provider-${bronId}`, bronId, dataleverancier, color: getColorForBronId(bronId), @@ -185,11 +195,31 @@ } }); - return Array.from(uniqueProviders.values()).sort( + const providers = Array.from(uniqueProviders.values()).sort( (a, b) => a.bronId - b.bronId ); + if (bomenLocationsStore.bomenLocations?.length > 0) { + providers.push({ + key: "trees", + bronId: null, + dataleverancier: "Bomen", + color: "#00a651", + isTree: true, + }); + } + return providers; }); + function onLegendItemClick(item) { + if (!item) return; + if (item.isTree) { + appStore.toggleTrees(); + return; + } + if (item.bronId == null) return; + appStore.toggleCategory(item.bronId); + } + function getColorForBronId(bronId) { const colors = { 1: "#008fc5", diff --git a/src/components/MapComponent.vue b/src/components/MapComponent.vue index 5b31489..7fa27ab 100644 --- a/src/components/MapComponent.vue +++ b/src/components/MapComponent.vue @@ -22,6 +22,11 @@ :id="'locations-layer'" :paint="locationsStore.computedPaint" /> + mapStore.mapboxLayers) const defaultMapStyle = computed(() => MAP_BASELAYER_DEFAULT.uri) const styleChangeCounter = ref(0) // Counter to force MapLayer re-render after style changes + const TREE_ICON_ID = 'tree-sdf-icon' + + function createTreeSdfImageData(size = 32) { + const canvas = document.createElement('canvas') + canvas.width = size + canvas.height = size + const ctx = canvas.getContext('2d') + if (!ctx) return null + + ctx.clearRect(0, 0, size, size) + ctx.fillStyle = '#000' + + // Crown (three overlapping circles) + ctx.beginPath() + ctx.arc(size * 0.38, size * 0.38, size * 0.16, 0, Math.PI * 2) + ctx.arc(size * 0.62, size * 0.38, size * 0.16, 0, Math.PI * 2) + ctx.arc(size * 0.5, size * 0.28, size * 0.18, 0, Math.PI * 2) + ctx.fill() + + // Lower crown taper + ctx.beginPath() + ctx.moveTo(size * 0.28, size * 0.5) + ctx.lineTo(size * 0.72, size * 0.5) + ctx.lineTo(size * 0.5, size * 0.7) + ctx.closePath() + ctx.fill() + + // Trunk and small base + ctx.fillRect(size * 0.45, size * 0.66, size * 0.1, size * 0.22) + ctx.fillRect(size * 0.4, size * 0.86, size * 0.2, size * 0.06) + + const { data } = ctx.getImageData(0, 0, size, size) + return { width: size, height: size, data } + } + + function ensureTreeIcon(map) { + if (!map || map.hasImage(TREE_ICON_ID)) return + const imageData = createTreeSdfImageData() + if (!imageData) return + map.addImage(TREE_ICON_ID, imageData, { sdf: true }) + } // Provide map instance for child components const map = computed(() => mapInstance.value) @@ -67,6 +115,7 @@ function onMapCreated(map) { mapInstance.value = map + ensureTreeIcon(map) // Create popup instance for hover hoverPopup.value = new mapboxgl.Popup({ @@ -77,6 +126,7 @@ // Listen for style changes (when basemap is changed) map.on('style.load', () => { + ensureTreeIcon(map) // Increment counter to force MapLayer components to re-render with new keys styleChangeCounter.value++ @@ -100,10 +150,12 @@ // Wait for initial style to load before adding sources/layers if (map.isStyleLoaded()) { + ensureTreeIcon(map) setupActiveLocationLayer() initializeMap() } else { map.once('style.load', () => { + ensureTreeIcon(map) setupActiveLocationLayer() initializeMap() }) @@ -112,7 +164,10 @@ function initializeMap() { mapStore.initializeMapboxLayers() - locationsStore.fetchLocations().then(() => { + Promise.all([ + locationsStore.fetchLocations(), + bomenLocationsStore.fetchBomenLocations(), + ]).then(() => { mapStore.refreshLayers() }) } @@ -141,9 +196,14 @@ // Handle click on location layer function handleLayerClick(feature, layerId) { - // Only handle clicks on locations-layer const mapObj = mapInstance.value - if (layerId !== 'locations-layer' || !feature || !mapObj) return + if (!feature || !mapObj) return + + if (layerId === 'bomen-locations-layer') { + if (appStore.disabledTrees) return + return + } + if (layerId !== 'locations-layer') return const bronId = feature.properties?.bron_id // Don't allow interaction if category is disabled @@ -168,9 +228,14 @@ // Handle mouseenter on location layer function handleLayerMouseenter(e, layerId) { - // Only handle hover on locations-layer const mapObj = mapInstance.value - if (layerId !== 'locations-layer' || !mapObj) return + if (!mapObj) return + + if (layerId === 'bomen-locations-layer') { + mapObj.getCanvas().style.cursor = appStore.disabledTrees ? 'grab' : 'pointer' + return + } + if (layerId !== 'locations-layer') return // Ensure popup is initialized if (!hoverPopup.value) { @@ -235,7 +300,7 @@ // Handle mouseleave on location layer function handleLayerMouseleave(layerId) { - if (layerId !== 'locations-layer') return + if (layerId !== 'locations-layer' && layerId !== 'bomen-locations-layer') return const mapObj = mapInstance.value if (mapObj) { @@ -284,7 +349,6 @@ } ) - // Setup direct map event listeners for locations-layer let locationsLayerListenersAttached = false diff --git a/src/components/MapLayer.vue b/src/components/MapLayer.vue index f64ddc5..82586ff 100644 --- a/src/components/MapLayer.vue +++ b/src/components/MapLayer.vue @@ -9,8 +9,7 @@ /> diff --git a/src/stores/boomData.js b/src/stores/boomData.js index 2a1e258..9852353 100644 --- a/src/stores/boomData.js +++ b/src/stores/boomData.js @@ -35,6 +35,36 @@ export const useBoomDataStore = defineStore('boomData', { groupThatBelongs (state) { return this.selectedTreeEntry?.group_that_belongs ?? null }, + + yAxisLabels (state) { + return state.data?.y_axis?.labels ?? [] + }, + + yAxisMin (state) { + return state.data?.y_axis?.min ?? 0 + }, + + yAxisMax (state) { + return state.data?.y_axis?.max ?? 8 + }, + + treeTimeseries (state) { + return this.selectedTreeEntry?.timeseries ?? [] + }, + + groupAverageEntry (state) { + const group = this.groupThatBelongs + const averages = state.data?.group_averages + if (!group || !Array.isArray(averages)) { + return null + } + + return averages.find((entry) => entry.group === group) ?? null + }, + + groupAverageTimeseries (state) { + return this.groupAverageEntry?.timeseries ?? [] + }, }, actions: { From 6b807f9d8a465601ccef9d3c1d51e3789cf64140 Mon Sep 17 00:00:00 2001 From: luisrodriguezgalvez <77997772+luisrodriguezgalvez@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:05:49 +0200 Subject: [PATCH 5/6] implementation cleanup --- src/App.vue | 38 +--- src/components/MapComponent.vue | 277 ++++++++---------------- src/components/TreeHealthChart.vue | 127 +++++------ src/lib/constants.js | 17 ++ src/lib/get-bomen-locations-data.js | 25 +-- src/lib/normalize-feature-collection.js | 16 ++ src/lib/tree-sdf-icon.js | 38 ++++ src/stores/bomenLocations.js | 67 +++--- src/stores/locations.js | 16 +- 9 files changed, 274 insertions(+), 347 deletions(-) create mode 100644 src/lib/normalize-feature-collection.js create mode 100644 src/lib/tree-sdf-icon.js diff --git a/src/App.vue b/src/App.vue index 13573a7..61575bb 100644 --- a/src/App.vue +++ b/src/App.vue @@ -193,6 +193,7 @@ import { useBoomDataStore } from "@/stores/boomData"; import { useChartTimeseriesStore } from "@/stores/chartTimeseries"; import { usePeilfilterDataStore } from "@/stores/peilfilterData"; + import { TREE_COLOR } from "@/lib/constants"; const appStore = useAppStore(); const chartTimeseriesStore = useChartTimeseriesStore(); @@ -219,7 +220,6 @@ const legendItems = computed(() => { const uniqueProviders = new Map(); - // locationsStore.locations is now an array, not a FeatureCollection const locations = locationsStore.locations || []; locations.forEach((location) => { @@ -244,7 +244,7 @@ key: "trees", bronId: null, dataleverancier: "Bomen", - color: "#00a651", + color: TREE_COLOR, isTree: true, }); } @@ -268,7 +268,7 @@ 3: "#ffc107", 4: "#895129", }; - return colors[bronId] || "#6c757d"; // Gray fallback + return colors[bronId] || "#6c757d"; } const peilfilterOptions = computed(() => { @@ -302,10 +302,14 @@ const hasValidDLabel = computed(() => hasNonEmptyValue(peilfilterDataStore.dlabelFilter)); const hasValidPompId = computed(() => hasNonEmptyValue(peilfilterDataStore.pompidFilter)); - function clearPanelDataStores() { + function clearLocationPanelStores() { chartTimeseriesStore.clearData(); peilfilterDataStore.clearData(); depthInfoStore.clearData(); + } + + function clearAllPanelStores() { + clearLocationPanelStores(); boomDataStore.clearData(); } @@ -314,40 +318,20 @@ return str.split(",").map((s) => s.trim()); } - watch( - () => bomenLocationsStore.activeTree, - (tree) => { - if (!tree) { - boomDataStore.clearData(); - return; - } - - const boomcode = tree.properties?.boomcode; - if (boomcode) { - boomDataStore.fetchBoomData(boomcode); - } else { - boomDataStore.clearData(); - } - }, - { immediate: true }, - ); - watch( () => locationsStore.activeLocation, (newLocation) => { if (!newLocation) { selectedPeilfilterId.value = null; if (!bomenLocationsStore.activeTree) { - clearPanelDataStores(); + clearAllPanelStores(); } else { - chartTimeseriesStore.clearData(); - peilfilterDataStore.clearData(); - depthInfoStore.clearData(); + clearLocationPanelStores(); } return; } bomenLocationsStore.setActiveTree(null); - clearPanelDataStores(); + clearAllPanelStores(); const options = peilfilterOptions.value; selectedPeilfilterId.value = options.length > 0 ? options[0].value : null; diff --git a/src/components/MapComponent.vue b/src/components/MapComponent.vue index 5274d8a..dfe2ff4 100644 --- a/src/components/MapComponent.vue +++ b/src/components/MapComponent.vue @@ -16,15 +16,14 @@ @mouseenter="(e) => handleLayerMouseenter(e, layer.id)" @mouseleave="() => handleLayerMouseleave(layer.id)" /> - @@ -55,7 +54,13 @@ import { useLocationsStore } from '@/stores/locations' import { useMapStore } from '@/stores/map' import { useAppStore } from '@/stores/app' - import { MAP_BASELAYERS, MAP_BASELAYER_DEFAULT } from '@/lib/constants' + import { + MAP_BASELAYERS, + MAP_BASELAYER_DEFAULT, + TREE_COLOR, + TREE_SELECTION_COLOR, + } from '@/lib/constants' + import { registerTreeSdfIcon, TREE_ICON_ID } from '@/lib/tree-sdf-icon' const accessToken = import.meta.env.VITE_MAPBOX_TOKEN const locationsStore = useLocationsStore() @@ -66,84 +71,60 @@ const hoverPopup = ref(null) const mapboxLayers = computed(() => mapStore.mapboxLayers) const defaultMapStyle = computed(() => MAP_BASELAYER_DEFAULT.uri) - const styleChangeCounter = ref(0) // Counter to force MapLayer re-render after style changes - const TREE_ICON_ID = 'tree-sdf-icon' + const styleChangeCounter = ref(0) + const TREE_ICON_SIZE = 1.15 const TREE_SELECTION_OUTLINE_SIZE = 1.55 - function createTreeSdfImageData(size = 32) { - const canvas = document.createElement('canvas') - canvas.width = size - canvas.height = size - const ctx = canvas.getContext('2d') - if (!ctx) return null - - ctx.clearRect(0, 0, size, size) - ctx.fillStyle = '#000' - - // Crown (three overlapping circles) - ctx.beginPath() - ctx.arc(size * 0.38, size * 0.38, size * 0.16, 0, Math.PI * 2) - ctx.arc(size * 0.62, size * 0.38, size * 0.16, 0, Math.PI * 2) - ctx.arc(size * 0.5, size * 0.28, size * 0.18, 0, Math.PI * 2) - ctx.fill() - - // Lower crown taper - ctx.beginPath() - ctx.moveTo(size * 0.28, size * 0.5) - ctx.lineTo(size * 0.72, size * 0.5) - ctx.lineTo(size * 0.5, size * 0.7) - ctx.closePath() - ctx.fill() - - // Trunk and small base - ctx.fillRect(size * 0.45, size * 0.66, size * 0.1, size * 0.22) - ctx.fillRect(size * 0.4, size * 0.86, size * 0.2, size * 0.06) - - const { data } = ctx.getImageData(0, 0, size, size) - return { width: size, height: size, data } + const map = computed(() => mapInstance.value) + provide('map', map) + + function ensureHoverPopup (mapObj) { + if (!hoverPopup.value) { + hoverPopup.value = new mapboxgl.Popup({ + closeButton: false, + closeOnClick: false, + className: 'location-hover-popup', + }) + } + return hoverPopup.value } - function ensureTreeIcon(map) { - if (!map || map.hasImage(TREE_ICON_ID)) return - const imageData = createTreeSdfImageData() - if (!imageData) return - map.addImage(TREE_ICON_ID, imageData, { sdf: true }) + function flyToFeature (mapObj, feature) { + const coords = feature.geometry.coordinates + const offsetY = mapObj.getCanvas().height * 0.25 + mapObj.flyTo({ + center: coords, + zoom: 12.5, + speed: 1.2, + offset: [0, -offsetY], + }) } - // Provide map instance for child components - const map = computed(() => mapInstance.value) - provide('map', map) + function cloneFeature (feature) { + return JSON.parse(JSON.stringify(feature)) + } - function onMapCreated(map) { - mapInstance.value = map - ensureTreeIcon(map) + function setupMapAfterStyleLoad (map) { + registerTreeSdfIcon(map) + setupActiveLocationLayer() + setupActiveTreeLayers() + initializeMap() + } - // Create popup instance for hover - hoverPopup.value = new mapboxgl.Popup({ - closeButton: false, - closeOnClick: false, - className: 'location-hover-popup', - }) + function onMapCreated (map) { + mapInstance.value = map + registerTreeSdfIcon(map) + ensureHoverPopup(map) - // Listen for style changes (when basemap is changed) map.on('style.load', () => { - ensureTreeIcon(map) - // Increment counter to force MapLayer components to re-render with new keys + registerTreeSdfIcon(map) styleChangeCounter.value++ - - // Wait for style to be fully loaded before re-initializing layers - // MapboxLayer components need the style to be ready before they can add layers + const waitForStyleReady = () => { if (map.isStyleLoaded()) { - // Clear existing layers from store to force re-render mapStore.mapboxLayers = [] - // Small delay to ensure MapboxLayer components have time to clean up - setTimeout(() => { - setupActiveLocationLayer() - setupActiveTreeLayers() - initializeMap() - }, 50) + setTimeout(() => setupMapAfterStyleLoad(map), 50) } else { setTimeout(waitForStyleReady, 50) } @@ -151,19 +132,10 @@ waitForStyleReady() }) - // Wait for initial style to load before adding sources/layers if (map.isStyleLoaded()) { - ensureTreeIcon(map) - setupActiveLocationLayer() - setupActiveTreeLayers() - initializeMap() + setupMapAfterStyleLoad(map) } else { - map.once('style.load', () => { - ensureTreeIcon(map) - setupActiveLocationLayer() - setupActiveTreeLayers() - initializeMap() - }) + map.once('style.load', () => setupMapAfterStyleLoad(map)) } } @@ -197,7 +169,7 @@ 'icon-padding': 0, }, paint: { - 'icon-color': '#ff0000', + 'icon-color': TREE_SELECTION_COLOR, 'icon-opacity': 1, }, }) @@ -213,7 +185,7 @@ 'icon-padding': 0, }, paint: { - 'icon-color': '#00a651', + 'icon-color': TREE_COLOR, 'icon-halo-color': '#ffffff', 'icon-halo-width': 0.8, 'icon-opacity': 1, @@ -221,14 +193,14 @@ }) } - function updateActiveTreeSourceData() { + function updateActiveTreeSourceData () { const mapObj = mapInstance.value if (!mapObj?.getSource('active-tree')) return const activeTree = bomenLocationsStore.activeTree mapObj.getSource('active-tree').setData({ type: 'FeatureCollection', - features: activeTree ? [JSON.parse(JSON.stringify(activeTree))] : [], + features: activeTree ? [cloneFeature(activeTree)] : [], }) } @@ -251,8 +223,7 @@ } } - // Setup active-location layer for highlighting selected location - function setupActiveLocationLayer() { + function setupActiveLocationLayer () { const mapObj = mapInstance.value if (!mapObj || mapObj.getSource('active-location')) return @@ -266,15 +237,14 @@ source: 'active-location', paint: { 'circle-color': '#fff', - 'circle-radius': 5.5, // Slightly larger than normal points (which are 5) - 'circle-stroke-width': 5, // Thick stroke to make red clearly visible - 'circle-stroke-color': '#ff0000', + 'circle-radius': 5.5, + 'circle-stroke-width': 5, + 'circle-stroke-color': TREE_SELECTION_COLOR, }, }) } - // Handle click on location layer - function handleLayerClick(feature, layerId) { + function handleLayerClick (feature, layerId) { const mapObj = mapInstance.value if (!feature || !mapObj) return @@ -283,23 +253,12 @@ bomenLocationsStore.setActiveTree(feature) locationsStore.setActiveLocation(null) appStore.expandPanel() - - const coords = feature.geometry.coordinates - const canvas = mapObj.getCanvas() - const offsetY = canvas.height * 0.25 - - mapObj.flyTo({ - center: coords, - zoom: 12.5, - speed: 1.2, - offset: [0, -offsetY], - }) + flyToFeature(mapObj, feature) return } if (layerId !== 'locations-layer') return const bronId = feature.properties?.bron_id - // Don't allow interaction if category is disabled if (appStore.disabledCategories.has(bronId)) { return } @@ -307,62 +266,32 @@ bomenLocationsStore.setActiveTree(null) locationsStore.setActiveLocation(feature) appStore.expandPanel() - - const coords = feature.geometry.coordinates - const canvas = mapObj.getCanvas() - const offsetY = canvas.height * 0.25 - - mapObj.flyTo({ - center: coords, - zoom: 12.5, - speed: 1.2, - offset: [0, -offsetY], - }) + flyToFeature(mapObj, feature) } - // Handle mouseenter on location layer - function handleLayerMouseenter(e, layerId) { + function handleLayerMouseenter (e, layerId) { const mapObj = mapInstance.value if (!mapObj) return - if (layerId === 'bomen-locations-layer') { - // Ensure popup is initialized - if (!hoverPopup.value) { - hoverPopup.value = new mapboxgl.Popup({ - closeButton: false, - closeOnClick: false, - className: 'location-hover-popup', - }) - } + const feature = e?.features?.[0] + if (!feature) return - const feature = e?.features?.[0] - if (!feature) return + const popup = ensureHoverPopup(mapObj) + if (layerId === 'bomen-locations-layer') { mapObj.getCanvas().style.cursor = appStore.disabledTrees ? 'grab' : 'pointer' if (appStore.disabledTrees) return - const coords = feature.geometry.coordinates.slice() const boomnaam = feature.properties?.boomnaam || 'Onbekend' - const htmlContent = `
Boom: ${boomnaam}
` - hoverPopup.value.setLngLat(coords).setHTML(htmlContent).addTo(mapObj) + popup + .setLngLat(feature.geometry.coordinates.slice()) + .setHTML(`
Boom: ${boomnaam}
`) + .addTo(mapObj) return } if (layerId !== 'locations-layer') return - - // Ensure popup is initialized - if (!hoverPopup.value) { - hoverPopup.value = new mapboxgl.Popup({ - closeButton: false, - closeOnClick: false, - className: 'location-hover-popup', - }) - } - - const feature = e?.features?.[0] - if (!feature) return const hoverBronId = feature.properties?.bron_id - // Don't show hover if category is disabled if (appStore.disabledCategories.has(hoverBronId)) { mapObj.getCanvas().style.cursor = 'grab' return @@ -384,7 +313,6 @@ .sort((a, b) => Number(a.id) - Number(b.id)) .map((item) => item.naam || item.id) - // Build HTML content for Locatie ID let locatieIdHtml = 'Locatie ID: ' if (locatienaamMaster) { locatieIdHtml += `${locatienaamMaster}` @@ -407,11 +335,10 @@ htmlContent += `
${label}: ${ids}
` } - hoverPopup.value.setLngLat(coords).setHTML(htmlContent).addTo(mapObj) + popup.setLngLat(coords).setHTML(htmlContent).addTo(mapObj) } - // Handle mouseleave on location layer - function handleLayerMouseleave(layerId) { + function handleLayerMouseleave (layerId) { if (layerId !== 'locations-layer' && layerId !== 'bomen-locations-layer') return const mapObj = mapInstance.value @@ -423,8 +350,7 @@ } } - // Function to update locations layer source data with filtered locations - function updateLocationsSourceData() { + function updateLocationsSourceData () { const mapObj = mapInstance.value if (!mapObj || !mapObj.getLayer('locations-layer')) { return @@ -443,7 +369,6 @@ } - // Watch for locations changes and refresh layers watch( () => locationsStore.locations, () => { @@ -451,7 +376,6 @@ } ) - // Watch for view mode changes and update layer source data watch( () => appStore.viewMode, () => { @@ -461,10 +385,8 @@ } ) - // Setup direct map event listeners for locations-layer let locationsLayerListenersAttached = false - // Store handler references for cleanup const locationsLayerHandlers = { click: null, mouseenter: null, @@ -477,7 +399,6 @@ return false } - // Remove any existing listeners first if (locationsLayerHandlers.click) { mapObj.off('click', 'locations-layer', locationsLayerHandlers.click) } @@ -488,7 +409,6 @@ mapObj.off('mouseleave', 'locations-layer', locationsLayerHandlers.mouseleave) } - // Click handler locationsLayerHandlers.click = (e) => { const feature = e.features?.[0] if (feature) { @@ -496,17 +416,14 @@ } } - // Mouseenter handler locationsLayerHandlers.mouseenter = (e) => { handleLayerMouseenter(e, 'locations-layer') } - // Mouseleave handler locationsLayerHandlers.mouseleave = () => { handleLayerMouseleave('locations-layer') } - // Attach listeners mapObj.on('click', 'locations-layer', locationsLayerHandlers.click) mapObj.on('mouseenter', 'locations-layer', locationsLayerHandlers.mouseenter) mapObj.on('mouseleave', 'locations-layer', locationsLayerHandlers.mouseleave) @@ -515,43 +432,36 @@ return true } - // Watch for layer to be created and apply initial disabled categories state + setup listeners let retryCount = 0 - const MAX_RETRIES = 50 // Maximum 5 seconds (50 * 100ms) - let setupInProgress = false // Flag to prevent concurrent setups + const MAX_RETRIES = 50 + let setupInProgress = false watch( () => mapboxLayers.value.find(l => l.id === 'locations-layer'), (locationsLayer, oldLocationsLayer) => { - // Only reset if layer state actually changed (not found → found) const wasFound = !!oldLocationsLayer const isFound = !!locationsLayer - + if (!wasFound && isFound) { - // Layer just appeared, reset flags locationsLayerListenersAttached = false retryCount = 0 - } else if (wasFound && isFound) { - // Layer already existed, don't reset - might be a duplicate trigger - if (locationsLayerListenersAttached) { - return - } + } else if (wasFound && isFound && locationsLayerListenersAttached) { + return } if (locationsLayer && mapInstance.value && !setupInProgress) { - setupInProgress = true // Prevent concurrent setups + setupInProgress = true const checkAndSetup = () => { const mapObj = mapInstance.value const layerExists = mapObj?.getLayer('locations-layer') const styleLoaded = mapObj?.isStyleLoaded() if (layerExists && styleLoaded) { - retryCount = 0 // Reset on success + retryCount = 0 setupLocationsLayerListeners() updateLocationsSourceData() - moveSelectionLayersToTop() - setupInProgress = false // Clear flag on success + setupInProgress = false } else { retryCount++ if (retryCount >= MAX_RETRIES) { @@ -576,36 +486,25 @@ { immediate: true } ) - // Watch for active location changes (handles active-location layer) watch( () => locationsStore.activeLocation, (activeLocation) => { const mapObj = mapInstance.value - if (!mapObj || !mapObj.getSource('active-location')) return - - if (activeLocation) { - const plainFeature = JSON.parse(JSON.stringify(activeLocation)) - mapObj.getSource('active-location').setData({ - type: 'FeatureCollection', - features: [plainFeature], - }) - } else { - mapObj.getSource('active-location').setData({ - type: 'FeatureCollection', - features: [], - }) - } + if (!mapObj?.getSource('active-location')) return + + mapObj.getSource('active-location').setData({ + type: 'FeatureCollection', + features: activeLocation ? [cloneFeature(activeLocation)] : [], + }) }, - { immediate: true } + { immediate: true }, ) - // Cleanup onBeforeUnmount(() => { if (hoverPopup.value) { hoverPopup.value.remove() } - // Remove direct map listeners const mapObj = mapInstance.value if (mapObj && locationsLayerListenersAttached && mapObj.getLayer('locations-layer')) { if (locationsLayerHandlers.click) { diff --git a/src/components/TreeHealthChart.vue b/src/components/TreeHealthChart.vue index 63dc859..dba4277 100644 --- a/src/components/TreeHealthChart.vue +++ b/src/components/TreeHealthChart.vue @@ -19,6 +19,11 @@