Skip to content

Commit e8b6113

Browse files
authored
Merge pull request #681 from mapforge-org/oop_layers
Object oriented layer classes
2 parents 044d50a + bce49e0 commit e8b6113

26 files changed

Lines changed: 1042 additions & 612 deletions

File tree

app/assets/stylesheets/docs.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ nav.fixed-top~#docs {
2121
padding-bottom: 0.4rem;
2222
width: fit-content;
2323
border-bottom: 0.15rem solid transparent;
24-
border-image: linear-gradient(to right, var(--color-dark-moss-green), var(--color-light-sand)) 1;
24+
border-image: linear-gradient(to right, var(--color-light-sand), var(--color-dark-moss-green)) 1;
2525
}
2626

2727
h2, h3, h4 {

app/javascript/channels/map_channel.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import consumer from 'channels/consumer'
2-
import { initializeLayerStyles, layers, loadLayerDefinitions } from 'maplibre/layers/layers'
2+
import { createLayerInstance } from 'maplibre/layers/factory'
3+
import { initializeLayerSources, initializeLayerStyles, layers, loadLayerDefinitions } from 'maplibre/layers/layers'
34
import {
45
destroyFeature,
56
initializeMaplibreProperties, map,
@@ -101,25 +102,24 @@ export function initializeSocket () {
101102
case 'update_layer':
102103
const index = layers.findIndex(l => l.id === data.layer.id)
103104
if (index > -1) {
104-
// Remove geojson key before comparison
105-
const { ['geojson']: _, ...layerDef } = layers[index]
105+
const layerDef = layers[index].toJSON()
106106
if (JSON.stringify(layerDef) !== JSON.stringify(data.layer)) {
107-
// preserve geojson data when updating layer definition
108-
const geojson = layers[index].geojson
109-
layers[index] = data.layer
110-
if (geojson) { layers[index].geojson = geojson }
111107
console.log('Layer updated on server, reloading layer styles', data.layer)
108+
layers[index].update(data.layer)
112109
initializeLayerStyles(data.layer.id)
113-
setLayerVisibility(data.layer.type + '-source-' + data.layer.id, data.layer.show !== false)
110+
setLayerVisibility(layers[index].sourceId, data.layer.show !== false)
114111
}
115112
} else {
116-
layers.push(data.layer)
113+
const newLayer = createLayerInstance(data.layer)
114+
layers.push(newLayer)
115+
initializeLayerSources(data.layer.id)
117116
initializeLayerStyles(data.layer.id)
118117
}
119118
break
120119
case 'delete_layer':
121120
const delIndex = layers.findIndex(l => l.id === data.layer.id)
122121
if (delIndex > -1) {
122+
layers[delIndex].cleanup()
123123
layers.splice(delIndex, 1)
124124
// trigger a full map redraw
125125
setBackgroundMapLayer(mapProperties.base_map, true)

app/javascript/controllers/feature/edit_controller.js

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import { status } from 'helpers/status'
66
import { flyToFeature } from 'maplibre/animations'
77
import { draw, handleDelete } from 'maplibre/edit'
88
import { confirmImageLocation, featureIcon, featureImage, uploadImageToFeature } from 'maplibre/feature'
9-
import { renderGeoJSONLayer } from 'maplibre/layers/geojson'
10-
import { getFeature, getLayer } from 'maplibre/layers/layers'
9+
import { getFeature, getLayer, renderLayer } from 'maplibre/layers/layers'
1110
import { featureColor, featureOutlineColor } from 'maplibre/styles/styles'
1211
import { addUndoState } from 'maplibre/undo'
1312

@@ -41,7 +40,7 @@ export default class extends Controller {
4140
document.querySelector('#feature-edit-raw .error').innerHTML = ''
4241
try {
4342
feature.properties = JSON.parse(document.querySelector('#feature-edit-raw textarea').value)
44-
renderGeoJSONLayer(this.layerIdValue, true)
43+
renderLayer(this.layerIdValue, true)
4544
mapChannel.send_message('update_feature', feature)
4645
} catch (error) {
4746
console.error('Error updating feature:', error.message)
@@ -56,7 +55,7 @@ export default class extends Controller {
5655
feature.properties.title = title
5756
if (document.querySelector('#feature-show-title-on-map')?.checked) {
5857
feature.properties.label = title
59-
renderGeoJSONLayer(this.layerIdValue, false)
58+
renderLayer(this.layerIdValue, false)
6059
}
6160
document.querySelector('#feature-title').textContent = title
6261
functions.debounce(() => { this.saveFeature() }, 'title')
@@ -70,7 +69,7 @@ export default class extends Controller {
7069
} else {
7170
delete feature.properties.label
7271
}
73-
renderGeoJSONLayer(this.layerIdValue, false)
72+
renderLayer(this.layerIdValue, false)
7473
functions.debounce(() => { this.saveFeature() }, 'show-title-on-map', 1000)
7574
}
7675

@@ -86,7 +85,7 @@ export default class extends Controller {
8685
}
8786
feature.properties[propertyName] = value
8887
draw.setFeatureProperty(this.featureIdValue, propertyName, value)
89-
renderGeoJSONLayer(this.layerIdValue, true)
88+
renderLayer(this.layerIdValue, true)
9089
}
9190

9291
// called as preview on slider change
@@ -134,15 +133,15 @@ export default class extends Controller {
134133
document.querySelector('#stroke-color').removeAttribute('disabled')
135134
}
136135
feature.properties.stroke = color
137-
renderGeoJSONLayer(this.layerIdValue, true)
136+
renderLayer(this.layerIdValue, true)
138137
}
139138

140139
updateFillColor () {
141140
const feature = this.getEditFeature()
142141
const color = document.querySelector('#fill-color').value
143142
if (feature.geometry.type === 'Polygon' || feature.geometry.type === 'MultiPolygon') { feature.properties.fill = color }
144143
if (feature.geometry.type === 'Point') { feature.properties['marker-color'] = color }
145-
renderGeoJSONLayer(this.layerIdValue, true)
144+
renderLayer(this.layerIdValue, true)
146145
}
147146

148147
updateFillColorTransparent () {
@@ -158,7 +157,7 @@ export default class extends Controller {
158157
}
159158
if (feature.geometry.type === 'Polygon' || feature.geometry.type === 'MultiPolygon') { feature.properties.fill = color }
160159
if (feature.geometry.type === 'Point') { feature.properties['marker-color'] = color }
161-
renderGeoJSONLayer(this.layerIdValue, true)
160+
renderLayer(this.layerIdValue, true)
162161
}
163162

164163
updateShowKmMarkers () {
@@ -170,7 +169,7 @@ export default class extends Controller {
170169
delete feature.properties['show-km-markers']
171170
delete feature.properties['stroke-image-url']
172171
}
173-
renderGeoJSONLayer(this.layerIdValue, true)
172+
renderLayer(this.layerIdValue, true)
174173
}
175174

176175
updateMarkerSymbol () {
@@ -183,7 +182,7 @@ export default class extends Controller {
183182
// draw layer feature properties aren't getting updated by draw.set()
184183
draw.setFeatureProperty(this.featureIdValue, 'marker-symbol', symbol)
185184
functions.e('.feature-symbol', e => { e.innerHTML = featureIcon(feature) })
186-
renderGeoJSONLayer(this.layerIdValue, true)
185+
renderLayer(this.layerIdValue, true)
187186
}
188187

189188
async updateMarkerImage () {
@@ -205,7 +204,7 @@ export default class extends Controller {
205204
feature.geometry.coordinates = imageLocation
206205
flyToFeature(feature)
207206
}
208-
renderGeoJSONLayer(this.layerIdValue, true)
207+
renderLayer(this.layerIdValue, true)
209208
this.saveFeature()
210209
})
211210
}

app/javascript/controllers/map/context_menu_controller.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { Controller } from '@hotwired/stimulus'
22
import { mapChannel } from 'channels/map_channel'
3-
import { status } from 'helpers/status'
43
import * as functions from 'helpers/functions'
4+
import { status } from 'helpers/status'
55
import { hideContextMenu } from 'maplibre/controls/context_menu'
6-
import { getFeature } from 'maplibre/layers/layers'
6+
import { getFeature, renderLayers } from 'maplibre/layers/layers'
77
import { addFeature } from 'maplibre/map'
88
import { addUndoState } from 'maplibre/undo'
9-
import { renderGeoJSONLayers } from 'maplibre/layers/geojson'
109

1110
export default class extends Controller {
1211

@@ -18,7 +17,7 @@ export default class extends Controller {
1817
addUndoState('Feature update', feature)
1918
if (feature.geometry.type === 'LineString') { feature.geometry.coordinates.splice(vertexIndex, 1) }
2019
if (feature.geometry.type === 'Polygon') { feature.geometry.coordinates[0].splice(vertexIndex, 1) }
21-
renderGeoJSONLayers(true)
20+
renderLayers('geojson', true)
2221
mapChannel.send_message('update_feature', { ...feature })
2322
status('Point deleted')
2423
hideContextMenu()
@@ -27,7 +26,7 @@ export default class extends Controller {
2726
cutLine(event) {
2827
const target = event.currentTarget
2928
const feature = getFeature(target.dataset.featureId, 'geojson')
30-
29+
3130
const vertexIndex = parseInt(target.dataset.index, 10)
3231
const coords = feature.geometry.coordinates
3332
const firstCoords = coords.slice(0, vertexIndex + 1)
@@ -36,7 +35,7 @@ export default class extends Controller {
3635
// Keep original feature, shorten it to the first segment
3736
addUndoState('Feature update', feature)
3837
feature.geometry.coordinates = firstCoords
39-
renderGeoJSONLayers(true)
38+
renderLayers('geojson', true)
4039
mapChannel.send_message('update_feature', { ...feature })
4140

4241
const secondFeature = {
@@ -52,4 +51,12 @@ export default class extends Controller {
5251
status('Line cut into 2 segments')
5352
hideContextMenu()
5453
}
54+
55+
addToGeojsonLayer(event) {
56+
const target = event.currentTarget
57+
const feature = getFeature(target.dataset.featureId, 'basemap')
58+
addFeature(feature)
59+
addUndoState('Feature added', feature)
60+
mapChannel.send_message('new_feature', feature)
61+
}
5562
}

app/javascript/controllers/map/layers_controller.js

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ import { status } from 'helpers/status'
66
import { flyToFeature } from 'maplibre/animations'
77
import { initLayersModal } from 'maplibre/controls/shared'
88
import { confirmImageLocation, uploadImageToFeature } from 'maplibre/feature'
9-
import { renderGeoJSONLayer } from 'maplibre/layers/geojson'
10-
import { initializeLayerSources, initializeLayerStyles, layers, loadAllLayerData, loadLayerData } from 'maplibre/layers/layers'
11-
import { initializeOverpassLayers } from 'maplibre/layers/overpass'
9+
import { createLayerInstance } from 'maplibre/layers/factory'
10+
import { initializeLayerSources, initializeLayerStyles, layers, loadAllLayerData, loadLayerData, renderLayer } from 'maplibre/layers/layers'
1211
import { queries } from 'maplibre/layers/queries'
1312
import { map, mapProperties, removeGeoJSONSource, setLayerVisibility, upsert } from 'maplibre/map'
1413

@@ -111,7 +110,7 @@ export default class extends Controller {
111110
uploadImageToFeature(file, feature).then( () => {
112111
upsert(feature)
113112
// redraw first geojson layer
114-
renderGeoJSONLayer(layers.find(l => l.type === 'geojson').id)
113+
renderLayer(layers.find(l => l.type === 'geojson').id)
115114
mapChannel.send_message('new_feature', { ...feature })
116115
status('Added image')
117116
flyToFeature(feature)
@@ -159,22 +158,19 @@ export default class extends Controller {
159158
layer["cluster"] = clustered
160159
layer["heatmap"] = layer.query.includes("heatmap=true")
161160
event.target.closest('.layer-item').querySelector('.layer-name').innerHTML = layer.name
162-
const { geojson: _geojson, ...sendLayer } = layer
163-
mapChannel.send_message('update_layer', sendLayer)
161+
mapChannel.send_message('update_layer', layer.toJSON())
164162
event.target.closest('.layer-item').querySelector('.reload-icon').classList.add('layer-refresh-animate')
165-
initializeOverpassLayers(layerId)
163+
layer.initialize().then(() => { initLayersModal() })
166164
}
167165

168166
refreshLayer (event) {
169167
event.preventDefault()
170168
const layerId = event.target.closest('.layer-item').getAttribute('data-layer-id')
171169
functions.e('#layer-reload', e => { e.classList.add('hidden') })
172170
functions.e('#layer-loading', e => { e.classList.remove('hidden') })
173-
event.target.closest('.layer-item').querySelector('.reload-icon').classList.add('layer-refresh-animate')
174171
loadLayerData(layerId).then( () => {
175172
initLayersModal()
176173
functions.e('#layer-loading', e => { e.classList.add('hidden') })
177-
functions.e(`#layer-list-${layerId} .reload-icon`, e => { e.classList.remove('layer-refresh-animate') })
178174
})
179175
}
180176

@@ -209,7 +205,7 @@ export default class extends Controller {
209205
const wasVisible = layer.show !== false
210206
layer.show = !wasVisible
211207

212-
setLayerVisibility(layer.type + '-source-' + layerId, layer.show)
208+
setLayerVisibility(layer.sourceId, layer.show)
213209

214210
// update UI (both desktop and mobile visibility buttons)
215211
layerElement.querySelectorAll('button.layer-visibility i, button.layer-visibility-mobile i').forEach(icon => {
@@ -240,13 +236,12 @@ export default class extends Controller {
240236

241237
// sync to server only in rw mode
242238
if (window.gon.map_mode === "rw") {
243-
const { geojson: _geojson, ...sendLayer } = layer
244-
mapChannel.send_message('update_layer', sendLayer)
239+
mapChannel.send_message('update_layer', layer.toJSON())
245240
}
246241
}
247242

248243
createWikipediaLayer() {
249-
this.createLayer('wikipedia', 'Wikipedia', '')
244+
this.createLayer('wikipedia', 'Wikipedia')
250245
}
251246

252247
createSelectedOverpassLayer(event) {
@@ -263,25 +258,29 @@ export default class extends Controller {
263258
}
264259
}
265260

266-
createLayer(type, name, query) {
261+
createBaseMapLayer(_event) {
262+
this.createLayer('basemap', 'Basemap layer')
263+
}
264+
265+
createLayer(type, name, query=null) {
267266
let layerId = functions.featureId()
268267
// must match server attribute order, for proper comparison in map_channel
269-
let layer = { "id": layerId, "type": type, "name": name, "heatmap": false, "cluster": true, "show": true}
268+
let layerData = { "id": layerId, "type": type, "name": name, "heatmap": false, "cluster": true, "show": true}
270269
if (type == 'overpass') {
271-
layer["query"] = query
270+
layerData["query"] = query
272271
// TODO: move cluster + heatmap to layer checkboxes
273-
const clustered = !layer.query.includes("heatmap=true") &&
274-
!layer.query.includes("cluster=false") &&
275-
!layer.query.includes("geom") // clustering breaks lines & geometries
276-
layer["cluster"] = clustered
277-
layer["heatmap"] = layer.query.includes("heatmap=true")
272+
const clustered = !layerData.query.includes("heatmap=true") &&
273+
!layerData.query.includes("cluster=false") &&
274+
!layerData.query.includes("geom") // clustering breaks lines & geometries
275+
layerData["cluster"] = clustered
276+
layerData["heatmap"] = layerData.query.includes("heatmap=true")
278277
}
278+
let layer = createLayerInstance(layerData)
279279
layers.push(layer)
280+
initLayersModal()
280281
initializeLayerSources(layerId)
281282
initializeLayerStyles(layerId)
282-
mapChannel.send_message('new_layer', layer)
283-
initLayersModal()
284-
document.querySelector('#layer-list-' + layerId + ' .reload-icon').classList.add('layer-refresh-animate')
283+
mapChannel.send_message('new_layer', layerData)
285284
return layerId
286285
}
287286

@@ -291,12 +290,11 @@ export default class extends Controller {
291290
dom.closeTooltips()
292291
const layerElement = event.target.closest('.layer-item')
293292
const layerId = layerElement.getAttribute('data-layer-id')
294-
const layerType = layerElement.getAttribute('data-layer-type')
295293
const layer = layers.find(f => f.id === layerId)
296-
const { geojson: _geojson, ...sendLayer } = layer
294+
layer.cleanup()
297295
layers.splice(layers.indexOf(layer), 1)
298-
removeGeoJSONSource(layerType + '-source-' + layerId)
299-
mapChannel.send_message('delete_layer', sendLayer)
296+
removeGeoJSONSource(layer.sourceId)
297+
mapChannel.send_message('delete_layer', layer.toJSON())
300298
initLayersModal()
301299
}
302300
}

app/javascript/controllers/map_controller.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
import { Controller } from '@hotwired/stimulus'
22
import * as functions from 'helpers/functions'
3-
import { initializeMap, setBackgroundMapLayer, initializeViewMode,
3+
import { initializeMap, setBackgroundMapLayer, initializeViewMode,
44
initializeStaticMode, addFeature } from 'maplibre/map'
55
import { initializeEditMode } from 'maplibre/edit'
66
import { initializeSocket, mapChannel } from 'channels/map_channel'
7-
import { addUndoState } from 'maplibre/undo'
7+
import { addUndoState, clearUndoHistory } from 'maplibre/undo'
8+
import { resetInitializationState } from 'maplibre/layers/layers'
9+
import { clearImageState, resetLabelFont } from 'maplibre/styles/styles'
810

911
export default class extends Controller {
1012
async connect () {
13+
// Clear module-level state from previous map
14+
resetInitializationState()
15+
clearImageState()
16+
clearUndoHistory()
17+
resetLabelFont()
18+
1119
functions.e('#map-header nav', e => { e.style.display = 'none' })
1220
await initializeMap('maplibre-map')
1321
// static mode is used for screenshots
@@ -20,6 +28,27 @@ export default class extends Controller {
2028
setBackgroundMapLayer()
2129
}
2230

31+
disconnect() {
32+
// Clean up when navigating away from the map
33+
console.log('Map controller disconnecting, cleaning up...')
34+
35+
// Remove the map instance
36+
if (window.map) {
37+
try {
38+
window.map.remove()
39+
window.map = null
40+
} catch (e) {
41+
console.warn('Error removing map instance:', e)
42+
}
43+
}
44+
45+
// Clear module-level state
46+
resetInitializationState()
47+
clearImageState()
48+
clearUndoHistory()
49+
resetLabelFont()
50+
}
51+
2352
// paste feature from clipboard
2453
async paste(_event) {
2554

0 commit comments

Comments
 (0)