From 4d77cc31daf8fe522cab91be6aa5c20b79ef1aeb Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Sat, 23 May 2026 23:12:22 +0200 Subject: [PATCH 01/14] Add indoorequal layer --- app/assets/stylesheets/controls.css | 44 +++++ .../controllers/map/layers_controller.js | 4 + app/javascript/maplibre/controls/shared.js | 10 +- app/javascript/maplibre/edit.js | 2 +- app/javascript/maplibre/layers/factory.js | 4 +- .../maplibre/layers/indoor/control.js | 112 ++++++++++++ .../maplibre/layers/indoor/indoor.js | 165 ++++++++++++++++++ .../maplibre/layers/indoor/styles.js | 96 ++++++++++ app/models/map.rb | 3 +- app/views/maps/modals/_layers.haml | 8 + 10 files changed, 440 insertions(+), 8 deletions(-) create mode 100644 app/javascript/maplibre/layers/indoor/control.js create mode 100644 app/javascript/maplibre/layers/indoor/indoor.js create mode 100644 app/javascript/maplibre/layers/indoor/styles.js diff --git a/app/assets/stylesheets/controls.css b/app/assets/stylesheets/controls.css index 33f78e45..7d387642 100644 --- a/app/assets/stylesheets/controls.css +++ b/app/assets/stylesheets/controls.css @@ -15,6 +15,45 @@ border-top: 1px solid #0000001c; } +.indoor-level-control { + position: absolute; + bottom: 3rem; + right: 0.5rem; + z-index: 1; + display: flex; + flex-direction: column; + background: white; + border-radius: 4px; + box-shadow: 0 0 0 2px rgb(0 0 0 / 10%); +} + +.indoor-level-control button { + height: 2rem; + width: 2rem; + border: none; + background: white; + cursor: pointer; + font-size: 1rem; + text-align: center; +} + +.indoor-level-control button.active { + background-color: rgb(0 0 0/15%) !important; + color: var(--color-ctrl-active) !important; +} + +.indoor-level-control button:first-child { + border-radius: 4px 4px 0 0; +} + +.indoor-level-control button:last-child { + border-radius: 0 0 4px 4px; +} + +.indoor-level-control button:only-child { + border-radius: 4px; +} + .maplibregl-ctrl button:disabled { opacity: 0.2; } @@ -43,6 +82,11 @@ color: var(--color-ctrl-active) !important; } + .indoor-level-control button:hover { + background-color: rgb(0 0 0/15%) !important; + color: var(--color-ctrl-active) !important; + } + /* Need to overwrite the mapbox draw icons with hover color */ .mapbox-gl-draw_point:hover { /* stylelint-disable-line */ background-image: url('data:image/svg+xml;utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="20" height="20">%3Cpath style="fill: %232091b0;" d="m10 2c-3.3 0-6 2.7-6 6s6 9 6 9 6-5.7 6-9-2.7-6-6-6zm0 2c2.1 0 3.8 1.7 3.8 3.8 0 1.5-1.8 3.9-2.9 5.2h-1.7c-1.1-1.4-2.9-3.8-2.9-5.2-.1-2.1 1.6-3.8 3.7-3.8z"/>%3C/svg>'); diff --git a/app/javascript/controllers/map/layers_controller.js b/app/javascript/controllers/map/layers_controller.js index 540fa0c3..e8b73bb7 100644 --- a/app/javascript/controllers/map/layers_controller.js +++ b/app/javascript/controllers/map/layers_controller.js @@ -345,6 +345,10 @@ export default class extends Controller { this.createLayer('basemap', 'Basemap layer') } + createIndoorLayer(_event) { + this.createLayer('indoor', 'Indoor map') + } + createLayer(type, name, query=null, geojson=null) { let layerId = functions.featureId() // must match server attribute order, for proper comparison in map_channel diff --git a/app/javascript/maplibre/controls/shared.js b/app/javascript/maplibre/controls/shared.js index f6fba7b3..0c9934d2 100644 --- a/app/javascript/maplibre/controls/shared.js +++ b/app/javascript/maplibre/controls/shared.js @@ -204,16 +204,16 @@ export function initLayersModal () { head.textContent = layerName } - // Don't show feature count for raster layers - if (layer.type !== 'raster') { + // Don't show feature count for raster and indoor layers + if (layer.type !== 'raster' && layer.type !== 'indoor') { const featureCount = document.createElement('span') featureCount.classList.add('small') featureCount.textContent = '(' + features.length + ')' head.parentNode.insertBefore(featureCount, head.nextSibling) } - // Make raster layers non-expandable - if (layer.type === 'raster') { + // Make raster and indoor layers non-expandable + if (layer.type === 'raster' || layer.type === 'indoor') { const toggleLink = layerElement.querySelector('.link[data-action*="toggleLayerList"]') if (toggleLink) { toggleLink.style.cursor = 'default' @@ -303,7 +303,7 @@ export function initLayersModal () { } dom.initTooltips(layerElement) - if (features.length === 0 && layer.type !== 'raster') { + if (features.length === 0 && layer.type !== 'raster' && layer.type !== 'indoor') { const newNode = document.createElement('i') newNode.classList.add('ms-3') newNode.textContent = 'No elements in this layer' diff --git a/app/javascript/maplibre/edit.js b/app/javascript/maplibre/edit.js index cdf95809..32e2be29 100644 --- a/app/javascript/maplibre/edit.js +++ b/app/javascript/maplibre/edit.js @@ -83,7 +83,7 @@ export async function initializeEditMode () { }, styles: editStyles(), clickBuffer: 5, - touchBuffer: 25, // default 25 + touchBuffer: 20, // default 25 // user properties are available, prefixed with 'user_' userProperties: true, modes diff --git a/app/javascript/maplibre/layers/factory.js b/app/javascript/maplibre/layers/factory.js index 4c52cbfc..3a2c0ca1 100644 --- a/app/javascript/maplibre/layers/factory.js +++ b/app/javascript/maplibre/layers/factory.js @@ -1,5 +1,6 @@ import { BasemapLayer } from 'maplibre/layers/basemap' import { GeoJSONLayer } from 'maplibre/layers/geojson' +import { IndoorLayer } from 'maplibre/layers/indoor/indoor' import { Layer } from 'maplibre/layers/layer' import { OverpassLayer } from 'maplibre/layers/overpass/overpass' import { RasterLayer } from 'maplibre/layers/raster/raster' @@ -10,7 +11,8 @@ const layerTypes = { overpass: OverpassLayer, wikipedia: WikipediaLayer, basemap: BasemapLayer, - raster: RasterLayer + raster: RasterLayer, + indoor: IndoorLayer } export function createLayerInstance(data) { diff --git a/app/javascript/maplibre/layers/indoor/control.js b/app/javascript/maplibre/layers/indoor/control.js new file mode 100644 index 00000000..bb5d1b7e --- /dev/null +++ b/app/javascript/maplibre/layers/indoor/control.js @@ -0,0 +1,112 @@ +import { initTooltips } from 'helpers/dom' + +/** + * Level control UI for indoor maps + * Displays a vertical stack of buttons for switching between floor levels + */ +export class IndoorLevelControl { + constructor(layerId, onLevelChange) { + this.layerId = layerId + this.onLevelChange = onLevelChange + this.element = null + this.currentLevel = null + } + + /** + * Creates and shows the level control + */ + create() { + if (this.element) return + + this.element = document.createElement('div') + this.element.className = 'indoor-level-control' + this.element.setAttribute('data-layer-id', this.layerId) + + const mapContainer = document.querySelector('#maplibre-map') + if (mapContainer) { + mapContainer.appendChild(this.element) + } + } + + /** + * Disposes all tooltips on buttons in this control + */ + disposeTooltips() { + if (!this.element || typeof bootstrap === 'undefined') return + + this.element.querySelectorAll('button').forEach(button => { + const tooltip = bootstrap.Tooltip.getInstance(button) + if (tooltip) { + tooltip.dispose() + } + }) + } + + /** + * Updates the control with the given levels + * @param {string[]} levels - Array of level strings, sorted descending + * @param {string} currentLevel - The currently active level + */ + update(levels, currentLevel) { + if (!this.element) { + this.create() + } + + this.currentLevel = currentLevel + this.disposeTooltips() + this.element.innerHTML = '' + + levels.forEach(level => { + const button = document.createElement('button') + button.textContent = level + button.title = `Level ${level}` + button.setAttribute('data-level', level) + button.setAttribute('data-toggle', 'tooltip') + button.setAttribute('data-bs-trigger', 'hover') + + if (level === currentLevel) { + button.classList.add('active') + } + + button.addEventListener('click', () => { + if (this.onLevelChange) { + this.onLevelChange(level) + } + }) + + this.element.appendChild(button) + }) + + initTooltips(this.element) + } + + /** + * Removes the control from the DOM + */ + remove() { + if (this.element && this.element.parentNode) { + this.disposeTooltips() + this.element.parentNode.removeChild(this.element) + } + this.element = null + } + + /** + * Shows the control + */ + show() { + if (this.element) { + this.element.style.display = 'flex' + } + } + + /** + * Hides the control + */ + hide() { + if (this.element) { + this.disposeTooltips() + this.element.style.display = 'none' + } + } +} diff --git a/app/javascript/maplibre/layers/indoor/indoor.js b/app/javascript/maplibre/layers/indoor/indoor.js new file mode 100644 index 00000000..469d42a6 --- /dev/null +++ b/app/javascript/maplibre/layers/indoor/indoor.js @@ -0,0 +1,165 @@ +import { Layer } from 'maplibre/layers/layer' +import { map, removeStyleLayers } from 'maplibre/map' +import { IndoorLevelControl } from 'maplibre/layers/indoor/control' +import { addIndoorLayers, getIndoorLayerIds } from 'maplibre/layers/indoor/styles' + +export class IndoorLayer extends Layer { + constructor(layer) { + super(layer) + this.currentLevel = '0' + this.levels = [] + this.levelControl = null + this.moveEndHandler = null + } + + createSource() { + const apiKey = window.gon?.map_keys?.indoorequal + if (!apiKey) { + console.warn('Indoor Equal API key not found in window.gon.map_keys.indoorequal') + return + } + + if (map.getSource(this.sourceId)) { + console.log('Indoor layer: source ' + this.sourceId + ' already exists, skipping add') + return + } + + console.log('Indoor layer: creating source with API key') + map.addSource(this.sourceId, { + type: 'vector', + tiles: [`https://tiles.indoorequal.org/tiles/{z}/{x}/{y}.pbf?key=${apiKey}`], + minzoom: 0, + maxzoom: 20, + attribution: '© Indoor Equal' + }) + } + + initialize() { + console.log('Indoor layer: initializing with level', this.currentLevel) + removeStyleLayers(this.sourceId) + this.removeLevelControl() + + const levelFilter = ['==', ['get', 'level'], this.currentLevel] + addIndoorLayers(this.sourceId, levelFilter) + + this.setupLevelDetection() + + return Promise.resolve() + } + + loadData() { + return Promise.resolve() + } + + render() { + // No-op - vector tiles render automatically + } + + setupEventHandlers() { + // No-op - indoor features are not selectable like GeoJSON features + } + + setLevel(level) { + if (this.currentLevel === level) return + + this.currentLevel = level + const levelFilter = ['==', ['get', 'level'], level] + + const layerIds = getIndoorLayerIds(this.sourceId) + + layerIds.forEach(layerId => { + if (map.getLayer(layerId)) { + map.setFilter(layerId, levelFilter) + } + }) + + this.updateLevelControlUI() + } + + setupLevelDetection() { + this.moveEndHandler = () => { + if (!this.show) return + if (!map.getSource(this.sourceId)) return + + // Query source features directly to get ALL levels, not just currently filtered ones + const sourceLayers = ['area'] + const levelSet = new Set() + + sourceLayers.forEach(sourceLayer => { + try { + const features = map.querySourceFeatures(this.sourceId, { + sourceLayer: sourceLayer + }) + + features.forEach(feature => { + const level = feature.properties?.level + if (level !== undefined && level !== null) { + levelSet.add(String(level)) + } + }) + } catch (e) { + // Source might not be loaded yet + console.log('Indoor layer: source not ready for querying', e.message) + } + }) + + const newLevels = Array.from(levelSet).sort((a, b) => { + const numA = parseFloat(a) + const numB = parseFloat(b) + return numB - numA + }) + + if (JSON.stringify(newLevels) !== JSON.stringify(this.levels)) { + this.levels = newLevels + console.log('Indoor layer: detected levels', newLevels) + this.updateLevelControl() + } + } + + map.on('moveend', this.moveEndHandler) + map.on('idle', this.moveEndHandler) + map.on('sourcedata', this.moveEndHandler) + setTimeout(() => this.moveEndHandler(), 1000) + } + + updateLevelControl() { + if (this.levels.length > 0) { + if (!this.levelControl) { + this.createLevelControl() + } + this.updateLevelControlUI() + } else { + this.removeLevelControl() + } + } + + createLevelControl() { + this.levelControl = new IndoorLevelControl(this.id, (level) => { + this.setLevel(level) + }) + this.levelControl.create() + } + + updateLevelControlUI() { + if (!this.levelControl) return + this.levelControl.update(this.levels, this.currentLevel) + } + + removeLevelControl() { + if (this.levelControl) { + this.levelControl.remove() + this.levelControl = null + } + } + + cleanup() { + if (this.moveEndHandler) { + map.off('moveend', this.moveEndHandler) + map.off('idle', this.moveEndHandler) + map.off('sourcedata', this.moveEndHandler) + this.moveEndHandler = null + } + this.removeLevelControl() + super.cleanup() + } +} diff --git a/app/javascript/maplibre/layers/indoor/styles.js b/app/javascript/maplibre/layers/indoor/styles.js new file mode 100644 index 00000000..4160529f --- /dev/null +++ b/app/javascript/maplibre/layers/indoor/styles.js @@ -0,0 +1,96 @@ +import { map } from 'maplibre/map' + +/** + * Adds indoor map style layers for a given source + * Only leval plans right now, no POI points + * @param {string} sourceId - The source ID to use for the layers + * @param {string} levelFilter - MapLibre filter expression for the current level + */ +export function addIndoorLayers(sourceId, levelFilter) { + map.addLayer({ + id: `indoor-area-fill_${sourceId}`, + type: 'fill', + source: sourceId, + 'source-layer': 'area', + minzoom: 17, + filter: levelFilter, + paint: { + 'fill-color': [ + 'match', + ['get', 'class'], + 'room', '#fdfcfa', + 'corridor', '#fefefe', + 'platform', '#e8edff', + 'wall', '#d5d5d5', + '#f0f0f0' + ], + 'fill-opacity': 0.9 + } + }) + + map.addLayer({ + id: `indoor-area-line_${sourceId}`, + type: 'line', + source: sourceId, + 'source-layer': 'area', + minzoom: 17, + filter: levelFilter, + paint: { + 'line-color': '#000', + 'line-width': [ + 'match', + ['get', 'class'], + 'wall', 3, + 2 + ] + } + }) + + map.addLayer({ + id: `indoor-transportation_${sourceId}`, + type: 'line', + source: sourceId, + 'source-layer': 'transportation', + minzoom: 17, + filter: levelFilter, + paint: { + 'line-color': '#999', + 'line-width': 2, + 'line-dasharray': [2, 2] + } + }) + + map.addLayer({ + id: `indoor-area-label_${sourceId}`, + type: 'symbol', + source: sourceId, + 'source-layer': 'area_name', + minzoom: 18, + filter: levelFilter, + layout: { + 'text-field': ['coalesce', ['get', 'name'], ['get', 'ref']], + 'text-font': ['Noto Sans Regular'], + 'text-size': 10, + 'text-max-width': 10 + }, + paint: { + 'text-color': '#666', + 'text-halo-color': '#fff', + 'text-halo-width': 1 + } + }) +} + +/** + * Returns the list of indoor layer IDs for a given source + * @param {string} sourceId - The source ID + * @returns {string[]} Array of layer IDs + */ +export function getIndoorLayerIds(sourceId) { + return [ + `indoor-area-fill_${sourceId}`, + `indoor-area-line_${sourceId}`, + `indoor-transportation_${sourceId}`, + `indoor-area-label_${sourceId}` + ] +} diff --git a/app/models/map.rb b/app/models/map.rb index 43041db7..acc1ff85 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -148,7 +148,8 @@ def self.provider_keys { mapbox: ENV["MAPBOX_KEY"], maptiler: ENV["MAPTILER_KEY"], openrouteservice: ENV["OPENROUTESERVICE_KEY"], - thunderforest: ENV["THUNDERFOREST_KEY"] } + thunderforest: ENV["THUNDERFOREST_KEY"], + indoorequal: ENV["INDOOREQUAL_KEY"] } end def to_json diff --git a/app/views/maps/modals/_layers.haml b/app/views/maps/modals/_layers.haml index 2c39fd26..b9662193 100644 --- a/app/views/maps/modals/_layers.haml +++ b/app/views/maps/modals/_layers.haml @@ -93,6 +93,14 @@ } %i.bi.bi-wikipedia.me-2 Wikipedia articles + - if ENV['INDOOREQUAL_KEY'].present? + %li + %button.dropdown-item{ + type: "button", + data: { action: "click->map--layers#createIndoorLayer" } + } + %i.bi.bi-building.me-2 + OpenStreetMap indoor %li %hr.dropdown-divider %li From e1897deabe60dd28456fb384ce826d7338ef9df9 Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Sun, 24 May 2026 11:45:09 +0200 Subject: [PATCH 02/14] improve event handling --- CHANGELOG.md | 2 +- .../maplibre/layers/indoor/indoor.js | 83 +++++++++++-------- 2 files changed, 48 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b42ffb99..72f95b71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. * Switched headline font to "Bree Serif" * Enhanced waymarkedtrails.org integration: Select and copy trails to your map +* Add indoor layer (vector data from [indoorequal](https://indoorequal.org/)) ## 2026-04 @@ -16,7 +17,6 @@ All notable changes to this project will be documented in this file. * Allow to color code routes by steepness + surface * Option to convert gpx tracks into routes * New layer type 'raster' with waymarkedtrails.org examples -* Switched headline font to "Bree Serif" ## 2026-03 diff --git a/app/javascript/maplibre/layers/indoor/indoor.js b/app/javascript/maplibre/layers/indoor/indoor.js index 469d42a6..36c95d4a 100644 --- a/app/javascript/maplibre/layers/indoor/indoor.js +++ b/app/javascript/maplibre/layers/indoor/indoor.js @@ -9,7 +9,19 @@ export class IndoorLayer extends Layer { this.currentLevel = '0' this.levels = [] this.levelControl = null - this.moveEndHandler = null + this.idleHandler = null + this.initialTimeout = null + } + + get show() { + return this.layer.show + } + + set show(value) { + this.layer.show = value + if (this.levelControl) { + value ? this.levelControl.show() : this.levelControl.hide() + } } createSource() { @@ -37,6 +49,7 @@ export class IndoorLayer extends Layer { initialize() { console.log('Indoor layer: initializing with level', this.currentLevel) removeStyleLayers(this.sourceId) + this.removeLevelDetection() this.removeLevelControl() const levelFilter = ['==', ['get', 'level'], this.currentLevel] @@ -77,37 +90,31 @@ export class IndoorLayer extends Layer { } setupLevelDetection() { - this.moveEndHandler = () => { + this.idleHandler = () => { if (!this.show) return if (!map.getSource(this.sourceId)) return // Query source features directly to get ALL levels, not just currently filtered ones - const sourceLayers = ['area'] const levelSet = new Set() - sourceLayers.forEach(sourceLayer => { - try { - const features = map.querySourceFeatures(this.sourceId, { - sourceLayer: sourceLayer - }) - - features.forEach(feature => { - const level = feature.properties?.level - if (level !== undefined && level !== null) { - levelSet.add(String(level)) - } - }) - } catch (e) { - // Source might not be loaded yet - console.log('Indoor layer: source not ready for querying', e.message) - } - }) - - const newLevels = Array.from(levelSet).sort((a, b) => { - const numA = parseFloat(a) - const numB = parseFloat(b) - return numB - numA - }) + try { + const features = map.querySourceFeatures(this.sourceId, { + sourceLayer: 'area' + }) + + features.forEach(feature => { + const level = feature.properties?.level + if (level !== undefined && level !== null) { + levelSet.add(String(level)) + } + }) + } catch (e) { + // Source might not be loaded yet + console.log('Indoor layer: source not ready for querying', e.message) + return + } + + const newLevels = Array.from(levelSet).sort((a, b) => parseFloat(b) - parseFloat(a)) if (JSON.stringify(newLevels) !== JSON.stringify(this.levels)) { this.levels = newLevels @@ -116,10 +123,19 @@ export class IndoorLayer extends Layer { } } - map.on('moveend', this.moveEndHandler) - map.on('idle', this.moveEndHandler) - map.on('sourcedata', this.moveEndHandler) - setTimeout(() => this.moveEndHandler(), 1000) + map.on('idle', this.idleHandler) + this.initialTimeout = setTimeout(() => this.idleHandler(), 1000) + } + + removeLevelDetection() { + if (this.initialTimeout) { + clearTimeout(this.initialTimeout) + this.initialTimeout = null + } + if (this.idleHandler) { + map.off('idle', this.idleHandler) + this.idleHandler = null + } } updateLevelControl() { @@ -153,12 +169,7 @@ export class IndoorLayer extends Layer { } cleanup() { - if (this.moveEndHandler) { - map.off('moveend', this.moveEndHandler) - map.off('idle', this.moveEndHandler) - map.off('sourcedata', this.moveEndHandler) - this.moveEndHandler = null - } + this.removeLevelDetection() this.removeLevelControl() super.cleanup() } From 72a70f04339c81cd9d9eb4fa3e2aec3e385bcaf1 Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Sun, 24 May 2026 15:58:20 +0200 Subject: [PATCH 03/14] handle edge cases --- .../maplibre/layers/indoor/indoor.js | 18 +++++++++++++++--- .../maplibre/layers/indoor/styles.js | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/javascript/maplibre/layers/indoor/indoor.js b/app/javascript/maplibre/layers/indoor/indoor.js index 36c95d4a..e4ba953d 100644 --- a/app/javascript/maplibre/layers/indoor/indoor.js +++ b/app/javascript/maplibre/layers/indoor/indoor.js @@ -22,6 +22,11 @@ export class IndoorLayer extends Layer { if (this.levelControl) { value ? this.levelControl.show() : this.levelControl.hide() } + if (value) { + this.setupLevelDetection() + } else { + this.removeLevelDetection() + } } createSource() { @@ -52,6 +57,12 @@ export class IndoorLayer extends Layer { this.removeLevelDetection() this.removeLevelControl() + if (!map.getSource(this.sourceId)) { + console.warn('Indoor layer: source not available, skipping layer initialization') + this.layer.show = false + return Promise.resolve() + } + const levelFilter = ['==', ['get', 'level'], this.currentLevel] addIndoorLayers(this.sourceId, levelFilter) @@ -90,10 +101,11 @@ export class IndoorLayer extends Layer { } setupLevelDetection() { - this.idleHandler = () => { - if (!this.show) return - if (!map.getSource(this.sourceId)) return + this.removeLevelDetection() + if (!map.getSource(this.sourceId)) return + + this.idleHandler = () => { // Query source features directly to get ALL levels, not just currently filtered ones const levelSet = new Set() diff --git a/app/javascript/maplibre/layers/indoor/styles.js b/app/javascript/maplibre/layers/indoor/styles.js index 4160529f..2a045157 100644 --- a/app/javascript/maplibre/layers/indoor/styles.js +++ b/app/javascript/maplibre/layers/indoor/styles.js @@ -2,7 +2,7 @@ import { map } from 'maplibre/map' /** * Adds indoor map style layers for a given source - * Only leval plans right now, no POI points + * Only level plans right now, no POI points * @param {string} sourceId - The source ID to use for the layers * @param {string} levelFilter - MapLibre filter expression for the current level */ From b812cb32d494f546a746c817983ed5ab61ce7df0 Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Sun, 24 May 2026 23:59:23 +0200 Subject: [PATCH 04/14] extrusion styles for indoor layers lvel 0+ --- .../maplibre/layers/indoor/styles.js | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/app/javascript/maplibre/layers/indoor/styles.js b/app/javascript/maplibre/layers/indoor/styles.js index 2a045157..db7baba6 100644 --- a/app/javascript/maplibre/layers/indoor/styles.js +++ b/app/javascript/maplibre/layers/indoor/styles.js @@ -12,7 +12,7 @@ export function addIndoorLayers(sourceId, levelFilter) { type: 'fill', source: sourceId, 'source-layer': 'area', - minzoom: 17, + minzoom: 16, filter: levelFilter, paint: { 'fill-color': [ @@ -28,12 +28,27 @@ export function addIndoorLayers(sourceId, levelFilter) { } }) + map.addLayer({ + id: `indoor-area-extrusion_${sourceId}`, + type: 'fill-extrusion', + source: sourceId, + 'source-layer': 'area', + minzoom: 16, + filter: levelFilter, + paint: { + 'fill-extrusion-color': '#fdbe87', + 'fill-extrusion-height': ['+', ['*', ['to-number', ['get', 'level']], 5], 5], + 'fill-extrusion-base': ['*', ['to-number', ['get', 'level']], 5], + 'fill-extrusion-opacity': 0.8 + } + }) + map.addLayer({ id: `indoor-area-line_${sourceId}`, type: 'line', source: sourceId, 'source-layer': 'area', - minzoom: 17, + minzoom: 16, filter: levelFilter, paint: { 'line-color': '#000', @@ -51,7 +66,7 @@ export function addIndoorLayers(sourceId, levelFilter) { type: 'line', source: sourceId, 'source-layer': 'transportation', - minzoom: 17, + minzoom: 16, filter: levelFilter, paint: { 'line-color': '#999', @@ -59,26 +74,6 @@ export function addIndoorLayers(sourceId, levelFilter) { 'line-dasharray': [2, 2] } }) - - map.addLayer({ - id: `indoor-area-label_${sourceId}`, - type: 'symbol', - source: sourceId, - 'source-layer': 'area_name', - minzoom: 18, - filter: levelFilter, - layout: { - 'text-field': ['coalesce', ['get', 'name'], ['get', 'ref']], - 'text-font': ['Noto Sans Regular'], - 'text-size': 10, - 'text-max-width': 10 - }, - paint: { - 'text-color': '#666', - 'text-halo-color': '#fff', - 'text-halo-width': 1 - } - }) } /** @@ -89,6 +84,7 @@ export function addIndoorLayers(sourceId, levelFilter) { export function getIndoorLayerIds(sourceId) { return [ `indoor-area-fill_${sourceId}`, + `indoor-area-extrusion_${sourceId}`, `indoor-area-line_${sourceId}`, `indoor-transportation_${sourceId}`, `indoor-area-label_${sourceId}` From daa644e95fe74d39aaf71fcc7b55471b87738605 Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Mon, 25 May 2026 00:00:08 +0200 Subject: [PATCH 05/14] debounce calls --- CHANGELOG.md | 2 +- .../maplibre/layers/indoor/indoor.js | 65 ++++++++++--------- app/javascript/maplibre/map.js | 8 ++- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72f95b71..1df93190 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ All notable changes to this project will be documented in this file. * Switched headline font to "Bree Serif" * Enhanced waymarkedtrails.org integration: Select and copy trails to your map -* Add indoor layer (vector data from [indoorequal](https://indoorequal.org/)) +* Added indoor layer (vector data from [indoorequal](https://indoorequal.org/)) ## 2026-04 diff --git a/app/javascript/maplibre/layers/indoor/indoor.js b/app/javascript/maplibre/layers/indoor/indoor.js index e4ba953d..710a28a2 100644 --- a/app/javascript/maplibre/layers/indoor/indoor.js +++ b/app/javascript/maplibre/layers/indoor/indoor.js @@ -1,7 +1,8 @@ -import { Layer } from 'maplibre/layers/layer' -import { map, removeStyleLayers } from 'maplibre/map' +import { debounce } from 'helpers/functions' import { IndoorLevelControl } from 'maplibre/layers/indoor/control' import { addIndoorLayers, getIndoorLayerIds } from 'maplibre/layers/indoor/styles' +import { Layer } from 'maplibre/layers/layer' +import { map, removeStyleLayers } from 'maplibre/map' export class IndoorLayer extends Layer { constructor(layer) { @@ -103,40 +104,44 @@ export class IndoorLayer extends Layer { setupLevelDetection() { this.removeLevelDetection() + if (this.show === false) return if (!map.getSource(this.sourceId)) return this.idleHandler = () => { - // Query source features directly to get ALL levels, not just currently filtered ones - const levelSet = new Set() - - try { - const features = map.querySourceFeatures(this.sourceId, { - sourceLayer: 'area' - }) - - features.forEach(feature => { - const level = feature.properties?.level - if (level !== undefined && level !== null) { - levelSet.add(String(level)) - } - }) - } catch (e) { - // Source might not be loaded yet - console.log('Indoor layer: source not ready for querying', e.message) - return - } - - const newLevels = Array.from(levelSet).sort((a, b) => parseFloat(b) - parseFloat(a)) - - if (JSON.stringify(newLevels) !== JSON.stringify(this.levels)) { - this.levels = newLevels - console.log('Indoor layer: detected levels', newLevels) - this.updateLevelControl() - } + debounce(() => { + // Query source features directly to get ALL levels, not just currently filtered ones + const levelSet = new Set() + + try { + const features = map.querySourceFeatures(this.sourceId, { + sourceLayer: 'area' + }) + // console.log(`${features.length} indoor features in current view`) + + features.forEach(feature => { + const level = feature.properties?.level + if (level !== undefined && level !== null) { + levelSet.add(String(level)) + } + }) + } catch (e) { + // Source might not be loaded yet + console.log('Indoor layer: source not ready for querying', e.message) + return + } + + const newLevels = Array.from(levelSet).sort((a, b) => parseFloat(b) - parseFloat(a)) + + if (JSON.stringify(newLevels) !== JSON.stringify(this.levels)) { + this.levels = newLevels + // console.log('Indoor layer: detected levels', newLevels) + this.updateLevelControl() + } + }, `indoor-level-${this.id}`, 500) } map.on('idle', this.idleHandler) - this.initialTimeout = setTimeout(() => this.idleHandler(), 1000) + this.initialTimeout = setTimeout(() => this.idleHandler(), 500) } removeLevelDetection() { diff --git a/app/javascript/maplibre/map.js b/app/javascript/maplibre/map.js index 628fbd7f..717db5a0 100644 --- a/app/javascript/maplibre/map.js +++ b/app/javascript/maplibre/map.js @@ -539,8 +539,8 @@ export function sortLayers () { const layers = map.getStyle().layers // increase opacity of 3D houses - if (map.getLayer('Building 3D')) { - map.setPaintProperty('Building 3D', 'fill-extrusion-opacity', 0.8) + if (map.getLayer('building-3d')) { // name in openfreemapLiberty + map.setPaintProperty('building-3d', 'fill-extrusion-opacity', 0.6) } // Each entry is a layer group; groups are listed bottom-to-top. mapSymbols @@ -555,7 +555,8 @@ export function sortLayers () { layers.filter(e => e.id.startsWith('line-layer_geojson-source') && !e.id.includes('outline')), layers.filter(e => e.id.includes('route-extras-source') && !e.id.startsWith('route-extras-labels')), layers.filter(e => e.paint && e.paint['fill-extrusion-height'] && e.id.startsWith('polygon-layer-extrusion')), - layers.filter(e => e.paint && e.paint['fill-extrusion-height'] && !e.id.startsWith('polygon-layer-extrusion')), + layers.filter(e => e.id.startsWith('indoor-area-extrusion_')), + layers.filter(e => e.paint && e.paint['fill-extrusion-height'] && !e.id.startsWith('polygon-layer-extrusion') && !e.id.startsWith('indoor-area-extrusion_')), layers.filter(e => e.id.startsWith('maplibre-gl-directions')), layers.filter(e => e.type === 'symbol' && !e.id.startsWith('symbols-layer') && !e.id.startsWith('symbols-border-layer') && @@ -577,6 +578,7 @@ export function sortLayers () { groups.forEach(group => { group.forEach(layer => { if (map.getLayer(layer.id)) map.moveLayer(layer.id) }) }) + // console.log('Sorted layers: ', map.getStyle().layers) } export function updateMapName (name) { From 1a04be95e7c316ff72d20708347c302950018890 Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Mon, 25 May 2026 13:46:04 +0200 Subject: [PATCH 06/14] address copilot review comments --- app/javascript/maplibre/layers/indoor/control.js | 7 +++++++ app/javascript/maplibre/layers/indoor/indoor.js | 6 +++--- app/javascript/maplibre/layers/indoor/styles.js | 5 ++--- app/javascript/maplibre/map.js | 5 ++++- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/javascript/maplibre/layers/indoor/control.js b/app/javascript/maplibre/layers/indoor/control.js index bb5d1b7e..a4c2ba1f 100644 --- a/app/javascript/maplibre/layers/indoor/control.js +++ b/app/javascript/maplibre/layers/indoor/control.js @@ -18,6 +18,13 @@ export class IndoorLevelControl { create() { if (this.element) return + // Check if a control for this layer already exists + const existingControl = document.querySelector(`.indoor-level-control[data-layer-id="${this.layerId}"]`) + if (existingControl) { + this.element = existingControl + return + } + this.element = document.createElement('div') this.element.className = 'indoor-level-control' this.element.setAttribute('data-layer-id', this.layerId) diff --git a/app/javascript/maplibre/layers/indoor/indoor.js b/app/javascript/maplibre/layers/indoor/indoor.js index 710a28a2..1ac9b088 100644 --- a/app/javascript/maplibre/layers/indoor/indoor.js +++ b/app/javascript/maplibre/layers/indoor/indoor.js @@ -45,7 +45,7 @@ export class IndoorLayer extends Layer { console.log('Indoor layer: creating source with API key') map.addSource(this.sourceId, { type: 'vector', - tiles: [`https://tiles.indoorequal.org/tiles/{z}/{x}/{y}.pbf?key=${apiKey}`], + tiles: [`https://tiles.indoorequal.org/tiles/{z}/{x}/{y}.pbf?key=${encodeURIComponent(apiKey)}`], minzoom: 0, maxzoom: 20, attribution: '© Indoor Equal' @@ -64,7 +64,7 @@ export class IndoorLayer extends Layer { return Promise.resolve() } - const levelFilter = ['==', ['get', 'level'], this.currentLevel] + const levelFilter = ['==', ['to-string', ['get', 'level']], this.currentLevel] addIndoorLayers(this.sourceId, levelFilter) this.setupLevelDetection() @@ -88,7 +88,7 @@ export class IndoorLayer extends Layer { if (this.currentLevel === level) return this.currentLevel = level - const levelFilter = ['==', ['get', 'level'], level] + const levelFilter = ['==', ['to-string', ['get', 'level']], level] const layerIds = getIndoorLayerIds(this.sourceId) diff --git a/app/javascript/maplibre/layers/indoor/styles.js b/app/javascript/maplibre/layers/indoor/styles.js index db7baba6..7af4868a 100644 --- a/app/javascript/maplibre/layers/indoor/styles.js +++ b/app/javascript/maplibre/layers/indoor/styles.js @@ -4,7 +4,7 @@ import { map } from 'maplibre/map' * Adds indoor map style layers for a given source * Only level plans right now, no POI points * @param {string} sourceId - The source ID to use for the layers - * @param {string} levelFilter - MapLibre filter expression for the current level + * @param {Array} levelFilter - MapLibre filter expression array for the current level */ export function addIndoorLayers(sourceId, levelFilter) { map.addLayer({ @@ -86,7 +86,6 @@ export function getIndoorLayerIds(sourceId) { `indoor-area-fill_${sourceId}`, `indoor-area-extrusion_${sourceId}`, `indoor-area-line_${sourceId}`, - `indoor-transportation_${sourceId}`, - `indoor-area-label_${sourceId}` + `indoor-transportation_${sourceId}` ] } diff --git a/app/javascript/maplibre/map.js b/app/javascript/maplibre/map.js index 717db5a0..7ca0a3e2 100644 --- a/app/javascript/maplibre/map.js +++ b/app/javascript/maplibre/map.js @@ -539,8 +539,11 @@ export function sortLayers () { const layers = map.getStyle().layers // increase opacity of 3D houses - if (map.getLayer('building-3d')) { // name in openfreemapLiberty + // Handle both 'building-3d' (openfreemapLiberty) and 'Building 3D' (MapTiler) + if (map.getLayer('building-3d')) { map.setPaintProperty('building-3d', 'fill-extrusion-opacity', 0.6) + } else if (map.getLayer('Building 3D')) { + map.setPaintProperty('Building 3D', 'fill-extrusion-opacity', 0.6) } // Each entry is a layer group; groups are listed bottom-to-top. mapSymbols From 4f42de2fcde67859f5bb85bcda305f81e339e31e Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Wed, 27 May 2026 00:09:32 +0200 Subject: [PATCH 07/14] allow to use highlight method also for vector features --- app/javascript/maplibre/feature.js | 32 +++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/app/javascript/maplibre/feature.js b/app/javascript/maplibre/feature.js index bbdac941..691ad5d9 100644 --- a/app/javascript/maplibre/feature.js +++ b/app/javascript/maplibre/feature.js @@ -16,6 +16,7 @@ window.marked = marked export let highlightedFeatureId export let highlightedFeatureSource +export let highlightedSourceLayer = null export let stickyFeatureHighlight = false let elevationChart @@ -252,9 +253,14 @@ export function featureImage(feature) { export function resetHighlightedFeature () { if (highlightedFeatureId && map.getSource(highlightedFeatureSource)) { - map.setFeatureState({ source: highlightedFeatureSource, id: highlightedFeatureId }, { active: false }) + const stateParams = { source: highlightedFeatureSource, id: highlightedFeatureId } + if (highlightedSourceLayer) { + stateParams.sourceLayer = highlightedSourceLayer + } + map.setFeatureState(stateParams, { active: false }) highlightedFeatureSource = null highlightedFeatureId = null + highlightedSourceLayer = null // drop feature param from url const url = new URL(window.location.href) if (url.searchParams.get('f')) { @@ -268,7 +274,8 @@ export function resetHighlightedFeature () { f.e('#feature-details-modal', e => { e.classList.remove('show') }) } -export function highlightFeature (feature, sticky = false, source) { +// For highlighting features from vector layers, we need to track their sourceLayer. +export function highlightFeature (feature, sticky = false, source, sourceLayer = null) { // Only reset if there's a different feature currently highlighted if (highlightedFeatureId === feature.id) { return } if (highlightedFeatureId && highlightedFeatureId !== feature.id) { resetHighlightedFeature() } @@ -278,6 +285,7 @@ export function highlightFeature (feature, sticky = false, source) { stickyFeatureHighlight = sticky highlightedFeatureId = feature?.id highlightedFeatureSource = source + highlightedSourceLayer = sourceLayer // load feature from source, the style only returns the dimensions on screen const sourceFeature = layers .filter(l => Array.isArray(l.geojson?.features)) @@ -285,14 +293,20 @@ export function highlightFeature (feature, sticky = false, source) { .find(f => f.id === feature.id) showFeatureDetails(sourceFeature || feature) - if (sourceFeature) { - // A feature's state is not part of the GeoJSON or vector tile data but can get used in styles - map.setFeatureState({ source, id: feature.id }, { active: true }) - // set url to feature - if (sticky) { - const newPath = `${window.location.pathname}?f=${feature.id}${window.location.hash}` - window.history.pushState({}, '', newPath) + + // Set feature state for both GeoJSON and vector tile features + if (feature?.id != null) { + const stateParams = { source, id: feature.id } + if (sourceLayer) { + stateParams.sourceLayer = sourceLayer } + map.setFeatureState(stateParams, { active: true }) + } + + // URL persistence only for GeoJSON features (vector tile IDs are not stable across sessions) + if (sourceFeature && sticky) { + const newPath = `${window.location.pathname}?f=${feature.id}${window.location.hash}` + window.history.pushState({}, '', newPath) } } From 432b505cced8ecb11c538cb08a4b5b1f0b2f3186 Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Wed, 27 May 2026 00:15:16 +0200 Subject: [PATCH 08/14] extract updateBuildingOpacity function --- app/javascript/maplibre/map.js | 65 ++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/app/javascript/maplibre/map.js b/app/javascript/maplibre/map.js index 7ca0a3e2..2549c4b8 100644 --- a/app/javascript/maplibre/map.js +++ b/app/javascript/maplibre/map.js @@ -522,6 +522,17 @@ export function setBackgroundMapLayer (mapName = mapProperties.base_map, force = return false } +export function updateBuildingOpacity () { + if (!map || !map.isStyleLoaded()) return + + const opacity = layers?.some(l => l.type === 'indoor' && l.levelControl) ? 0.4 : 0.6 + if (map.getLayer('building-3d')) { + map.setPaintProperty('building-3d', 'fill-extrusion-opacity', opacity) + } else if (map.getLayer('Building 3D')) { + map.setPaintProperty('Building 3D', 'fill-extrusion-opacity', opacity) + } +} + // re-sort layers to overlay geojson layers with labels & extrusion objects // workflows to consider: first map load, basemap update, socket reconnect // sorting (bottom to top): @@ -536,44 +547,38 @@ export function setBackgroundMapLayer (mapName = mapProperties.base_map, force = // here on top of the basemap setStyle in setBackgroundMapLayer left interaction // handlers attached to a stale canvas after a reconnect/background restore. export function sortLayers () { - const layers = map.getStyle().layers + const styleLayers = map.getStyle().layers - // increase opacity of 3D houses - // Handle both 'building-3d' (openfreemapLiberty) and 'Building 3D' (MapTiler) - if (map.getLayer('building-3d')) { - map.setPaintProperty('building-3d', 'fill-extrusion-opacity', 0.6) - } else if (map.getLayer('Building 3D')) { - map.setPaintProperty('Building 3D', 'fill-extrusion-opacity', 0.6) - } + updateBuildingOpacity() // Each entry is a layer group; groups are listed bottom-to-top. mapSymbols // excludes user symbol/label layers since the original mutating logic pulled // those out before computing mapSymbols. const groups = [ - layers.filter(e => e.id.startsWith('raster-layer_')), // raster overlays below all geojson layers - layers.filter(e => e.id.startsWith('polygon-layer_geojson-source') && !e.id.includes('extrusion') && !e.id.includes('shadow')), - layers.filter(e => e.id.startsWith('polygon-layer-outline_geojson-source')), - layers.filter(e => e.id.includes('-flat')), // keep flat layers behind houses - layers.filter(e => e.id.startsWith('line-layer-outline_geojson-source')), - layers.filter(e => e.id.startsWith('line-layer_geojson-source') && !e.id.includes('outline')), - layers.filter(e => e.id.includes('route-extras-source') && !e.id.startsWith('route-extras-labels')), - layers.filter(e => e.paint && e.paint['fill-extrusion-height'] && e.id.startsWith('polygon-layer-extrusion')), - layers.filter(e => e.id.startsWith('indoor-area-extrusion_')), - layers.filter(e => e.paint && e.paint['fill-extrusion-height'] && !e.id.startsWith('polygon-layer-extrusion') && !e.id.startsWith('indoor-area-extrusion_')), - layers.filter(e => e.id.startsWith('maplibre-gl-directions')), - layers.filter(e => e.type === 'symbol' && + styleLayers.filter(e => e.id.startsWith('raster-layer_')), // raster overlays below all geojson layers + styleLayers.filter(e => e.id.startsWith('polygon-layer_geojson-source') && !e.id.includes('extrusion') && !e.id.includes('shadow')), + styleLayers.filter(e => e.id.startsWith('polygon-layer-outline_geojson-source')), + styleLayers.filter(e => e.id.includes('-flat')), // keep flat layers behind houses + styleLayers.filter(e => e.id.startsWith('line-layer-outline_geojson-source')), + styleLayers.filter(e => e.id.startsWith('line-layer_geojson-source') && !e.id.includes('outline')), + styleLayers.filter(e => e.id.includes('route-extras-source') && !e.id.startsWith('route-extras-labels')), + styleLayers.filter(e => e.paint && e.paint['fill-extrusion-height'] && e.id.startsWith('polygon-layer-extrusion')), + styleLayers.filter(e => e.id.startsWith('indoor-area-extrusion_')), + styleLayers.filter(e => e.paint && e.paint['fill-extrusion-height'] && !e.id.startsWith('polygon-layer-extrusion') && !e.id.startsWith('indoor-area-extrusion_')), + styleLayers.filter(e => e.id.startsWith('maplibre-gl-directions')), + styleLayers.filter(e => e.type === 'symbol' && !e.id.startsWith('symbols-layer') && !e.id.startsWith('symbols-border-layer') && !e.id.startsWith('text-layer') && !e.id.startsWith('cluster_labels')), - layers.filter(e => e.id.startsWith('points-layer') || e.id.startsWith('cluster_points')), - layers.filter(e => e.id.startsWith('heatmap-layer')), - layers.filter(e => e.id.startsWith('gl-draw-')), - layers.filter(e => e.id.startsWith('km-marker') && !e.id.startsWith('km-marker-end')), - layers.filter(e => e.id.startsWith('route-extras-labels')), - layers.filter(e => e.id.startsWith('km-marker-end')), - layers.filter(e => e.id.startsWith('symbols-layer') || e.id.startsWith('symbols-border-layer')), - layers.filter(e => e.id.startsWith('text-layer') || e.id.startsWith('cluster_labels')), - layers.filter(e => e.id.startsWith('line-layer-hit_geojson-source')), - layers.filter(e => e.id.startsWith('points-hit-layer_geojson-source')) + styleLayers.filter(e => e.id.startsWith('points-layer') || e.id.startsWith('cluster_points')), + styleLayers.filter(e => e.id.startsWith('heatmap-layer')), + styleLayers.filter(e => e.id.startsWith('gl-draw-')), + styleLayers.filter(e => e.id.startsWith('km-marker') && !e.id.startsWith('km-marker-end')), + styleLayers.filter(e => e.id.startsWith('route-extras-labels')), + styleLayers.filter(e => e.id.startsWith('km-marker-end')), + styleLayers.filter(e => e.id.startsWith('symbols-layer') || e.id.startsWith('symbols-border-layer')), + styleLayers.filter(e => e.id.startsWith('text-layer') || e.id.startsWith('cluster_labels')), + styleLayers.filter(e => e.id.startsWith('line-layer-hit_geojson-source')), + styleLayers.filter(e => e.id.startsWith('points-hit-layer_geojson-source')) ] // moveLayer(id) with no second arg moves the layer to the top. Iterating From f231a7f7aa430f34dca44cad4ceb6c6de1b03f59 Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Wed, 27 May 2026 00:16:13 +0200 Subject: [PATCH 09/14] trigger levelcontrol only when levels changed --- .../maplibre/layers/indoor/control.js | 66 ++++++++++++------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/app/javascript/maplibre/layers/indoor/control.js b/app/javascript/maplibre/layers/indoor/control.js index a4c2ba1f..9f0d7a3c 100644 --- a/app/javascript/maplibre/layers/indoor/control.js +++ b/app/javascript/maplibre/layers/indoor/control.js @@ -10,6 +10,7 @@ export class IndoorLevelControl { this.onLevelChange = onLevelChange this.element = null this.currentLevel = null + this.levels = [] } /** @@ -44,7 +45,11 @@ export class IndoorLevelControl { this.element.querySelectorAll('button').forEach(button => { const tooltip = bootstrap.Tooltip.getInstance(button) if (tooltip) { - tooltip.dispose() + try { + tooltip.dispose() + } catch (e) { + // Tooltip might be mid-animation when dispose is called + } } }) } @@ -59,32 +64,47 @@ export class IndoorLevelControl { this.create() } - this.currentLevel = currentLevel - this.disposeTooltips() - this.element.innerHTML = '' + const levelsChanged = JSON.stringify(levels) !== JSON.stringify(this.levels) - levels.forEach(level => { - const button = document.createElement('button') - button.textContent = level - button.title = `Level ${level}` - button.setAttribute('data-level', level) - button.setAttribute('data-toggle', 'tooltip') - button.setAttribute('data-bs-trigger', 'hover') + if (levelsChanged) { + this.levels = levels + this.currentLevel = currentLevel + this.disposeTooltips() + this.element.innerHTML = '' + + levels.forEach(level => { + const button = document.createElement('button') + button.textContent = level + button.title = `Level ${level}` + button.setAttribute('data-level', level) + button.setAttribute('data-toggle', 'tooltip') + button.setAttribute('data-bs-trigger', 'hover') + + if (level === currentLevel) { + button.classList.add('active') + } - if (level === currentLevel) { - button.classList.add('active') - } + button.addEventListener('click', () => { + if (this.onLevelChange) { + this.onLevelChange(level) + } + }) - button.addEventListener('click', () => { - if (this.onLevelChange) { - this.onLevelChange(level) - } + this.element.appendChild(button) }) - this.element.appendChild(button) - }) - - initTooltips(this.element) + initTooltips(this.element) + } else if (this.currentLevel !== currentLevel) { + this.currentLevel = currentLevel + this.element.querySelectorAll('button').forEach(button => { + const level = button.getAttribute('data-level') + if (level === currentLevel) { + button.classList.add('active') + } else { + button.classList.remove('active') + } + }) + } } /** @@ -96,6 +116,8 @@ export class IndoorLevelControl { this.element.parentNode.removeChild(this.element) } this.element = null + this.levels = [] + this.currentLevel = null } /** From 1f170f5dc9fbfe5857b8e27de1cab1876ba89fea Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Wed, 27 May 2026 00:31:32 +0200 Subject: [PATCH 10/14] paint indoor types with different colors --- .../maplibre/layers/indoor/control.js | 2 +- .../maplibre/layers/indoor/indoor.js | 78 ++++++++++++++++++- .../maplibre/layers/indoor/styles.js | 42 ++++++---- 3 files changed, 105 insertions(+), 17 deletions(-) diff --git a/app/javascript/maplibre/layers/indoor/control.js b/app/javascript/maplibre/layers/indoor/control.js index 9f0d7a3c..4ba0e40e 100644 --- a/app/javascript/maplibre/layers/indoor/control.js +++ b/app/javascript/maplibre/layers/indoor/control.js @@ -47,7 +47,7 @@ export class IndoorLevelControl { if (tooltip) { try { tooltip.dispose() - } catch (e) { + } catch { // Tooltip might be mid-animation when dispose is called } } diff --git a/app/javascript/maplibre/layers/indoor/indoor.js b/app/javascript/maplibre/layers/indoor/indoor.js index 1ac9b088..266ae2a4 100644 --- a/app/javascript/maplibre/layers/indoor/indoor.js +++ b/app/javascript/maplibre/layers/indoor/indoor.js @@ -1,8 +1,9 @@ import { debounce } from 'helpers/functions' +import { highlightFeature, resetHighlightedFeature } from 'maplibre/feature' import { IndoorLevelControl } from 'maplibre/layers/indoor/control' -import { addIndoorLayers, getIndoorLayerIds } from 'maplibre/layers/indoor/styles' +import { addIndoorLayers, getIndoorLayerIds, indoorFillColor } from 'maplibre/layers/indoor/styles' import { Layer } from 'maplibre/layers/layer' -import { map, removeStyleLayers } from 'maplibre/map' +import { map, removeStyleLayers, updateBuildingOpacity } from 'maplibre/map' export class IndoorLayer extends Layer { constructor(layer) { @@ -48,6 +49,7 @@ export class IndoorLayer extends Layer { tiles: [`https://tiles.indoorequal.org/tiles/{z}/{x}/{y}.pbf?key=${encodeURIComponent(apiKey)}`], minzoom: 0, maxzoom: 20, + promoteId: { area: 'id', transportation: 'id' }, attribution: '© Indoor Equal' }) } @@ -67,7 +69,9 @@ export class IndoorLayer extends Layer { const levelFilter = ['==', ['to-string', ['get', 'level']], this.currentLevel] addIndoorLayers(this.sourceId, levelFilter) + this.updateFillPaint() this.setupLevelDetection() + this.setupEventHandlers() return Promise.resolve() } @@ -81,12 +85,32 @@ export class IndoorLayer extends Layer { } setupEventHandlers() { - // No-op - indoor features are not selectable like GeoJSON features + this.removeEventHandlers() + + this.clickHandler = (e) => { + if (window.gon.map_mode !== 'rw') return + + const feature = e.features?.[0] + if (!feature) return + + console.log('Indoor feature clicked:', feature) + console.log('Feature id:', feature.id, 'Source:', this.sourceId) + + feature.properties.label = feature.properties.name || feature.properties.class + feature.properties.desc = indoorDescription(feature.properties) + + const sourceLayer = feature.layer['source-layer'] || feature.sourceLayer + console.log('Source layer:', sourceLayer) + highlightFeature(feature, false, this.sourceId, sourceLayer) + } + + map.on('click', this.getStyleLayerIds(), this.clickHandler) } setLevel(level) { if (this.currentLevel === level) return + resetHighlightedFeature() this.currentLevel = level const levelFilter = ['==', ['to-string', ['get', 'level']], level] @@ -98,9 +122,31 @@ export class IndoorLayer extends Layer { } }) + this.updateFillPaint() this.updateLevelControlUI() } + updateFillPaint() { + const fillLayerId = `indoor-area-fill_${this.sourceId}` + if (!map.getLayer(fillLayerId)) return + + if (parseFloat(this.currentLevel) >= 1) { + map.setPaintProperty(fillLayerId, 'fill-color', [ + 'case', + ['boolean', ['feature-state', 'active'], false], '#b3d9ff', + 'gray' + ]) + map.setPaintProperty(fillLayerId, 'fill-opacity', 0.7) + } else { + map.setPaintProperty(fillLayerId, 'fill-color', [ + 'case', + ['boolean', ['feature-state', 'active'], false], '#b3d9ff', + indoorFillColor + ]) + map.setPaintProperty(fillLayerId, 'fill-opacity', 0.9) + } + } + setupLevelDetection() { this.removeLevelDetection() @@ -136,6 +182,9 @@ export class IndoorLayer extends Layer { this.levels = newLevels // console.log('Indoor layer: detected levels', newLevels) this.updateLevelControl() + } else if (this.levels.length > 0 && !this.levelControl) { + // Recreate control if it was removed (e.g., layer was hidden then shown) + this.updateLevelControl() } }, `indoor-level-${this.id}`, 500) } @@ -164,6 +213,7 @@ export class IndoorLayer extends Layer { } else { this.removeLevelControl() } + updateBuildingOpacity() } createLevelControl() { @@ -191,3 +241,25 @@ export class IndoorLayer extends Layer { super.cleanup() } } + +function indoorDescription(props) { + const skipKeys = ['name', 'label', 'desc'] + + let desc = '\n
\n' + desc += '| | |\n' + desc += '| ------------- | ------------- |\n' + + const keys = Object.keys(props).filter(key => !skipKeys.includes(key)) + keys.forEach(key => { + desc += `| **${key}** | ${props[key]} |\n` + }) + + desc += '\n
\n' + + if (props['id']) { + desc += '\n![osm link](/icons/osm-icon-small.png)' + desc += '[See node in OpenStreetMap](https://www.openstreetmap.org/' + props['id'] + ')' + } + + return desc +} diff --git a/app/javascript/maplibre/layers/indoor/styles.js b/app/javascript/maplibre/layers/indoor/styles.js index 7af4868a..b2951947 100644 --- a/app/javascript/maplibre/layers/indoor/styles.js +++ b/app/javascript/maplibre/layers/indoor/styles.js @@ -1,5 +1,18 @@ import { map } from 'maplibre/map' +export const indoorFillColor = [ + 'match', + ['get', 'class'], + 'room', '#fdbe87', + 'corridor', '#f4c97f', + 'wall', '#c89968', + 'platform', '#f7d794', + 'column', '#b8875e', + 'area', '#f2d9a8', + 'level', '#e5c88d', + '#f2d9a8' +] + /** * Adds indoor map style layers for a given source * Only level plans right now, no POI points @@ -16,13 +29,9 @@ export function addIndoorLayers(sourceId, levelFilter) { filter: levelFilter, paint: { 'fill-color': [ - 'match', - ['get', 'class'], - 'room', '#fdfcfa', - 'corridor', '#fefefe', - 'platform', '#e8edff', - 'wall', '#d5d5d5', - '#f0f0f0' + 'case', + ['boolean', ['feature-state', 'active'], false], '#b3d9ff', + indoorFillColor ], 'fill-opacity': 0.9 } @@ -36,7 +45,11 @@ export function addIndoorLayers(sourceId, levelFilter) { minzoom: 16, filter: levelFilter, paint: { - 'fill-extrusion-color': '#fdbe87', + 'fill-extrusion-color': [ + 'case', + ['boolean', ['feature-state', 'active'], false], '#b3d9ff', + indoorFillColor + ], 'fill-extrusion-height': ['+', ['*', ['to-number', ['get', 'level']], 5], 5], 'fill-extrusion-base': ['*', ['to-number', ['get', 'level']], 5], 'fill-extrusion-opacity': 0.8 @@ -51,12 +64,15 @@ export function addIndoorLayers(sourceId, levelFilter) { minzoom: 16, filter: levelFilter, paint: { - 'line-color': '#000', + 'line-color': [ + 'case', + ['boolean', ['feature-state', 'active'], false], '#000', + '#888' + ], 'line-width': [ - 'match', - ['get', 'class'], - 'wall', 3, - 2 + 'case', + ['boolean', ['feature-state', 'active'], false], 3, + 1 ] } }) From ea0924147eda7afa870e7cf573da9c4908525835 Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Wed, 27 May 2026 21:47:05 +0200 Subject: [PATCH 11/14] add poi indoor features --- .../maplibre/layers/indoor/indoor.js | 4 +- .../maplibre/layers/indoor/styles.js | 86 ++++++++++++++++++- app/javascript/maplibre/map.js | 2 +- 3 files changed, 87 insertions(+), 5 deletions(-) diff --git a/app/javascript/maplibre/layers/indoor/indoor.js b/app/javascript/maplibre/layers/indoor/indoor.js index 266ae2a4..2c33fe7d 100644 --- a/app/javascript/maplibre/layers/indoor/indoor.js +++ b/app/javascript/maplibre/layers/indoor/indoor.js @@ -49,7 +49,7 @@ export class IndoorLayer extends Layer { tiles: [`https://tiles.indoorequal.org/tiles/{z}/{x}/{y}.pbf?key=${encodeURIComponent(apiKey)}`], minzoom: 0, maxzoom: 20, - promoteId: { area: 'id', transportation: 'id' }, + promoteId: { area: 'id', transportation: 'id', poi: 'id' }, attribution: '© Indoor Equal' }) } @@ -258,7 +258,7 @@ function indoorDescription(props) { if (props['id']) { desc += '\n![osm link](/icons/osm-icon-small.png)' - desc += '[See node in OpenStreetMap](https://www.openstreetmap.org/' + props['id'] + ')' + desc += '[See node in OpenStreetMap](https://www.openstreetmap.org/' + props['id'].replace(':', '/') + ')' } return desc diff --git a/app/javascript/maplibre/layers/indoor/styles.js b/app/javascript/maplibre/layers/indoor/styles.js index b2951947..ebe58178 100644 --- a/app/javascript/maplibre/layers/indoor/styles.js +++ b/app/javascript/maplibre/layers/indoor/styles.js @@ -10,12 +10,13 @@ export const indoorFillColor = [ 'column', '#b8875e', 'area', '#f2d9a8', 'level', '#e5c88d', - '#f2d9a8' + '#c0c0c0' ] /** * Adds indoor map style layers for a given source * Only level plans right now, no POI points + * Schema of indoor layers: https://indoorequal.com/doc/schema * @param {string} sourceId - The source ID to use for the layers * @param {Array} levelFilter - MapLibre filter expression array for the current level */ @@ -90,6 +91,84 @@ export function addIndoorLayers(sourceId, levelFilter) { 'line-dasharray': [2, 2] } }) + + map.addLayer({ + id: `indoor-poi-circle_${sourceId}`, + type: 'circle', + source: sourceId, + 'source-layer': 'poi', + minzoom: 17, + filter: levelFilter, + paint: { + 'circle-radius': [ + 'case', + ['boolean', ['feature-state', 'active'], false], 6, + 4 + ], + 'circle-color': [ + 'case', + ['boolean', ['feature-state', 'active'], false], '#b3d9ff', + [ + 'match', + ['get', 'class'], + 'entrance', '#4CAF50', + 'stairs', '#FF9800', + 'elevator', '#2196F3', + 'escalator', '#2196F3', + 'toilet', '#9C27B0', + 'information', '#00BCD4', + '#757575' + ] + ], + 'circle-stroke-width': [ + 'case', + ['boolean', ['feature-state', 'active'], false], 2, + 1 + ], + 'circle-stroke-color': '#fff' + } + }) + + map.addLayer({ + id: `indoor-poi-label_${sourceId}`, + type: 'symbol', + source: sourceId, + 'source-layer': 'poi', + minzoom: 18, + filter: levelFilter, + layout: { + 'text-field': ['get', 'name'], + 'text-size': 11, + 'text-anchor': 'top', + 'text-offset': [0, 0.8], + 'text-optional': true + }, + paint: { + 'text-color': '#333', + 'text-halo-color': '#fff', + 'text-halo-width': 1 + } + }) + + map.addLayer({ + id: `indoor-area-name_${sourceId}`, + type: 'symbol', + source: sourceId, + 'source-layer': 'area_name', + minzoom: 18, + filter: levelFilter, + layout: { + 'text-field': ['get', 'name'], + 'text-size': 12, + 'text-font': ['Open Sans Regular'], + 'text-optional': true + }, + paint: { + 'text-color': '#555', + 'text-halo-color': '#fff', + 'text-halo-width': 1.5 + } + }) } /** @@ -102,6 +181,9 @@ export function getIndoorLayerIds(sourceId) { `indoor-area-fill_${sourceId}`, `indoor-area-extrusion_${sourceId}`, `indoor-area-line_${sourceId}`, - `indoor-transportation_${sourceId}` + `indoor-transportation_${sourceId}`, + `indoor-poi-circle_${sourceId}`, + `indoor-poi-label_${sourceId}`, + `indoor-area-name_${sourceId}` ] } diff --git a/app/javascript/maplibre/map.js b/app/javascript/maplibre/map.js index 2549c4b8..c6ef7e38 100644 --- a/app/javascript/maplibre/map.js +++ b/app/javascript/maplibre/map.js @@ -563,7 +563,7 @@ export function sortLayers () { styleLayers.filter(e => e.id.startsWith('line-layer_geojson-source') && !e.id.includes('outline')), styleLayers.filter(e => e.id.includes('route-extras-source') && !e.id.startsWith('route-extras-labels')), styleLayers.filter(e => e.paint && e.paint['fill-extrusion-height'] && e.id.startsWith('polygon-layer-extrusion')), - styleLayers.filter(e => e.id.startsWith('indoor-area-extrusion_')), + //styleLayers.filter(e => e.id.startsWith('indoor-area-extrusion_')), styleLayers.filter(e => e.paint && e.paint['fill-extrusion-height'] && !e.id.startsWith('polygon-layer-extrusion') && !e.id.startsWith('indoor-area-extrusion_')), styleLayers.filter(e => e.id.startsWith('maplibre-gl-directions')), styleLayers.filter(e => e.type === 'symbol' && From 39ae60c38c536a1ac5d91631725d2d11814edae1 Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Wed, 27 May 2026 22:38:40 +0200 Subject: [PATCH 12/14] fix tooltip exception --- .../maplibre/layers/indoor/control.js | 71 +++++++++++-------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/app/javascript/maplibre/layers/indoor/control.js b/app/javascript/maplibre/layers/indoor/control.js index 4ba0e40e..89365bd8 100644 --- a/app/javascript/maplibre/layers/indoor/control.js +++ b/app/javascript/maplibre/layers/indoor/control.js @@ -54,6 +54,26 @@ export class IndoorLevelControl { }) } + /** + * Creates a button for a level + * @param {string} level - The level value + * @returns {HTMLButtonElement} The created button + */ + createButton(level) { + const button = document.createElement('button') + button.textContent = level + button.title = `Level ${level}` + button.setAttribute('data-level', level) + button.setAttribute('data-toggle', 'tooltip') + button.setAttribute('data-bs-trigger', 'hover') + button.addEventListener('click', () => { + if (this.onLevelChange) { + this.onLevelChange(level) + } + }) + return button + } + /** * Updates the control with the given levels * @param {string[]} levels - Array of level strings, sorted descending @@ -68,43 +88,36 @@ export class IndoorLevelControl { if (levelsChanged) { this.levels = levels - this.currentLevel = currentLevel - this.disposeTooltips() - this.element.innerHTML = '' - levels.forEach(level => { - const button = document.createElement('button') - button.textContent = level - button.title = `Level ${level}` - button.setAttribute('data-level', level) - button.setAttribute('data-toggle', 'tooltip') - button.setAttribute('data-bs-trigger', 'hover') - - if (level === currentLevel) { - button.classList.add('active') - } + const existingButtons = new Map() + this.element.querySelectorAll('button').forEach(button => { + existingButtons.set(button.getAttribute('data-level'), button) + }) - button.addEventListener('click', () => { - if (this.onLevelChange) { - this.onLevelChange(level) - } - }) + const newLevelSet = new Set(levels) + existingButtons.forEach((button, level) => { + if (!newLevelSet.has(level)) { + const tooltip = bootstrap.Tooltip.getInstance(button) + if (tooltip) tooltip.dispose() + button.remove() + } + }) + levels.forEach(level => { + let button = existingButtons.get(level) + if (!button) { + button = this.createButton(level) + } this.element.appendChild(button) }) initTooltips(this.element) - } else if (this.currentLevel !== currentLevel) { - this.currentLevel = currentLevel - this.element.querySelectorAll('button').forEach(button => { - const level = button.getAttribute('data-level') - if (level === currentLevel) { - button.classList.add('active') - } else { - button.classList.remove('active') - } - }) } + + this.currentLevel = currentLevel + this.element.querySelectorAll('button').forEach(button => { + button.classList.toggle('active', button.getAttribute('data-level') === currentLevel) + }) } /** From d841471df67cd6fd856c179347faa7b072ba4c8d Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Wed, 27 May 2026 22:38:57 +0200 Subject: [PATCH 13/14] render some poi icons --- .../maplibre/layers/indoor/styles.js | 75 +++++++++++++++++-- 1 file changed, 68 insertions(+), 7 deletions(-) diff --git a/app/javascript/maplibre/layers/indoor/styles.js b/app/javascript/maplibre/layers/indoor/styles.js index ebe58178..7e254606 100644 --- a/app/javascript/maplibre/layers/indoor/styles.js +++ b/app/javascript/maplibre/layers/indoor/styles.js @@ -1,4 +1,5 @@ import { map } from 'maplibre/map' +import { labelFont } from 'maplibre/styles/styles' export const indoorFillColor = [ 'match', @@ -51,8 +52,8 @@ export function addIndoorLayers(sourceId, levelFilter) { ['boolean', ['feature-state', 'active'], false], '#b3d9ff', indoorFillColor ], - 'fill-extrusion-height': ['+', ['*', ['to-number', ['get', 'level']], 5], 5], - 'fill-extrusion-base': ['*', ['to-number', ['get', 'level']], 5], + 'fill-extrusion-height': ['+', ['*', ['to-number', ['get', 'level']], 4], 4], + 'fill-extrusion-base': ['*', ['to-number', ['get', 'level']], 4], 'fill-extrusion-opacity': 0.8 } }) @@ -98,7 +99,18 @@ export function addIndoorLayers(sourceId, levelFilter) { source: sourceId, 'source-layer': 'poi', minzoom: 17, - filter: levelFilter, + filter: [ + 'all', + levelFilter, + [ + '!', + [ + 'any', + ['in', ['get', 'subclass'], ['literal', ['toilet', 'toilets', 'door', 'elevator']]], + ['in', ['get', 'class'], ['literal', ['entrance', 'shop']]] + ] + ] + ], paint: { 'circle-radius': [ 'case', @@ -111,11 +123,8 @@ export function addIndoorLayers(sourceId, levelFilter) { [ 'match', ['get', 'class'], - 'entrance', '#4CAF50', 'stairs', '#FF9800', - 'elevator', '#2196F3', 'escalator', '#2196F3', - 'toilet', '#9C27B0', 'information', '#00BCD4', '#757575' ] @@ -129,6 +138,56 @@ export function addIndoorLayers(sourceId, levelFilter) { } }) + map.addLayer({ + id: `indoor-poi-icon_${sourceId}`, + type: 'symbol', + source: sourceId, + 'source-layer': 'poi', + minzoom: 17, + filter: [ + 'all', + levelFilter, + [ + 'any', + ['in', ['get', 'subclass'], ['literal', ['toilet', 'toilets', 'door', 'elevator']]], + ['in', ['get', 'class'], ['literal', ['entrance', 'shop']]] + ] + ], + layout: { + 'text-field': [ + 'case', + ['in', ['get', 'subclass'], ['literal', ['toilet', 'toilets']]], '', + ['==', ['get', 'subclass'], 'door'], '', + ['==', ['get', 'subclass'], 'elevator'], '', + ['==', ['get', 'class'], 'entrance'], '', + ['==', ['get', 'class'], 'shop'], '', + '' + ], + 'text-font': ['bootstrap-icons'], + 'text-size': 18, + 'text-allow-overlap': true, + 'text-ignore-placement': true + }, + paint: { + 'text-color': [ + 'case', + ['boolean', ['feature-state', 'active'], false], '#b3d9ff', + ['in', ['get', 'subclass'], ['literal', ['toilet', 'toilets']]], '#9C27B0', + ['==', ['get', 'subclass'], 'door'], '#4CAF50', + ['==', ['get', 'subclass'], 'elevator'], '#2196F3', + ['==', ['get', 'class'], 'entrance'], '#4CAF50', + ['==', ['get', 'class'], 'shop'], '#FF9800', + '#757575' + ], + 'text-halo-color': '#fff', + 'text-halo-width': [ + 'case', + ['boolean', ['feature-state', 'active'], false], 3, + 2 + ] + } + }) + map.addLayer({ id: `indoor-poi-label_${sourceId}`, type: 'symbol', @@ -139,6 +198,7 @@ export function addIndoorLayers(sourceId, levelFilter) { layout: { 'text-field': ['get', 'name'], 'text-size': 11, + 'text-font': labelFont, 'text-anchor': 'top', 'text-offset': [0, 0.8], 'text-optional': true @@ -160,7 +220,7 @@ export function addIndoorLayers(sourceId, levelFilter) { layout: { 'text-field': ['get', 'name'], 'text-size': 12, - 'text-font': ['Open Sans Regular'], + 'text-font': labelFont, 'text-optional': true }, paint: { @@ -183,6 +243,7 @@ export function getIndoorLayerIds(sourceId) { `indoor-area-line_${sourceId}`, `indoor-transportation_${sourceId}`, `indoor-poi-circle_${sourceId}`, + `indoor-poi-icon_${sourceId}`, `indoor-poi-label_${sourceId}`, `indoor-area-name_${sourceId}` ] From bb2a4d70552f29a0891ec4abdf1f2a12e6642b20 Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Wed, 27 May 2026 23:55:31 +0200 Subject: [PATCH 14/14] work on styles --- .../maplibre/layers/indoor/indoor.js | 4 +- .../maplibre/layers/indoor/styles.js | 41 ++++++------------- 2 files changed, 14 insertions(+), 31 deletions(-) diff --git a/app/javascript/maplibre/layers/indoor/indoor.js b/app/javascript/maplibre/layers/indoor/indoor.js index 2c33fe7d..2c9e665f 100644 --- a/app/javascript/maplibre/layers/indoor/indoor.js +++ b/app/javascript/maplibre/layers/indoor/indoor.js @@ -245,7 +245,7 @@ export class IndoorLayer extends Layer { function indoorDescription(props) { const skipKeys = ['name', 'label', 'desc'] - let desc = '\n
\n' + let desc = '\n' desc += '| | |\n' desc += '| ------------- | ------------- |\n' @@ -254,7 +254,7 @@ function indoorDescription(props) { desc += `| **${key}** | ${props[key]} |\n` }) - desc += '\n
\n' + desc += '\n' if (props['id']) { desc += '\n![osm link](/icons/osm-icon-small.png)' diff --git a/app/javascript/maplibre/layers/indoor/styles.js b/app/javascript/maplibre/layers/indoor/styles.js index 7e254606..e4f87467 100644 --- a/app/javascript/maplibre/layers/indoor/styles.js +++ b/app/javascript/maplibre/layers/indoor/styles.js @@ -106,8 +106,7 @@ export function addIndoorLayers(sourceId, levelFilter) { '!', [ 'any', - ['in', ['get', 'subclass'], ['literal', ['toilet', 'toilets', 'door', 'elevator']]], - ['in', ['get', 'class'], ['literal', ['entrance', 'shop']]] + ['in', ['get', 'subclass'], ['literal', ['toilet', 'toilets', 'elevator']]] ] ] ], @@ -149,41 +148,25 @@ export function addIndoorLayers(sourceId, levelFilter) { levelFilter, [ 'any', - ['in', ['get', 'subclass'], ['literal', ['toilet', 'toilets', 'door', 'elevator']]], - ['in', ['get', 'class'], ['literal', ['entrance', 'shop']]] + ['in', ['get', 'subclass'], ['literal', ['toilet', 'toilets', 'elevator']]] ] ], layout: { - 'text-field': [ + 'icon-image': [ 'case', - ['in', ['get', 'subclass'], ['literal', ['toilet', 'toilets']]], '', - ['==', ['get', 'subclass'], 'door'], '', - ['==', ['get', 'subclass'], 'elevator'], '', - ['==', ['get', 'class'], 'entrance'], '', - ['==', ['get', 'class'], 'shop'], '', + ['in', ['get', 'subclass'], ['literal', ['toilet', 'toilets']]], '/emojis/noto/🚻.png', + ['==', ['get', 'subclass'], 'elevator'], '/emojis/noto/🛗.png', '' ], - 'text-font': ['bootstrap-icons'], - 'text-size': 18, - 'text-allow-overlap': true, - 'text-ignore-placement': true + 'icon-size': 0.3, + 'icon-allow-overlap': true, + 'icon-ignore-placement': true }, paint: { - 'text-color': [ + 'icon-opacity': [ 'case', - ['boolean', ['feature-state', 'active'], false], '#b3d9ff', - ['in', ['get', 'subclass'], ['literal', ['toilet', 'toilets']]], '#9C27B0', - ['==', ['get', 'subclass'], 'door'], '#4CAF50', - ['==', ['get', 'subclass'], 'elevator'], '#2196F3', - ['==', ['get', 'class'], 'entrance'], '#4CAF50', - ['==', ['get', 'class'], 'shop'], '#FF9800', - '#757575' - ], - 'text-halo-color': '#fff', - 'text-halo-width': [ - 'case', - ['boolean', ['feature-state', 'active'], false], 3, - 2 + ['boolean', ['feature-state', 'active'], false], 1, + 0.85 ] } }) @@ -197,7 +180,7 @@ export function addIndoorLayers(sourceId, levelFilter) { filter: levelFilter, layout: { 'text-field': ['get', 'name'], - 'text-size': 11, + 'text-size': 13, 'text-font': labelFont, 'text-anchor': 'top', 'text-offset': [0, 0.8],