Skip to content
Merged
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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ jobs:
# image: ghcr.io/mapforge-org/mapforge-ci-runner:latest
services:
mongodb:
image: mongo:8.2
image: mongo:8.3
ports:
- 27017:27017
# credentials are only needed when not running on ubuntu-latest VM
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.

## 2026-09

* Option to organize geojson features into multiple layers
* URL parameter `nocontrols=true` hides the map controls
* Search can find map local geojson results
* Color picker offers the site and map default colors
Expand Down
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Before running the application, make sure MongoDB and Redis are running. For exa
podman run -d --name mongo \
-v <local_dir>:/data/db:U,z \
-p 27017:27017 \
mongo:8.2
mongo:8.3

podman run -d --name redis \
-p 6379:6379 \
Expand Down
1 change: 0 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ group :test do
gem "rspec"
gem "rspec-rails"
gem "rspec-wait"
gem "selenium-webdriver"
gem "simplecov"
gem "database_cleaner-mongoid"
gem "mongoid-rspec"
Expand Down
9 changes: 0 additions & 9 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -625,14 +625,7 @@ GEM
simplecov (>= 0.22.0)
tty-which (~> 0.5.0)
virtus (~> 2.0)
rubyzip (3.7.0)
securerandom (0.4.1)
selenium-webdriver (4.49.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 4.0)
websocket (~> 1.0)
sexp_processor (4.17.5)
simplecov (1.2.0)
singleton (0.3.0)
Expand Down Expand Up @@ -694,7 +687,6 @@ GEM
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
websocket (1.2.11)
websocket-driver (0.8.2)
base64
websocket-extensions (>= 0.1.0)
Expand Down Expand Up @@ -784,7 +776,6 @@ DEPENDENCIES
rubocop-rubycw
rubocop-thread_safety
rubycritic
selenium-webdriver
simplecov
sprockets-rails
standard
Expand Down
14 changes: 14 additions & 0 deletions app/assets/stylesheets/buttons.css
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@
}
}

/* .form-control:focus paints a white background, which beats .btn-blue after a dropdown closes */
.btn-blue.form-control:focus:not(:hover, .show) {
background-color: var(--color-mid-steel-blue);
border-color: var(--color-mid-steel-blue);
}

/* same blue as .btn-download in the share modal */
.btn-layer-actions {
border-radius: 0.3rem;
Expand All @@ -132,3 +138,11 @@
--bs-btn-hover-bg: var(--color-mid-blue-slate);
--bs-btn-active-bg: var(--color-mid-blue-slate);
}

.btn-layer-menu {
--bs-btn-color: var(--color-mid-steel-blue);
--bs-btn-bg: transparent;
--bs-btn-border-color: var(--color-mid-steel-blue);
--bs-btn-hover-color: #fff;
--bs-btn-active-color: #fff;
}
36 changes: 26 additions & 10 deletions app/assets/stylesheets/modals.css
Original file line number Diff line number Diff line change
Expand Up @@ -472,8 +472,9 @@ body.map-mode-rw .ro-only {
font-weight: 600;
}

/* dim layer name and content when hidden, but not the action buttons/dropdown */
.layer-dimmed > h5 .link,
/* dim the whole hidden layer, but not the open menu */
.layer-dimmed > h5 > :not(.layer-actions-dropdown),
.layer-dimmed .layer-actions-dropdown > button,
.layer-dimmed > .layer-content {
opacity: 0.5;
}
Expand All @@ -495,6 +496,24 @@ body.map-mode-rw .ro-only {
text-overflow: ellipsis;
}

@media (hover: hover) {
.layer-item-header .layer-active:hover {
opacity: 1 !important;
}
}

.layer-name-input {
width: 12em;
max-width: 60%;
padding: 0 0.25rem;
font: inherit;
}

.layer-item-header .text-nowrap {
display: inline-flex;
align-items: center;
}

