Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
* Added indoor layer (vector data from [indoorequal](https://indoorequal.org/))

## 2026-04

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

Expand Down
44 changes: 44 additions & 0 deletions app/assets/stylesheets/controls.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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>');
Expand Down
4 changes: 4 additions & 0 deletions app/javascript/controllers/map/layers_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions app/javascript/maplibre/controls/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down
4 changes: 3 additions & 1 deletion app/javascript/maplibre/layers/factory.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -10,7 +11,8 @@ const layerTypes = {
overpass: OverpassLayer,
wikipedia: WikipediaLayer,
basemap: BasemapLayer,
raster: RasterLayer
raster: RasterLayer,
indoor: IndoorLayer
}

export function createLayerInstance(data) {
Expand Down
119 changes: 119 additions & 0 deletions app/javascript/maplibre/layers/indoor/control.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
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

// 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)

const mapContainer = document.querySelector('#maplibre-map')
if (mapContainer) {
mapContainer.appendChild(this.element)
}
Comment on lines +18 to +35
}

/**
* 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')

Comment on lines +66 to +73
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'
}
Comment on lines +104 to +117
}
}
Loading
Loading