v1.3.2: SEO fixes + map bugfixes
SEO: - Landing page converted to Server Component (SSR) - Extracted NavAuthButtons + ContactForm as client islands - Removed fake aggregateRating from JSON-LD - Added FAQPage JSON-LD schema (7 questions) - Extended sitemap: /datenschutz, /spenden, /demo Map fixes: - WebGL context lost recovery (black tiles after inactivity) - Page visibility handler for tile reload on tab switch - Arrow direction: geographic bearing instead of screen angle - All markers rotationAlignment viewport->map (geographic orientation) - DEL key now deletes selected lines/polygons/arrows (not just symbols) - Default drawing color: black
This commit is contained in:
@@ -728,6 +728,46 @@ export function MapView({
|
||||
// Expose map instance to parent for export
|
||||
if (externalMapRef) externalMapRef.current = map.current
|
||||
|
||||
// --- WebGL context loss recovery ---
|
||||
// When the browser reclaims GPU memory (background tab, memory pressure),
|
||||
// the WebGL context is lost and tiles go black. This recovers automatically.
|
||||
const canvas = map.current.getCanvas()
|
||||
canvas.addEventListener('webglcontextlost', (e) => {
|
||||
console.warn('[Map] WebGL context lost — will restore when possible')
|
||||
e.preventDefault() // allows context to be restored
|
||||
})
|
||||
canvas.addEventListener('webglcontextrestored', () => {
|
||||
console.info('[Map] WebGL context restored — reloading map style')
|
||||
const m = map.current
|
||||
if (m) {
|
||||
// Force full tile reload by re-setting the style
|
||||
const style = m.getStyle()
|
||||
if (style) {
|
||||
m.setStyle(style)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// --- Page visibility recovery ---
|
||||
// When user switches back to this tab after a while, tiles may be stale/black.
|
||||
// Force a resize + tile re-request on visibility change.
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible' && map.current) {
|
||||
// Small delay to let browser finish tab switch
|
||||
setTimeout(() => {
|
||||
if (!map.current) return
|
||||
map.current.resize()
|
||||
// Nudge the map to force tile re-requests
|
||||
const center = map.current.getCenter()
|
||||
map.current.setCenter(center)
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
|
||||
// Store cleanup reference
|
||||
const cleanupVisibility = () => document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
|
||||
map.current.addControl(new maplibregl.NavigationControl(), 'bottom-right')
|
||||
map.current.addControl(new maplibregl.ScaleControl(), 'bottom-left')
|
||||
|
||||
@@ -1286,6 +1326,7 @@ export function MapView({
|
||||
})
|
||||
|
||||
return () => {
|
||||
cleanupVisibility()
|
||||
map.current?.remove()
|
||||
map.current = null
|
||||
}
|
||||
@@ -1421,26 +1462,27 @@ export function MapView({
|
||||
const lineCoords = f.geometry.coordinates as number[][]
|
||||
if (lineCoords.length < 2) return
|
||||
|
||||
// Get last two points to calculate arrow direction using screen-projected coords
|
||||
// Geographic bearing from p1 to p2 (works for short distances)
|
||||
const p1 = lineCoords[lineCoords.length - 2]
|
||||
const p2 = lineCoords[lineCoords.length - 1]
|
||||
const px1 = map.current.project(p1 as [number, number])
|
||||
const px2 = map.current.project(p2 as [number, number])
|
||||
const screenAngle = Math.atan2(px2.y - px1.y, px2.x - px1.x) * (180 / Math.PI) + 90
|
||||
const dLng = p2[0] - p1[0]
|
||||
const dLat = p2[1] - p1[1]
|
||||
// atan2(dLng, dLat) gives angle from north (up), clockwise — matches CSS triangle ▲ default
|
||||
const geoBearing = Math.atan2(dLng, dLat) * (180 / Math.PI)
|
||||
|
||||
const color = (f.properties.color as string) || '#000000'
|
||||
const arrowEl = document.createElement('div')
|
||||
arrowEl.style.cssText = `
|
||||
width: 0; height: 0;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
border-bottom: 20px solid ${color};
|
||||
transform: rotate(${screenAngle}deg);
|
||||
border-left: 12px solid transparent;
|
||||
border-right: 12px solid transparent;
|
||||
border-bottom: 24px solid ${color};
|
||||
transform: rotate(${geoBearing}deg);
|
||||
transform-origin: center center;
|
||||
pointer-events: none;
|
||||
`
|
||||
|
||||
const marker = new maplibregl.Marker({ element: arrowEl, anchor: 'center', rotationAlignment: 'viewport' })
|
||||
const marker = new maplibregl.Marker({ element: arrowEl, anchor: 'center', rotationAlignment: 'map' })
|
||||
.setLngLat(p2 as [number, number])
|
||||
.addTo(map.current)
|
||||
markersRef.current.push(marker)
|
||||
@@ -1545,7 +1587,7 @@ export function MapView({
|
||||
})
|
||||
}
|
||||
|
||||
const marker = new maplibregl.Marker({ element: el, anchor: 'center', draggable: canEdit, rotationAlignment: 'viewport' })
|
||||
const marker = new maplibregl.Marker({ element: el, anchor: 'center', draggable: canEdit, rotationAlignment: 'map' })
|
||||
.setLngLat(midpoint)
|
||||
.addTo(map.current)
|
||||
|
||||
@@ -1664,7 +1706,7 @@ export function MapView({
|
||||
}
|
||||
|
||||
try {
|
||||
const marker = new maplibregl.Marker({ element: wrapper, draggable: canEdit, anchor: 'center', rotationAlignment: 'viewport' })
|
||||
const marker = new maplibregl.Marker({ element: wrapper, draggable: canEdit, anchor: 'center', rotationAlignment: 'map' })
|
||||
.setLngLat(coords)
|
||||
.addTo(map.current)
|
||||
|
||||
@@ -1730,7 +1772,7 @@ export function MapView({
|
||||
el.textContent = (f.properties.text as string) || ''
|
||||
wrapper.appendChild(el)
|
||||
|
||||
const marker = new maplibregl.Marker({ element: wrapper, draggable: canEdit, anchor: 'center', rotationAlignment: 'viewport' })
|
||||
const marker = new maplibregl.Marker({ element: wrapper, draggable: canEdit, anchor: 'center', rotationAlignment: 'map' })
|
||||
.setLngLat(coords)
|
||||
.addTo(map.current)
|
||||
|
||||
@@ -1892,18 +1934,27 @@ export function MapView({
|
||||
}
|
||||
}, [drawMode, deselectSymbol])
|
||||
|
||||
// ESC to cancel drawing, DEL to delete selected symbol
|
||||
// ESC to cancel drawing, DEL to delete selected symbol/line/polygon
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// DEL / Backspace → delete selected symbol
|
||||
// DEL / Backspace → delete selected symbol or line/polygon
|
||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
const tag = (e.target as HTMLElement)?.tagName
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || (e.target as HTMLElement)?.isContentEditable) return
|
||||
// Delete selected symbol/text
|
||||
if (selectedSymbolRef.current) {
|
||||
e.preventDefault()
|
||||
deleteSelectedSymbol()
|
||||
return
|
||||
}
|
||||
// Delete selected line/polygon/arrow (vertex-editing selection)
|
||||
if (selectedLineIdRef.current) {
|
||||
e.preventDefault()
|
||||
const updated = featuresRef.current.filter(f => f.id !== selectedLineIdRef.current)
|
||||
onFeaturesChangeRef.current(updated)
|
||||
showVertexMarkersRef.current(null)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
// In measure mode: finalize (keep line + labels), just stop adding
|
||||
|
||||
Reference in New Issue
Block a user