diff --git a/Gemfile.lock b/Gemfile.lock index 02aededb3..6b34d1019 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -91,8 +91,8 @@ GEM securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) + addressable (2.8.8) + public_suffix (>= 2.0.2, < 8.0) amazing_print (2.0.0) anyway_config (2.7.2) ruby-next-core (~> 1.0) @@ -164,23 +164,23 @@ GEM dry-configurable (1.3.0) dry-core (~> 1.1) zeitwerk (~> 2.6) - dry-core (1.1.0) + dry-core (1.2.0) concurrent-ruby (~> 1.0) logger zeitwerk (~> 2.6) - dry-inflector (1.2.0) + dry-inflector (1.3.1) dry-initializer (3.2.0) dry-logic (1.6.0) bigdecimal concurrent-ruby (~> 1.0) dry-core (~> 1.1) zeitwerk (~> 2.6) - dry-schema (1.14.1) + dry-schema (1.15.0) concurrent-ruby (~> 1.0) dry-configurable (~> 1.0, >= 1.0.1) dry-core (~> 1.1) dry-initializer (~> 3.2) - dry-logic (~> 1.5) + dry-logic (~> 1.6) dry-types (~> 1.8) zeitwerk (~> 2.6) dry-types (1.9.0) @@ -210,14 +210,14 @@ GEM webrick (~> 1.7) websocket-driver (~> 0.7) ffi (1.17.3-x86_64-linux-gnu) - flay (2.13.3) + flay (2.14.2) erubi (~> 1.10) - path_expander (~> 1.0) - ruby_parser (~> 3.0) + path_expander (~> 2.0) + prism (~> 1.7) sexp_processor (~> 4.0) - flog (4.8.0) - path_expander (~> 1.0) - ruby_parser (~> 3.1, > 3.1.0) + flog (4.9.4) + path_expander (~> 2.0) + prism (~> 1.7) sexp_processor (~> 4.8) globalid (1.3.0) activesupport (>= 6.1) @@ -354,7 +354,7 @@ GEM parser (3.3.10.1) ast (~> 2.4.1) racc - path_expander (1.1.3) + path_expander (2.0.1) pp (0.6.3) prettyprint prettyprint (0.2.0) @@ -364,7 +364,7 @@ GEM psych (5.3.1) date stringio - public_suffix (6.0.2) + public_suffix (7.0.2) puma (7.2.0) nio4r (~> 2.0) puppeteer-ruby (0.45.6) @@ -516,11 +516,12 @@ GEM ruby_parser (3.22.0) racc (~> 1.5) sexp_processor (~> 4.16) - rubycritic (4.11.0) + rubycritic (4.12.0) flay (~> 2.13) flog (~> 4.7) launchy (>= 2.5.2) parser (>= 3.3.0.5) + prism (>= 1.6.0) rainbow (~> 3.1.1) reek (~> 6.5.0, < 7.0) rexml diff --git a/app/javascript/controllers/feature/edit_controller.js b/app/javascript/controllers/feature/edit_controller.js index 55d190e72..681809a0f 100644 --- a/app/javascript/controllers/feature/edit_controller.js +++ b/app/javascript/controllers/feature/edit_controller.js @@ -1,6 +1,6 @@ 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' @@ -21,14 +21,14 @@ export default class extends Controller { 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) @@ -42,7 +42,7 @@ 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 @@ -50,7 +50,7 @@ export default class extends Controller { } updateLabel () { - const feature = this.getFeature() + const feature = this.getEditFeature() const label = document.querySelector('#feature-label input').value feature.properties.label = label redrawGeojson(false) @@ -59,7 +59,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 @@ -69,7 +69,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() @@ -79,7 +79,7 @@ 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 @@ -90,7 +90,7 @@ export default class extends Controller { // 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 @@ -101,7 +101,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) @@ -112,7 +112,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 @@ -122,7 +122,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() @@ -131,7 +131,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' @@ -146,7 +146,7 @@ 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 } @@ -154,7 +154,7 @@ export default class extends Controller { } updateFillColorTransparent () { - const feature = this.getFeature() + const feature = this.getEditFeature() let color if (document.querySelector('#fill-color-transparent').checked) { color = 'transparent' @@ -170,7 +170,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" @@ -182,7 +182,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 @@ -195,7 +195,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 } @@ -265,19 +265,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) } } diff --git a/app/javascript/controllers/feature/modal_controller.js b/app/javascript/controllers/feature/modal_controller.js index a4c460444..2129170b8 100644 --- a/app/javascript/controllers/feature/modal_controller.js +++ b/app/javascript/controllers/feature/modal_controller.js @@ -1,6 +1,5 @@ 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' @@ -8,6 +7,7 @@ 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 @@ -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 = '' @@ -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() }) @@ -134,7 +134,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.hideElements(['#feature-edit-ui']) dom.showElements(['#feature-edit-raw']) document.querySelector('#feature-edit-raw textarea') @@ -142,7 +142,7 @@ export default class extends Controller { } 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']) } @@ -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', @@ -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() @@ -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 }) @@ -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) } 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() @@ -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) diff --git a/app/javascript/maplibre/animations.js b/app/javascript/maplibre/animations.js index a5857064a..57c344898 100644 --- a/app/javascript/maplibre/animations.js +++ b/app/javascript/maplibre/animations.js @@ -153,7 +153,7 @@ export function animateViewFromProperties () { }) } -export function flyToFeature(feature, source='geojson-source') { +export function flyToFeature(feature, source) { // Calculate the centroid const center = centroid(feature) console.log('Fly to: ' + feature.id + ' ' + center.geometry.coordinates) diff --git a/app/javascript/maplibre/controls/shared.js b/app/javascript/maplibre/controls/shared.js index 17d88a684..f79873d7b 100644 --- a/app/javascript/maplibre/controls/shared.js +++ b/app/javascript/maplibre/controls/shared.js @@ -204,7 +204,7 @@ export function initLayersModal () { listItem.classList.add('flex-center') listItem.classList.add('align-items-center') listItem.setAttribute('data-feature-id', feature.id) - const source = layer.type === 'geojson' ? 'geojson-source' : layer.type + '-source-' + layer.id + const source = layer.type + '-source-' + layer.id listItem.setAttribute('data-feature-source', source) listItem.setAttribute('data-controller', 'map--layers') listItem.setAttribute('data-action', 'click->map--layers#flyToLayerElement') diff --git a/app/javascript/maplibre/edit.js b/app/javascript/maplibre/edit.js index 22589315e..efbf2c7a4 100644 --- a/app/javascript/maplibre/edit.js +++ b/app/javascript/maplibre/edit.js @@ -1,4 +1,4 @@ -import { map, geojsonData, destroyFeature, redrawGeojson, addFeature, layers, mapProperties } from 'maplibre/map' +import { map, destroyFeature, redrawGeojson, addFeature, layers, mapProperties } from 'maplibre/map' import { editStyles } from 'maplibre/edit_styles' import { highlightFeature } from 'maplibre/feature' import { getRouteUpdate, getRouteElevation } from 'maplibre/routing/openrouteservice' @@ -7,6 +7,7 @@ import { mapChannel } from 'channels/map_channel' import { resetControls, initializeDefaultControls } from 'maplibre/controls/shared' import { initializeEditControls, disableEditControls, enableEditControls } from 'maplibre/controls/edit' import { status } from 'helpers/status' +import { hasFeatures, getFeature } from 'maplibre/layers/layers' import { undo, redo, addUndoState } from 'maplibre/undo' import * as functions from 'helpers/functions' import equal from 'fast-deep-equal' // https://github.com/epoberezkin/fast-deep-equal @@ -74,14 +75,14 @@ export async function initializeEditMode () { // Show map settings modal on untouched map map.once('load', function (_e) { - if (!mapProperties.name && !geojsonData?.features?.length && !layers?.filter(l => l.type !== 'geojson').length) { + if (!mapProperties.name && !hasFeatures('geojson') && !layers?.filter(l => l.type !== 'geojson').length) { functions.e('.maplibregl-ctrl-map', e => { e.click() }) } }) map.on('geojson.load', function (_e) { const urlFeatureId = new URLSearchParams(window.location.search).get('f') - const feature = geojsonData.features.find(f => f.id === urlFeatureId) + const feature = getFeature(urlFeatureId, 'geojson') if (feature) { map.fire('draw.selectionchange', {features: [feature]}) } }) @@ -275,7 +276,7 @@ function handleCreate (e) { async function handleUpdate (e) { let feature = e.features[0] // Assuming one feature is updated at a time - const geojsonFeature = geojsonData.features.find(f => f.id === feature.id) + const geojsonFeature = getFeature(feature.id) // mapbox-gl-draw-waypoint sends empty update when dragging on selected feature if (equal(geojsonFeature.geometry, feature.geometry)) { // console.log('Feature update event triggered without update') diff --git a/app/javascript/maplibre/edit_styles.js b/app/javascript/maplibre/edit_styles.js index bb9510956..758380690 100644 --- a/app/javascript/maplibre/edit_styles.js +++ b/app/javascript/maplibre/edit_styles.js @@ -13,7 +13,7 @@ export const highlightColor = '#fbb03b' const midpointSize = 6 const vertexSize = 6 -export function editStyles () { +export function editStyles() { return [ // removeSource(styles()['polygon-layer']), // gl-draw-polygon-fill-inactive removeSource(styles()['polygon-layer-outline']), @@ -112,7 +112,7 @@ export function editStyles () { }, // inactive single point features removeSource(styles()['points-layer']), - removeSource(styles()['heatmap-layer']), + // removeSource(styles()['heatmap-layer']), // outline border of inactive vertex points on lines + polygons, // rendering outline seperately to generate nicer overlay effect diff --git a/app/javascript/maplibre/feature.js b/app/javascript/maplibre/feature.js index bd6635659..3cbb4b91b 100644 --- a/app/javascript/maplibre/feature.js +++ b/app/javascript/maplibre/feature.js @@ -1,4 +1,4 @@ -import { map, geojsonData, layers, mapProperties } from 'maplibre/map' +import { map, layers, mapProperties } from 'maplibre/map' import * as f from 'helpers/functions' import * as dom from 'helpers/dom' import { marked } from 'marked' @@ -9,6 +9,7 @@ import { area } from "@turf/area" import { along } from "@turf/along" import { buffer } from "@turf/buffer" import { lineString, multiLineString, polygon, multiPolygon } from "@turf/helpers" +import { getFeature, getFeatures, getFeatureSource } from "maplibre/layers/layers" window.marked = marked @@ -89,7 +90,7 @@ export async function showFeatureDetails (feature) { dom.hideElements(['#feature-edit-raw', '#feature-edit-ui']) f.e('#edit-buttons button', (e) => { e.classList.remove('active') }) // allow edit in rw mode for geojson features only - if (window.gon.map_mode === 'rw' && geojsonData.features.find(f => f.id === feature.id)) { + if (window.gon.map_mode === 'rw' && getFeature(feature.id)) { document.querySelector('#edit-buttons').classList.remove('hidden') } dom.showElements('#feature-details-body') @@ -252,9 +253,10 @@ export function resetHighlightedFeature () { f.e('#feature-details-modal', e => { e.classList.remove('show') }) } -export function highlightFeature (feature, sticky = false, source = 'geojson-source') { +export function highlightFeature (feature, sticky = false, source) { if (highlightedFeatureId !== feature.id) { resetHighlightedFeature() } // console.log('highlight', feature) + if (!source) { source = getFeatureSource(feature.id) } stickyFeatureHighlight = sticky highlightedFeatureId = feature?.id highlightedFeatureSource = source @@ -366,7 +368,7 @@ export function initializeKmMarkerStyles () { export function renderKmMarkers () { let kmMarkerFeatures = [] - geojsonData.features.filter(feature => (feature.geometry.type === 'LineString' && + getFeatures('geojson').filter(feature => (feature.geometry.type === 'LineString' && feature.properties['show-km-markers'] && feature.geometry.coordinates.length >= 2)).forEach((f, index) => { @@ -406,7 +408,7 @@ export function renderExtrusionLines () { // Disable extrusionlines on 3D terrain, it does not work if (mapProperties.terrain) { return [] } - let extrusionLines = geojsonData.features.filter(feature => ( + let extrusionLines = getFeatures('geojson').filter(feature => ( feature.geometry.type === 'LineString' && feature.properties['fill-extrusion-height'] && feature.geometry.coordinates.length !== 1 // don't break line animation diff --git a/app/javascript/maplibre/layers/layers.js b/app/javascript/maplibre/layers/layers.js index 2f1ebc90a..e85c72351 100644 --- a/app/javascript/maplibre/layers/layers.js +++ b/app/javascript/maplibre/layers/layers.js @@ -1,18 +1,71 @@ import { initializeWikipediaLayers, loadWikipediaLayer } from 'maplibre/layers/wikipedia' import { initializeOverpassLayers, loadOverpassLayer } from 'maplibre/overpass/overpass' import { layers } from 'maplibre/map' +import { initializeViewStyles } from 'maplibre/styles' +import { map, addGeoJSONSource, redrawGeojson } from 'maplibre/map' +import * as functions from 'helpers/functions' + // initialize layers: create source, apply styles and load data export function initializeLayers(id = null) { + let initLayers = layers + if (id) { initLayers = initLayers.filter(l => l.id === id) } + initLayers.forEach((layer) => { + console.log('Adding source for layer', layer.type, layer.id, layer.cluster) + addGeoJSONSource(layer.type + '-source-' + layer.id, layer.cluster) + }) + + // draw geojson layer before loading overpass layers + console.log('Initializing geojson layers') + initLayers.filter(l => l.type === 'geojson').forEach((layer) => { + initializeViewStyles('geojson-source-' + layer.id, !!layer.cluster) + }) + redrawGeojson() + functions.e('#maplibre-map', e => { e.setAttribute('data-geojson-loaded', true) }) + map.fire('geojson.load', { detail: { message: 'geojson source loaded' } }) + + //initializeGeoJSONLayers(id) initializeOverpassLayers(id) initializeWikipediaLayers(id) } export function loadLayer(id) { const layer = layers.find(f => f.id === id) + //if (layer.type === 'geojson') { + // return loadGeoJSONLayer(id) + //} else if (layer.type === 'wikipedia') { if (layer.type === 'wikipedia') { return loadWikipediaLayer(id) } else if (layer.type === 'overpass') { return loadOverpassLayer(id) } +} + +export function getFeature(id, type = null) { + const searchLayers = type ? layers.filter(l => l.type === type) : layers + for (const layer of searchLayers) { + if (layer.geojson) { + let feature = layer.geojson.features.find(f => f.id === id) + if (feature) { return feature } + } + } + return null +} + +export function getFeatures(type = 'geojson') { + return layers.filter(l => l.type === type).flatMap(l => l.geojson?.features || []) +} + +export function hasFeatures(type = 'geojson') { + return layers.some(l => l.type === type && l.geojson?.features?.length > 0) +} + +export function getFeatureSource(featureId) { + for (const layer of layers) { + if (layer.geojson) { + let feature = layer.geojson.features.find(f => f.id === featureId) + if (feature) { return layer.type + '-source-' + layer.id } + } + } + return null } \ No newline at end of file diff --git a/app/javascript/maplibre/layers/wikipedia.js b/app/javascript/maplibre/layers/wikipedia.js index 49692299b..42545ed98 100644 --- a/app/javascript/maplibre/layers/wikipedia.js +++ b/app/javascript/maplibre/layers/wikipedia.js @@ -1,4 +1,4 @@ -import { map, layers, addGeoJSONSource, redrawGeojson } from 'maplibre/map' +import { map, layers, redrawGeojson } from 'maplibre/map' import { initializeViewStyles } from 'maplibre/styles' import * as functions from 'helpers/functions' import { status } from 'helpers/status' @@ -9,7 +9,6 @@ export function initializeWikipediaLayers(id = null) { if (id) { initLayers = initLayers.filter(l => l.id === id) } initLayers.forEach((layer) => { - addGeoJSONSource('wikipedia-source-' + layer.id, false) initializeViewStyles('wikipedia-source-' + layer.id) loadWikipediaLayer(layer.id) }) diff --git a/app/javascript/maplibre/map.js b/app/javascript/maplibre/map.js index 46f946268..8849406a7 100644 --- a/app/javascript/maplibre/map.js +++ b/app/javascript/maplibre/map.js @@ -10,13 +10,12 @@ import { initializeViewControls } from 'maplibre/controls/view' import { draw, select } from 'maplibre/edit' import { highlightFeature, resetHighlightedFeature, renderKmMarkers, renderExtrusionLines, initializeKmMarkerStyles } from 'maplibre/feature' -import { initializeViewStyles, setStyleDefaultFont, loadImage } from 'maplibre/styles' -import { initializeLayers } from 'maplibre/layers/layers' +import { setStyleDefaultFont, loadImage } from 'maplibre/styles' +import { initializeLayers, getFeature } from 'maplibre/layers/layers' import { centroid } from "@turf/centroid" export let map export let layers // [{ id:, type: "overpass"||"geojson", name:, query:, geojson: { type: 'FeatureCollection', features: [] } }] -export let geojsonData //= { type: 'FeatureCollection', features: [] } export let mapProperties export let lastMousePosition export let backgroundMapLayer @@ -55,13 +54,11 @@ export function initializeMaplibreProperties () { // reset map data export function resetLayers () { functions.e('#maplibre-map', e => { e.setAttribute('data-geojson-loaded', false) }) - geojsonData = null layers = [] } export function resetGeojsonLayers () { functions.e('#maplibre-map', e => { e.setAttribute('data-geojson-loaded', false) }) - geojsonData = null layers = layers.filter(l => l.type !== 'geojson') } @@ -119,7 +116,7 @@ export async function initializeMap (divId = 'maplibre-map') { console.log("Map loaded ('load')") const urlFeatureId = new URLSearchParams(window.location.search).get('f') - let feature = geojsonData?.features?.find(f => f.id === urlFeatureId) + let feature = getFeature(urlFeatureId) if (feature) { resetControls() highlightFeature(feature, true) @@ -127,7 +124,7 @@ export async function initializeMap (divId = 'maplibre-map') { map.setCenter(center.geometry.coordinates) } const urlFeatureAnimateId = new URLSearchParams(window.location.search).get('a') - feature = geojsonData?.features?.find(f => f.id === urlFeatureAnimateId) + feature = getFeature(urlFeatureAnimateId) if (feature) { console.log('Animating ' + feature.id) resetControls() @@ -181,14 +178,19 @@ function updateCursorPosition(e) { } } -export function addGeoJSONSource (sourceName, cluster=true ) { +// Each map layer has its own source, so different style layers can be applied +// sourceName convention: layer.type + '-source-' + layer.id +export function addGeoJSONSource(sourceName, cluster=false) { // https://maplibre.org/maplibre-style-spec/sources/#geojson // console.log("Adding source: " + sourceName) - if (map.getSource(sourceName)) { return } // source already exists + if (map.getSource(sourceName)) { + console.log('Source ' + sourceName + ' already exists, skipping add') + return + } map.addSource(sourceName, { type: 'geojson', promoteId: 'id', - data: { type: 'FeatureCollection', features: [] }, // geojsonData, + data: { type: 'FeatureCollection', features: [] }, cluster: cluster, clusterMaxZoom: 14, clusterRadius: 50 @@ -214,8 +216,8 @@ export function removeGeoJSONSource(sourceName) { } export function loadLayers () { - // return if all layers already loaded (eg. in case of basemap style change) - if (geojsonData && gon.map_layers.length == layers.length) { + // do not reload from server if all layers already loaded (eg. in case of basemap style change) + if (gon.map_layers.length == layers.length) { // console.log('All layers already loaded, re-rendering from cache', layers) initializeLayers() redrawGeojson() @@ -233,26 +235,14 @@ export function loadLayers () { .then(data => { console.log('Loaded map layers from server: ', data.layers) // make sure we're still showing the map the request came from - if (window.gon.map_properties.public_id !== data.properties.public_id){ - return - } - data.layers.filter(f => f.type === 'geojson').forEach((layer) => { + if (window.gon.map_properties.public_id !== data.properties.public_id) { return } + data.layers.forEach((layer) => { if (!layers.find( l => l.id === layer.id) ) { layers.push(layer) } }) - // draw geojson layer before loading overpass layers - geojsonData = mergedGeoJSONLayers() - redrawGeojson() - functions.e('#maplibre-map', e => { e.setAttribute('data-geojson-loaded', true) }) - map.fire('geojson.load', { detail: { message: 'geojson-source loaded' } }) - - data.layers.filter(f => f.type !== 'geojson').forEach((layer) => { - if (!layers.find(l => l.id === layer.id)) { layers.push(layer) } - }) initializeLayers() }) .catch(error => { - console.error('Failed to fetch GeoJSON:', error) - console.error('GeoJSONData:', geojsonData) + console.error('Failed to fetch map layers:', error) }) } @@ -409,7 +399,7 @@ export function redrawGeojson (resetDraw = true) { draw.deleteAll() drawFeatureIds.forEach((featureId) => { - let feature = geojsonData.features.find(f => f.id === featureId) + let feature = getFeature(featureId) if (feature) { draw.add(feature) // if we're in edit mode, re-select feature @@ -420,11 +410,11 @@ export function redrawGeojson (resetDraw = true) { } // updateData requires a 'GeoJSONSourceDiff', with add/update/remove lists - map.getSource('geojson-source').setData(renderedGeojsonData()) + //map.getSource('geojson-source').setData(renderedGeojsonData()) console.log('layers:', layers) - layers.filter(f => f.type !== 'geojson').forEach((layer) => { + layers.forEach((layer) => { if (layer.geojson) { - console.log("Setting layer data", layer.type, layer.id, layer.geojson) + console.log("Setting source data for layer", layer.type, layer.id, layer.geojson) map.getSource(layer.type + '-source-' + layer.id).setData(layer.geojson, false) } }) @@ -440,7 +430,7 @@ export function renderedGeojsonData () { } export function upsert (updatedFeature) { - const feature = geojsonData.features.find(f => f.id === updatedFeature.id) + const feature = getFeature(updatedFeature.id) if (!feature) { addFeature(updatedFeature); return } // only update feature if it was changed, disregard properties.id @@ -454,7 +444,7 @@ export function upsert (updatedFeature) { export function addFeature (feature) { feature.properties.id = feature.id layers.find(l => l.type === 'geojson').geojson.features.push(feature) - geojsonData = mergedGeoJSONLayers() + //geojsonData = mergedGeoJSONLayers() redrawGeojson(false) status('Added feature') } @@ -475,9 +465,8 @@ function updateFeature (feature, updatedFeature) { } export function destroyFeature (featureId) { - if (geojsonData.features.find(f => f.id === featureId)) { + if (getFeature(featureId)) { status('Deleting feature ' + featureId) - geojsonData.features = geojsonData.features.filter(f => f.id !== featureId) layers.forEach(l => l.geojson.features = l.geojson.features.filter(f => f.id !== featureId)) redrawGeojson() resetHighlightedFeature() @@ -488,7 +477,7 @@ export function destroyFeature (featureId) { // load geojson data function initializeStyles() { console.log('Initializing sources and layer styles after basemap load/change') - addGeoJSONSource('geojson-source', false) + addGeoJSONSource('km-marker-source', false) loadLayers() demSource.setupMaplibre(maplibregl) @@ -496,7 +485,7 @@ function initializeStyles() { if (mapProperties.hillshade) { addHillshade() } if (mapProperties.globe) { addGlobe() } if (mapProperties.contours) { addContours() } - initializeViewStyles('geojson-source') + // initializeViewStyles('geojson-source') initializeKmMarkerStyles() } @@ -556,19 +545,15 @@ export function sortLayers () { const pointsLayerHits = functions.reduceArray(layers, (e) => e.id === 'points-hit-layer_geojson-source') const directions = functions.reduceArray(layers, (e) => (e.id.startsWith('maplibre-gl-directions'))) const heatmap = functions.reduceArray(layers, (e) => (e.id.startsWith('heatmap-layer'))) + const kmMarkers = functions.reduceArray(layers, (e) => (e.id.includes('km-marker'))) layers = layers.concat(flatLayers).concat(lineLayers).concat(mapExtrusions).concat(directions) .concat(mapSymbols).concat(points).concat(heatmap).concat(editLayer) .concat(lineLayerHits).concat(pointsLayerHits) - .concat(userSymbols).concat(userLabels) + .concat(kmMarkers).concat(userSymbols).concat(userLabels) const newStyle = { ...currentStyle, layers } map.setStyle(newStyle, { diff: true }) - - // place km markers under symbols layer (icons) - layers.filter(layer => layer.id.includes('km-marker')).forEach((layer) => { - map.moveLayer(layer.id, 'symbols-layer_geojson-source') - }) console.log("Sorted layers:", map.getStyle().layers) } @@ -587,7 +572,7 @@ export function mergedGeoJSONLayers(type='geojson') { } export function frontFeature(frontFeature) { - // move feature to end of its layer's features array (for overpass) + // move feature to end of its layer's features array for (const layer of layers) { if (!layer?.geojson?.features) { continue } const features = layer.geojson.features @@ -598,13 +583,6 @@ export function frontFeature(frontFeature) { break // done, exit loop } } - // move feature to end of geojsonData features array - const features = geojsonData.features - const idx = features.findIndex(f => f.id === frontFeature.id) - if (idx !== -1) { - const [feature] = features.splice(idx, 1) // Remove it - features.push(feature) // Add to end - } redrawGeojson() } diff --git a/app/javascript/maplibre/overpass/overpass.js b/app/javascript/maplibre/overpass/overpass.js index 6ab5fd702..80485cdbc 100644 --- a/app/javascript/maplibre/overpass/overpass.js +++ b/app/javascript/maplibre/overpass/overpass.js @@ -1,4 +1,4 @@ -import { map, layers, redrawGeojson, addGeoJSONSource, viewUnchanged, sortLayers } from 'maplibre/map' +import { map, layers, redrawGeojson, viewUnchanged, sortLayers } from 'maplibre/map' import { applyOverpassQueryStyle } from 'maplibre/overpass/queries' import { initializeViewStyles, initializeClusterStyles } from 'maplibre/styles' import * as functions from 'helpers/functions' @@ -12,8 +12,6 @@ export function initializeOverpassLayers(id = null) { const clustered = !layer.query.includes("heatmap=true") && !layer.query.includes("cluster=false") && !layer.query.includes("geom") // clustering breaks lines & geometries - // TODO: changing cluster setup requires a map reload - addGeoJSONSource('overpass-source-' + layer.id, clustered) initializeViewStyles('overpass-source-' + layer.id) if (clustered) { const clusterIcon = getCommentValue(layer.query, 'cluster-symbol') || getCommentValue(layer.query, 'cluster-image-url') || diff --git a/app/javascript/maplibre/routing/osrm.js b/app/javascript/maplibre/routing/osrm.js index f91dea733..0b5411ecf 100644 --- a/app/javascript/maplibre/routing/osrm.js +++ b/app/javascript/maplibre/routing/osrm.js @@ -1,6 +1,6 @@ import { layersFactory } from "@maplibre/maplibre-gl-directions" import CustomMapLibreGlDirections from "maplibre/routing/custom_directions" -import { map, mapProperties, upsert, geojsonData } from 'maplibre/map' +import { map, mapProperties, upsert } from 'maplibre/map' import { highlightColor } from 'maplibre/edit_styles' import { updateElevation, setSelectedFeature } from 'maplibre/edit' import { styles, featureColor } from 'maplibre/styles' @@ -11,6 +11,7 @@ import { status } from 'helpers/status' import * as functions from 'helpers/functions' import { showFeatureDetails } from 'maplibre/feature' import { addUndoState } from 'maplibre/undo' +import { getFeature } from 'maplibre/layers/layers' // https://github.com/maplibre/maplibre-gl-directions // Examples: https://maplibre.org/maplibre-gl-directions/#/examples @@ -121,7 +122,7 @@ export function initDirections (profile, feature) { } function updateTrack(feature) { - let geojsonFeature = geojsonData.features.find(f => f.id === feature.id) + let geojsonFeature = getFeature(feature.id) if (geojsonFeature) { // store undo state from unchanged feature addUndoState('Track update', geojsonFeature) diff --git a/app/javascript/maplibre/styles.js b/app/javascript/maplibre/styles.js index 25a87f1fb..662f2f35f 100644 --- a/app/javascript/maplibre/styles.js +++ b/app/javascript/maplibre/styles.js @@ -1,10 +1,11 @@ -import { map, frontFeature, removeStyleLayers, geojsonData } from 'maplibre/map' +import { map, frontFeature, removeStyleLayers } from 'maplibre/map' import { highlightedFeatureId, stickyFeatureHighlight, highlightedFeatureSource, resetHighlightedFeature, highlightFeature } from 'maplibre/feature' import { draw } from 'maplibre/edit' import { flyToFeature } from 'maplibre/animations' +import { getFeature } from 'maplibre/layers/layers' export const viewStyleNames = [ 'polygon-layer', @@ -17,7 +18,6 @@ export const viewStyleNames = [ 'points-layer-flat', 'points-layer', 'points-hit-layer', - 'heatmap-layer', 'symbols-layer-flat', 'symbols-layer', 'text-layer-flat', @@ -28,11 +28,13 @@ export const viewStyleNames = [ export function setStyleDefaultFont (font) { labelFont = [font] } -export function initializeViewStyles (sourceName) { +export function initializeViewStyles (sourceName, heatmap=false) { + console.log('Initializing view styles for source ' + sourceName) removeStyleLayers(sourceName) viewStyleNames.forEach(styleName => { map.addLayer(setSource(styles()[styleName], sourceName)) }) + if (heatmap) { map.addLayer(setSource(styles()['heatmap-layer'], sourceName)) } // console.log('View styles added for source ' + sourceName) // click is needed to select on mobile and for sticky highlight @@ -47,7 +49,7 @@ export function initializeViewStyles (sourceName) { } if (e.features[0].properties?.onclick === 'feature' && e.features[0].properties?.['onclick-target']) { const targetId = e.features[0].properties?.['onclick-target'] - const feature = geojsonData.features.find(f => f.id === targetId) + const feature = getFeature(targetId) if (feature) { flyToFeature(feature) } else { @@ -537,9 +539,11 @@ export function styles () { 'heatmap-layer': { id: 'heatmap-layer', type: 'heatmap', - filter: ['all', - ['any', ["has", "heatmap"], ["has", "user_heatmap"]], - minZoomFilter], + filter: [ + "all", + ["==", ["geometry-type"], "Point"], + minZoomFilter + ], paint: { 'heatmap-opacity': 0.7, 'heatmap-intensity': 1.3, diff --git a/app/javascript/maplibre/undo.js b/app/javascript/maplibre/undo.js index dd5faa624..9fcf3be93 100644 --- a/app/javascript/maplibre/undo.js +++ b/app/javascript/maplibre/undo.js @@ -1,4 +1,4 @@ -import { geojsonData, redrawGeojson, addFeature, destroyFeature } from 'maplibre/map' +import { redrawGeojson, addFeature, destroyFeature } from 'maplibre/map' import { select, selectedFeature } from 'maplibre/edit' import { showFeatureDetails } from 'maplibre/feature' import { resetDirections } from 'maplibre/routing/osrm' diff --git a/app/models/layer.rb b/app/models/layer.rb index 095a01220..9f65f9893 100644 --- a/app/models/layer.rb +++ b/app/models/layer.rb @@ -12,13 +12,15 @@ class Layer field :type field :name field :query + field :heatmap, type: Boolean + field :cluster, type: Boolean 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 } + json = { id: id, type: type, name: name, heatmap: !!heatmap, cluster: !!cluster } json[:query] = query if type == "overpass" json end diff --git a/config/importmap.rb b/config/importmap.rb index d66cff4fd..0a650a294 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -75,10 +75,10 @@ pin "@turf/projection", to: "@turf--projection.js", preload: false # @7.3.2 pin "@turf/clone", to: "@turf--clone.js", preload: false # @7.3.2 pin "@turf/helpers", to: "@turf--helpers.js" # @7.3.3 -pin "@turf/meta", to: "@turf--meta.js" # @7.3.2 +pin "@turf/meta", to: "@turf--meta.js" # @7.3.3 # Turf libs needed by app pin "@turf/simplify", to: "@turf--simplify.js", preload: false # @7.3.2 -pin "@turf/boolean-point-on-line", to: "@turf--boolean-point-on-line.js", preload: false # @7.3.2 +pin "@turf/boolean-point-on-line", to: "@turf--boolean-point-on-line.js", preload: false # @7.3.3 pin "@turf/clean-coords", to: "@turf--clean-coords.js", preload: false # @7.3.2 pin "@turf/invariant", to: "@turf--invariant.js", preload: false # @7.3.3 pin "@turf/centroid", to: "@turf--centroid.js", preload: false # @7.3.2 @@ -89,7 +89,7 @@ pin "@turf/length", to: "@turf--length.js", preload: false # @7.3.2 pin "@turf/area", to: "@turf--area.js", preload: false # @7.3.2 pin "@turf/buffer", to: "@turf--buffer.js", preload: false # @7.3.2 -pin "@turf/bbox", to: "@turf--bbox.js", preload: false # @7.3.2 +pin "@turf/bbox", to: "@turf--bbox.js", preload: false # @7.3.3 pin "@turf/center", to: "@turf--center.js", preload: false # @7.3.2 pin "@turf/jsts", to: "@turf--jsts.js", preload: false # @2.7.2 # dependencies of turf/buffer diff --git a/package-lock.json b/package-lock.json index 0660d1859..a57945df4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "devDependencies": { "@eslint/js": "^9.39.2", "eslint": "^9.39.2", - "globals": "^17.2.0", + "globals": "^17.3.0", "stylelint": "^16.26.1", "stylelint-config-standard": "^39.0.1" } @@ -1194,9 +1194,9 @@ } }, "node_modules/globals": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.2.0.tgz", - "integrity": "sha512-tovnCz/fEq+Ripoq+p/gN1u7l6A7wwkoBT9pRCzTHzsD/LvADIzXZdjmRymh5Ztf0DYC3Rwg5cZRYjxzBmzbWg==", + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", + "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", "dev": true, "engines": { "node": ">=18" diff --git a/package.json b/package.json index a0350b173..f645248d3 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "devDependencies": { "@eslint/js": "^9.39.2", "eslint": "^9.39.2", - "globals": "^17.2.0", + "globals": "^17.3.0", "stylelint": "^16.26.1", "stylelint-config-standard": "^39.0.1" } diff --git a/public/icons/wikipedia.png b/public/icons/wikipedia.png index f8f7fdcc9..c93881c59 100644 Binary files a/public/icons/wikipedia.png and b/public/icons/wikipedia.png differ