mirror of
https://github.com/zvx-echo6/navi.git
synced 2026-05-20 14:44:51 +02:00
fix(map): boundary fill opacity and highlight text duplication
Two fixes for rendering at close zoom (z14+):
1. Boundary rendering:
- Add subtle fill layer with 0.05 opacity (barely visible tint)
- Insert fill and outline layers BELOW symbol layers using
firstSymbolId so labels render on top
- Dashed outline remains primary indicator
2. Highlight text duplication:
- Remove separate hover-hl-* and selected-hl-* symbol layers
that were creating duplicate/ghost text
- Instead, modify ORIGINAL layer paint properties directly using
setPaintProperty on text-color, text-halo-color, text-halo-width
- Store original paint values for restoration on clear
- Single symbol layer per label = no duplication
Test at z14+ on small places like Rock Creek Park - no text ghosting.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
68f0a8dff7
commit
39996fdafe
1 changed files with 93 additions and 35 deletions
|
|
@ -43,44 +43,78 @@ const MEASURE_SOURCE = 'measure-source'
|
||||||
const MEASURE_LINE_LAYER = 'measure-line-layer'
|
const MEASURE_LINE_LAYER = 'measure-line-layer'
|
||||||
const MEASURE_POINT_LAYER = 'measure-point-layer'
|
const MEASURE_POINT_LAYER = 'measure-point-layer'
|
||||||
|
|
||||||
// Highlight layers (filter-based for PMTiles compatibility)
|
// Highlight state - modify original layer paint properties (no duplicate layers)
|
||||||
const HIGHLIGHT_SOURCE_LAYERS = ['places', 'pois']
|
const INTERACTIVE_LABEL_LAYERS = ['pois', 'places_subplace', 'places_locality', 'places_region', 'places_country']
|
||||||
const EMPTY_FILTER = ['==', ['get', 'name'], '___NOMATCH___']
|
let originalPaintValues = {} // Store original paint values for restoration
|
||||||
|
let currentHighlightedLayer = null
|
||||||
|
let currentHoveredLayer = null
|
||||||
|
|
||||||
function setupHighlightLayers(map, isDark) {
|
function storeOriginalPaint(map, layerId) {
|
||||||
const accentColor = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim() || '#7a9a6b'
|
if (originalPaintValues[layerId]) return // Already stored
|
||||||
HIGHLIGHT_SOURCE_LAYERS.forEach(sl => {
|
if (!map.getLayer(layerId)) return
|
||||||
if (map.getLayer('hover-hl-' + sl)) map.removeLayer('hover-hl-' + sl)
|
originalPaintValues[layerId] = {
|
||||||
if (map.getLayer('selected-hl-' + sl)) map.removeLayer('selected-hl-' + sl)
|
'text-color': map.getPaintProperty(layerId, 'text-color'),
|
||||||
})
|
'text-halo-color': map.getPaintProperty(layerId, 'text-halo-color'),
|
||||||
HIGHLIGHT_SOURCE_LAYERS.forEach(sourceLayer => {
|
'text-halo-width': map.getPaintProperty(layerId, 'text-halo-width'),
|
||||||
map.addLayer({
|
}
|
||||||
id: 'hover-hl-' + sourceLayer, type: 'symbol', source: 'protomaps', 'source-layer': sourceLayer,
|
}
|
||||||
filter: EMPTY_FILTER,
|
|
||||||
layout: { 'text-field': ['coalesce', ['get', 'name:en'], ['get', 'name']], 'text-font': ['Noto Sans Regular'], 'text-size': ['interpolate', ['linear'], ['zoom'], 4, 10, 10, 14, 16, 18], 'text-allow-overlap': true, 'text-ignore-placement': true },
|
function restoreOriginalPaint(map, layerId) {
|
||||||
paint: { 'text-color': isDark ? '#ffffff' : '#000000', 'text-halo-color': isDark ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.2)', 'text-halo-width': 2.5 },
|
if (!originalPaintValues[layerId] || !map.getLayer(layerId)) return
|
||||||
})
|
const orig = originalPaintValues[layerId]
|
||||||
map.addLayer({
|
if (orig['text-color'] !== undefined) map.setPaintProperty(layerId, 'text-color', orig['text-color'])
|
||||||
id: 'selected-hl-' + sourceLayer, type: 'symbol', source: 'protomaps', 'source-layer': sourceLayer,
|
if (orig['text-halo-color'] !== undefined) map.setPaintProperty(layerId, 'text-halo-color', orig['text-halo-color'])
|
||||||
filter: EMPTY_FILTER,
|
if (orig['text-halo-width'] !== undefined) map.setPaintProperty(layerId, 'text-halo-width', orig['text-halo-width'])
|
||||||
layout: { 'text-field': ['coalesce', ['get', 'name:en'], ['get', 'name']], 'text-font': ['Noto Sans Regular'], 'text-size': ['interpolate', ['linear'], ['zoom'], 4, 10, 10, 14, 16, 18], 'text-allow-overlap': true, 'text-ignore-placement': true },
|
|
||||||
paint: { 'text-color': accentColor, 'text-halo-color': isDark ? 'rgba(122,154,107,0.5)' : 'rgba(122,154,107,0.3)', 'text-halo-width': 3 },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setHoverHighlight(map, feature) {
|
function setHoverHighlight(map, feature) {
|
||||||
HIGHLIGHT_SOURCE_LAYERS.forEach(sl => { if (map.getLayer('hover-hl-' + sl)) map.setFilter('hover-hl-' + sl, EMPTY_FILTER) })
|
// Restore previous hovered layer
|
||||||
|
if (currentHoveredLayer && currentHoveredLayer !== currentHighlightedLayer) {
|
||||||
|
restoreOriginalPaint(map, currentHoveredLayer)
|
||||||
|
}
|
||||||
|
currentHoveredLayer = null
|
||||||
|
|
||||||
if (!feature) return
|
if (!feature) return
|
||||||
const name = feature.properties?.name, sourceLayer = feature.sourceLayer
|
const layerId = feature.layer?.id
|
||||||
if (name && sourceLayer && map.getLayer('hover-hl-' + sourceLayer)) map.setFilter('hover-hl-' + sourceLayer, ['==', ['get', 'name'], name])
|
if (!layerId || !map.getLayer(layerId)) return
|
||||||
|
if (layerId === currentHighlightedLayer) return // Don't hover over selected
|
||||||
|
|
||||||
|
storeOriginalPaint(map, layerId)
|
||||||
|
currentHoveredLayer = layerId
|
||||||
|
|
||||||
|
const isDark = document.documentElement.getAttribute('data-theme') === 'dark'
|
||||||
|
map.setPaintProperty(layerId, 'text-color', isDark ? '#ffffff' : '#000000')
|
||||||
|
map.setPaintProperty(layerId, 'text-halo-color', isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.25)')
|
||||||
|
map.setPaintProperty(layerId, 'text-halo-width', 2.5)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSelectedHighlight(map, feature) {
|
function setSelectedHighlight(map, feature) {
|
||||||
HIGHLIGHT_SOURCE_LAYERS.forEach(sl => { if (map.getLayer('selected-hl-' + sl)) map.setFilter('selected-hl-' + sl, EMPTY_FILTER) })
|
// Restore previous highlighted layer
|
||||||
|
if (currentHighlightedLayer) {
|
||||||
|
restoreOriginalPaint(map, currentHighlightedLayer)
|
||||||
|
}
|
||||||
|
currentHighlightedLayer = null
|
||||||
|
currentHoveredLayer = null // Also clear hover
|
||||||
|
|
||||||
if (!feature) return
|
if (!feature) return
|
||||||
const name = feature.properties?.name, sourceLayer = feature.sourceLayer
|
const layerId = feature.layer?.id
|
||||||
if (name && sourceLayer && map.getLayer('selected-hl-' + sourceLayer)) map.setFilter('selected-hl-' + sourceLayer, ['==', ['get', 'name'], name])
|
if (!layerId || !map.getLayer(layerId)) return
|
||||||
|
|
||||||
|
storeOriginalPaint(map, layerId)
|
||||||
|
currentHighlightedLayer = layerId
|
||||||
|
|
||||||
|
const accentColor = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim() || '#7a9a6b'
|
||||||
|
const isDark = document.documentElement.getAttribute('data-theme') === 'dark'
|
||||||
|
map.setPaintProperty(layerId, 'text-color', accentColor)
|
||||||
|
map.setPaintProperty(layerId, 'text-halo-color', isDark ? 'rgba(122,154,107,0.6)' : 'rgba(122,154,107,0.4)')
|
||||||
|
map.setPaintProperty(layerId, 'text-halo-width', 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAllHighlights(map) {
|
||||||
|
if (currentHoveredLayer) restoreOriginalPaint(map, currentHoveredLayer)
|
||||||
|
if (currentHighlightedLayer) restoreOriginalPaint(map, currentHighlightedLayer)
|
||||||
|
currentHoveredLayer = null
|
||||||
|
currentHighlightedLayer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build a full MapLibre style object for the given theme */
|
/** Build a full MapLibre style object for the given theme */
|
||||||
|
|
@ -656,7 +690,9 @@ function removeContoursTest10ft(map) {
|
||||||
if (map.getLayer(CONTOUR_TEST_10FT_MINOR)) map.removeLayer(CONTOUR_TEST_10FT_MINOR)
|
if (map.getLayer(CONTOUR_TEST_10FT_MINOR)) map.removeLayer(CONTOUR_TEST_10FT_MINOR)
|
||||||
if (map.getSource(CONTOUR_TEST_10FT_SOURCE)) map.removeSource(CONTOUR_TEST_10FT_SOURCE)
|
if (map.getSource(CONTOUR_TEST_10FT_SOURCE)) map.removeSource(CONTOUR_TEST_10FT_SOURCE)
|
||||||
}
|
}
|
||||||
/** Add boundary polygon layer with computed accent color (MapLibre rejects CSS vars in paint) */
|
/** Add boundary polygon layers with computed accent color (MapLibre rejects CSS vars in paint) */
|
||||||
|
const BOUNDARY_FILL_LAYER = 'boundary-fill-layer'
|
||||||
|
|
||||||
function addBoundaryLayer(map) {
|
function addBoundaryLayer(map) {
|
||||||
if (!map || map.getLayer(BOUNDARY_LAYER)) return
|
if (!map || map.getLayer(BOUNDARY_LAYER)) return
|
||||||
if (!map.getSource(BOUNDARY_SOURCE)) {
|
if (!map.getSource(BOUNDARY_SOURCE)) {
|
||||||
|
|
@ -666,6 +702,29 @@ function addBoundaryLayer(map) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const accentColor = getComputedStyle(document.documentElement).getPropertyValue("--accent").trim() || "#7a9a6b"
|
const accentColor = getComputedStyle(document.documentElement).getPropertyValue("--accent").trim() || "#7a9a6b"
|
||||||
|
|
||||||
|
// Find first symbol layer to insert boundary layers below labels
|
||||||
|
const layers = map.getStyle().layers
|
||||||
|
let firstSymbolId = null
|
||||||
|
for (const layer of layers) {
|
||||||
|
if (layer.type === 'symbol') {
|
||||||
|
firstSymbolId = layer.id
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add subtle fill layer (barely visible tint)
|
||||||
|
map.addLayer({
|
||||||
|
id: BOUNDARY_FILL_LAYER,
|
||||||
|
type: "fill",
|
||||||
|
source: BOUNDARY_SOURCE,
|
||||||
|
paint: {
|
||||||
|
"fill-color": accentColor,
|
||||||
|
"fill-opacity": 0.05,
|
||||||
|
},
|
||||||
|
}, firstSymbolId)
|
||||||
|
|
||||||
|
// Add dashed outline layer
|
||||||
map.addLayer({
|
map.addLayer({
|
||||||
id: BOUNDARY_LAYER,
|
id: BOUNDARY_LAYER,
|
||||||
type: "line",
|
type: "line",
|
||||||
|
|
@ -676,7 +735,7 @@ function addBoundaryLayer(map) {
|
||||||
"line-opacity": 0.7,
|
"line-opacity": 0.7,
|
||||||
"line-dasharray": [3, 2],
|
"line-dasharray": [3, 2],
|
||||||
},
|
},
|
||||||
})
|
}, firstSymbolId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const MapView = forwardRef(function MapView(_, ref) {
|
const MapView = forwardRef(function MapView(_, ref) {
|
||||||
|
|
@ -1434,8 +1493,6 @@ const MapView = forwardRef(function MapView(_, ref) {
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
// Set up highlight layers
|
|
||||||
setupHighlightLayers(map, document.documentElement.getAttribute('data-theme') === 'dark')
|
|
||||||
|
|
||||||
// Register updateBoundary function - called directly when boundary data arrives
|
// Register updateBoundary function - called directly when boundary data arrives
|
||||||
const updateBoundaryFn = (boundaryGeometry) => {
|
const updateBoundaryFn = (boundaryGeometry) => {
|
||||||
|
|
@ -1620,8 +1677,9 @@ const MapView = forwardRef(function MapView(_, ref) {
|
||||||
if (activeLayersRef.current.publicLands) addPublicLands(map)
|
if (activeLayersRef.current.publicLands) addPublicLands(map)
|
||||||
if (activeLayersRef.current.contours) addContours(map)
|
if (activeLayersRef.current.contours) addContours(map)
|
||||||
|
|
||||||
// Re-setup highlight layers
|
// Clear highlights on theme change (paint values will be re-stored on next interaction)
|
||||||
setupHighlightLayers(map, theme === 'dark')
|
clearAllHighlights(map)
|
||||||
|
originalPaintValues = {}
|
||||||
|
|
||||||
// Restore view
|
// Restore view
|
||||||
map.jumpTo({ center, zoom, bearing, pitch })
|
map.jumpTo({ center, zoom, bearing, pitch })
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue