fix: allow to save/undo/sync drag'n'drop of datalayers (#2677)

This also:
- introduces a new DataLayerManager
- removes the _umap_options coming from the geojson form the server
- removes all backup related functions (now we have undo/redo)

fix #2674
This commit is contained in:
Yohan Boniface 2025-04-25 15:35:23 +02:00 committed by GitHub
commit bffbeb5230
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 188 additions and 175 deletions

View file

@ -551,6 +551,7 @@ class DataLayer(NamedModel):
if self.old_id: if self.old_id:
metadata["old_id"] = self.old_id metadata["old_id"] = self.old_id
metadata["id"] = self.pk metadata["id"] = self.pk
metadata["rank"] = self.rank
metadata["permissions"] = {"edit_status": self.edit_status} metadata["permissions"] = {"edit_status": self.edit_status}
metadata["editMode"] = "advanced" if self.can_edit(request) else "disabled" metadata["editMode"] = "advanced" if self.can_edit(request) else "disabled"
metadata["_referenceVersion"] = self.reference_version metadata["_referenceVersion"] = self.reference_version

View file

@ -113,7 +113,7 @@ export default class Browser {
} }
onFormChange() { onFormChange() {
this._umap.eachBrowsableDataLayer((datalayer) => { this._umap.datalayers.browsable().map((datalayer) => {
datalayer.resetLayer(true) datalayer.resetLayer(true)
this.updateDatalayer(datalayer) this.updateDatalayer(datalayer)
if (this._umap.fullPanel?.isOpen()) datalayer.tableEdit() if (this._umap.fullPanel?.isOpen()) datalayer.tableEdit()
@ -136,7 +136,7 @@ export default class Browser {
onMoveEnd() { onMoveEnd() {
if (!this.isOpen()) return if (!this.isOpen()) return
const isListDynamic = this.options.inBbox const isListDynamic = this.options.inBbox
this._umap.eachBrowsableDataLayer((datalayer) => { this._umap.datalayers.browsable().map((datalayer) => {
if (!isListDynamic && !datalayer.hasDynamicData()) return if (!isListDynamic && !datalayer.hasDynamicData()) return
this.updateDatalayer(datalayer) this.updateDatalayer(datalayer)
}) })
@ -145,7 +145,7 @@ export default class Browser {
update() { update() {
if (!this.isOpen()) return if (!this.isOpen()) return
this.dataContainer.innerHTML = '' this.dataContainer.innerHTML = ''
this._umap.eachBrowsableDataLayer((datalayer) => { this._umap.datalayers.browsable().map((datalayer) => {
this.addDataLayer(datalayer, this.dataContainer) this.addDataLayer(datalayer, this.dataContainer)
}) })
} }
@ -254,10 +254,10 @@ export default class Browser {
// If at least one layer is shown, hide it // If at least one layer is shown, hide it
// otherwise show all // otherwise show all
let allHidden = true let allHidden = true
this._umap.eachBrowsableDataLayer((datalayer) => { this._umap.datalayers.browsable().map((datalayer) => {
if (datalayer.isVisible()) allHidden = false if (datalayer.isVisible()) allHidden = false
}) })
this._umap.eachBrowsableDataLayer((datalayer) => { this._umap.datalayers.browsable().map((datalayer) => {
datalayer._forcedVisibility = true datalayer._forcedVisibility = true
if (allHidden) { if (allHidden) {
datalayer.show() datalayer.show()

View file

@ -68,7 +68,9 @@ export default class Caption extends Utils.WithTemplate {
this.elements.description.hidden = true this.elements.description.hidden = true
} }
this.elements.datalayersContainer.innerHTML = '' this.elements.datalayersContainer.innerHTML = ''
this._umap.eachDataLayerReverse((datalayer) => this._umap.datalayers
.reverse()
.map((datalayer) =>
this.addDataLayer(datalayer, this.elements.datalayersContainer) this.addDataLayer(datalayer, this.elements.datalayersContainer)
) )
this.addCredits() this.addCredits()
@ -85,7 +87,7 @@ export default class Caption extends Utils.WithTemplate {
} }
this._umap.panel.open({ content: this.element }).then(() => { this._umap.panel.open({ content: this.element }).then(() => {
// Create the legend when the panel is actually on the DOM // Create the legend when the panel is actually on the DOM
this._umap.eachDataLayerReverse((datalayer) => datalayer.renderLegend()) this._umap.datalayers.reverse().map((datalayer) => datalayer.renderLegend())
this._umap.propagate() this._umap.propagate()
}) })
} }

View file

@ -64,6 +64,9 @@ export class DataLayer {
this.setOptions(data) this.setOptions(data)
this.pane.dataset.id = this.id this.pane.dataset.id = this.id
if (this.options.rank === undefined) {
this.options.rank = this._umap.datalayers.count()
}
if (!Utils.isObject(this.options.remoteData)) { if (!Utils.isObject(this.options.remoteData)) {
this.options.remoteData = {} this.options.remoteData = {}
@ -153,6 +156,9 @@ export class DataLayer {
case 'remote-data': case 'remote-data':
this.fetchRemoteData() this.fetchRemoteData()
break break
case 'datalayer-rank':
this._umap.reorderDataLayers()
break
} }
} }
} }
@ -249,16 +255,9 @@ export class DataLayer {
if (!error) { if (!error) {
this._umap.modifiedAt = response.headers.get('last-modified') this._umap.modifiedAt = response.headers.get('last-modified')
this.setReferenceVersion({ response, sync: false }) this.setReferenceVersion({ response, sync: false })
// FIXME: for now the _umap_options property is set dynamically from backend delete geojson._umap_options
// And thus it's not in the geojson file in the server
// So do not let all options to be reset
// Fix is a proper migration so all datalayers settings are
// in DB, and we remove it from geojson flat files.
if (geojson._umap_options) {
geojson._umap_options.editMode = this.options.editMode
}
// In case of maps pre 1.0 still around // In case of maps pre 1.0 still around
if (geojson._storage) geojson._storage.editMode = this.options.editMode delete geojson._storage
await this.fromUmapGeoJSON(geojson) await this.fromUmapGeoJSON(geojson)
this.backupOptions() this.backupOptions()
this._loading = false this._loading = false
@ -287,7 +286,6 @@ export class DataLayer {
async fromUmapGeoJSON(geojson) { async fromUmapGeoJSON(geojson) {
if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat
geojson._umap_options.id = this.id
if (geojson._umap_options) this.setOptions(geojson._umap_options) if (geojson._umap_options) this.setOptions(geojson._umap_options)
if (this.isRemoteLayer()) { if (this.isRemoteLayer()) {
await this.fetchRemoteData() await this.fetchRemoteData()
@ -297,6 +295,7 @@ export class DataLayer {
} }
clear() { clear() {
// TODO do not startBatch for remoteData layer
this.sync.startBatch() this.sync.startBatch()
for (const feature of Object.values(this._features)) { for (const feature of Object.values(this._features)) {
feature.del() feature.del()
@ -395,12 +394,7 @@ export class DataLayer {
} }
connectToMap() { connectToMap() {
if (!this._umap.datalayers[this.id]) { this._umap.datalayers.add(this)
this._umap.datalayers[this.id] = this
}
if (!this._umap.datalayersIndex.includes(this)) {
this._umap.datalayersIndex.push(this)
}
this._umap.onDataLayersChanged() this._umap.onDataLayersChanged()
} }
@ -644,11 +638,19 @@ export class DataLayer {
del(sync = true) { del(sync = true) {
const oldValue = Utils.CopyJSON(this.umapGeoJSON()) const oldValue = Utils.CopyJSON(this.umapGeoJSON())
this.erase() // TODO merge datalayer del and features del in same
// batch
this.clear()
if (sync) { if (sync) {
this.isDeleted = true this.isDeleted = true
this.sync.delete(oldValue) this.sync.delete(oldValue)
} }
this.hide()
this.parentPane.removeChild(this.pane)
this._umap.onDataLayersChanged()
this.layer.onDelete(this._leafletMap)
this.propagateDelete()
this._leaflet_events_bk = this._leaflet_events
} }
empty() { empty() {
@ -666,17 +668,6 @@ export class DataLayer {
return datalayer return datalayer
} }
erase() {
this.hide()
this._umap.datalayersIndex.splice(this.getRank(), 1)
this.parentPane.removeChild(this.pane)
this._umap.onDataLayersChanged()
this.layer.onDelete(this._leafletMap)
this.propagateDelete()
this._leaflet_events_bk = this._leaflet_events
this.clear()
}
redraw() { redraw() {
if (!this.isVisible()) return if (!this.isVisible()) return
this.eachFeature((feature) => feature.redraw()) this.eachFeature((feature) => feature.redraw())
@ -1091,23 +1082,11 @@ export class DataLayer {
} }
getPreviousBrowsable() { getPreviousBrowsable() {
let id = this.getRank() return this._umap.datalayers.prev(this)
let next
const index = this._umap.datalayersIndex
while (((id = index[++id] ? id : 0), (next = index[id]))) {
if (next === this || next.canBrowse()) break
}
return next
} }
getNextBrowsable() { getNextBrowsable() {
let id = this.getRank() return this._umap.datalayers.next(this)
let prev
const index = this._umap.datalayersIndex
while (((id = index[--id] ? id : index.length - 1), (prev = index[id]))) {
if (prev === this || prev.canBrowse()) break
}
return prev
} }
umapGeoJSON() { umapGeoJSON() {
@ -1118,8 +1097,8 @@ export class DataLayer {
} }
} }
getRank() { getDOMOrder() {
return this._umap.datalayersIndex.indexOf(this) return Array.from(this.parentPane.children).indexOf(this.pane)
} }
isReadOnly() { isReadOnly() {
@ -1141,6 +1120,12 @@ export class DataLayer {
} }
} }
prepareOptions() {
const options = Utils.CopyJSON(this.options)
delete options.permissions
return JSON.stringify(options)
}
async save() { async save() {
if (this.isDeleted) return await this.saveDelete() if (this.isDeleted) return await this.saveDelete()
if (!this.isRemoteLayer() && !this.isLoaded()) return if (!this.isRemoteLayer() && !this.isLoaded()) return
@ -1148,8 +1133,8 @@ export class DataLayer {
const formData = new FormData() const formData = new FormData()
formData.append('name', this.options.name) formData.append('name', this.options.name)
formData.append('display_on_load', !!this.options.displayOnLoad) formData.append('display_on_load', !!this.options.displayOnLoad)
formData.append('rank', this.getRank()) formData.append('rank', this.options.rank)
formData.append('settings', JSON.stringify(this.options)) formData.append('settings', this.prepareOptions())
// Filename support is shaky, don't do it for now. // Filename support is shaky, don't do it for now.
const blob = new Blob([JSON.stringify(geojson)], { type: 'application/json' }) const blob = new Blob([JSON.stringify(geojson)], { type: 'application/json' })
formData.append('geojson', blob) formData.append('geojson', blob)

View file

@ -24,7 +24,7 @@ export default class Facets {
this.selected[name] = selected this.selected[name] = selected
} }
this._umap.eachBrowsableDataLayer((datalayer) => { this._umap.datalayers.browsable().map((datalayer) => {
datalayer.eachFeature((feature) => { datalayer.eachFeature((feature) => {
for (const name of names) { for (const name of names) {
let value = feature.properties[name] let value = feature.properties[name]

View file

@ -560,7 +560,7 @@ Fields.SlideshowDelay = class extends Fields.IntSelect {
Fields.DataLayerSwitcher = class extends Fields.Select { Fields.DataLayerSwitcher = class extends Fields.Select {
getOptions() { getOptions() {
const options = [] const options = []
this.builder._umap.eachDataLayerReverse((datalayer) => { this.builder._umap.datalayers.reverse().map((datalayer) => {
if ( if (
datalayer.isLoaded() && datalayer.isLoaded() &&
!datalayer.isDataReadOnly() && !datalayer.isDataReadOnly() &&

View file

@ -243,7 +243,7 @@ export default class Importer extends Utils.WithTemplate {
this.raw = null this.raw = null
const layerSelect = this.qs('[name="layer-id"]') const layerSelect = this.qs('[name="layer-id"]')
layerSelect.innerHTML = '' layerSelect.innerHTML = ''
this._umap.eachDataLayerReverse((datalayer) => { this._umap.datalayers.reverse().map((datalayer) => {
if (datalayer.isLoaded() && !datalayer.isRemoteLayer()) { if (datalayer.isLoaded() && !datalayer.isRemoteLayer()) {
DomUtil.element({ DomUtil.element({
tagName: 'option', tagName: 'option',

View file

@ -0,0 +1,46 @@
export class DataLayerManager extends Object {
add(datalayer) {
this[datalayer.id] = datalayer
}
active() {
return Object.values(this)
.filter((datalayer) => !datalayer.isDeleted)
.sort((a, b) => a.options.rank > b.options.rank)
}
reverse() {
return this.active().reverse()
}
count() {
return this.active().length
}
find(func) {
for (const datalayer of this.reverse()) {
if (func.call(datalayer, datalayer)) {
return datalayer
}
}
}
filter(func) {
return this.active().filter(func)
}
visible() {
return this.filter((datalayer) => datalayer.isVisible())
}
browsable() {
return this.reverse().filter((datalayer) => datalayer.allowBrowse())
}
prev(datalayer) {
const browsable = this.browsable()
const current = browsable.indexOf(datalayer)
const prev = browsable[current - 1] || browsable[browsable.length - 1]
if (!prev.canBrowse()) return this.prev(prev)
return prev
}
next(datalayer) {
const browsable = this.browsable()
const current = browsable.indexOf(datalayer)
const next = browsable[current + 1] || browsable[0]
if (!next.canBrowse()) return this.next(next)
return next
}
}

View file

@ -159,7 +159,7 @@ export class MapPermissions {
`<fieldset class="separator"><legend>${translate('Datalayers')}</legend></fieldset>` `<fieldset class="separator"><legend>${translate('Datalayers')}</legend></fieldset>`
) )
container.appendChild(fieldset) container.appendChild(fieldset)
this._umap.eachDataLayer((datalayer) => { this._umap.datalayers.active().map((datalayer) => {
datalayer.permissions.edit(fieldset) datalayer.permissions.edit(fieldset)
}) })
} }

View file

@ -327,7 +327,7 @@ export const LeafletMap = BaseMap.extend({
} else if (this.options.defaultView === 'latest') { } else if (this.options.defaultView === 'latest') {
this._umap.onceDataLoaded(() => { this._umap.onceDataLoaded(() => {
if (!this._umap.hasData()) return if (!this._umap.hasData()) return
const datalayer = this._umap.firstVisibleDatalayer() const datalayer = this._umap.datalayers.visible()[0]
let feature let feature
if (datalayer) { if (datalayer) {
const feature = datalayer.getFeatureByIndex(-1) const feature = datalayer.getFeatureByIndex(-1)

View file

@ -10,7 +10,7 @@ import { translate } from './i18n.js'
* - `type`: The type of the data * - `type`: The type of the data
* - `impacts`: A list of impacts than happen when this property is updated, among * - `impacts`: A list of impacts than happen when this property is updated, among
* 'ui', 'data', 'limit-bounds', 'datalayer-index', 'remote-data', * 'ui', 'data', 'limit-bounds', 'datalayer-index', 'remote-data',
* 'background' 'sync'. * 'background', 'sync', 'datalayer-rank'.
* *
* - Extra keys are being passed to the FormBuilder automatically. * - Extra keys are being passed to the FormBuilder automatically.
*/ */
@ -436,6 +436,10 @@ export const SCHEMA = {
], ],
default: 'Default', default: 'Default',
}, },
rank: {
type: Number,
impacts: ['datalayer-rank'],
},
remoteData: { remoteData: {
type: Object, type: Object,
impacts: ['remote-data'], impacts: ['remote-data'],

View file

@ -204,8 +204,8 @@ class IframeExporter {
delete this.queryString.feature delete this.queryString.feature
} }
if (this.options.keepCurrentDatalayers) { if (this.options.keepCurrentDatalayers) {
this._umap.eachDataLayer((datalayer) => { this._umap.datalayers.visible().map((datalayer) => {
if (datalayer.isVisible() && datalayer.createdOnServer) { if (datalayer.createdOnServer) {
datalayers.push(datalayer.id) datalayers.push(datalayer.id)
} }
}) })

View file

@ -66,7 +66,7 @@ export default class Slideshow extends WithTemplate {
} }
defaultDatalayer() { defaultDatalayer() {
return this._umap.findDataLayer((d) => d.canBrowse()) return this._umap.datalayers.find((d) => d.canBrowse())
} }
startSpinner() { startSpinner() {

View file

@ -207,22 +207,35 @@ export class SyncEngine {
this._send(operation) this._send(operation)
} }
async save() { _getDirtyObjects() {
const needSave = new Map() const dirty = new Map()
if (!this._umap.id) { if (!this._umap.id) {
// There is no operation for first map save // There is no operation for first map save
needSave.set(this._umap, []) dirty.set(this._umap, [])
}
const addDirtyObject = (operation) => {
const updater = this._getUpdater(operation.subject)
const obj = updater.getStoredObject(operation.metadata)
if (!dirty.has(obj)) {
dirty.set(obj, [])
}
dirty.get(obj).push(operation)
} }
for (const operation of this._operations.sorted()) { for (const operation of this._operations.sorted()) {
if (operation.dirty) { if (operation.dirty) {
const updater = this._getUpdater(operation.subject) addDirtyObject(operation)
const obj = updater.getStoredObject(operation.metadata) if (operation.verb === 'batch') {
if (!needSave.has(obj)) { for (const op of operation.operations) {
needSave.set(obj, []) addDirtyObject(op)
}
needSave.get(obj).push(operation)
} }
} }
}
}
return dirty
}
async save() {
const needSave = this._getDirtyObjects()
for (const [obj, operations] of needSave.entries()) { for (const [obj, operations] of needSave.entries()) {
const ok = await obj.save() const ok = await obj.save()
if (!ok) return false if (!ok) return false

View file

@ -207,7 +207,7 @@ export class BottomBar extends WithTemplate {
const select = this.elements.layers const select = this.elements.layers
const selected = select.options[select.selectedIndex].value const selected = select.options[select.selectedIndex].value
if (!selected) return if (!selected) return
this._umap.eachDataLayer((datalayer) => { this._umap.datalayers.active().map((datalayer) => {
datalayer.toggle(datalayer.id === selected) datalayer.toggle(datalayer.id === selected)
}) })
}) })
@ -228,7 +228,7 @@ export class BottomBar extends WithTemplate {
buildDataLayerSwitcher() { buildDataLayerSwitcher() {
this.elements.layers.innerHTML = '' this.elements.layers.innerHTML = ''
const datalayers = this._umap.datalayersIndex.filter((d) => d.options.inCaption) const datalayers = this._umap.datalayers.filter((d) => d.options.inCaption)
if (datalayers.length < 2) { if (datalayers.length < 2) {
this.elements.layers.hidden = true this.elements.layers.hidden = true
} else { } else {

View file

@ -33,6 +33,7 @@ import { EditPanel, FullPanel, Panel } from './ui/panel.js'
import Tooltip from './ui/tooltip.js' import Tooltip from './ui/tooltip.js'
import URLs from './urls.js' import URLs from './urls.js'
import * as Utils from './utils.js' import * as Utils from './utils.js'
import { DataLayerManager } from './managers.js'
export default class Umap { export default class Umap {
constructor(element, geojson) { constructor(element, geojson) {
@ -166,8 +167,7 @@ export default class Umap {
} }
// Global storage for retrieving datalayers and features. // Global storage for retrieving datalayers and features.
this.datalayers = {} // All datalayers, including deleted. this.datalayers = new DataLayerManager()
this.datalayersIndex = [] // Datalayers actually on the map and ordered.
this.featuresIndex = {} this.featuresIndex = {}
this.formatter = new Formatter(this) this.formatter = new Formatter(this)
@ -217,7 +217,6 @@ export default class Umap {
} }
window.onbeforeunload = () => (this.editEnabled && this.isDirty) || null window.onbeforeunload = () => (this.editEnabled && this.isDirty) || null
this.backup()
} }
get isDirty() { get isDirty() {
@ -616,7 +615,7 @@ export default class Umap {
this.datalayersLoaded = true this.datalayersLoaded = true
this.fire('datalayersloaded') this.fire('datalayersloaded')
const toLoad = [] const toLoad = []
for (const datalayer of this.datalayersIndex) { for (const datalayer of this.datalayers.active()) {
if (datalayer.showAtLoad()) toLoad.push(() => datalayer.show()) if (datalayer.showAtLoad()) toLoad.push(() => datalayer.show())
} }
while (toLoad.length) { while (toLoad.length) {
@ -630,7 +629,7 @@ export default class Umap {
createDataLayer(options = {}, sync = true) { createDataLayer(options = {}, sync = true) {
options.name = options.name =
options.name || `${translate('Layer')} ${this.datalayersIndex.length + 1}` options.name || `${translate('Layer')} ${this.datalayers.count() + 1}`
const datalayer = new DataLayer(this, this._leafletMap, options) const datalayer = new DataLayer(this, this._leafletMap, options)
if (sync !== false) { if (sync !== false) {
@ -651,19 +650,21 @@ export default class Umap {
} }
reindexDataLayers() { reindexDataLayers() {
this.eachDataLayer((datalayer) => datalayer.reindex()) this.datalayers.active().map((datalayer) => datalayer.reindex())
this.onDataLayersChanged() this.onDataLayersChanged()
} }
indexDatalayers() { reorderDataLayers() {
const panes = this._leafletMap.getPane('overlayPane') const parent = this._leafletMap.getPane('overlayPane')
const datalayers = Object.values(this.datalayers)
this.datalayersIndex = [] .filter((datalayer) => !datalayer._isDeleted)
for (const pane of panes.children) { .sort(
if (!pane.dataset || !pane.dataset.id) continue (datalayer1, datalayer2) => datalayer1.options.rank > datalayer2.options.rank
this.datalayersIndex.push(this.datalayers[pane.dataset.id]) )
for (const datalayer of datalayers) {
const child = parent.querySelector(`[data-id="${datalayer.id}"]`)
parent.appendChild(child)
} }
this.onDataLayersChanged()
} }
onceDatalayersLoaded(callback, context) { onceDatalayersLoaded(callback, context) {
@ -694,7 +695,6 @@ export default class Umap {
async saveAll() { async saveAll() {
if (!this.isDirty) return if (!this.isDirty) return
if (this._defaultExtent) this._setCenterAndZoom() if (this._defaultExtent) this._setCenterAndZoom()
this.backup()
const status = await this.sync.save() const status = await this.sync.save()
if (!status) return if (!status) return
// Do a blind render for now, as we are not sure what could // Do a blind render for now, as we are not sure what could
@ -714,24 +714,6 @@ export default class Umap {
return this.properties.name || translate('Untitled map') return this.properties.name || translate('Untitled map')
} }
backup() {
this.backupProperties()
this._datalayersIndex_bk = [].concat(this.datalayersIndex)
}
backupProperties() {
this._backupProperties = Object.assign({}, this.properties)
this._backupProperties.tilelayer = Object.assign({}, this.properties.tilelayer)
this._backupProperties.limitBounds = Object.assign({}, this.properties.limitBounds)
this._backupProperties.permissions = Object.assign({}, this.permissions.properties)
}
resetProperties() {
this.properties = Object.assign({}, this._backupProperties)
this.properties.tilelayer = Object.assign({}, this._backupProperties.tilelayer)
this.permissions.properties = Object.assign({}, this._backupProperties.permissions)
}
setProperties(newProperties) { setProperties(newProperties) {
for (const key of Object.keys(SCHEMA)) { for (const key of Object.keys(SCHEMA)) {
if (newProperties[key] !== undefined) { if (newProperties[key] !== undefined) {
@ -744,24 +726,24 @@ export default class Umap {
} }
hasData() { hasData() {
for (const datalayer of this.datalayersIndex) { for (const datalayer of this.datalayers.active()) {
if (datalayer.hasData()) return true if (datalayer.hasData()) return true
} }
} }
hasLayers() { hasLayers() {
return Boolean(this.datalayersIndex.length) return Boolean(this.datalayers.count())
} }
allProperties() { allProperties() {
return [].concat(...this.datalayersIndex.map((dl) => dl.allProperties())) return [].concat(...this.datalayers.active().map((dl) => dl.allProperties()))
} }
sortedValues(property) { sortedValues(property) {
return [] return []
.concat(...this.datalayersIndex.map((dl) => dl.sortedValues(property))) .concat(...this.datalayers.active().map((dl) => dl.sortedValues(property)))
.filter((val, idx, arr) => arr.indexOf(val) === idx) .filter((val, idx, arr) => arr.indexOf(val) === idx)
.sort(U.Utils.naturalSort) .sort(Utils.naturalSort)
} }
editCaption() { editCaption() {
@ -1278,7 +1260,7 @@ export default class Umap {
toGeoJSON() { toGeoJSON() {
let features = [] let features = []
this.eachDataLayer((datalayer) => { this.datalayers.active().map((datalayer) => {
if (datalayer.isVisible()) { if (datalayer.isVisible()) {
features = features.concat(datalayer.featuresToGeoJSON()) features = features.concat(datalayer.featuresToGeoJSON())
} }
@ -1354,13 +1336,20 @@ export default class Umap {
if (fields.includes('properties.rules')) { if (fields.includes('properties.rules')) {
this.rules.load() this.rules.load()
} }
this.eachVisibleDataLayer((datalayer) => { this.datalayers.visible().map((datalayer) => {
datalayer.redraw() datalayer.redraw()
}) })
break break
case 'datalayer-index': case 'datalayer-index':
this.reindexDataLayers() this.reindexDataLayers()
break break
case 'datalayer-rank':
// When drag'n'dropping datalayers,
// this get called once per datalayers.
// (and same for undo/redo of the action)
// TODO: call only once
this.reorderDataLayers()
break
case 'background': case 'background':
this._leafletMap.initTileLayers() this._leafletMap.initTileLayers()
break break
@ -1449,7 +1438,7 @@ export default class Umap {
) { ) {
return datalayer return datalayer
} }
datalayer = this.findDataLayer((datalayer) => { datalayer = this.datalayers.find((datalayer) => {
if (!datalayer.isDataReadOnly() && datalayer.isBrowsable()) { if (!datalayer.isDataReadOnly() && datalayer.isBrowsable()) {
fallback = datalayer fallback = datalayer
if (datalayer.isVisible()) return true if (datalayer.isVisible()) return true
@ -1464,49 +1453,20 @@ export default class Umap {
return this.createDirtyDataLayer() return this.createDirtyDataLayer()
} }
findDataLayer(method, context) { eachFeature(callback) {
for (let i = this.datalayersIndex.length - 1; i >= 0; i--) { this.datalayers.browsable().map((datalayer) => {
if (method.call(context, this.datalayersIndex[i])) { if (datalayer.isVisible()) datalayer.eachFeature(callback)
return this.datalayersIndex[i]
}
}
}
eachDataLayer(method, context) {
for (let i = 0; i < this.datalayersIndex.length; i++) {
method.call(context, this.datalayersIndex[i])
}
}
eachDataLayerReverse(method, context, filter) {
for (let i = this.datalayersIndex.length - 1; i >= 0; i--) {
if (filter && !filter.call(context, this.datalayersIndex[i])) continue
method.call(context, this.datalayersIndex[i])
}
}
eachBrowsableDataLayer(method, context) {
this.eachDataLayerReverse(method, context, (d) => d.allowBrowse())
}
eachVisibleDataLayer(method, context) {
this.eachDataLayerReverse(method, context, (d) => d.isVisible())
}
eachFeature(callback, context) {
this.eachBrowsableDataLayer((datalayer) => {
if (datalayer.isVisible()) datalayer.eachFeature(callback, context)
}) })
} }
removeDataLayers() { removeDataLayers() {
this.eachDataLayerReverse((datalayer) => { this.datalayers.active().map((datalayer) => {
datalayer.del() datalayer.del()
}) })
} }
emptyDataLayers() { emptyDataLayers() {
this.eachDataLayerReverse((datalayer) => { this.datalayers.active().map((datalayer) => {
datalayer.empty() datalayer.empty()
}) })
} }
@ -1520,7 +1480,7 @@ export default class Umap {
</div> </div>
` `
const [container, { ul }] = Utils.loadTemplateWithRefs(template) const [container, { ul }] = Utils.loadTemplateWithRefs(template)
this.eachDataLayerReverse((datalayer) => { this.datalayers.reverse().map((datalayer) => {
const row = Utils.loadTemplate( const row = Utils.loadTemplate(
`<li class="orderable"><i class="icon icon-16 icon-drag" title="${translate('Drag to reorder')}"></i></li>` `<li class="orderable"><i class="icon icon-16 icon-drag" title="${translate('Drag to reorder')}"></i></li>`
) )
@ -1539,16 +1499,22 @@ export default class Umap {
const onReorder = (src, dst, initialIndex, finalIndex) => { const onReorder = (src, dst, initialIndex, finalIndex) => {
const movedLayer = this.datalayers[src.dataset.id] const movedLayer = this.datalayers[src.dataset.id]
const targetLayer = this.datalayers[dst.dataset.id] const targetLayer = this.datalayers[dst.dataset.id]
const minIndex = Math.min(movedLayer.getRank(), targetLayer.getRank()) const minIndex = Math.min(movedLayer.getDOMOrder(), targetLayer.getDOMOrder())
const maxIndex = Math.max(movedLayer.getRank(), targetLayer.getRank()) const maxIndex = Math.max(movedLayer.getDOMOrder(), targetLayer.getDOMOrder())
if (finalIndex === 0) movedLayer.bringToTop() if (finalIndex === 0) movedLayer.bringToTop()
else if (finalIndex > initialIndex) movedLayer.insertBefore(targetLayer) else if (finalIndex > initialIndex) movedLayer.insertBefore(targetLayer)
else movedLayer.insertAfter(targetLayer) else movedLayer.insertAfter(targetLayer)
this.eachDataLayerReverse((datalayer) => { this.sync.startBatch()
if (datalayer.getRank() >= minIndex && datalayer.getRank() <= maxIndex) this.datalayers.reverse().map((datalayer) => {
datalayer.isDirty = true const rank = datalayer.getDOMOrder()
if (rank >= minIndex && rank <= maxIndex) {
const oldRank = datalayer.options.rank
datalayer.options.rank = rank
datalayer.sync.update('options.rank', rank, oldRank)
}
}) })
this.indexDatalayers() this.sync.commitBatch()
this.onDataLayersChanged()
} }
const orderable = new Orderable(ul, onReorder) const orderable = new Orderable(ul, onReorder)
@ -1570,18 +1536,6 @@ export default class Umap {
return datalayer return datalayer
} }
firstVisibleDatalayer() {
return this.findDataLayer((datalayer) => {
if (datalayer.isVisible()) return true
})
}
ensurePanesOrder() {
this.eachDataLayer((datalayer) => {
datalayer.bringToTop()
})
}
openBrowser(mode) { openBrowser(mode) {
this.onceDatalayersLoaded(() => this.browser.open(mode)) this.onceDatalayersLoaded(() => this.browser.open(mode))
} }
@ -1732,7 +1686,7 @@ export default class Umap {
getLayersBounds() { getLayersBounds() {
const bounds = new latLngBounds() const bounds = new latLngBounds()
this.eachBrowsableDataLayer((d) => { this.datalayers.browsable().map((d) => {
if (d.isVisible()) bounds.extend(d.layer.getBounds()) if (d.isVisible()) bounds.extend(d.layer.getBounds())
}) })
return bounds return bounds

View file

@ -131,8 +131,8 @@ class DataLayerFactory(factory.django.DjangoModelFactory):
data.setdefault("_umap_options", {}) data.setdefault("_umap_options", {})
if "name" in data["_umap_options"] and kwargs["name"] == cls.name: if "name" in data["_umap_options"] and kwargs["name"] == cls.name:
kwargs["name"] = data["_umap_options"]["name"] kwargs["name"] = data["_umap_options"]["name"]
if "settings" not in kwargs: kwargs.setdefault("settings", {})
kwargs["settings"] = data.get("_umap_options", {}) kwargs["settings"].update(data.get("_umap_options", {}))
else: else:
data = DATALAYER_DATA.copy() data = DATALAYER_DATA.copy()
data["_umap_options"] = { data["_umap_options"] = {

View file

@ -50,6 +50,8 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
"editMode": "advanced", "editMode": "advanced",
"inCaption": True, "inCaption": True,
"id": str(datalayer.pk), "id": str(datalayer.pk),
"rank": 0,
"remoteData": {},
} }
# Now navigate to this map from another tab # Now navigate to this map from another tab
@ -87,6 +89,8 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
"inCaption": True, "inCaption": True,
"editMode": "advanced", "editMode": "advanced",
"id": str(datalayer.pk), "id": str(datalayer.pk),
"rank": 0,
"remoteData": {},
} }
# Now create another marker in the first tab # Now create another marker in the first tab
@ -105,7 +109,8 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
"inCaption": True, "inCaption": True,
"editMode": "advanced", "editMode": "advanced",
"id": str(datalayer.pk), "id": str(datalayer.pk),
"permissions": {"edit_status": 1}, "rank": 0,
"remoteData": {},
} }
# And again # And again
@ -124,7 +129,8 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
"inCaption": True, "inCaption": True,
"editMode": "advanced", "editMode": "advanced",
"id": str(datalayer.pk), "id": str(datalayer.pk),
"permissions": {"edit_status": 1}, "rank": 0,
"remoteData": {},
} }
expect(marker_pane_p1).to_have_count(4) expect(marker_pane_p1).to_have_count(4)
@ -145,7 +151,8 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
"inCaption": True, "inCaption": True,
"editMode": "advanced", "editMode": "advanced",
"id": str(datalayer.pk), "id": str(datalayer.pk),
"permissions": {"edit_status": 1}, "rank": 0,
"remoteData": {},
} }
expect(marker_pane_p2).to_have_count(5) expect(marker_pane_p2).to_have_count(5)
@ -271,7 +278,8 @@ def test_same_second_edit_doesnt_conflict(context, live_server, tilelayer):
"inCaption": True, "inCaption": True,
"editMode": "advanced", "editMode": "advanced",
"id": str(datalayer.pk), "id": str(datalayer.pk),
"permissions": {"edit_status": 1}, "rank": 0,
"remoteData": {},
} }