.layer-item-header .btn-layer-actions {
display: flex;
align-items: center;
Expand Down Expand Up @@ -558,6 +577,11 @@ body.map-mode-rw .ro-only {
min-height: 1.5rem;
}

/* the name of a geojson layer is a drop target, also when the layer is collapsed */
.layer-item-header.drop-target {
background-color: var(--bs-tertiary-bg);
}

.layer-feature-item.sortable-chosen {
background-color: var(--bs-tertiary-bg);
}
Expand Down Expand Up @@ -602,14 +626,6 @@ body.map-mode-rw .ro-only {

/* Media queries at end to ensure they override defaults */

@media (width > 574px) {
/* Keep layer action buttons on one line on desktop */
.layer-item-header .text-nowrap {
display: inline-flex !important;
align-items: center;
}
}

@media (width <= 640px), (height < 410px) {
.modal-pull-button {
top: 0.7rem;
Expand Down
13 changes: 10 additions & 3 deletions app/channels/map_channel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,20 @@ def update_feature(data)
def new_feature(data)
Yabeda.websocket.messages_received.increment({ action: "new_feature", channel: "MapChannel" })
map = get_map_rw!(data["map_id"])
@feature = map.layers.geojson.first.features.create!(feature_atts(data).merge({ id: data["id"] }))
layer = data["layer_id"] ? map.layers.geojson.find(data["layer_id"]) : map.layers.geojson.first
raise "Layer #{data["layer_id"]} not found on map #{data["map_id"]}" unless layer
@feature = layer.features.create!(feature_atts(data).merge({ id: data["id"] }))
associate_image(data["properties"])
end

# new_layer uses the layer + feature ids set by the client
def new_layer(data)
Yabeda.websocket.messages_received.increment({ action: "new_layer", channel: "MapChannel" })
map = get_map_rw!(data["map_id"])
# Layers have no position field, they sort by created_at
first_created_at = map.layers.first&.created_at if data["first"]
layer = map.layers.new(layer_atts(data).merge({ id: data["id"] }))
layer.created_at = first_created_at - 1 if first_created_at
Comment on lines +75 to +78
documents = import_documents(layer, data.dig("geojson", "features") || [])
Feature.collection.insert_many(documents) if documents.any?
layer.features_count = documents.size
Expand All @@ -91,8 +96,10 @@ def delete_layer(data)
Yabeda.websocket.messages_received.increment({ action: "delete_layer", channel: "MapChannel" })
map = get_map_rw!(data["map_id"])
layer = map.layers.find(data["id"])
# new_feature always writes into the first geojson layer
raise "Cannot delete the first geojson layer of map '#{map.public_id}'" if layer == map.layers.geojson.first
# new_feature needs a geojson layer to write into
if layer.type == "geojson" && map.layers.geojson.count == 1
raise "Cannot delete the last geojson layer of map '#{map.public_id}'"
end
layer.destroy
end

Expand Down
12 changes: 9 additions & 3 deletions app/javascript/channels/map_channel.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import consumer from 'channels/consumer'
import { initLayersModal } from 'maplibre/controls/shared'
import { createLayerInstance } from 'maplibre/layers/factory'
import { destroyFeature, initializeLayerSources, initializeLayerStyles, layers, loadLayerDefinitions, upsert } from 'maplibre/layers/layers'
import { destroyFeature, getLayer, initializeLayerSources, initializeLayerStyles, layers, loadLayerDefinitions, upsert } from 'maplibre/layers/layers'
import {
initializeMaplibreProperties,
loadedMapUpdatedAt,
Expand Down Expand Up @@ -143,6 +144,7 @@ export function initializeSocket () {
break
case 'update_feature':
upsert(data.feature, data.layer_id)
initLayersModal()
break
case 'fly_to': {
// maplibre reads 'pitch' in options, so an undefined key would give it NaN
Expand All @@ -154,6 +156,7 @@ export function initializeSocket () {
}
case 'delete_feature':
destroyFeature(data.feature.id)
initLayersModal()
break
case 'update_map':
window.gon.map_properties = data.map
Expand All @@ -174,14 +177,14 @@ export function initializeSocket () {
console.log('Layer updated on server, reloading layer styles', data.layer)
layers[index].update(data.layer)
layers[index].applyFeatureOrder(data.layer.feature_order)
initializeLayerStyles(data.layer.id)
initializeLayerStyles(data.layer.id).then(() => { initLayersModal() })
setLayerVisibility(layers[index].sourceId, data.layer.show !== false)
}
} else {
const newLayer = createLayerInstance(data.layer)
layers.push(newLayer)
initializeLayerSources(data.layer.id)
initializeLayerStyles(data.layer.id)
initializeLayerStyles(data.layer.id).then(() => { initLayersModal() })
Comment thread
digitaltom marked this conversation as resolved.
}
break
case 'delete_layer':
Expand All @@ -191,6 +194,7 @@ export function initializeSocket () {
layers.splice(delIndex, 1)
// trigger a full map redraw
setBackgroundMapLayer(mapProperties.base_map, true)
initLayersModal()
}
break
case 'mouse':
Expand Down Expand Up @@ -232,6 +236,8 @@ export function sendMessage (event, data) {
const payload = { ...data }
payload.map_id = window.gon.map_id
payload.uuid = connectionUUID
// callers add the feature locally first, so its layer is known
if (event === 'new_feature') { payload.layer_id ||= getLayer(data.id, 'geojson')?.id }
// dropping properties.id before sending to server
if (payload.properties && payload.properties.id) {
payload.properties = { ...payload.properties }
Expand Down
4 changes: 2 additions & 2 deletions app/javascript/controllers/map/context_menu_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { hideContextMenu } from 'maplibre/controls/context_menu'
import { handleDelete } from 'maplibre/edit'
import { updateElevation } from 'maplibre/routing/openrouteservice'
import { getFeatureTypeName } from 'maplibre/feature'
import { addFeature, applyFeatureUpdate, getFeature, renderLayers } from 'maplibre/layers/layers'
import { addFeature, applyFeatureUpdate, getFeature, getLayer, renderLayers } from 'maplibre/layers/layers'
import { addUndoState } from 'maplibre/undo'

export default class extends Controller {
Expand Down Expand Up @@ -49,7 +49,7 @@ export default class extends Controller {
properties: JSON.parse(JSON.stringify(feature.properties || {}))
}
addUndoState('Feature added', secondFeature)
addFeature(secondFeature)
addFeature(secondFeature, getLayer(feature.id, 'geojson')?.id)
sendMessage('new_feature', secondFeature)

status(window.__('Line cut into 2 segments'))
Expand Down
Loading
Loading