From fcd1ff15ae9dba5f6127ea69f82c21a9edeeef51 Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Fri, 24 Apr 2026 00:56:45 +0200 Subject: [PATCH 1/4] steepness labels --- app/javascript/maplibre/layers/geojson.js | 3 +- .../maplibre/layers/geojson/route_extras.js | 96 +++++++++++++++++++ app/javascript/maplibre/map.js | 5 +- app/javascript/maplibre/styles/styles.js | 3 + 4 files changed, 104 insertions(+), 3 deletions(-) diff --git a/app/javascript/maplibre/layers/geojson.js b/app/javascript/maplibre/layers/geojson.js index ca166855..ef6f29f5 100644 --- a/app/javascript/maplibre/layers/geojson.js +++ b/app/javascript/maplibre/layers/geojson.js @@ -1,7 +1,7 @@ import { buffer } from "@turf/buffer" import { draw, select } from 'maplibre/edit' import { initializeKmMarkerStyles, renderKmMarkers } from 'maplibre/layers/geojson/km_markers' -import { renderRouteExtras } from 'maplibre/layers/geojson/route_extras' +import { initializeSteepnessLabelStyles, renderRouteExtras } from 'maplibre/layers/geojson/route_extras' import { Layer } from 'maplibre/layers/layer' import { getFeature } from 'maplibre/layers/layers' import { addGeoJSONSource, map, mapProperties } from 'maplibre/map' @@ -27,6 +27,7 @@ export class GeoJSONLayer extends Layer { if (this.layer.cluster) { initializeClusterStyles(this.sourceId, null) } initializeKmMarkerStyles(this.kmMarkerSourceId) initializeViewStyles(this.routeExtrasSourceId) + initializeSteepnessLabelStyles(this.routeExtrasSourceId) // Override line-cap to 'butt' for route extras line layer to prevent color overlap at segment junctions // Keep outline as 'round' to ensure continuous white border diff --git a/app/javascript/maplibre/layers/geojson/route_extras.js b/app/javascript/maplibre/layers/geojson/route_extras.js index 0bf8dfa5..434b9944 100644 --- a/app/javascript/maplibre/layers/geojson/route_extras.js +++ b/app/javascript/maplibre/layers/geojson/route_extras.js @@ -3,6 +3,13 @@ import { distance } from "@turf/distance" import { point } from "@turf/helpers" import { map } from 'maplibre/map' +// Steepness value to percentage range mapping for labels +const STEEPNESS_RANGES = { + 3: '7-11%', + 4: '12-15%', + 5: '>16%' +} + // ORS route extras color configurations // Each type maps values to colors and labels for data-driven styling export const EXTRAS_COLOR_CONFIGS = { @@ -76,6 +83,60 @@ export function computeExtrasTotals (feature, extrasType) { return { type: extrasType, config, totals, totalDistance } } +// Create point features with steepness labels for steep segments +function createSteepnessLabelFeatures (coords, extrasValues) { + const labelFeatures = [] + + extrasValues.forEach(([startIdx, endIdx, value]) => { + if (Math.abs(value) < 3) return + if (endIdx <= startIdx || startIdx >= coords.length) return + + const end = Math.min(endIdx, coords.length - 1) + const firstCoord = coords[startIdx] + const lastCoord = coords[end] + + // Calculate segment length by summing consecutive point distances + let segmentLength = 0 + for (let i = startIdx; i < end; i++) { + segmentLength += distance(point(coords[i]), point(coords[i + 1]), { units: 'meters' }) + } + + // Skip very short segments + if (segmentLength < 50) return + + // Compute midpoint by averaging first and last coordinates + const midpoint = [ + (firstCoord[0] + lastCoord[0]) / 2, + (firstCoord[1] + lastCoord[1]) / 2 + ] + + // Format distance label + let distanceLabel + if (segmentLength >= 1000) { + distanceLabel = `${(segmentLength / 1000).toFixed(1)} km` + } else { + distanceLabel = `${Math.round(segmentLength)} m` + } + + // Format label with direction arrow, range, and length on second line + const rangeLabel = STEEPNESS_RANGES[Math.abs(value)] + const arrow = value > 0 ? '▲' : '▼' + const label = `${arrow} ${rangeLabel}\n${distanceLabel}` + + labelFeatures.push({ + type: 'Feature', + geometry: { type: 'Point', coordinates: midpoint }, + properties: { + 'steepness-label': label, + 'steepness-color': resolveExtrasColor('steepness', value), + 'steepness-priority': Math.abs(value) + } + }) + }) + + return labelFeatures +} + // Show/hide the map legend for route extras export function showExtrasLegend (extrasType, activeValues) { let container = document.getElementById('route-extras-legend') @@ -177,6 +238,12 @@ export function renderRouteExtras (features, sourceId) { } }) }) + + // Add steepness labels for steep segments + if (extrasType === 'steepness') { + const labelFeatures = createSteepnessLabelFeatures(coords, extrasData.values) + extrasFeatures.push(...labelFeatures) + } }) // Filter activeValues for discrete legends: remove values < 0.5% of total distance @@ -225,3 +292,32 @@ export function renderRouteExtras (features, sourceId) { features: extrasFeatures.concat(extrusionFeatures) }) } + +export function initializeSteepnessLabelStyles (sourceId) { + const layerId = `steepness-labels_${sourceId}` + if (map.getLayer(layerId)) return + + map.addLayer({ + id: layerId, + source: sourceId, + type: 'symbol', + filter: ['has', 'steepness-label'], + layout: { + 'text-field': ['get', 'steepness-label'], + 'text-font': ['noto_sans_bold'], + 'text-size': 11, + 'text-allow-overlap': false, + 'text-ignore-placement': false, + 'text-anchor': 'center', + 'text-justify': 'center', + 'text-padding': 4, + 'symbol-sort-key': ['-', 10, ['get', 'steepness-priority']] + }, + paint: { + 'text-color': '#ffffff', + 'text-halo-color': ['get', 'steepness-color'], + 'text-halo-width': 14, + 'text-halo-blur': 0 + } + }) +} diff --git a/app/javascript/maplibre/map.js b/app/javascript/maplibre/map.js index 1aa7e9c3..84cc5c87 100644 --- a/app/javascript/maplibre/map.js +++ b/app/javascript/maplibre/map.js @@ -526,7 +526,8 @@ export function sortLayers () { // console.log('Sorting layers', layers) const userExtrusions = functions.reduceArray(layers, (e) => e.paint && e.paint['fill-extrusion-height'] && e.id.startsWith('polygon-layer-extrusion')) const flatLayers = functions.reduceArray(layers, (e) => (e.id.includes('-flat'))) // keep flat layers behin houses - const routeExtras = functions.reduceArray(layers, (e) => (e.id.includes('route-extras-source'))) + const routeExtras = functions.reduceArray(layers, (e) => (e.id.includes('route-extras-source') && !e.id.startsWith('steepness-labels'))) + const steepnessLabels = functions.reduceArray(layers, (e) => (e.id.startsWith('steepness-labels'))) const kmMarkers = functions.reduceArray(layers, (e) => (e.id.startsWith('km-marker'))) const editLayer = functions.reduceArray(layers, (e) => (e.id.startsWith('gl-draw-'))) const userSymbols = functions.reduceArray(layers, (e) => (e.id.startsWith('symbols-layer') || e.id.startsWith('symbols-border-layer'))) @@ -541,7 +542,7 @@ export function sortLayers () { layers = layers.concat(flatLayers).concat(lineLayers).concat(routeExtras).concat(userExtrusions).concat(mapExtrusions).concat(directions) .concat(mapSymbols).concat(points).concat(heatmap).concat(editLayer) - .concat(kmMarkers).concat(userSymbols).concat(userLabels).concat(lineLayerHits).concat(pointsLayerHits) + .concat(kmMarkers).concat(steepnessLabels).concat(userSymbols).concat(userLabels).concat(lineLayerHits).concat(pointsLayerHits) const newStyle = { ...currentStyle, layers } map.setStyle(newStyle, { diff: true }) diff --git a/app/javascript/maplibre/styles/styles.js b/app/javascript/maplibre/styles/styles.js index d1d42f41..57ef3c93 100644 --- a/app/javascript/maplibre/styles/styles.js +++ b/app/javascript/maplibre/styles/styles.js @@ -526,6 +526,7 @@ export function styles () { ['==', ['get', 'flat'], true], ["!", ["has", "point_count"]], ["!", ["has", "marker-image-url"]], + ["!", ["has", "steepness-label"]], minZoomFilter], paint: { "circle-pitch-alignment": "map", @@ -578,6 +579,7 @@ export function styles () { ['!=', ['get', 'flat'], true], ["!", ["has", "point_count"]], ["!", ["has", "marker-image-url"]], + ["!", ["has", "steepness-label"]], minZoomFilter ], paint: { @@ -626,6 +628,7 @@ export function styles () { filter: [ "all", ["==", ["geometry-type"], "Point"], + ["!", ["has", "steepness-label"]], minZoomFilter ], paint: { From d1350ef14935b350a552d57ec452d224fe4eafcd Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Fri, 24 Apr 2026 15:27:38 +0200 Subject: [PATCH 2/4] render km markers as one symbol (background + text) for proper symbol sorting --- .../maplibre/layers/geojson/km_markers.js | 108 +++++++++++------- .../maplibre/layers/geojson/route_extras.js | 6 +- app/javascript/maplibre/map.js | 3 +- 3 files changed, 73 insertions(+), 44 deletions(-) diff --git a/app/javascript/maplibre/layers/geojson/km_markers.js b/app/javascript/maplibre/layers/geojson/km_markers.js index ae7b7aea..8401c556 100644 --- a/app/javascript/maplibre/layers/geojson/km_markers.js +++ b/app/javascript/maplibre/layers/geojson/km_markers.js @@ -2,7 +2,40 @@ import { along } from "@turf/along" import { lineString } from "@turf/helpers" import { length } from "@turf/length" import { map, removeStyleLayers } from 'maplibre/map' -import { featureColor, labelFont, setSource, styles } from 'maplibre/styles/styles' +import { featureColor, labelFont, setSource } from 'maplibre/styles/styles' + +function createKmMarkerImage (color) { + const imageName = `km-marker-circle-${color.replace('#', '')}` + + if (!map.hasImage(imageName)) { + const size = 32 + const canvas = document.createElement('canvas') + canvas.width = size + canvas.height = size + const ctx = canvas.getContext('2d') + + const centerX = size / 2 + const centerY = size / 2 + const radius = size / 2 - 3 + + // Draw gray border + ctx.strokeStyle = '#CCC' + ctx.lineWidth = 2 + ctx.beginPath() + ctx.arc(centerX, centerY, radius, 0, Math.PI * 2) + ctx.stroke() + + // Draw filled circle + ctx.fillStyle = color + ctx.beginPath() + ctx.arc(centerX, centerY, radius - 1, 0, Math.PI * 2) + ctx.fill() + + map.addImage(imageName, ctx.getImageData(0, 0, size, size)) + } + + return imageName +} export function renderKmMarkers (features, sourceId) { let kmMarkerFeatures = [] @@ -12,10 +45,14 @@ export function renderKmMarkers (features, sourceId) { const line = lineString(f.geometry.coordinates) const distance = length(line, { units: 'kilometers' }) + const markerColor = f.properties['stroke'] || featureColor + const markerImageName = createKmMarkerImage(markerColor) + let interval = 1 for (let i = 0; i < Math.ceil(distance) + interval; i += interval) { const point = along(line, i, { units: 'kilometers' }) - point.properties['marker-color'] = f.properties['stroke'] || featureColor + point.properties['marker-color'] = markerColor + point.properties['marker-image'] = markerImageName point.properties['marker-size'] = 11 point.properties['marker-opacity'] = 1 point.properties['km'] = i @@ -45,6 +82,7 @@ export function renderKmMarkers (features, sourceId) { export function initializeKmMarkerStyles (sourceId) { removeStyleLayers(sourceId) + kmMarkerStyles().forEach(style => { style = setSource(style, sourceId) map.addLayer(style) @@ -54,33 +92,27 @@ export function initializeKmMarkerStyles (sourceId) { function kmMarkerStyles () { let styleLayers = [] - styleLayers.push(makePointsLayer(1, 14)) - styleLayers.push(makeNumbersLayer(1, 14)) - styleLayers.push(makePointsLayer(2, 11, 14)) - styleLayers.push(makeNumbersLayer(2, 11, 14)) - styleLayers.push(makePointsLayer(5, 10, 11)) - styleLayers.push(makeNumbersLayer(5, 10, 11)) - styleLayers.push(makePointsLayer(10, 9, 10)) - styleLayers.push(makeNumbersLayer(10, 9, 10)) - styleLayers.push(makePointsLayer(25, 8, 9)) - styleLayers.push(makeNumbersLayer(25, 8, 9)) - styleLayers.push(makePointsLayer(50, 7, 8)) - styleLayers.push(makeNumbersLayer(50, 7, 8)) - styleLayers.push(makePointsLayer(100, 5, 7)) - styleLayers.push(makeNumbersLayer(100, 5, 7)) - - const base = { ...styles()['points-layer'] } - styleLayers.push({ - ...base, - id: `km-marker-points-end`, - filter: ["==", ["get", "km-marker-numbers-end"], 1] - }) + // Combined marker layers (icon + text in one symbol layer) + styleLayers.push(makeKmMarkerLayer(1, 14)) + styleLayers.push(makeKmMarkerLayer(2, 12, 14)) + styleLayers.push(makeKmMarkerLayer(5, 10, 12)) + styleLayers.push(makeKmMarkerLayer(10, 9, 10)) + styleLayers.push(makeKmMarkerLayer(25, 8, 9)) + styleLayers.push(makeKmMarkerLayer(50, 7, 8)) + styleLayers.push(makeKmMarkerLayer(100, 5, 7)) + + // End marker (total distance) - combined icon + text styleLayers.push({ - id: `km-marker-numbers-end`, + id: `km-marker-end`, type: 'symbol', filter: ["==", ["get", "km-marker-numbers-end"], 1], layout: { - 'text-allow-overlap': true, + 'icon-image': ['get', 'marker-image'], + 'icon-size': ['/', ['get', 'marker-size'], 14], + 'icon-allow-overlap': false, + 'icon-padding': 0, + 'text-allow-overlap': false, + 'text-padding': 0, 'text-field': ['format', ['get', 'km'], { 'font-scale': 1.0 }, ['concat', '\n', ['get', 'km-unit']], { 'font-scale': 0.7 } @@ -90,7 +122,8 @@ function kmMarkerStyles () { 'text-justify': 'center', 'text-anchor': 'center', 'text-line-height': 1.0, - 'text-offset': [0, 0.3] + 'text-offset': [0, 0.3], + 'symbol-sort-key': 20 }, paint: { 'text-color': '#ffffff' @@ -100,31 +133,26 @@ function kmMarkerStyles () { return styleLayers } -function makePointsLayer (divisor, minzoom, maxzoom = 24) { - const base = { ...styles()['points-layer'] } +function makeKmMarkerLayer (divisor, minzoom, maxzoom = 24) { return { - ...base, - id: `km-marker-points-${divisor}`, - filter: ["==", ["%", ["get", "km"], divisor], 0], - minzoom, - maxzoom - } -} - -function makeNumbersLayer (divisor, minzoom, maxzoom = 24) { - return { - id: `km-marker-numbers-${divisor}`, + id: `km-marker-${divisor}`, type: 'symbol', filter: ["==", ["%", ["get", "km"], divisor], 0], minzoom, maxzoom, layout: { + 'icon-image': ['get', 'marker-image'], + 'icon-size': ['/', ['get', 'marker-size'], 14], + 'icon-allow-overlap': false, + 'icon-padding': 0, 'text-allow-overlap': false, + 'text-padding': 0, 'text-field': ['get', 'km'], 'text-size': 11, 'text-font': labelFont, 'text-justify': 'center', - 'text-anchor': 'center' + 'text-anchor': 'center', + 'symbol-sort-key': 10 }, paint: { 'text-color': '#ffffff' diff --git a/app/javascript/maplibre/layers/geojson/route_extras.js b/app/javascript/maplibre/layers/geojson/route_extras.js index 434b9944..b0106ef9 100644 --- a/app/javascript/maplibre/layers/geojson/route_extras.js +++ b/app/javascript/maplibre/layers/geojson/route_extras.js @@ -310,14 +310,14 @@ export function initializeSteepnessLabelStyles (sourceId) { 'text-ignore-placement': false, 'text-anchor': 'center', 'text-justify': 'center', - 'text-padding': 4, + 'text-padding': 0, + // on collission, show the strongest steepness 'symbol-sort-key': ['-', 10, ['get', 'steepness-priority']] }, paint: { 'text-color': '#ffffff', 'text-halo-color': ['get', 'steepness-color'], - 'text-halo-width': 14, - 'text-halo-blur': 0 + 'text-halo-width': 2 } }) } diff --git a/app/javascript/maplibre/map.js b/app/javascript/maplibre/map.js index 84cc5c87..c7c95704 100644 --- a/app/javascript/maplibre/map.js +++ b/app/javascript/maplibre/map.js @@ -528,6 +528,7 @@ export function sortLayers () { const flatLayers = functions.reduceArray(layers, (e) => (e.id.includes('-flat'))) // keep flat layers behin houses const routeExtras = functions.reduceArray(layers, (e) => (e.id.includes('route-extras-source') && !e.id.startsWith('steepness-labels'))) const steepnessLabels = functions.reduceArray(layers, (e) => (e.id.startsWith('steepness-labels'))) + const kmEndMarkers = functions.reduceArray(layers, (e) => (e.id.startsWith('km-marker-end'))) const kmMarkers = functions.reduceArray(layers, (e) => (e.id.startsWith('km-marker'))) const editLayer = functions.reduceArray(layers, (e) => (e.id.startsWith('gl-draw-'))) const userSymbols = functions.reduceArray(layers, (e) => (e.id.startsWith('symbols-layer') || e.id.startsWith('symbols-border-layer'))) @@ -542,7 +543,7 @@ export function sortLayers () { layers = layers.concat(flatLayers).concat(lineLayers).concat(routeExtras).concat(userExtrusions).concat(mapExtrusions).concat(directions) .concat(mapSymbols).concat(points).concat(heatmap).concat(editLayer) - .concat(kmMarkers).concat(steepnessLabels).concat(userSymbols).concat(userLabels).concat(lineLayerHits).concat(pointsLayerHits) + .concat(kmMarkers).concat(steepnessLabels).concat(kmEndMarkers).concat(userSymbols).concat(userLabels).concat(lineLayerHits).concat(pointsLayerHits) const newStyle = { ...currentStyle, layers } map.setStyle(newStyle, { diff: true }) From b3fd42b039255e73d43c5a3dc04074ace23fa6f6 Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Fri, 24 Apr 2026 15:47:39 +0200 Subject: [PATCH 3/4] render surface labels --- app/javascript/maplibre/layers/geojson.js | 4 +- .../maplibre/layers/geojson/route_extras.js | 51 +++++++++++-------- app/javascript/maplibre/map.js | 6 +-- app/javascript/maplibre/styles/styles.js | 6 +-- 4 files changed, 39 insertions(+), 28 deletions(-) diff --git a/app/javascript/maplibre/layers/geojson.js b/app/javascript/maplibre/layers/geojson.js index ef6f29f5..b783944b 100644 --- a/app/javascript/maplibre/layers/geojson.js +++ b/app/javascript/maplibre/layers/geojson.js @@ -1,7 +1,7 @@ import { buffer } from "@turf/buffer" import { draw, select } from 'maplibre/edit' import { initializeKmMarkerStyles, renderKmMarkers } from 'maplibre/layers/geojson/km_markers' -import { initializeSteepnessLabelStyles, renderRouteExtras } from 'maplibre/layers/geojson/route_extras' +import { initializeExtrasLabelStyles, renderRouteExtras } from 'maplibre/layers/geojson/route_extras' import { Layer } from 'maplibre/layers/layer' import { getFeature } from 'maplibre/layers/layers' import { addGeoJSONSource, map, mapProperties } from 'maplibre/map' @@ -27,7 +27,7 @@ export class GeoJSONLayer extends Layer { if (this.layer.cluster) { initializeClusterStyles(this.sourceId, null) } initializeKmMarkerStyles(this.kmMarkerSourceId) initializeViewStyles(this.routeExtrasSourceId) - initializeSteepnessLabelStyles(this.routeExtrasSourceId) + initializeExtrasLabelStyles(this.routeExtrasSourceId) // Override line-cap to 'butt' for route extras line layer to prevent color overlap at segment junctions // Keep outline as 'round' to ensure continuous white border diff --git a/app/javascript/maplibre/layers/geojson/route_extras.js b/app/javascript/maplibre/layers/geojson/route_extras.js index b0106ef9..36eda82d 100644 --- a/app/javascript/maplibre/layers/geojson/route_extras.js +++ b/app/javascript/maplibre/layers/geojson/route_extras.js @@ -83,12 +83,13 @@ export function computeExtrasTotals (feature, extrasType) { return { type: extrasType, config, totals, totalDistance } } -// Create point features with steepness labels for steep segments -function createSteepnessLabelFeatures (coords, extrasValues) { +// Create point features with labels for route extras segments +function createExtrasLabelFeatures (coords, extrasValues, extrasType) { const labelFeatures = [] extrasValues.forEach(([startIdx, endIdx, value]) => { - if (Math.abs(value) < 3) return + // Type-specific filtering + if (extrasType === 'steepness' && Math.abs(value) < 3) return if (endIdx <= startIdx || startIdx >= coords.length) return const end = Math.min(endIdx, coords.length - 1) @@ -118,18 +119,28 @@ function createSteepnessLabelFeatures (coords, extrasValues) { distanceLabel = `${Math.round(segmentLength)} m` } - // Format label with direction arrow, range, and length on second line - const rangeLabel = STEEPNESS_RANGES[Math.abs(value)] - const arrow = value > 0 ? '▲' : '▼' - const label = `${arrow} ${rangeLabel}\n${distanceLabel}` + // Type-specific label formatting + let label, priority + if (extrasType === 'steepness') { + const rangeLabel = STEEPNESS_RANGES[Math.abs(value)] + const arrow = value > 0 ? '▲' : '▼' + label = `${arrow} ${rangeLabel}\n${distanceLabel}` + priority = Math.abs(value) + } else if (extrasType === 'surface') { + const surfaceName = EXTRAS_COLOR_CONFIGS.surface.labels[String(value)] || 'Unknown' + label = `${surfaceName}\n${distanceLabel}` + priority = segmentLength + } else { + return // Unknown type + } labelFeatures.push({ type: 'Feature', geometry: { type: 'Point', coordinates: midpoint }, properties: { - 'steepness-label': label, - 'steepness-color': resolveExtrasColor('steepness', value), - 'steepness-priority': Math.abs(value) + 'route-extras-label': label, + 'route-extras-color': resolveExtrasColor(extrasType, value), + 'route-extras-priority': priority } }) }) @@ -239,9 +250,9 @@ export function renderRouteExtras (features, sourceId) { }) }) - // Add steepness labels for steep segments - if (extrasType === 'steepness') { - const labelFeatures = createSteepnessLabelFeatures(coords, extrasData.values) + // Add labels for steepness or surface segments + if (extrasType === 'steepness' || extrasType === 'surface') { + const labelFeatures = createExtrasLabelFeatures(coords, extrasData.values, extrasType) extrasFeatures.push(...labelFeatures) } }) @@ -293,17 +304,17 @@ export function renderRouteExtras (features, sourceId) { }) } -export function initializeSteepnessLabelStyles (sourceId) { - const layerId = `steepness-labels_${sourceId}` +export function initializeExtrasLabelStyles (sourceId) { + const layerId = `route-extras-labels_${sourceId}` if (map.getLayer(layerId)) return map.addLayer({ id: layerId, source: sourceId, type: 'symbol', - filter: ['has', 'steepness-label'], + filter: ['has', 'route-extras-label'], layout: { - 'text-field': ['get', 'steepness-label'], + 'text-field': ['get', 'route-extras-label'], 'text-font': ['noto_sans_bold'], 'text-size': 11, 'text-allow-overlap': false, @@ -311,12 +322,12 @@ export function initializeSteepnessLabelStyles (sourceId) { 'text-anchor': 'center', 'text-justify': 'center', 'text-padding': 0, - // on collission, show the strongest steepness - 'symbol-sort-key': ['-', 10, ['get', 'steepness-priority']] + // on collision, show the highest priority (steepest grade or longest surface segment) + 'symbol-sort-key': ['-', 10, ['get', 'route-extras-priority']] }, paint: { 'text-color': '#ffffff', - 'text-halo-color': ['get', 'steepness-color'], + 'text-halo-color': ['get', 'route-extras-color'], 'text-halo-width': 2 } }) diff --git a/app/javascript/maplibre/map.js b/app/javascript/maplibre/map.js index c7c95704..76605a38 100644 --- a/app/javascript/maplibre/map.js +++ b/app/javascript/maplibre/map.js @@ -526,8 +526,8 @@ export function sortLayers () { // console.log('Sorting layers', layers) const userExtrusions = functions.reduceArray(layers, (e) => e.paint && e.paint['fill-extrusion-height'] && e.id.startsWith('polygon-layer-extrusion')) const flatLayers = functions.reduceArray(layers, (e) => (e.id.includes('-flat'))) // keep flat layers behin houses - const routeExtras = functions.reduceArray(layers, (e) => (e.id.includes('route-extras-source') && !e.id.startsWith('steepness-labels'))) - const steepnessLabels = functions.reduceArray(layers, (e) => (e.id.startsWith('steepness-labels'))) + const routeExtras = functions.reduceArray(layers, (e) => (e.id.includes('route-extras-source') && !e.id.startsWith('route-extras-labels'))) + const extrasLabels = functions.reduceArray(layers, (e) => (e.id.startsWith('route-extras-labels'))) const kmEndMarkers = functions.reduceArray(layers, (e) => (e.id.startsWith('km-marker-end'))) const kmMarkers = functions.reduceArray(layers, (e) => (e.id.startsWith('km-marker'))) const editLayer = functions.reduceArray(layers, (e) => (e.id.startsWith('gl-draw-'))) @@ -543,7 +543,7 @@ export function sortLayers () { layers = layers.concat(flatLayers).concat(lineLayers).concat(routeExtras).concat(userExtrusions).concat(mapExtrusions).concat(directions) .concat(mapSymbols).concat(points).concat(heatmap).concat(editLayer) - .concat(kmMarkers).concat(steepnessLabels).concat(kmEndMarkers).concat(userSymbols).concat(userLabels).concat(lineLayerHits).concat(pointsLayerHits) + .concat(kmMarkers).concat(extrasLabels).concat(kmEndMarkers).concat(userSymbols).concat(userLabels).concat(lineLayerHits).concat(pointsLayerHits) const newStyle = { ...currentStyle, layers } map.setStyle(newStyle, { diff: true }) diff --git a/app/javascript/maplibre/styles/styles.js b/app/javascript/maplibre/styles/styles.js index 57ef3c93..81cf623a 100644 --- a/app/javascript/maplibre/styles/styles.js +++ b/app/javascript/maplibre/styles/styles.js @@ -526,7 +526,7 @@ export function styles () { ['==', ['get', 'flat'], true], ["!", ["has", "point_count"]], ["!", ["has", "marker-image-url"]], - ["!", ["has", "steepness-label"]], + ["!", ["has", "route-extras-label"]], minZoomFilter], paint: { "circle-pitch-alignment": "map", @@ -579,7 +579,7 @@ export function styles () { ['!=', ['get', 'flat'], true], ["!", ["has", "point_count"]], ["!", ["has", "marker-image-url"]], - ["!", ["has", "steepness-label"]], + ["!", ["has", "route-extras-label"]], minZoomFilter ], paint: { @@ -628,7 +628,7 @@ export function styles () { filter: [ "all", ["==", ["geometry-type"], "Point"], - ["!", ["has", "steepness-label"]], + ["!", ["has", "route-extras-label"]], minZoomFilter ], paint: { From 45cc0d0b77c2ea80b68724741956ca886fdbc80f Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Fri, 24 Apr 2026 16:23:47 +0200 Subject: [PATCH 4/4] use std font, more efficient seection length calculation --- .../maplibre/layers/geojson/km_markers.js | 2 +- .../maplibre/layers/geojson/route_extras.js | 31 ++++++++++++++----- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/app/javascript/maplibre/layers/geojson/km_markers.js b/app/javascript/maplibre/layers/geojson/km_markers.js index 8401c556..61c6d895 100644 --- a/app/javascript/maplibre/layers/geojson/km_markers.js +++ b/app/javascript/maplibre/layers/geojson/km_markers.js @@ -118,7 +118,7 @@ function kmMarkerStyles () { ['concat', '\n', ['get', 'km-unit']], { 'font-scale': 0.7 } ], 'text-size': 12, - 'text-font': ['noto_sans_bold'], + 'text-font': labelFont, 'text-justify': 'center', 'text-anchor': 'center', 'text-line-height': 1.0, diff --git a/app/javascript/maplibre/layers/geojson/route_extras.js b/app/javascript/maplibre/layers/geojson/route_extras.js index 36eda82d..04907dc7 100644 --- a/app/javascript/maplibre/layers/geojson/route_extras.js +++ b/app/javascript/maplibre/layers/geojson/route_extras.js @@ -2,6 +2,7 @@ import { buffer } from "@turf/buffer" import { distance } from "@turf/distance" import { point } from "@turf/helpers" import { map } from 'maplibre/map' +import { labelFont } from 'maplibre/styles/styles' // Steepness value to percentage range mapping for labels const STEEPNESS_RANGES = { @@ -10,6 +11,17 @@ const STEEPNESS_RANGES = { 5: '>16%' } +// Precompute cumulative distances for a coordinate array to enable O(1) segment length lookups +function computeCumulativeDistances (coords) { + const cumulative = new Array(coords.length) + cumulative[0] = 0 + for (let i = 1; i < coords.length; i++) { + const segmentDist = distance(point(coords[i - 1]), point(coords[i]), { units: 'meters' }) + cumulative[i] = cumulative[i - 1] + segmentDist + } + return cumulative +} + // ORS route extras color configurations // Each type maps values to colors and labels for data-driven styling export const EXTRAS_COLOR_CONFIGS = { @@ -84,7 +96,7 @@ export function computeExtrasTotals (feature, extrasType) { } // Create point features with labels for route extras segments -function createExtrasLabelFeatures (coords, extrasValues, extrasType) { +function createExtrasLabelFeatures (coords, extrasValues, extrasType, cumulativeDistances) { const labelFeatures = [] extrasValues.forEach(([startIdx, endIdx, value]) => { @@ -96,11 +108,8 @@ function createExtrasLabelFeatures (coords, extrasValues, extrasType) { const firstCoord = coords[startIdx] const lastCoord = coords[end] - // Calculate segment length by summing consecutive point distances - let segmentLength = 0 - for (let i = startIdx; i < end; i++) { - segmentLength += distance(point(coords[i]), point(coords[i + 1]), { units: 'meters' }) - } + // O(1) segment length lookup using precomputed cumulative distances + const segmentLength = cumulativeDistances[end] - cumulativeDistances[startIdx] // Skip very short segments if (segmentLength < 50) return @@ -232,6 +241,12 @@ export function renderRouteExtras (features, sourceId) { if (!extrasData?.values) return const coords = feature.geometry.coordinates + + // Precompute cumulative distances once for O(1) segment length lookups in label creation + const cumulativeDistances = (extrasType === 'steepness' || extrasType === 'surface') + ? computeCumulativeDistances(coords) + : null + extrasData.values.forEach(([startIdx, endIdx, value]) => { if (endIdx <= startIdx || startIdx >= coords.length) return const segment = coords.slice(startIdx, Math.min(endIdx + 1, coords.length)) @@ -252,7 +267,7 @@ export function renderRouteExtras (features, sourceId) { // Add labels for steepness or surface segments if (extrasType === 'steepness' || extrasType === 'surface') { - const labelFeatures = createExtrasLabelFeatures(coords, extrasData.values, extrasType) + const labelFeatures = createExtrasLabelFeatures(coords, extrasData.values, extrasType, cumulativeDistances) extrasFeatures.push(...labelFeatures) } }) @@ -315,7 +330,7 @@ export function initializeExtrasLabelStyles (sourceId) { filter: ['has', 'route-extras-label'], layout: { 'text-field': ['get', 'route-extras-label'], - 'text-font': ['noto_sans_bold'], + 'text-font': labelFont, 'text-size': 11, 'text-allow-overlap': false, 'text-ignore-placement': false,