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 app/channels/map_channel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def map_atts(data)
end

def layer_atts(data)
data.slice("id", "type", "name", "query", "heatmap", "cluster")
data.slice("id", "type", "name", "query", "heatmap", "cluster", "show")
end

# load map with write access
Expand Down
8 changes: 6 additions & 2 deletions app/javascript/channels/map_channel.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import consumer from 'channels/consumer'
import {
upsert, destroyFeature, setBackgroundMapLayer, mapProperties,
initializeMaplibreProperties, map,
initializeMaplibreProperties, map, setLayerVisibility,
reloadMapProperties } from 'maplibre/map'
import { layers, initializeLayerStyles, loadLayerDefinitions } from 'maplibre/layers/layers'

Expand Down Expand Up @@ -99,9 +99,13 @@ export function initializeSocket () {
// Remove geojson key before comparison
const { ['geojson']: _, ...layerDef } = layers[index]
if (JSON.stringify(layerDef) !== JSON.stringify(data.layer)) {
// preserve geojson data when updating layer definition
const geojson = layers[index].geojson
layers[index] = data.layer
if (geojson) { layers[index].geojson = geojson }
console.log('Layer updated on server, reloading layer styles', data.layer)
initializeLayerStyles(data.layer.id)
if (data.layer.show !== false) { initializeLayerStyles(data.layer.id) }
setLayerVisibility(data.layer.type + '-source-' + data.layer.id, data.layer.show !== false)
}
} else {
layers.push(data.layer)
Expand Down
37 changes: 35 additions & 2 deletions app/javascript/controllers/map/layers_controller.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Controller } from '@hotwired/stimulus'
import { mapChannel } from 'channels/map_channel'
import { map, upsert, mapProperties, removeGeoJSONSource } from 'maplibre/map'
import { map, upsert, mapProperties, removeGeoJSONSource, setLayerVisibility } from 'maplibre/map'
import { initLayersModal } from 'maplibre/controls/shared'
import { uploadImageToFeature, confirmImageLocation } from 'maplibre/feature'
import { status } from 'helpers/status'
Expand Down Expand Up @@ -200,6 +200,39 @@ export default class extends Controller {
list.classList.toggle('hidden')
}

toggleLayerVisibility (event) {
event.preventDefault()
dom.closeTooltips()
const layerElement = event.target.closest('.layer-item')
const layerId = layerElement.getAttribute('data-layer-id')
const layer = layers.find(l => l.id === layerId)
const wasVisible = layer.show !== false
layer.show = !wasVisible

setLayerVisibility(layer.type + '-source-' + layerId, layer.show)

// update UI
const icon = layerElement.querySelector('button.layer-visibility i')
if (layer.show) {
icon.classList.replace('bi-eye-slash', 'bi-eye')
layerElement.classList.remove('opacity-50')
} else {
icon.classList.replace('bi-eye', 'bi-eye-slash')
layerElement.classList.add('opacity-50')
}

// when showing: initialize styles (and load data for overpass/wikipedia if needed)
if (layer.show) {
initializeLayerStyles(layerId)
}

// sync to server only in rw mode
if (window.gon.map_mode === "rw") {
const { geojson: _geojson, ...sendLayer } = layer
mapChannel.send_message('update_layer', sendLayer)
}
}

