Skip to content

Commit 1035908

Browse files
authored
Merge pull request #605 from mapforge-org/unify_layer_handling
Handle geojson layers like other layers
2 parents a966735 + 294da9d commit 1035908

32 files changed

Lines changed: 644 additions & 532 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
# Mapforge
99

10-
Mapforge is an open source web application that lets you create and share your places, tracks and events as GeoJSON layers on top of different base maps. It uses [MapLibre GL JS](https://maplibre.org/maplibre-gl-js/docs/) as its map library and supports both desktop and mobile.
10+
Mapforge is an open source, easy to use GIS software. It's a web application that lets you create and share your places, tracks and events as GeoJSON layers on top of different base maps. It uses [MapLibre GL JS](https://maplibre.org/maplibre-gl-js/docs/) as map library and supports both desktop and mobile.
1111

12-
The browser connects to the server via WebSockets, so that changes are immediately synced to all clients. This enables collaborative editing and sharing real-time maps.
12+
Your browser connects to the server via WebSockets, so that changes are immediately synced to all clients. This enables collaborative editing and sharing real-time maps.
1313

1414
The main instance is running at [mapforge.org](https://mapforge.org), see [self-hosting](#selfhosting) how to run your own. Check the [changelog](CHANGELOG.md) for recent changes.
1515

app/channels/map_channel.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def map_atts(data)
9595
end
9696

9797
def layer_atts(data)
98-
data.slice("id", "type", "name", "query")
98+
data.slice("id", "type", "name", "query", "heatmap", "cluster")
9999
end
100100

101101
# load map with write access

app/controllers/maps_controller.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ def show
4444
gon.rails_env = Rails.env
4545
gon.csrf_token = form_authenticity_token
4646
gon.map_properties = @map_properties
47-
gon.map_layers = @map.layers.map(&:to_summary_json)
4847

4948
case params["engine"]
5049
when "deck"

app/javascript/channels/map_channel.js

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import consumer from 'channels/consumer'
22
import {
33
upsert, destroyFeature, setBackgroundMapLayer, mapProperties,
4-
initializeMaplibreProperties, map, layers, resetGeojsonLayers, loadLayers,
5-
reloadMapProperties, removeGeoJSONSource, redrawGeojson
6-
} from 'maplibre/map'
7-
import { initializeLayers } from 'maplibre/layers/layers'
8-
import { initLayersModal } from 'maplibre/controls/shared'
4+
initializeMaplibreProperties, map,
5+
reloadMapProperties } from 'maplibre/map'
6+
import { layers, initializeLayerStyles, loadLayerDefinitions } from 'maplibre/layers/layers'
97

108

119
export let mapChannel
@@ -41,10 +39,10 @@ export function initializeSocket () {
4139
if (channelStatus === 'off') {
4240
reloadMapProperties().then(() => {
4341
initializeMaplibreProperties()
44-
resetGeojsonLayers()
45-
loadLayers()
46-
setBackgroundMapLayer(mapProperties.base_map, false)
47-
map.fire('load', { detail: { message: 'Map re-loaded by map_channel' } })
42+
loadLayerDefinitions().then(() => {
43+
setBackgroundMapLayer(mapProperties.base_map, true)
44+
map.fire('load', { detail: { message: 'Map re-loaded by map_channel' } })
45+
})
4846
// status('Connection to server re-established')
4947
})
5048
} else {
@@ -102,20 +100,20 @@ export function initializeSocket () {
102100
const { ['geojson']: _, ...layerDef } = layers[index]
103101
if (JSON.stringify(layerDef) !== JSON.stringify(data.layer)) {
104102
layers[index] = data.layer
105-
initializeLayers(data.layer.id)
103+
console.log('Layer updated on server, reloading layer styles', data.layer)
104+
initializeLayerStyles(data.layer.id)
106105
}
107106
} else {
108107
layers.push(data.layer)
109-
initializeLayers(data.layer.id)
108+
initializeLayerStyles(data.layer.id)
110109
}
111110
break
112111
case 'delete_layer':
113112
const delIndex = layers.findIndex(l => l.id === data.layer.id)
114113
if (delIndex > -1) {
115114
layers.splice(delIndex, 1)
116-
removeGeoJSONSource('overpass-source-' + data.layer.id)
117-
initLayersModal()
118-
redrawGeojson()
115+
// trigger a full map redraw
116+
setBackgroundMapLayer(mapProperties.base_map, true)
119117
}
120118
break
121119
case 'mouse':
@@ -150,7 +148,7 @@ export function initializeSocket () {
150148
payload.map_id = window.gon.map_id
151149
payload.user_id = window.gon.user_id
152150
payload.uuid = connectionUUID
153-
// dropping properties.id from redrawGeojson() before sending to server
151+
// dropping properties.id before sending to server
154152
if (payload.properties && payload.properties.id) { delete payload.properties.id }
155153
if (event !== 'mouse') console.log('Sending: [' + event + '] :', payload)
156154
// Call the original perform method

app/javascript/controllers/feature/edit_controller.js

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,46 @@
11
import { Controller } from '@hotwired/stimulus'
22
import { mapChannel } from 'channels/map_channel'
3-
import { geojsonData, redrawGeojson } from 'maplibre/map'
43
import { featureIcon, featureImage, uploadImageToFeature, confirmImageLocation } from 'maplibre/feature'
54
import { handleDelete, draw } from 'maplibre/edit'
65
import { featureColor, featureOutlineColor } from 'maplibre/styles'
76
import { status } from 'helpers/status'
87
import * as functions from 'helpers/functions'
98
import * as dom from 'helpers/dom'
109
import { addUndoState } from 'maplibre/undo'
10+
import { getFeature, getLayer } from 'maplibre/layers/layers'
11+
import { renderGeoJSONLayer } from 'maplibre/layers/geojson'
1112

1213
export default class extends Controller {
1314
// https://stimulus.hotwired.dev/reference/values
1415
static values = {
15-
featureId: String
16+
featureId: String,
17+
layerId: String
1618
}
1719

1820
// emoji picker
1921
picker = null
2022

23+
featureIdValueChanged(value) {
24+
if (value) {
25+
this.layerIdValue = getLayer(value).id
26+
}
27+
}
28+
2129
delete_feature (e) {
2230
if (dom.isInputElement(e.target)) return // Don't trigger if typing in input
2331

24-
const feature = this.getFeature()
32+
const feature = this.getEditFeature()
2533
if (confirm(`Really delete this ${feature.geometry.type}?`)) {
2634
handleDelete({ features: [feature] })
2735
}
2836
}
2937

3038
update_feature_raw () {
31-
const feature = this.getFeature()
39+
const feature = this.getEditFeature()
3240
document.querySelector('#feature-edit-raw .error').innerHTML = ''
3341
try {
3442
feature.properties = JSON.parse(document.querySelector('#feature-edit-raw textarea').value)
35-
redrawGeojson()
43+
renderGeoJSONLayer(this.layerIdValue, true)
3644
mapChannel.send_message('update_feature', feature)
3745
} catch (error) {
3846
console.error('Error updating feature:', error.message)
@@ -42,96 +50,96 @@ export default class extends Controller {
4250
}
4351

4452
updateTitle () {
45-
const feature = this.getFeature()
53+
const feature = this.getEditFeature()
4654
const title = document.querySelector('#feature-title-input input').value
4755
feature.properties.title = title
4856
document.querySelector('#feature-title').textContent = title
4957
functions.debounce(() => { this.saveFeature() }, 'title')
5058
}
5159

5260
updateLabel () {
53-
const feature = this.getFeature()
61+
const feature = this.getEditFeature()
5462
const label = document.querySelector('#feature-label input').value
5563
feature.properties.label = label
56-
redrawGeojson(false)
64+
renderGeoJSONLayer(this.layerIdValue, false)
5765
functions.debounce(() => { this.saveFeature() }, 'label', 1000)
5866
}
5967

6068
// called as preview on slider change
6169
updatePointSize () {
62-
const feature = this.getFeature()
70+
const feature = this.getEditFeature()
6371
const size = document.querySelector('#point-size').value
6472
document.querySelector('#point-size-val').textContent = size
6573
feature.properties['marker-size'] = size
6674
// draw layer feature properties aren't getting updated by draw.set()
6775
draw.setFeatureProperty(this.featureIdValue, 'marker-size', size)
68-
redrawGeojson(true)
76+
renderGeoJSONLayer(this.layerIdValue, true)
6977
}
7078

7179
updatePointScaling() {
72-
const feature = this.getFeature()
80+
const feature = this.getEditFeature()
7381
const val = document.querySelector('#point-scaling').checked
7482
feature.properties['marker-scaling'] = val
7583
// draw layer feature properties aren't getting updated by draw.set()
7684
draw.setFeatureProperty(this.featureIdValue, 'marker-scaling', val)
77-
redrawGeojson(true)
85+
renderGeoJSONLayer(this.layerIdValue, true)
7886
}
7987

8088
// called as preview on slider change
8189
updateLineWidth () {
82-
const feature = this.getFeature()
90+
const feature = this.getEditFeature()
8391
const size = document.querySelector('#line-width').value
8492
document.querySelector('#line-width-val').textContent = size
8593
feature.properties['stroke-width'] = size
8694
// draw layer feature properties aren't getting updated by draw.set()
8795
draw.setFeatureProperty(this.featureIdValue, 'stroke-width', size)
88-
redrawGeojson(true)
96+
renderGeoJSONLayer(this.layerIdValue, true)
8997
}
9098

9199
// called as preview on slider change
92100
updateOutLineWidth () {
93-
const feature = this.getFeature()
101+
const feature = this.getEditFeature()
94102
const size = document.querySelector('#outline-width').value
95103
document.querySelector('#outline-width-val').textContent = size
96104
feature.properties['stroke-width'] = size
97105
// draw layer feature properties aren't getting updated by draw.set()
98106
draw.setFeatureProperty(this.featureIdValue, 'stroke-width', size)
99-
redrawGeojson(true)
107+
renderGeoJSONLayer(this.layerIdValue, true)
100108
}
101109

102110
// called as preview on slider change
103111
updateFillExtrusionHeight () {
104-
const feature = this.getFeature()
112+
const feature = this.getEditFeature()
105113
const size = document.querySelector('#fill-extrusion-height').value
106114
document.querySelector('#fill-extrusion-height-val').textContent = size + 'm'
107115
feature.properties['fill-extrusion-height'] = Number(size)
108116
// draw layer feature properties aren't getting updated by draw.set()
109117
draw.setFeatureProperty(this.featureIdValue, 'fill-extrusion-height', Number(size))
110118
// needs redraw to add extrusion
111-
redrawGeojson(true)
119+
renderGeoJSONLayer(this.layerIdValue, true)
112120
}
113121

114122
updateOpacity () {
115-
const feature = this.getFeature()
123+
const feature = this.getEditFeature()
116124
const opacity = document.querySelector('#opacity').value / 10
117125
document.querySelector('#opacity-val').textContent = opacity * 100 + '%'
118126
feature.properties['fill-opacity'] = opacity
119127
// draw layer feature properties aren't getting updated by draw.set()
120128
draw.setFeatureProperty(this.featureIdValue, 'fill-opacity', opacity)
121-
redrawGeojson(true)
129+
renderGeoJSONLayer(this.layerIdValue, true)
122130
}
123131

124132
updateStrokeColor () {
125-
const feature = this.getFeature()
133+
const feature = this.getEditFeature()
126134
const color = document.querySelector('#stroke-color').value
127135
feature.properties.stroke = color
128136
// draw layer feature properties aren't getting updated by draw.set()
129137
draw.setFeatureProperty(this.featureIdValue, 'stroke', color)
130-
redrawGeojson(true)
138+
renderGeoJSONLayer(this.layerIdValue, true)
131139
}
132140

133141
updateStrokeColorTransparent () {
134-
const feature = this.getFeature()
142+
const feature = this.getEditFeature()
135143
let color
136144
if (document.querySelector('#stroke-color-transparent').checked) {
137145
color = 'transparent'
@@ -142,19 +150,19 @@ export default class extends Controller {
142150
document.querySelector('#stroke-color').removeAttribute('disabled')
143151
}
144152
feature.properties.stroke = color
145-
redrawGeojson(true)
153+
renderGeoJSONLayer(this.layerIdValue, true)
146154
}
147155

148156
updateFillColor () {
149-
const feature = this.getFeature()
157+
const feature = this.getEditFeature()
150158
const color = document.querySelector('#fill-color').value
151159
if (feature.geometry.type === 'Polygon' || feature.geometry.type === 'MultiPolygon') { feature.properties.fill = color }
152160
if (feature.geometry.type === 'Point') { feature.properties['marker-color'] = color }
153-
redrawGeojson(true)
161+
renderGeoJSONLayer(this.layerIdValue, true)
154162
}
155163

156164
updateFillColorTransparent () {
157-
const feature = this.getFeature()
165+
const feature = this.getEditFeature()
158166
let color
159167
if (document.querySelector('#fill-color-transparent').checked) {
160168
color = 'transparent'
@@ -166,23 +174,23 @@ export default class extends Controller {
166174
}
167175
if (feature.geometry.type === 'Polygon' || feature.geometry.type === 'MultiPolygon') { feature.properties.fill = color }
168176
if (feature.geometry.type === 'Point') { feature.properties['marker-color'] = color }
169-
redrawGeojson(true)
177+
renderGeoJSONLayer(this.layerIdValue, true)
170178
}
171179

172180
updateShowKmMarkers () {
173-
const feature = this.getFeature()
181+
const feature = this.getEditFeature()
174182
if (document.querySelector('#show-km-markers').checked) {
175183
feature.properties['show-km-markers'] = true
176184
// feature.properties['stroke-image-url'] = "/icons/direction-arrow.png"
177185
} else {
178186
delete feature.properties['show-km-markers']
179187
delete feature.properties['stroke-image-url']
180188
}
181-
redrawGeojson(false)
189+
renderGeoJSONLayer(this.layerIdValue, true)
182190
}
183191

184192
updateMarkerSymbol () {
185-
const feature = this.getFeature()
193+
const feature = this.getEditFeature()
186194
let symbol = document.querySelector('#marker-symbol').value
187195
document.querySelector('#emoji').textContent = symbol
188196
// strip variation selector (emoji) U+FE0F to match icon file names
@@ -191,11 +199,11 @@ export default class extends Controller {
191199
// draw layer feature properties aren't getting updated by draw.set()
192200
draw.setFeatureProperty(this.featureIdValue, 'marker-symbol', symbol)
193201
functions.e('.feature-symbol', e => { e.innerHTML = featureIcon(feature) })
194-
redrawGeojson(true)
202+
renderGeoJSONLayer(this.layerIdValue, true)
195203
}
196204

197205
async updateMarkerImage () {
198-
const feature = this.getFeature()
206+
const feature = this.getEditFeature()
199207
const image = document.querySelector('#marker-image').files[0]
200208
const imageLocation = await confirmImageLocation(image)
201209
if (imageLocation) { feature.geometry.coordinates = imageLocation }
@@ -214,7 +222,7 @@ export default class extends Controller {
214222
functions.e('.feature-symbol', e => { e.innerHTML = featureIcon(feature) })
215223
functions.e('.feature-image', e => { e.innerHTML = featureImage(feature) })
216224

217-
redrawGeojson(true)
225+
renderGeoJSONLayer(this.layerIdValue, true)
218226
this.saveFeature()
219227
})
220228
}
@@ -265,19 +273,19 @@ export default class extends Controller {
265273
}
266274

267275
saveFeature () {
268-
const feature = this.getFeature()
276+
const feature = this.getEditFeature()
269277
status('Saving feature ' + feature.id)
270278
// send shallow copy of feature to avoid changes during send
271279
mapChannel.send_message('update_feature', { ...feature })
272280
}
273281

274282
addUndo() {
275-
const feature = this.getFeature()
283+
const feature = this.getEditFeature()
276284
addUndoState('Feature property update', feature)
277285
}
278286

279-
getFeature () {
287+
getEditFeature () {
280288
const id = this.featureIdValue
281-
return geojsonData.features.find(f => f.id === id)
289+
return getFeature(id)
282290
}
283291
}

0 commit comments

Comments
 (0)