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 @@