Skip to content
Draft
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 Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ GEM
bindex (0.8.1)
bootsnap (1.21.1)
msgpack (~> 1.2)
brakeman (8.0.1)
brakeman (8.0.2)
racc
bson (5.2.0)
builder (3.3.0)
Expand Down
1 change: 0 additions & 1 deletion app/controllers/maps_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ def show
gon.rails_env = Rails.env
gon.csrf_token = form_authenticity_token
gon.map_properties = @map_properties
gon.map_layers = @map.layers.map(&:to_summary_json)

case params["engine"]
when "deck"
Expand Down
10 changes: 5 additions & 5 deletions app/javascript/channels/map_channel.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import consumer from 'channels/consumer'
import {
upsert, destroyFeature, setBackgroundMapLayer, mapProperties,
initializeMaplibreProperties, map, layers, resetGeojsonLayers, loadLayers,
initializeMaplibreProperties, map, resetGeojsonLayers,
reloadMapProperties, removeGeoJSONSource, redrawGeojson
} from 'maplibre/map'
import { initializeLayers } from 'maplibre/layers/layers'
import { layers, initializeLayerStyles } from 'maplibre/layers/layers'
import { initLayersModal } from 'maplibre/controls/shared'


Expand Down Expand Up @@ -42,7 +42,7 @@ export function initializeSocket () {
reloadMapProperties().then(() => {
initializeMaplibreProperties()
resetGeojsonLayers()
loadLayers()
initializeLayerStyles()
setBackgroundMapLayer(mapProperties.base_map, false)
map.fire('load', { detail: { message: 'Map re-loaded by map_channel' } })
// status('Connection to server re-established')
Expand Down Expand Up @@ -102,11 +102,11 @@ export function initializeSocket () {
const { ['geojson']: _, ...layerDef } = layers[index]
if (JSON.stringify(layerDef) !== JSON.stringify(data.layer)) {
layers[index] = data.layer
initializeLayers(data.layer.id)
initializeLayerStyles(data.layer.id)
}
} else {
layers.push(data.layer)
initializeLayers(data.layer.id)
initializeLayerStyles(data.layer.id)
}
break
case 'delete_layer':
Expand Down
57 changes: 33 additions & 24 deletions app/javascript/controllers/feature/edit_controller.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,43 @@
import { Controller } from '@hotwired/stimulus'
import { mapChannel } from 'channels/map_channel'
import { geojsonData, redrawGeojson } from 'maplibre/map'
import { redrawGeojson } from 'maplibre/map'
import { featureIcon, featureImage, uploadImageToFeature, confirmImageLocation } from 'maplibre/feature'
import { handleDelete, draw } from 'maplibre/edit'
import { featureColor, featureOutlineColor } from 'maplibre/styles'
import { status } from 'helpers/status'
import * as functions from 'helpers/functions'
import * as dom from 'helpers/dom'
import { addUndoState } from 'maplibre/undo'
import { getFeature, getLayer } from 'maplibre/layers/layers'
import { renderGeoJSONLayer } from 'maplibre/layers/geojson'

export default class extends Controller {
// https://stimulus.hotwired.dev/reference/values
static values = {
featureId: String
featureId: String,
layerId: String
}

// emoji picker
picker = null

featureIdValueChanged(value) {
if (value) {
this.layerIdValue = getLayer(value).id
}
}

delete_feature (e) {
if (dom.isInputElement(e.target)) return // Don't trigger if typing in input

const feature = this.getFeature()
const feature = this.getEditFeature()
if (confirm(`Really delete this ${feature.geometry.type}?`)) {
handleDelete({ features: [feature] })
}
}

update_feature_raw () {
const feature = this.getFeature()
const feature = this.getEditFeature()
document.querySelector('#feature-edit-raw .error').innerHTML = ''
try {
feature.properties = JSON.parse(document.querySelector('#feature-edit-raw textarea').value)
Expand All @@ -42,15 +51,15 @@ export default class extends Controller {
}

updateTitle () {
const feature = this.getFeature()
const feature = this.getEditFeature()
const title = document.querySelector('#feature-title-input input').value
feature.properties.title = title
document.querySelector('#feature-title').textContent = title
functions.debounce(() => { this.saveFeature() }, 'title')
}

updateLabel () {
const feature = this.getFeature()
const feature = this.getEditFeature()
const label = document.querySelector('#feature-label input').value
feature.properties.label = label
redrawGeojson(false)
Expand All @@ -59,7 +68,7 @@ export default class extends Controller {

// called as preview on slider change
updatePointSize () {
const feature = this.getFeature()
const feature = this.getEditFeature()
const size = document.querySelector('#point-size').value
document.querySelector('#point-size-val').textContent = size
feature.properties['marker-size'] = size
Expand All @@ -69,7 +78,7 @@ export default class extends Controller {
}

updatePointScaling() {
const feature = this.getFeature()
const feature = this.getEditFeature()
const val = document.querySelector('#point-scaling').checked
feature.properties['marker-scaling'] = val
// draw layer feature properties aren't getting updated by draw.set()
Expand All @@ -79,18 +88,18 @@ export default class extends Controller {

// called as preview on slider change
updateLineWidth () {
const feature = this.getFeature()
const feature = this.getEditFeature()
const size = document.querySelector('#line-width').value
document.querySelector('#line-width-val').textContent = size
feature.properties['stroke-width'] = size
// draw layer feature properties aren't getting updated by draw.set()
draw.setFeatureProperty(this.featureIdValue, 'stroke-width', size)
redrawGeojson(true)
renderGeoJSONLayer(this.layerIdValue, false)
}

// called as preview on slider change
updateOutLineWidth () {
const feature = this.getFeature()
const feature = this.getEditFeature()
const size = document.querySelector('#outline-width').value
document.querySelector('#outline-width-val').textContent = size
feature.properties['stroke-width'] = size
Expand All @@ -101,7 +110,7 @@ export default class extends Controller {

// called as preview on slider change
updateFillExtrusionHeight () {
const feature = this.getFeature()
const feature = this.getEditFeature()
const size = document.querySelector('#fill-extrusion-height').value
document.querySelector('#fill-extrusion-height-val').textContent = size + 'm'
feature.properties['fill-extrusion-height'] = Number(size)
Expand All @@ -112,7 +121,7 @@ export default class extends Controller {
}

updateOpacity () {
const feature = this.getFeature()
const feature = this.getEditFeature()
const opacity = document.querySelector('#opacity').value / 10
document.querySelector('#opacity-val').textContent = opacity * 100 + '%'
feature.properties['fill-opacity'] = opacity
Expand All @@ -122,7 +131,7 @@ export default class extends Controller {
}

updateStrokeColor () {
const feature = this.getFeature()
const feature = this.getEditFeature()
const color = document.querySelector('#stroke-color').value
feature.properties.stroke = color
// draw layer feature properties aren't getting updated by draw.set()
Expand All @@ -131,7 +140,7 @@ export default class extends Controller {
}

updateStrokeColorTransparent () {
const feature = this.getFeature()
const feature = this.getEditFeature()
let color
if (document.querySelector('#stroke-color-transparent').checked) {
color = 'transparent'
Expand All @@ -146,15 +155,15 @@ export default class extends Controller {
}

updateFillColor () {
const feature = this.getFeature()
const feature = this.getEditFeature()
const color = document.querySelector('#fill-color').value
if (feature.geometry.type === 'Polygon' || feature.geometry.type === 'MultiPolygon') { feature.properties.fill = color }
if (feature.geometry.type === 'Point') { feature.properties['marker-color'] = color }
redrawGeojson(true)
}

updateFillColorTransparent () {
const feature = this.getFeature()
const feature = this.getEditFeature()
let color
if (document.querySelector('#fill-color-transparent').checked) {
color = 'transparent'
Expand All @@ -170,7 +179,7 @@ export default class extends Controller {
}

updateShowKmMarkers () {
const feature = this.getFeature()
const feature = this.getEditFeature()
if (document.querySelector('#show-km-markers').checked) {
feature.properties['show-km-markers'] = true
// feature.properties['stroke-image-url'] = "/icons/direction-arrow.png"
Expand All @@ -182,7 +191,7 @@ export default class extends Controller {
}

updateMarkerSymbol () {
const feature = this.getFeature()
const feature = this.getEditFeature()
let symbol = document.querySelector('#marker-symbol').value
document.querySelector('#emoji').textContent = symbol
// strip variation selector (emoji) U+FE0F to match icon file names
Expand All @@ -195,7 +204,7 @@ export default class extends Controller {
}

async updateMarkerImage () {
const feature = this.getFeature()
const feature = this.getEditFeature()
const image = document.querySelector('#marker-image').files[0]
const imageLocation = await confirmImageLocation(image)
if (imageLocation) { feature.geometry.coordinates = imageLocation }
Expand Down Expand Up @@ -265,19 +274,19 @@ export default class extends Controller {
}

saveFeature () {
const feature = this.getFeature()
const feature = this.getEditFeature()
status('Saving feature ' + feature.id)
// send shallow copy of feature to avoid changes during send
mapChannel.send_message('update_feature', { ...feature })
}

addUndo() {
const feature = this.getFeature()
const feature = this.getEditFeature()
addUndoState('Feature property update', feature)
}

getFeature () {
getEditFeature () {
const id = this.featureIdValue
return geojsonData.features.find(f => f.id === id)
return getFeature(id)
}
Comment on lines +288 to 291
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getEditFeature() calls getFeature(id) but this file does not import getFeature, so the controller will crash when invoked. Import getFeature from maplibre/layers/layers (and consider handling null if the feature cannot be found).

Copilot uses AI. Check for mistakes.
}
26 changes: 13 additions & 13 deletions app/javascript/controllers/feature/modal_controller.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Controller } from '@hotwired/stimulus'
import { mapChannel } from 'channels/map_channel'
import { geojsonData } from 'maplibre/map'
import { defaultLineWidth, featureColor, featureOutlineColor } from 'maplibre/styles'
import { AnimateLineAnimation, AnimatePolygonAnimation, animateViewFromProperties } from 'maplibre/animations'
import { status } from 'helpers/status'
import { showFeatureDetails, highlightedFeatureId } from 'maplibre/feature'
import * as functions from 'helpers/functions'
import * as dom from 'helpers/dom'
import { draw, select, unselect } from 'maplibre/edit'
import { getFeature } from 'maplibre/layers/layers'

let easyMDE

Expand All @@ -33,12 +33,12 @@ export default class extends Controller {
this.show_feature_edit_ui()

// add feature to draw
const feature = this.getFeature()
const feature = this.getSelectedFeature()
draw.add(feature)
select(feature)
} else {
// repeated click on the current edit mode returns to feature description
showFeatureDetails(this.getFeature())
showFeatureDetails(this.getSelectedFeature())
unselect()
}
document.querySelector('#feature-edit-raw .error').innerHTML = ''
Expand All @@ -49,7 +49,7 @@ export default class extends Controller {
if (this.element.classList.contains('modal-pull-down')) {
this.pullUpModal(this.element)
}
const feature = this.getFeature()
const feature = this.getSelectedFeature()
dom.showElements(['#feature-edit-ui', '#button-add-label', '#button-add-desc'])
dom.hideElements(['#feature-edit-raw', '#feature-label', '#feature-desc'])
functions.e('em-emoji-picker', e => { e.remove() })
Expand Down Expand Up @@ -134,15 +134,15 @@ export default class extends Controller {
if (this.element.classList.contains('modal-pull-down')) {
this.pullUpModal(this.element)
}
const feature = this.getFeature()
const feature = this.getSelectedFeature()
dom.hideElements(['#feature-edit-ui'])
dom.showElements(['#feature-edit-raw'])
document.querySelector('#feature-edit-raw textarea')
.value = JSON.stringify(feature.properties, undefined, 2)
}

show_add_label () {
document.querySelector('#feature-label input').value = this.getFeature().properties.label || null
document.querySelector('#feature-label input').value = this.getSelectedFeature().properties.label || null
dom.hideElements(['#button-add-label'])
dom.showElements(['#feature-label'])
}
Expand All @@ -153,7 +153,7 @@ export default class extends Controller {
// https://github.com/Ionaru/easy-markdown-editor
await import('easymde') // import EasyMDE UMD bundle
if (easyMDE) { easyMDE.toTextArea() }
document.querySelector('#feature-desc-input').value = this.getFeature().properties.desc || ''
document.querySelector('#feature-desc-input').value = this.getSelectedFeature().properties.desc || ''
easyMDE = new window.EasyMDE({
element: document.getElementById('feature-desc-input'),
placeholder: 'Add a description text',
Expand All @@ -168,7 +168,7 @@ export default class extends Controller {
}

updateDesc () {
const feature = this.getFeature()
const feature = this.getSelectedFeature()
try {
if (easyMDE && feature.properties.desc !== easyMDE.value()) {
feature.properties.desc = easyMDE.value()
Expand All @@ -181,7 +181,7 @@ export default class extends Controller {
}

saveFeature () {
const feature = this.getFeature()
const feature = this.getSelectedFeature()
status('Saving feature ' + feature.id)
// send shallow copy of feature to avoid changes during send
mapChannel.send_message('update_feature', { ...feature })
Expand Down Expand Up @@ -214,16 +214,16 @@ export default class extends Controller {
modal.style.removeProperty('height')
}

getFeature () {
getSelectedFeature () {
const id = this.featureIdValue
return geojsonData.features.find(f => f.id === id)
return getFeature(id)
}
Comment on lines +217 to 220
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getSelectedFeature() calls getFeature(id) but getFeature is not imported in this controller, which will raise a ReferenceError at runtime. Import getFeature from maplibre/layers/layers (and consider handling a null return to avoid passing undefined into draw/select).

Copilot uses AI. Check for mistakes.

async copy(event) {
if (functions.isFormFieldFocused()) { return }
if (!highlightedFeatureId) { return }

const feature = this.getFeature()
const feature = this.getSelectedFeature()
if (feature) {
await navigator.clipboard.writeText(JSON.stringify(feature))
event.preventDefault()
Expand All @@ -234,7 +234,7 @@ export default class extends Controller {
}

animate () {
const feature = this.getFeature()
const feature = this.getSelectedFeature()
console.log('Animating ' + feature.id)
if (feature.geometry.type === 'LineString') {
new AnimateLineAnimation().run(feature)
Expand Down
Loading
Loading