createWikipediaLayer() {
this.createLayer('wikipedia', 'Wikipedia', '')
}
Expand All @@ -221,7 +254,7 @@ export default class extends Controller {
createLayer(type, name, query) {
let layerId = functions.featureId()
// must match server attribute order, for proper comparison in map_channel
let layer = { "id": layerId, "type": type, "name": name, "heatmap": false, "cluster": true}
let layer = { "id": layerId, "type": type, "name": name, "heatmap": false, "cluster": true, "show": true}
if (type == 'overpass') {
layer["query"] = query
// TODO: move cluster + heatmap to layer checkboxes
Expand Down
10 changes: 5 additions & 5 deletions app/javascript/helpers/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ export function initTooltips (root = document) {
}
}

export function closeTooltips () {
functions.e('[data-toggle="tooltip"]', e => {
const tooltip = bootstrap.Tooltip.getInstance(e);
if (tooltip) tooltip.dispose()
})
export function closeTooltips (root = document) {
root.querySelectorAll('[data-toggle="tooltip"]').forEach(e => {
const tooltip = bootstrap.Tooltip.getInstance(e)
if (tooltip) tooltip.dispose()
})
}

export function scrollToId(elementId) {
Expand Down
8 changes: 8 additions & 0 deletions app/javascript/maplibre/controls/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export function initSettingsModal () {
// create the list of layers + features
export function initLayersModal () {
functions.e('#layers', e => {
dom.closeTooltips(e)
e.innerHTML = ''
const template = document.querySelector('#layer-item-template')
layers.forEach(layer => {
Expand All @@ -200,6 +201,13 @@ export function initLayersModal () {
featureCount.textContent = '(' + features.length + ')'
head.parentNode.insertBefore(featureCount, head.nextSibling)
e.appendChild(layerElement)
// visibility toggle for all layers
const visBtn = layerElement.querySelector('button.layer-visibility')
visBtn.classList.remove('hidden')
if (layer.show === false) {
visBtn.querySelector('i').classList.replace('bi-eye', 'bi-eye-slash')
layerElement.classList.add('opacity-50')
}
if (layer.type !== 'geojson') {
layerElement.querySelector('button.layer-refresh').classList.remove('hidden')
layerElement.querySelector('button.layer-delete').classList.remove('hidden')
Expand Down
5 changes: 3 additions & 2 deletions app/javascript/maplibre/layers/geojson.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { lineString } from "@turf/helpers"
import { length } from "@turf/length"
import { draw, select } from 'maplibre/edit'
import { getFeature, getFeatures, layers } from 'maplibre/layers/layers'
import { map, mapProperties } from 'maplibre/map'
import { map, mapProperties, removeStyleLayers } from 'maplibre/map'
import { defaultLineWidth, featureColor, initializeClusterStyles, initializeViewStyles, labelFont, setSource, styles } from 'maplibre/styles/styles'

export function initializeGeoJSONLayers(id = null) {
// console.log('Initializing geojson layers')
let initLayers = layers.filter(l => l.type === 'geojson')
let initLayers = layers.filter(l => l.type === 'geojson' && l.show !== false)
if (id) { initLayers = initLayers.filter(l => l.id === id) }

initLayers.forEach((layer) => {
Expand Down Expand Up @@ -188,6 +188,7 @@ export function kmMarkerStyles (_id) {
}

export function initializeKmMarkerStyles(id) {
removeStyleLayers('km-marker-source-' + id)
kmMarkerStyles(id).forEach(style => {
style = setSource (style, 'km-marker-source-' + id)
map.addLayer(style)
Expand Down
3 changes: 2 additions & 1 deletion app/javascript/maplibre/layers/layers.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export async function initializeLayerStyles(id = null) {
// triggered by layer reload in the layers modal
export function loadLayerData(id) {
let layer = layers.find(l => l.id === id)
if (layer.show === false) { return Promise.resolve() }
// geojson layers are loaded in loadLayerDefinitions
if (layer.type === 'wikipedia') {
return loadWikipediaLayer(layer.id)
Expand All @@ -76,7 +77,7 @@ export function loadLayerData(id) {

// triggered by layer reload in the UI
export async function loadAllLayerData() {
await Promise.all(layers.map((layer) => { return loadLayerData(layer.id) }))
await Promise.all(layers.filter(l => l.show !== false).map((layer) => { return loadLayerData(layer.id) }))
}

export function getFeature(id, type = null) {
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/maplibre/layers/overpass.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { map } from 'maplibre/map'
import { initializeClusterStyles, initializeViewStyles } from 'maplibre/styles/styles'

export function initializeOverpassLayers(id = null) {
let initLayers = layers.filter(l => l.type === 'overpass')
let initLayers = layers.filter(l => l.type === 'overpass' && l.show !== false)
if (id) { initLayers = initLayers.filter(l => l.id === id) }
return initLayers.map((layer) => {
const clustered = !layer.query.includes("heatmap=true") &&
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/maplibre/layers/wikipedia.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { initializeClusterStyles, initializeViewStyles } from 'maplibre/styles/s

export function initializeWikipediaLayers(id = null) {
// console.log('Initializing Wikipedia layers')
let initLayers = layers.filter(l => l.type === 'wikipedia')
let initLayers = layers.filter(l => l.type === 'wikipedia' && l.show !== false)
if (id) { initLayers = initLayers.filter(l => l.id === id) }

return initLayers.map((layer) => {
Expand Down
15 changes: 15 additions & 0 deletions app/javascript/maplibre/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,21 @@ export function removeStyleLayers(sourceName) {
}
}

export function setLayerVisibility(sourceName, visible) {
if (map.getStyle && map.getStyle().layers) {
const sources = [sourceName]
// geojson layers have a companion km-marker source
if (sourceName.startsWith('geojson-source-')) {
sources.push(sourceName.replace('geojson-source-', 'km-marker-source-'))
}
map.getStyle().layers
.filter(l => sources.includes(l.source))
.forEach(l => {
if (map.getLayer(l.id)) map.setLayoutProperty(l.id, 'visibility', visible ? 'visible' : 'none')
})
}
}

export function removeGeoJSONSource(sourceName) {
removeStyleLayers(sourceName)
if (map.getSource(sourceName)) {
Expand Down
5 changes: 3 additions & 2 deletions app/models/layer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ class Layer
field :query
field :heatmap, type: Boolean
field :cluster, type: Boolean
field :show, type: Boolean, default: true
field :features_count, type: Integer, default: 0

after_save :broadcast_update, if: -> { map.present? }
after_destroy :broadcast_destroy, if: -> { map.present? }

def to_summary_json
json = { id: id, type: type, name: name, heatmap: !!heatmap, cluster: !!cluster }
json = { id: id, type: type, name: name, heatmap: !!heatmap, cluster: !!cluster, show: show != false }
json[:query] = query if type == "overpass"
json
end
Expand All @@ -44,7 +45,7 @@ def clone_with_features
end

def broadcast_update
if (%w[name query heatmap cluster] & previous_changes.keys).any?
if (%w[name query heatmap cluster show] & previous_changes.keys).any?
# broadcast to private + public channel
[ map.private_id, map.public_id ].each do |map_id|
ActionCable.server.broadcast("map_channel_#{map_id}",
Expand Down
6 changes: 4 additions & 2 deletions app/views/maps/modals/_layers.haml
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,14 @@
%i.bi.bi-caret-right-fill
%span.mapforge-font.layer-name.me-2 Layer name
%span.text-nowrap
%button.hidden.ms-2.btn.btn-secondary.btn-layer-actions.layer-refresh{ data: { action: "click->map--layers#refreshLayer", "toggle": 'tooltip', 'bs-custom-class': 'maplibregl-ctrl-tooltip' }, title: "Refresh layer for current view" }
%button.hidden.ms-2.btn.btn-secondary.btn-layer-actions.layer-visibility{ data: { action: "click->map--layers#toggleLayerVisibility", "toggle": 'tooltip', 'bs-custom-class': 'maplibregl-ctrl-tooltip' }, title: "Toggle visibility" }
%i.bi.bi-eye
%button.hidden.btn.btn-secondary.btn-layer-actions.layer-refresh{ data: { action: "click->map--layers#refreshLayer", "toggle": 'tooltip', 'bs-custom-class': 'maplibregl-ctrl-tooltip' }, title: "Refresh layer for current view" }
%i.bi.bi-arrow-clockwise.reload-icon
%button.hidden.btn.btn-secondary.btn-layer-actions.layer-edit{ data: { action: "click->map--layers#toggleEdit", "toggle": 'tooltip', 'bs-custom-class': 'maplibregl-ctrl-tooltip' }, title: "Edit query" }
%i.bi.bi-pencil-square
%button.hidden.btn.btn-orange.btn-layer-actions.layer-delete{ data: { action: "click->map--layers#deleteLayer", "toggle": 'tooltip', 'bs-custom-class': 'maplibregl-ctrl-tooltip' }, title: "Delete Layer" }
%i.bi.bi-trash
%i.bi.bi-trash

.layer-content.hidden

Expand Down
43 changes: 43 additions & 0 deletions spec/features/map_layers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,49 @@
end
end

context "layer visibility" do
before do
find(".maplibregl-ctrl-layers").click
end

it "toggles layer visibility" do
find("button.layer-visibility").click
expect(page).to have_css(".layer-item.opacity-50")
expect(page).to have_css("button.layer-visibility i.bi-eye-slash")
wait_for { map.layers.first.reload.show }.to be false
end

it "shows hidden layer" do
map.layers.first.update!(show: false)
visit map.private_map_path
expect_map_loaded
find(".maplibregl-ctrl-layers").click
expect(page).to have_css("button.layer-visibility i.bi-eye-slash")
find("button.layer-visibility").click
expect(page).to have_css("button.layer-visibility i.bi-eye")
expect(page).not_to have_css(".layer-item.opacity-50")
wait_for { map.layers.first.reload.show }.to be true
end
end

context "layer visibility in readonly mode" do
it "does not sync visibility change to server" do
find(".maplibregl-ctrl-layers").click
find("button.layer-visibility").click
wait_for { map.layers.first.reload.show }.to be false

visit map.public_map_path
expect_map_loaded
find(".maplibregl-ctrl-layers").click
expect(page).to have_css("button.layer-visibility i.bi-eye-slash")
# toggle in ro mode
find("button.layer-visibility").click
expect(page).to have_css("button.layer-visibility i.bi-eye")
# server state unchanged
expect(map.layers.first.reload.show).to be false
end
end

context "add new layer" do
before do
find(".maplibregl-ctrl-layers").click
Expand Down
41 changes: 41 additions & 0 deletions spec/models/layer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,45 @@
expect(clone.id).not_to eq subject.id
end
end

describe "#show" do
it "defaults to true" do
layer = create(:layer)
expect(layer.show).to be true
end

it "can be set to false" do
layer = create(:layer, show: false)
expect(layer.show).to be false
end
end

describe "#to_summary_json" do
it "includes show: true by default" do
layer = create(:layer)
expect(layer.to_summary_json[:show]).to be true
end

it "includes show: false when hidden" do
layer = create(:layer, show: false)
expect(layer.to_summary_json[:show]).to be false
end
end

describe "#broadcast_update" do
let!(:map) { create(:map) }
let!(:layer) { map.layers.first }

it "broadcasts when show changes" do
allow(ActionCable.server).to receive(:broadcast)
layer.update!(show: false)
expect(ActionCable.server).to have_received(:broadcast).twice
end

it "does not broadcast when show is unchanged" do
allow(ActionCable.server).to receive(:broadcast)
layer.update!(features_count: 5)
expect(ActionCable.server).not_to have_received(:broadcast)
end
end
end
Loading