From c952fed96ab77264ea745e9cd5fa1ebb2cd08ac9 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 5 Nov 2024 11:53:57 +0100 Subject: [PATCH] chore: split umap.js in two modules --- umap/static/umap/js/components/fragment.js | 4 +- umap/static/umap/js/modules/browser.js | 36 +- umap/static/umap/js/modules/caption.js | 42 +- umap/static/umap/js/modules/data/features.js | 93 +- umap/static/umap/js/modules/data/layer.js | 149 +- umap/static/umap/js/modules/facets.js | 18 +- umap/static/umap/js/modules/formatter.js | 10 +- umap/static/umap/js/modules/importer.js | 30 +- umap/static/umap/js/modules/permissions.js | 71 +- .../umap/js/modules/rendering/layers/base.js | 2 +- .../js/modules/rendering/layers/classified.js | 2 +- .../js/modules/rendering/layers/cluster.js | 2 +- .../umap/js/modules/rendering/layers/heat.js | 2 +- umap/static/umap/js/modules/rendering/map.js | 583 ++++++ .../static/umap/js/modules/rendering/popup.js | 6 +- umap/static/umap/js/modules/rendering/ui.js | 20 +- umap/static/umap/js/modules/rules.js | 38 +- umap/static/umap/js/modules/saving.js | 1 - umap/static/umap/js/modules/share.js | 41 +- umap/static/umap/js/modules/slideshow.js | 12 +- umap/static/umap/js/modules/tableeditor.js | 34 +- umap/static/umap/js/modules/ui/panel.js | 18 +- umap/static/umap/js/modules/umap.js | 1603 ++++++++++++++++ umap/static/umap/js/modules/utils.js | 2 +- umap/static/umap/js/umap.controls.js | 345 +--- umap/static/umap/js/umap.core.js | 42 - umap/static/umap/js/umap.forms.js | 24 +- umap/static/umap/js/umap.js | 1674 ----------------- .../formbuilder/Leaflet.FormBuilder.js | 6 +- umap/templates/umap/js.html | 3 +- umap/templates/umap/map_init.html | 9 +- umap/templates/umap/user_dashboard.html | 37 +- umap/tests/integration/test_import.py | 4 +- 33 files changed, 2605 insertions(+), 2358 deletions(-) create mode 100644 umap/static/umap/js/modules/rendering/map.js create mode 100644 umap/static/umap/js/modules/umap.js diff --git a/umap/static/umap/js/components/fragment.js b/umap/static/umap/js/components/fragment.js index 63613ca1..d76aaf46 100644 --- a/umap/static/umap/js/components/fragment.js +++ b/umap/static/umap/js/components/fragment.js @@ -1,6 +1,8 @@ +import Umap from '../modules/umap.js' + class UmapFragment extends HTMLElement { connectedCallback() { - new U.Map(this.firstElementChild.id, JSON.parse(this.dataset.settings)) + new Umap(this.firstElementChild.id, JSON.parse(this.dataset.settings)) } } diff --git a/umap/static/umap/js/modules/browser.js b/umap/static/umap/js/modules/browser.js index 6af51fa6..b8f59dac 100644 --- a/umap/static/umap/js/modules/browser.js +++ b/umap/static/umap/js/modules/browser.js @@ -6,9 +6,9 @@ import { EXPORT_FORMATS } from './formatter.js' import ContextMenu from './ui/contextmenu.js' export default class Browser { - constructor(map) { - this.map = map - this.map.on('moveend', this.onMoveEnd, this) + constructor(umap) { + this.umap = umap + this.umap._leafletMap.on('moveend', this.onMoveEnd, this) this.options = { filter: '', inBbox: false, @@ -82,7 +82,7 @@ export default class Browser { updateDatalayer(datalayer) { // Compute once, but use it for each feature later. - this.bounds = this.map.getBounds() + this.bounds = this.umap._leafletMap.getBounds() const parent = DomUtil.get(this.datalayerId(datalayer)) // Panel is not open if (!parent) return @@ -115,10 +115,10 @@ export default class Browser { } onFormChange() { - this.map.eachBrowsableDataLayer((datalayer) => { + this.umap.eachBrowsableDataLayer((datalayer) => { datalayer.resetLayer(true) this.updateDatalayer(datalayer) - if (this.map.fullPanel?.isOpen()) datalayer.tableEdit() + if (this.umap.fullPanel?.isOpen()) datalayer.tableEdit() }) this.toggleBadge() } @@ -132,13 +132,13 @@ export default class Browser { } hasFilters() { - return !!this.options.filter || this.map.facets.isActive() + return !!this.options.filter || this.umap.facets.isActive() } onMoveEnd() { if (!this.isOpen()) return const isListDynamic = this.options.inBbox - this.map.eachBrowsableDataLayer((datalayer) => { + this.umap.eachBrowsableDataLayer((datalayer) => { if (!isListDynamic && !datalayer.hasDynamicData()) return this.updateDatalayer(datalayer) }) @@ -147,7 +147,7 @@ export default class Browser { update() { if (!this.isOpen()) return this.dataContainer.innerHTML = '' - this.map.eachBrowsableDataLayer((datalayer) => { + this.umap.eachBrowsableDataLayer((datalayer) => { this.addDataLayer(datalayer, this.dataContainer) }) } @@ -186,9 +186,9 @@ export default class Browser { DomEvent.on(builder.form, 'reset', () => { window.setTimeout(builder.syncAll.bind(builder)) }) - if (this.map.options.facetKey) { - fields = this.map.facets.build() - filtersBuilder = new L.FormBuilder(this.map.facets, fields, { + if (this.umap.properties.facetKey) { + fields = this.umap.facets.build() + filtersBuilder = new L.FormBuilder(this.umap.facets, fields, { callback: () => this.onFormChange(), }) DomEvent.on(filtersBuilder.form, 'reset', () => { @@ -206,7 +206,7 @@ export default class Browser { textContent: translate('Reset all'), }) - this.map.panel.open({ + this.umap.panel.open({ content: container, className: 'umap-browser', }) @@ -230,7 +230,7 @@ export default class Browser { `) container.appendChild(toolbox) toggle.addEventListener('click', () => this.toggleLayers()) - fitBounds.addEventListener('click', () => this.map.fitDataBounds()) + fitBounds.addEventListener('click', () => this.umap.fitDataBounds()) download.addEventListener('click', () => this.downloadVisible(download)) } @@ -240,7 +240,7 @@ export default class Browser { for (const format of Object.keys(EXPORT_FORMATS)) { items.push({ label: format, - action: () => this.map.share.download(format), + action: () => this.umap.share.download(format), }) } menu.openBelow(element, items) @@ -250,10 +250,10 @@ export default class Browser { // If at least one layer is shown, hide it // otherwise show all let allHidden = true - this.map.eachBrowsableDataLayer((datalayer) => { + this.umap.eachBrowsableDataLayer((datalayer) => { if (datalayer.isVisible()) allHidden = false }) - this.map.eachBrowsableDataLayer((datalayer) => { + this.umap.eachBrowsableDataLayer((datalayer) => { if (allHidden) { datalayer.show() } else { @@ -271,7 +271,7 @@ export default class Browser { // Fixme: remove me when this is merged and released // https://github.com/Leaflet/Leaflet/pull/9052 DomEvent.disableClickPropagation(button) - DomEvent.on(button, 'click', map.openBrowser, map) + DomEvent.on(button, 'click', map.umap.openBrowser, map.umap) return button } } diff --git a/umap/static/umap/js/modules/caption.js b/umap/static/umap/js/modules/caption.js index d68d478f..1fa62e48 100644 --- a/umap/static/umap/js/modules/caption.js +++ b/umap/static/umap/js/modules/caption.js @@ -3,8 +3,8 @@ import { translate } from './i18n.js' import * as Utils from './utils.js' export default class Caption { - constructor(map) { - this.map = map + constructor(umap) { + this.umap = umap } isOpen() { @@ -21,38 +21,36 @@ export default class Caption { const hgroup = DomUtil.element({ tagName: 'hgroup', parent: container }) DomUtil.createTitle( hgroup, - this.map.getDisplayName(), + this.umap.getDisplayName(), 'icon-caption icon-block', 'map-name' ) - this.map.addAuthorLink('h4', hgroup) - if (this.map.options.description) { + const title = Utils.loadTemplate('

') + hgroup.appendChild(title) + this.umap.addAuthorLink(title) + if (this.umap.properties.description) { const description = DomUtil.element({ tagName: 'div', className: 'umap-map-description text', - safeHTML: Utils.toHTML(this.map.options.description), + safeHTML: Utils.toHTML(this.umap.properties.description), parent: container, }) } const datalayerContainer = DomUtil.create('div', 'datalayer-container', container) - this.map.eachDataLayerReverse((datalayer) => + this.umap.eachDataLayerReverse((datalayer) => this.addDataLayer(datalayer, datalayerContainer) ) const creditsContainer = DomUtil.create('div', 'credits-container', container) this.addCredits(creditsContainer) - this.map.panel.open({ content: container }).then(() => { + this.umap.panel.open({ content: container }).then(() => { // Create the legend when the panel is actually on the DOM - this.map.eachDataLayerReverse((datalayer) => datalayer.renderLegend()) + this.umap.eachDataLayerReverse((datalayer) => datalayer.renderLegend()) }) } addDataLayer(datalayer, container) { if (!datalayer.options.inCaption) return - const p = DomUtil.create( - 'p', - `caption-item ${datalayer.cssId}`, - container - ) + const p = DomUtil.create('p', `caption-item ${datalayer.cssId}`, container) const legend = DomUtil.create('span', 'datalayer-legend', p) const headline = DomUtil.create('strong', '', p) if (datalayer.options.description) { @@ -69,16 +67,16 @@ export default class Caption { addCredits(container) { const credits = DomUtil.createFieldset(container, translate('Credits')) let title = DomUtil.add('h5', '', credits, translate('User content credits')) - if (this.map.options.shortCredit || this.map.options.longCredit) { + if (this.umap.properties.shortCredit || this.umap.properties.longCredit) { DomUtil.element({ tagName: 'p', parent: credits, safeHTML: Utils.toHTML( - this.map.options.longCredit || this.map.options.shortCredit + this.umap.properties.longCredit || this.umap.properties.shortCredit ), }) } - if (this.map.options.licence) { + if (this.umap.properties.licence) { const licence = DomUtil.add( 'p', '', @@ -88,8 +86,8 @@ export default class Caption { DomUtil.createLink( '', licence, - this.map.options.licence.name, - this.map.options.licence.url + this.umap.properties.licence.name, + this.umap.properties.licence.url ) } else { DomUtil.add('p', '', credits, translate('No licence has been set')) @@ -100,19 +98,19 @@ export default class Caption { DomUtil.element({ tagName: 'strong', parent: tilelayerCredit, - textContent: `${this.map.selected_tilelayer.options.name} `, + textContent: `${this.umap._leafletMap.selectedTilelayer.options.name} `, }) DomUtil.element({ tagName: 'span', parent: tilelayerCredit, - safeHTML: this.map.selected_tilelayer.getAttribution(), + safeHTML: this.umap._leafletMap.selectedTilelayer.getAttribution(), }) const urls = { leaflet: 'http://leafletjs.com', django: 'https://www.djangoproject.com', umap: 'https://umap-project.org/', changelog: 'https://docs.umap-project.org/en/master/changelog/', - version: this.map.options.umap_version, + version: this.umap.properties.umap_version, } const creditHTML = translate( ` diff --git a/umap/static/umap/js/modules/data/features.js b/umap/static/umap/js/modules/data/features.js index 260ed28f..cd118d30 100644 --- a/umap/static/umap/js/modules/data/features.js +++ b/umap/static/umap/js/modules/data/features.js @@ -19,7 +19,7 @@ import loadPopup from '../rendering/popup.js' class Feature { constructor(datalayer, geojson = {}, id = null) { - this.sync = datalayer.map.sync_engine.proxy(this) + this.sync = datalayer.umap.sync_engine.proxy(this) this._marked_for_deletion = false this._isDirty = false this._ui = null @@ -69,8 +69,8 @@ class Feature { return this._ui } - get map() { - return this.datalayer?.map + get umap() { + return this.datalayer?.umap } get center() { @@ -168,7 +168,7 @@ class Feature { } getSlug() { - return this.properties[this.map.getOption('slugKey') || 'name'] || '' + return this.properties[this.umap.getOption('slugKey') || 'name'] || '' } getPermalink() { @@ -196,10 +196,10 @@ class Feature { return } // TODO deal with an event instead? - if (this.map.slideshow) { - this.map.slideshow.current = this + if (this.umap.slideshow) { + this.umap.slideshow.current = this } - this.map.currentFeature = this + this.umap.currentFeature = this this.attachPopup() this.ui.openPopup(latlng || this.center) } @@ -209,7 +209,7 @@ class Feature { return field.startsWith('properties.') }) if (impactData) { - if (this.map.currentFeature === this) { + if (this.umap.currentFeature === this) { this.view() } } @@ -217,7 +217,7 @@ class Feature { } edit(event) { - if (!this.map.editEnabled || this.isReadOnly()) return + if (!this.umap.editEnabled || this.isReadOnly()) return const container = DomUtil.create('div', 'umap-feature-container') DomUtil.createTitle( container, @@ -256,12 +256,12 @@ class Feature { translate('Advanced actions') ) this.getAdvancedEditActions(advancedActions) - const onLoad = this.map.editPanel.open({ content: container }) + const onLoad = this.umap.editPanel.open({ content: container }) onLoad.then(() => { builder.helpers['properties.name'].input.focus() }) - this.map.editedFeature = this - if (!this.ui.isOnScreen(this.map.getBounds())) this.zoomTo(event) + this.umap.editedFeature = this + if (!this.ui.isOnScreen(this.umap._leafletMap.getBounds())) this.zoomTo(event) } getAdvancedEditActions(container) { @@ -270,7 +270,7 @@ class Feature { ${translate('Delete')} `) button.addEventListener('click', () => { - this.confirmDelete().then(() => this.map.editPanel.close()) + this.confirmDelete().then(() => this.umap.editPanel.close()) }) container.appendChild(button) } @@ -333,7 +333,7 @@ class Feature { if (this.datalayer.isRemoteLayer() && this.datalayer.options.remoteData.dynamic) { return false } - return this.map.getOption('displayPopupFooter') + return this.umap.getOption('displayPopupFooter') } getPopupClass() { @@ -347,7 +347,7 @@ class Feature { } async confirmDelete() { - const confirmed = await this.map.dialog.confirm( + const confirmed = await this.umap.dialog.confirm( translate('Are you sure you want to delete the feature?') ) if (confirmed) { @@ -359,7 +359,7 @@ class Feature { del(sync) { this.isDirty = true - this.map.closePopup() + this.umap._leafletMap.closePopup() if (this.datalayer) { this.datalayer.removeFeature(this, sync) } @@ -422,7 +422,7 @@ class Feature { } else if (this.datalayer) { value = this.datalayer.getOption(option, this) } else { - value = this.map.getOption(option) + value = this.umap.getOption(option) } return value } @@ -432,19 +432,19 @@ class Feature { // There is a variable inside. if (U.Utils.hasVar(value)) { value = U.Utils.greedyTemplate(value, this.properties, true) - if (U.Utils.hasVar(value)) value = this.map.getDefaultOption(option) + if (U.Utils.hasVar(value)) value = this.umap.getDefaultOption(option) } return value } zoomTo({ easing, latlng, callback } = {}) { - if (easing === undefined) easing = this.map.getOption('easing') - if (callback) this.map.once('moveend', callback.bind(this)) + if (easing === undefined) easing = this.umap.getOption('easing') + if (callback) this.umap._leafletMap.once('moveend', callback.bind(this)) if (easing) { - this.map.flyTo(this.center, this.getBestZoom()) + this.umap._leafletMap.flyTo(this.center, this.getBestZoom()) } else { latlng = latlng || this.center - this.map.setView(latlng, this.getBestZoom() || this.map.getZoom()) + this.umap._leafletMap.setView(latlng, this.getBestZoom() || this.umap._leafletMap.getZoom()) } } @@ -500,7 +500,7 @@ class Feature { isFiltered() { const filterKeys = this.datalayer.getFilterKeys() - const filter = this.map.browser.options.filter + const filter = this.umap.browser.options.filter if (filter && !this.matchFilter(filter, filterKeys)) return true if (!this.matchFacets()) return true return false @@ -525,10 +525,10 @@ class Feature { } matchFacets() { - const selected = this.map.facets.selected + const selected = this.umap.facets.selected for (const [name, { type, min, max, choices }] of Object.entries(selected)) { let value = this.properties[name] - const parser = this.map.facets.getParser(type) + const parser = this.umap.facets.getParser(type) value = parser(value) switch (type) { case 'date': @@ -562,10 +562,10 @@ class Feature { extendedProperties() { // Include context properties - const properties = this.map.getGeoContext() + const properties = this.umap.getGeoContext() const locale = L.getLocale() if (locale) properties.locale = locale - if (L.lang) properties.lang = L.lang + if (U.lang) properties.lang = U.lang properties.rank = this.getRank() + 1 properties.layer = this.datalayer.getName() if (this.ui._map && this.hasGeom()) { @@ -612,10 +612,10 @@ class Feature { label: translate('Copy as GeoJSON'), action: () => { L.Util.copyToClipboard(JSON.stringify(this.toGeoJSON())) - this.map.tooltip.open({ content: L._('✅ Copied!') }) + this.umap.tooltip.open({ content: L._('✅ Copied!') }) }, }) - if (this.map.editEnabled && !this.isReadOnly()) { + if (this.umap.editEnabled && !this.isReadOnly()) { items = items.concat(this.getContextMenuEditItems(event)) } return items @@ -623,7 +623,7 @@ class Feature { getContextMenuEditItems() { let items = ['-'] - if (this.map.editedFeature !== this) { + if (this.umap.editedFeature !== this) { items.push({ label: `${translate('Edit this feature')} (⇧+Click)`, action: () => this.edit(), @@ -631,7 +631,7 @@ class Feature { } items = items.concat( { - label: this.map.help.displayLabel('EDIT_FEATURE_LAYER'), + label: this.umap.help.displayLabel('EDIT_FEATURE_LAYER'), action: () => this.datalayer.edit(), }, { @@ -750,17 +750,17 @@ class Path extends Feature { } edit(event) { - if (this.map.editEnabled) { + if (this.umap.editEnabled) { super.edit(event) if (!this.ui.editEnabled()) this.ui.makeGeometryEditable() } } _toggleEditing(event) { - if (this.map.editEnabled) { + if (this.umap.editEnabled) { if (this.ui.editEnabled()) { this.endEdit() - this.map.editPanel.close() + this.umap.editPanel.close() } else { this.edit(event) } @@ -786,7 +786,9 @@ class Path extends Feature { } getBestZoom() { - return this.getOption('zoomTo') || this.map.getBoundsZoom(this.bounds, true) + return ( + this.getOption('zoomTo') || this.umap._leafletMap.getBoundsZoom(this.bounds, true) + ) } endEdit() { @@ -825,11 +827,14 @@ class Path extends Feature { zoomTo({ easing, callback }) { // Use bounds instead of centroid for paths. - easing = easing || this.map.getOption('easing') + easing = easing || this.umap.getOption('easing') if (easing) { - this.map.flyToBounds(this.bounds, this.getBestZoom()) + this.umap._leafletMap.flyToBounds(this.bounds, this.getBestZoom()) } else { - this.map.fitBounds(this.bounds, this.getBestZoom() || this.map.getZoom()) + this.umap._leafletMap.fitBounds( + this.bounds, + this.getBestZoom() || this.umap._leafletMap.getZoom() + ) } if (callback) callback.call(this) } @@ -840,7 +845,7 @@ class Path extends Feature { label: translate('Display measure'), action: () => Alert.info(this.ui.getMeasure()), }) - if (this.map.editEnabled && !this.isReadOnly() && this.isMulti()) { + if (this.umap.editEnabled && !this.isReadOnly() && this.isMulti()) { items.push(...this.getContextMenuMultiItems(event)) } return items @@ -871,11 +876,11 @@ class Path extends Feature { getContextMenuEditItems(event) { const items = super.getContextMenuEditItems(event) - if (this.map?.editedFeature !== this && this.isSameClass(this.map.editedFeature)) { + if (this.map?.editedFeature !== this && this.isSameClass(this.umap.editedFeature)) { items.push({ label: translate('Transfer shape to edited feature'), action: () => { - this.transferShape(event.latlng, this.map.editedFeature) + this.transferShape(event.latlng, this.umap.editedFeature) }, }) } @@ -977,8 +982,8 @@ export class LineString extends Path { } const a = toMerge[0] const b = toMerge[1] - const p1 = this.map.latLngToContainerPoint(a[a.length - 1]) - const p2 = this.map.latLngToContainerPoint(b[0]) + const p1 = this.umap._leafletMap.latLngToContainerPoint(a[a.length - 1]) + const p2 = this.umap._leafletMap.latLngToContainerPoint(b[0]) const tolerance = 5 // px on screen if (Math.abs(p1.x - p2.x) <= tolerance && Math.abs(p1.y - p2.y) <= tolerance) { a.pop() @@ -1022,7 +1027,7 @@ export class LineString extends Path { }) } else if (index === 0 || index === event.vertex.getLastIndex()) { items.push({ - label: this.map.help.displayLabel('CONTINUE_LINE'), + label: this.umap.help.displayLabel('CONTINUE_LINE'), action: () => event.vertex.continue(), }) } diff --git a/umap/static/umap/js/modules/data/layer.js b/umap/static/umap/js/modules/data/layer.js index 1602cf52..33098d2c 100644 --- a/umap/static/umap/js/modules/data/layer.js +++ b/umap/static/umap/js/modules/data/layer.js @@ -37,10 +37,10 @@ const LAYER_MAP = LAYER_TYPES.reduce((acc, klass) => { }, {}) export class DataLayer extends ServerStored { - constructor(map, data) { + constructor(umap, data) { super() - this.map = map - this.sync = map.sync_engine.proxy(this) + this.umap = umap + this.sync = umap.sync_engine.proxy(this) this._index = Array() this._features = {} this._geojson = null @@ -48,8 +48,11 @@ export class DataLayer extends ServerStored { this._loaded = false // Are layer metadata loaded this._dataloaded = false // Are layer data loaded - this.parentPane = this.map.getPane('overlayPane') - this.pane = this.map.createPane(`datalayer${stamp(this)}`, this.parentPane) + this.parentPane = this.umap._leafletMap.getPane('overlayPane') + this.pane = this.umap._leafletMap.createPane( + `datalayer${stamp(this)}`, + this.parentPane + ) this.pane.dataset.id = stamp(this) // FIXME: should be on layer this.renderer = L.svg({ pane: this.pane }) @@ -128,7 +131,7 @@ export class DataLayer extends ServerStored { for (const impact of impacts) { switch (impact) { case 'ui': - this.map.onDataLayersChanged() + this.umap.onDataLayersChanged() break case 'data': if (fields.includes('options.type')) { @@ -153,8 +156,8 @@ export class DataLayer extends ServerStored { } autoLoaded() { - if (!this.map.datalayersFromQueryString) return this.options.displayOnLoad - const datalayerIds = this.map.datalayersFromQueryString + if (!this.umap.datalayersFromQueryString) return this.options.displayOnLoad + const datalayerIds = this.umap.datalayersFromQueryString let loadMe = datalayerIds.includes(this.umap_id.toString()) if (this.options.old_id) { loadMe = loadMe || datalayerIds.includes(this.options.old_id.toString()) @@ -192,7 +195,7 @@ export class DataLayer extends ServerStored { const visible = this.isVisible() if (this.layer) this.layer.clearLayers() // delete this.layer? - if (visible) this.map.removeLayer(this.layer) + if (visible) this.umap._leafletMap.removeLayer(this.layer) const Class = LAYER_MAP[this.options.type] || DefaultLayer this.layer = new Class(this) // Rendering layer changed, so let's force reset the feature rendering too. @@ -213,7 +216,7 @@ export class DataLayer extends ServerStored { if (!this.umap_id) return if (this._loading) return this._loading = true - const [geojson, response, error] = await this.map.server.get(this._dataUrl()) + const [geojson, response, error] = await this.umap.server.get(this._dataUrl()) if (!error) { this._reference_version = response.headers.get('X-Datalayer-Version') // FIXME: for now this property is set dynamically from backend @@ -234,7 +237,7 @@ export class DataLayer extends ServerStored { dataChanged() { if (!this.hasDataLoaded()) return - this.map.onDataLayersChanged() + this.umap.onDataLayersChanged() this.layer.dataChanged() } @@ -275,14 +278,14 @@ export class DataLayer extends ServerStored { reindex() { const features = Object.values(this._features) - Utils.sortFeatures(features, this.map.getOption('sortKey'), L.lang) + Utils.sortFeatures(features, this.umap.getOption('sortKey'), U.lang) this._index = features.map((feature) => stamp(feature)) } showAtZoom() { const from = Number.parseInt(this.options.fromZoom, 10) const to = Number.parseInt(this.options.toZoom, 10) - const zoom = this.map.getZoom() + const zoom = this.umap._leafletMap.getZoom() return !((!Number.isNaN(from) && zoom < from) || (!Number.isNaN(to) && zoom > to)) } @@ -294,14 +297,14 @@ export class DataLayer extends ServerStored { if (!this.isRemoteLayer()) return if (!this.hasDynamicData() && this.hasDataLoaded() && !force) return if (!this.isVisible()) return - let url = this.map.localizeUrl(this.options.remoteData.url) + let url = this.umap.localizeUrl(this.options.remoteData.url) if (this.options.remoteData.proxy) { - url = this.map.proxyUrl(url, this.options.remoteData.ttl) + url = this.umap.proxyUrl(url, this.options.remoteData.ttl) } - const response = await this.map.request.get(url) + const response = await this.umap.request.get(url) if (response?.ok) { this.clear() - this.map.formatter + this.umap.formatter .parse(await response.text(), this.options.remoteData.format) .then((geojson) => this.fromGeoJSON(geojson)) } @@ -341,25 +344,23 @@ export class DataLayer extends ServerStored { connectToMap() { const id = stamp(this) - if (!this.map.datalayers[id]) { - this.map.datalayers[id] = this + if (!this.umap.datalayers[id]) { + this.umap.datalayers[id] = this } - if (!this.map.datalayers_index.includes(this)) { - this.map.datalayers_index.push(this) + if (!this.umap.datalayersIndex.includes(this)) { + this.umap.datalayersIndex.push(this) } - this.map.onDataLayersChanged() + this.umap.onDataLayersChanged() } _dataUrl() { - const template = this.map.options.urls.datalayer_view - - let url = Utils.template(template, { + let url = this.umap.urls.get('datalayer_view', { pk: this.umap_id, - map_id: this.map.options.umap_id, + map_id: this.umap.properties.umap_id, }) // No browser cache for owners/editors. - if (this.map.hasEditMode()) url = `${url}?${Date.now()}` + if (this.umap.hasEditMode()) url = `${url}?${Date.now()}` return url } @@ -386,7 +387,7 @@ export class DataLayer extends ServerStored { this._index.push(id) this._features[id] = feature this.indexProperties(feature) - this.map.features_index[feature.getSlug()] = feature + this.umap.featuresIndex[feature.getSlug()] = feature this.showFeature(feature) this.dataChanged() } @@ -395,7 +396,7 @@ export class DataLayer extends ServerStored { const id = stamp(feature) if (sync !== false) feature.sync.delete() this.hideFeature(feature) - delete this.map.features_index[feature.getSlug()] + delete this.umap.featuresIndex[feature.getSlug()] feature.disconnectFromDataLayer(this) this._index.splice(this._index.indexOf(id), 1) delete this._features[id] @@ -446,7 +447,8 @@ export class DataLayer extends ServerStored { const collection = Array.isArray(geojson) ? geojson : geojson.features || geojson.geometries - Utils.sortFeatures(collection, this.map.getOption('sortKey'), L.lang) + if (!collection) return + Utils.sortFeatures(collection, this.umap.getOption('sortKey'), U.lang) for (const feature of collection) { this.makeFeature(feature, sync) } @@ -486,7 +488,7 @@ export class DataLayer extends ServerStored { } async importRaw(raw, format) { - this.map.formatter + this.umap.formatter .parse(raw, format) .then((geojson) => this.addData(geojson)) .then(() => this.zoomTo()) @@ -507,35 +509,35 @@ export class DataLayer extends ServerStored { } async importFromUrl(uri, type) { - uri = this.map.localizeUrl(uri) - const response = await this.map.request.get(uri) + uri = this.umap.localizeUrl(uri) + const response = await this.umap.request.get(uri) if (response?.ok) { this.importRaw(await response.text(), type) } } getColor() { - return this.options.color || this.map.getOption('color') + return this.options.color || this.umap.getOption('color') } getDeleteUrl() { - return Utils.template(this.map.options.urls.datalayer_delete, { + return this.umap.urls.get('datalayer_delete', { pk: this.umap_id, - map_id: this.map.options.umap_id, + map_id: this.umap.properties.umap_id, }) } getVersionsUrl() { - return Utils.template(this.map.options.urls.datalayer_versions, { + return this.umap.urls.get('datalayer_versions', { pk: this.umap_id, - map_id: this.map.options.umap_id, + map_id: this.umap.properties.umap_id, }) } getVersionUrl(name) { - return Utils.template(this.map.options.urls.datalayer_version, { + return this.umap.urls.get('datalayer_version', { pk: this.umap_id, - map_id: this.map.options.umap_id, + map_id: this.umap.properties.umap_id, name: name, }) } @@ -556,17 +558,17 @@ export class DataLayer extends ServerStored { options.name = translate('Clone of {name}', { name: this.options.name }) delete options.id const geojson = Utils.CopyJSON(this._geojson) - const datalayer = this.map.createDataLayer(options) + const datalayer = this.umap.createDataLayer(options) datalayer.fromGeoJSON(geojson) return datalayer } erase() { this.hide() - this.map.datalayers_index.splice(this.getRank(), 1) + this.umap.datalayersIndex.splice(this.getRank(), 1) this.parentPane.removeChild(this.pane) - this.map.onDataLayersChanged() - this.layer.onDelete(this.map) + this.umap.onDataLayersChanged() + this.layer.onDelete(this.umap._leafletMap) this.propagateDelete() this._leaflet_events_bk = this._leaflet_events this.clear() @@ -597,7 +599,7 @@ export class DataLayer extends ServerStored { } edit() { - if (!this.map.editEnabled || !this.isLoaded()) { + if (!this.umap.editEnabled || !this.isLoaded()) { return } const container = DomUtil.create('div', 'umap-layer-properties-container') @@ -631,7 +633,7 @@ export class DataLayer extends ServerStored { DomUtil.createTitle(container, translate('Layer properties'), 'icon-layers') let builder = new U.FormBuilder(this, metadataFields, { callback(e) { - this.map.onDataLayersChanged() + this.umap.onDataLayersChanged() if (e.helper.field === 'options.type') { this.edit() } @@ -742,7 +744,7 @@ export class DataLayer extends ServerStored { }, ], ] - if (this.map.options.urls.ajax_proxy) { + if (this.umap.properties.urls.ajax_proxy) { remoteDataFields.push([ 'options.remoteData.proxy', { @@ -768,7 +770,8 @@ export class DataLayer extends ServerStored { this ) - if (this.map.options.urls.datalayer_versions) this.buildVersionsFieldset(container) + if (this.umap.properties.urls.datalayer_versions) + this.buildVersionsFieldset(container) const advancedActions = DomUtil.createFieldset( container, @@ -781,7 +784,7 @@ export class DataLayer extends ServerStored { `) deleteButton.addEventListener('click', () => { this._delete() - this.map.editPanel.close() + this.umap.editPanel.close() }) advancedButtons.appendChild(deleteButton) @@ -820,9 +823,9 @@ export class DataLayer extends ServerStored { // Fixme: remove me when this is merged and released // https://github.com/Leaflet/Leaflet/pull/9052 DomEvent.disableClickPropagation(backButton) - DomEvent.on(backButton, 'click', this.map.editDatalayers, this.map) + DomEvent.on(backButton, 'click', this.umap.editDatalayers, this.umap) - this.map.editPanel.open({ + this.umap.editPanel.open({ content: container, actions: [backButton], }) @@ -843,13 +846,13 @@ export class DataLayer extends ServerStored { if (this.layer?.defaults?.[option]) { return this.layer.defaults[option] } - return this.map.getOption(option, feature) + return this.umap.getOption(option, feature) } async buildVersionsFieldset(container) { const appendVersion = (data) => { const date = new Date(Number.parseInt(data.at, 10)) - const content = `${date.toLocaleString(L.lang)} (${Number.parseInt(data.size) / 1000}Kb)` + const content = `${date.toLocaleString(U.lang)} (${Number.parseInt(data.size) / 1000}Kb)` const el = DomUtil.create('div', 'umap-datalayer-version', versionsContainer) const button = DomUtil.createButton( '', @@ -864,7 +867,7 @@ export class DataLayer extends ServerStored { const versionsContainer = DomUtil.createFieldset(container, translate('Versions'), { async callback() { - const [{ versions }, response, error] = await this.map.server.get( + const [{ versions }, response, error] = await this.umap.server.get( this.getVersionsUrl() ) if (!error) versions.forEach(appendVersion) @@ -874,11 +877,11 @@ export class DataLayer extends ServerStored { } async restore(version) { - if (!this.map.editEnabled) return - this.map.dialog + if (!this.umap.editEnabled) return + this.umap.dialog .confirm(translate('Are you sure you want to restore this version?')) .then(async () => { - const [geojson, response, error] = await this.map.server.get( + const [geojson, response, error] = await this.umap.server.get( this.getVersionUrl(version) ) if (!error) { @@ -899,13 +902,13 @@ export class DataLayer extends ServerStored { } async show() { - this.map.addLayer(this.layer) + this.umap._leafletMap.addLayer(this.layer) if (!this.isLoaded()) await this.fetchData() this.propagateShow() } hide() { - this.map.removeLayer(this.layer) + this.umap._leafletMap.removeLayer(this.layer) this.propagateHide() } @@ -922,7 +925,7 @@ export class DataLayer extends ServerStored { const bounds = this.layer.getBounds() if (bounds.isValid()) { const options = { maxZoom: this.getOption('zoomTo') } - this.map.fitBounds(bounds, options) + this.umap._leafletMap.fitBounds(bounds, options) } } @@ -953,7 +956,7 @@ export class DataLayer extends ServerStored { } isVisible() { - return Boolean(this.layer && this.map.hasLayer(this.layer)) + return Boolean(this.layer && this.umap._leafletMap.hasLayer(this.layer)) } getFeatureByIndex(index) { @@ -990,7 +993,7 @@ export class DataLayer extends ServerStored { getPreviousBrowsable() { let id = this.getRank() let next - const index = this.map.datalayers_index + const index = this.umap.datalayersIndex while (((id = index[++id] ? id : 0), (next = index[id]))) { if (next === this || next.canBrowse()) break } @@ -1000,7 +1003,7 @@ export class DataLayer extends ServerStored { getNextBrowsable() { let id = this.getRank() let prev - const index = this.map.datalayers_index + const index = this.umap.datalayersIndex while (((id = index[--id] ? id : index.length - 1), (prev = index[id]))) { if (prev === this || prev.canBrowse()) break } @@ -1016,7 +1019,7 @@ export class DataLayer extends ServerStored { } getRank() { - return this.map.datalayers_index.indexOf(this) + return this.umap.datalayersIndex.indexOf(this) } isReadOnly() { @@ -1043,8 +1046,8 @@ export class DataLayer extends ServerStored { // Filename support is shaky, don't do it for now. const blob = new Blob([JSON.stringify(geojson)], { type: 'application/json' }) formData.append('geojson', blob) - const saveUrl = this.map.urls.get('datalayer_save', { - map_id: this.map.options.umap_id, + const saveUrl = this.umap.urls.get('datalayer_save', { + map_id: this.umap.properties.umap_id, pk: this.umap_id, }) const headers = this._reference_version @@ -1056,7 +1059,7 @@ export class DataLayer extends ServerStored { } async _trySave(url, headers, formData) { - const [data, response, error] = await this.map.server.post(url, headers, formData) + const [data, response, error] = await this.umap.server.post(url, headers, formData) if (error) { if (response && response.status === 412) { AlertConflict.error( @@ -1072,7 +1075,7 @@ export class DataLayer extends ServerStored { // Call the main save, in case something else needs to be saved // as the conflict stopped the saving flow - await this.map.saveAll() + await this.umap.saveAll() } } ) @@ -1101,9 +1104,9 @@ export class DataLayer extends ServerStored { async saveDelete() { if (this.umap_id) { - await this.map.server.post(this.getDeleteUrl()) + await this.umap.server.post(this.getDeleteUrl()) } - delete this.map.datalayers[stamp(this)] + delete this.umap.datalayers[stamp(this)] return true } @@ -1125,9 +1128,9 @@ export class DataLayer extends ServerStored { // This keys will be used to filter feature from the browser text input. // By default, it will we use the "name" property, which is also the one used as label in the features list. // When map owner has configured another label or sort key, we try to be smart and search in the same keys. - if (this.map.options.filterKey) return this.map.options.filterKey + if (this.umap.properties.filterKey) return this.umap.properties.filterKey if (this.getOption('labelKey')) return this.getOption('labelKey') - if (this.map.options.sortKey) return this.map.options.sortKey + if (this.umap.properties.sortKey) return this.umap.properties.sortKey return 'displayName' } @@ -1178,7 +1181,7 @@ export class DataLayer extends ServerStored { 'click', function () { if (!this.isVisible()) return - this.map.dialog + this.umap.dialog .confirm(translate('Are you sure you want to delete this layer?')) .then(() => { this._delete() diff --git a/umap/static/umap/js/modules/facets.js b/umap/static/umap/js/modules/facets.js index d595bab7..0debaaae 100644 --- a/umap/static/umap/js/modules/facets.js +++ b/umap/static/umap/js/modules/facets.js @@ -3,8 +3,8 @@ import { translate } from './i18n.js' import * as Utils from './utils.js' export default class Facets { - constructor(map) { - this.map = map + constructor(umap) { + this.umap = umap this.selected = {} } @@ -24,7 +24,7 @@ export default class Facets { this.selected[name] = selected } - this.map.eachBrowsableDataLayer((datalayer) => { + this.umap.eachBrowsableDataLayer((datalayer) => { datalayer.eachFeature((feature) => { for (const name of names) { let value = feature.properties[name] @@ -108,8 +108,8 @@ export default class Facets { const defaultType = 'checkbox' const allowedTypes = [defaultType, 'radio', 'number', 'date', 'datetime'] const defined = new Map() - if (!this.map.options.facetKey) return defined - return (this.map.options.facetKey || '').split(',').reduce((acc, curr) => { + if (!this.umap.properties.facetKey) return defined + return (this.umap.properties.facetKey || '').split(',').reduce((acc, curr) => { let [name, label, type] = curr.split('|') type = allowedTypes.includes(type) ? type : defaultType acc.set(name, { label: label || name, type: type }) @@ -146,15 +146,15 @@ export default class Facets { const defined = this.getDefined() if (!defined.has(property)) { defined.set(property, { label, type }) - this.map.options.facetKey = this.dumps(defined) - this.map.isDirty = true + this.umap.properties.facetKey = this.dumps(defined) + this.umap.isDirty = true } } remove(property) { const defined = this.getDefined() defined.delete(property) - this.map.options.facetKey = this.dumps(defined) - this.map.isDirty = true + this.umap.properties.facetKey = this.dumps(defined) + this.umap.isDirty = true } } diff --git a/umap/static/umap/js/modules/formatter.js b/umap/static/umap/js/modules/formatter.js index 4f1059ed..c641da2f 100644 --- a/umap/static/umap/js/modules/formatter.js +++ b/umap/static/umap/js/modules/formatter.js @@ -3,24 +3,24 @@ import { translate } from './i18n.js' export const EXPORT_FORMATS = { geojson: { - formatter: async (map) => JSON.stringify(map.toGeoJSON(), null, 2), + formatter: async (umap) => JSON.stringify(umap.toGeoJSON(), null, 2), ext: '.geojson', filetype: 'application/json', }, gpx: { - formatter: async (map) => await map.formatter.toGPX(map.toGeoJSON()), + formatter: async (umap) => await umap.formatter.toGPX(umap.toGeoJSON()), ext: '.gpx', filetype: 'application/gpx+xml', }, kml: { - formatter: async (map) => await map.formatter.toKML(map.toGeoJSON()), + formatter: async (umap) => await umap.formatter.toKML(umap.toGeoJSON()), ext: '.kml', filetype: 'application/vnd.google-earth.kml+xml', }, csv: { - formatter: async (map) => { + formatter: async (umap) => { const table = [] - map.eachFeature((feature) => { + umap.eachFeature((feature) => { const row = feature.toGeoJSON().properties const center = feature.center delete row._umap_options diff --git a/umap/static/umap/js/modules/importer.js b/umap/static/umap/js/modules/importer.js index 036a372d..df41b4f4 100644 --- a/umap/static/umap/js/modules/importer.js +++ b/umap/static/umap/js/modules/importer.js @@ -48,8 +48,8 @@ const TEMPLATE = ` ` export default class Importer { - constructor(map) { - this.map = map + constructor(umap) { + this.umap = umap this.TYPES = ['geojson', 'csv', 'gpx', 'kml', 'osm', 'georss', 'umap'] this.IMPORTERS = [] this.loadImporters() @@ -57,9 +57,9 @@ export default class Importer { } loadImporters() { - for (const [name, config] of Object.entries(this.map.options.importers || {})) { + for (const [name, config] of Object.entries(this.umap.properties.importers || {})) { const register = (mod) => { - this.IMPORTERS.push(new mod.Importer(this.map, config)) + this.IMPORTERS.push(new mod.Importer(this.umap, config)) } // We need to have explicit static paths for Django's collectstatic with hashes. switch (name) { @@ -139,8 +139,8 @@ export default class Importer { get layer() { return ( - this.map.datalayers[this.layerId] || - this.map.createDataLayer({ name: this.layerName }) + this.umap.datalayers[this.layerId] || + this.umap.createDataLayer({ name: this.layerName }) ) } @@ -167,7 +167,7 @@ export default class Importer { textContent: type, }) } - this.map.help.parse(this.container) + this.umap.help.parse(this.container) DomEvent.on(this.qs('[name=submit]'), 'click', this.submit, this) DomEvent.on(this.qs('[type=file]'), 'change', this.onFileChange, this) for (const element of this.container.querySelectorAll('[onchange]')) { @@ -206,7 +206,7 @@ export default class Importer { this.layerName = null const layerSelect = this.qs('[name="layer-id"]') layerSelect.innerHTML = '' - this.map.eachDataLayerReverse((datalayer) => { + this.umap.eachDataLayerReverse((datalayer) => { if (datalayer.isLoaded() && !datalayer.isRemoteLayer()) { DomUtil.element({ tagName: 'option', @@ -227,7 +227,7 @@ export default class Importer { open() { if (!this.container) this.build() - const onLoad = this.map.editPanel.open({ content: this.container }) + const onLoad = this.umap.editPanel.open({ content: this.container }) onLoad.then(() => this.onLoad()) } @@ -251,16 +251,16 @@ export default class Importer { } full() { - this.map.once('postsync', this.map._setDefaultCenter) + this.umap._leafletMap.once('postsync', this.umap._leafletMap._setDefaultCenter) try { if (this.files.length) { for (const file of this.files) { - this.map.processFileToImport(file, null, 'umap') + this.umap.processFileToImport(file, null, 'umap') } } else if (this.raw) { - this.map.importRaw(this.raw) + this.umap.importRaw(this.raw) } else if (this.url) { - this.map.importFromUrl(this.url, this.format) + this.umap.importFromUrl(this.url, this.format) } } catch (e) { Alert.error(translate('Invalid umap data')) @@ -282,7 +282,7 @@ export default class Importer { url: this.url, format: this.format, } - if (this.map.options.urls.ajax_proxy) { + if (this.umap.properties.urls.ajax_proxy) { layer.options.remoteData.proxy = true layer.options.remoteData.ttl = SCHEMA.ttl.default } @@ -300,7 +300,7 @@ export default class Importer { if (this.clear) layer.empty() if (this.files.length) { for (const file of this.files) { - this.map.processFileToImport(file, layer, this.format) + this.umap.processFileToImport(file, layer, this.format) } } else if (this.raw) { layer.importRaw(this.raw, this.format) diff --git a/umap/static/umap/js/modules/permissions.js b/umap/static/umap/js/modules/permissions.js index ce0a2655..ebf586ee 100644 --- a/umap/static/umap/js/modules/permissions.js +++ b/umap/static/umap/js/modules/permissions.js @@ -7,10 +7,10 @@ import * as Utils from './utils.js' // Dedicated object so we can deal with a separate dirty status, and thus // call the endpoint only when needed, saving one call at each save. export class MapPermissions extends ServerStored { - constructor(map) { + constructor(umap) { super() - this.setOptions(map.options.permissions) - this.map = map + this.setOptions(umap.properties.permissions) + this.umap = umap this._isDirty = false } @@ -28,11 +28,11 @@ export class MapPermissions extends ServerStored { } isOwner() { - return Boolean(this.map.options.user?.is_owner) + return Boolean(this.umap.properties.user?.is_owner) } isAnonymousMap() { - return !this.map.options.permissions.owner + return !this.umap.properties.permissions.owner } _editAnonymous(container) { @@ -43,7 +43,7 @@ export class MapPermissions extends ServerStored { { handler: 'IntSelect', label: translate('Who can edit'), - selectOptions: this.map.options.edit_statuses, + selectOptions: this.umap.properties.edit_statuses, }, ]) const builder = new U.FormBuilder(this, fields) @@ -58,7 +58,7 @@ export class MapPermissions extends ServerStored { ) } - if (this.map.options.user?.id) { + if (this.umap.properties.user?.id) { // We have a user, and this user has come through here, so they can edit the map, so let's allow to own the map. // Note: real check is made on the back office anyway. const advancedActions = DomUtil.createFieldset( @@ -90,7 +90,7 @@ export class MapPermissions extends ServerStored { { handler: 'IntSelect', label: translate('Who can edit'), - selectOptions: this.map.options.edit_statuses, + selectOptions: this.umap.properties.edit_statuses, }, ]) topFields.push([ @@ -98,20 +98,20 @@ export class MapPermissions extends ServerStored { { handler: 'IntSelect', label: translate('Who can view'), - selectOptions: this.map.options.share_statuses, + selectOptions: this.umap.properties.share_statuses, }, ]) collaboratorsFields.push([ 'options.owner', { handler: 'ManageOwner', label: translate("Map's owner") }, ]) - if (this.map.options.user?.teams?.length) { + if (this.umap.properties.user?.teams?.length) { collaboratorsFields.push([ 'options.team', { handler: 'ManageTeam', label: translate('Attach map to a team'), - teams: this.map.options.user.teams, + teams: this.umap.properties.user.teams, }, ]) } @@ -136,20 +136,20 @@ export class MapPermissions extends ServerStored { } _editDatalayers(container) { - if (this.map.hasLayers()) { + if (this.umap.hasLayers()) { const fieldset = Utils.loadTemplate( `
${translate('Datalayers')}
` ) container.appendChild(fieldset) - this.map.eachDataLayer((datalayer) => { + this.umap.eachDataLayer((datalayer) => { datalayer.permissions.edit(fieldset) }) } } edit() { - if (this.map.options.editMode !== 'advanced') return - if (!this.map.options.umap_id) { + if (this.umap.properties.editMode !== 'advanced') return + if (!this.umap.properties.umap_id) { Alert.info(translate('Please save the map first')) return } @@ -158,15 +158,15 @@ export class MapPermissions extends ServerStored { if (this.isAnonymousMap()) this._editAnonymous(container) else this._editWithOwner(container) this._editDatalayers(container) - this.map.editPanel.open({ content: container, className: 'dark' }) + this.umap.editPanel.open({ content: container, className: 'dark' }) } async attach() { - const [data, response, error] = await this.map.server.post(this.getAttachUrl()) + const [data, response, error] = await this.umap.server.post(this.getAttachUrl()) if (!error) { - this.options.owner = this.map.options.user + this.options.owner = this.umap.properties.user Alert.success(translate('Map has been attached to your account')) - this.map.editPanel.close() + this.umap.editPanel.close() } } @@ -186,40 +186,41 @@ export class MapPermissions extends ServerStored { formData.append('team', this.options.team?.id || '') formData.append('share_status', this.options.share_status) } - const [data, response, error] = await this.map.server.post( + const [data, response, error] = await this.umap.server.post( this.getUrl(), {}, formData ) if (!error) { this.commit() - this.map.fire('postsync') + this.umap._leafletMap.fire('postsync') return true } } getUrl() { - return Utils.template(this.map.options.urls.map_update_permissions, { - map_id: this.map.options.umap_id, + return this.umap.urls.get('map_update_permissions', { + map_id: this.umap.properties.umap_id, }) } getAttachUrl() { - return Utils.template(this.map.options.urls.map_attach_owner, { - map_id: this.map.options.umap_id, + return this.umap.urls.get('map_attach_owner', { + map_id: this.umap.properties.umap_id, }) } commit() { - this.map.options.permissions = Object.assign( - this.map.options.permissions, + this.umap.properties.permissions = Object.assign( + {}, + this.umap.properties.permissions, this.options ) } getShareStatusDisplay() { - if (this.map.options.share_statuses) { - return Object.fromEntries(this.map.options.share_statuses)[ + if (this.umap.properties.share_statuses) { + return Object.fromEntries(this.umap.properties.share_statuses)[ this.options.share_status ] } @@ -239,8 +240,8 @@ export class DataLayerPermissions extends ServerStored { this.datalayer = datalayer } - get map() { - return this.datalayer.map + get umap() { + return this.datalayer.umap } edit(container) { @@ -252,7 +253,7 @@ export class DataLayerPermissions extends ServerStored { label: translate('Who can edit "{layer}"', { layer: this.datalayer.getName(), }), - selectOptions: this.map.options.datalayer_edit_statuses, + selectOptions: this.umap.properties.datalayer_edit_statuses, }, ], ] @@ -264,8 +265,8 @@ export class DataLayerPermissions extends ServerStored { } getUrl() { - return this.map.urls.get('datalayer_permissions', { - map_id: this.map.options.umap_id, + return this.umap.urls.get('datalayer_permissions', { + map_id: this.umap.properties.umap_id, pk: this.datalayer.umap_id, }) } @@ -274,7 +275,7 @@ export class DataLayerPermissions extends ServerStored { if (!this.isDirty) return const formData = new FormData() formData.append('edit_status', this.options.edit_status) - const [data, response, error] = await this.map.server.post( + const [data, response, error] = await this.umap.server.post( this.getUrl(), {}, formData diff --git a/umap/static/umap/js/modules/rendering/layers/base.js b/umap/static/umap/js/modules/rendering/layers/base.js index 25951a27..2a7c5085 100644 --- a/umap/static/umap/js/modules/rendering/layers/base.js +++ b/umap/static/umap/js/modules/rendering/layers/base.js @@ -73,7 +73,7 @@ export const Default = FeatureGroup.extend({ initialize: function (datalayer) { this.datalayer = datalayer FeatureGroup.prototype.initialize.call(this) - LayerMixin.onInit.call(this, this.datalayer.map) + LayerMixin.onInit.call(this, this.datalayer.umap._leafletMap) }, onAdd: function (map) { diff --git a/umap/static/umap/js/modules/rendering/layers/classified.js b/umap/static/umap/js/modules/rendering/layers/classified.js index 7b5eada4..b5bb509e 100644 --- a/umap/static/umap/js/modules/rendering/layers/classified.js +++ b/umap/static/umap/js/modules/rendering/layers/classified.js @@ -20,7 +20,7 @@ const ClassifiedMixin = { } this.ensureOptions(this.datalayer.options[key]) FeatureGroup.prototype.initialize.call(this, [], this.datalayer.options[key]) - LayerMixin.onInit.call(this, this.datalayer.map) + LayerMixin.onInit.call(this, this.datalayer.umap._leafletMap) }, ensureOptions: () => {}, diff --git a/umap/static/umap/js/modules/rendering/layers/cluster.js b/umap/static/umap/js/modules/rendering/layers/cluster.js index af206795..0ce8249d 100644 --- a/umap/static/umap/js/modules/rendering/layers/cluster.js +++ b/umap/static/umap/js/modules/rendering/layers/cluster.js @@ -40,7 +40,7 @@ export const Cluster = L.MarkerClusterGroup.extend({ options.maxClusterRadius = this.datalayer.options.cluster.radius } L.MarkerClusterGroup.prototype.initialize.call(this, options) - LayerMixin.onInit.call(this, this.datalayer.map) + LayerMixin.onInit.call(this, this.datalayer.umap._leafletMap) this._markerCluster = MarkerCluster this._layers = [] }, diff --git a/umap/static/umap/js/modules/rendering/layers/heat.js b/umap/static/umap/js/modules/rendering/layers/heat.js index a8d7dca0..24e6b411 100644 --- a/umap/static/umap/js/modules/rendering/layers/heat.js +++ b/umap/static/umap/js/modules/rendering/layers/heat.js @@ -21,7 +21,7 @@ export const Heat = L.HeatLayer.extend({ initialize: function (datalayer) { this.datalayer = datalayer L.HeatLayer.prototype.initialize.call(this, [], this.datalayer.options.heat) - LayerMixin.onInit.call(this, this.datalayer.map) + LayerMixin.onInit.call(this, this.datalayer.umap._leafletMap) if (!Utils.isObject(this.datalayer.options.heat)) { this.datalayer.options.heat = {} } diff --git a/umap/static/umap/js/modules/rendering/map.js b/umap/static/umap/js/modules/rendering/map.js new file mode 100644 index 00000000..ce91b416 --- /dev/null +++ b/umap/static/umap/js/modules/rendering/map.js @@ -0,0 +1,583 @@ +// Goes here all code related to Leaflet, DOM and user interactions. +import { + Map as BaseMap, + DomUtil, + DomEvent, + latLngBounds, + latLng, + Control, + setOptions, +} from '../../../vendors/leaflet/leaflet-src.esm.js' +import { translate } from '../i18n.js' +import { uMapAlert as Alert } from '../../components/alerts/alert.js' +import * as Utils from '../utils.js' +import * as Icon from './icon.js' + +// Those options are not saved on the server, so they can live here +// instead of in umap.properties +BaseMap.mergeOptions({ + demoTileInfos: { s: 'a', z: 9, x: 265, y: 181, '-y': 181, r: '' }, + attributionControl: false, +}) + +const ControlsMixin = { + HIDDABLE_CONTROLS: [ + 'zoom', + 'search', + 'fullscreen', + 'embed', + 'datalayers', + 'caption', + 'locate', + 'measure', + 'editinosm', + 'star', + 'tilelayers', + ], + + initControls: function () { + this.helpMenuActions = {} + this._controls = {} + + if (this.umap.hasEditMode() && !this.options.noControl) { + new U.EditControl(this).addTo(this) + + new U.DrawToolbar({ map: this }).addTo(this) + const editActions = [ + U.EditCaptionAction, + U.EditPropertiesAction, + U.EditLayersAction, + U.ChangeTileLayerAction, + U.UpdateExtentAction, + U.UpdatePermsAction, + U.ImportAction, + ] + if (this.options.editMode === 'advanced') { + new U.SettingsToolbar({ actions: editActions }).addTo(this) + } + } + this._controls.zoom = new Control.Zoom({ + zoomInTitle: translate('Zoom in'), + zoomOutTitle: translate('Zoom out'), + }) + this._controls.datalayers = new U.DataLayersControl(this.umap) + this._controls.caption = new U.CaptionControl(this.umap) + this._controls.locate = new U.Locate(this, { + strings: { + title: translate('Center map on your location'), + }, + showPopup: false, + // We style this control in our own CSS for consistency with other controls, + // but the control breaks if we don't specify a class here, so a fake class + // will do. + icon: 'umap-fake-class', + iconLoading: 'umap-fake-class', + flyTo: this.options.easing, + onLocationError: (err) => U.Alert.error(err.message), + }) + this._controls.fullscreen = new Control.Fullscreen({ + title: { + false: translate('View Fullscreen'), + true: translate('Exit Fullscreen'), + }, + }) + this._controls.search = new U.SearchControl() + this._controls.embed = new Control.Embed(this.umap) + this._controls.tilelayersChooser = new U.TileLayerChooser(this) + if (this.options.user?.id) this._controls.star = new U.StarControl(this.umap) + this._controls.editinosm = new Control.EditInOSM({ + position: 'topleft', + widgetOptions: { + helpText: translate( + 'Open this map extent in a map editor to provide more accurate data to OpenStreetMap' + ), + }, + }) + this._controls.measure = new L.MeasureControl().initHandler(this) + this._controls.more = new U.MoreControls() + this._controls.scale = L.control.scale() + this._controls.permanentCredit = new U.PermanentCreditsControl(this) + if (this.options.scrollWheelZoom) this.scrollWheelZoom.enable() + else this.scrollWheelZoom.disable() + this.umap.drop = new U.DropControl(this) + this._controls.tilelayers = new U.TileLayerControl(this) + }, + + renderControls: function () { + const hasSlideshow = Boolean(this.options.slideshow?.active) + const barEnabled = this.options.captionBar || hasSlideshow + document.body.classList.toggle('umap-caption-bar-enabled', barEnabled) + document.body.classList.toggle('umap-slideshow-enabled', hasSlideshow) + for (const control of Object.values(this._controls)) { + this.removeControl(control) + } + if (this.options.noControl) return + + this._controls.attribution = new U.AttributionControl().addTo(this) + if (this.options.miniMap) { + this.whenReady(function () { + if (this.selectedTilelayer) { + this._controls.miniMap = new Control.MiniMap(this.selectedTilelayer, { + aimingRectOptions: { + color: this.umap.getOption('color'), + fillColor: this.umap.getOption('fillColor'), + stroke: this.umap.getOption('stroke'), + fill: this.umap.getOption('fill'), + weight: this.umap.getOption('weight'), + opacity: this.umap.getOption('opacity'), + fillOpacity: this.umap.getOption('fillOpacity'), + }, + }).addTo(this) + this._controls.miniMap._miniMap.invalidateSize() + } + }) + } + for (const name of this.HIDDABLE_CONTROLS) { + const status = this.umap.getOption(`${name}Control`) + if (status === false) continue + const control = this._controls[name] + if (!control) continue + control.addTo(this) + if (status === undefined || status === null) { + DomUtil.addClass(control._container, 'display-on-more') + } else { + DomUtil.removeClass(control._container, 'display-on-more') + } + } + if (this.umap.getOption('permanentCredit')) + this._controls.permanentCredit.addTo(this) + if (this.umap.getOption('moreControl')) this._controls.more.addTo(this) + if (this.umap.getOption('scaleControl')) this._controls.scale.addTo(this) + this._controls.tilelayers.setLayers() + }, + + renderEditToolbar: function () { + const className = 'umap-main-edit-toolbox' + const container = + document.querySelector(`.${className}`) || + DomUtil.create('div', `${className} with-transition dark`, this._controlContainer) + container.innerHTML = '' + const leftContainer = DomUtil.create('div', 'umap-left-edit-toolbox', container) + const rightContainer = DomUtil.create('div', 'umap-right-edit-toolbox', container) + const logo = DomUtil.create('div', 'logo', leftContainer) + DomUtil.createLink('', logo, 'uMap', '/', null, translate('Go to the homepage')) + const nameButton = DomUtil.createButton('map-name', leftContainer, '') + DomEvent.on(nameButton, 'mouseover', () => { + this.umap.tooltip.open({ + content: translate('Edit the title of the map'), + anchor: nameButton, + position: 'bottom', + delay: 500, + duration: 5000, + }) + }) + const shareStatusButton = DomUtil.createButton( + 'share-status', + leftContainer, + '', + this.umap.permissions.edit, + this.umap.permissions + ) + DomEvent.on(shareStatusButton, 'mouseover', () => { + this.umap.tooltip.open({ + content: translate('Update who can see and edit the map'), + anchor: shareStatusButton, + position: 'bottom', + delay: 500, + duration: 5000, + }) + }) + if (this.options.editMode === 'advanced') { + DomEvent.on(nameButton, 'click', this.umap.editCaption, this.umap) + DomEvent.on( + shareStatusButton, + 'click', + this.umap.permissions.edit, + this.umap.permissions + ) + } + if (this.options.user?.id) { + const button = U.Utils.loadTemplate(` + + `) + rightContainer.appendChild(button) + const menu = new U.ContextMenu({ className: 'dark', fixed: true }) + const actions = [ + { + label: translate('New map'), + action: this.umap.urls.get('map_new'), + }, + { + label: translate('My maps'), + action: this.umap.urls.get('user_dashboard'), + }, + { + label: translate('My teams'), + action: this.umap.urls.get('user_teams'), + }, + ] + if (this.umap.urls.has('user_profile')) { + actions.push({ + label: translate('My profile'), + action: this.umap.urls.get('user_profile'), + }) + } + button.addEventListener('click', () => { + menu.openBelow(button, actions) + }) + } + + const connectedPeers = this.umap.sync.getNumberOfConnectedPeers() + if (connectedPeers !== 0) { + const connectedPeersCount = DomUtil.createButton( + 'leaflet-control-connected-peers', + rightContainer, + '' + ) + DomEvent.on(connectedPeersCount, 'mouseover', () => { + this.umap.tooltip.open({ + content: translate( + '{connectedPeers} peer(s) currently connected to this map', + { + connectedPeers: connectedPeers, + } + ), + anchor: connectedPeersCount, + position: 'bottom', + delay: 500, + duration: 5000, + }) + }) + + const updateConnectedPeersCount = () => { + connectedPeersCount.innerHTML = this.sync.getNumberOfConnectedPeers() + } + updateConnectedPeersCount() + } + + this.umap.help.getStartedLink(rightContainer) + const controlEditCancel = DomUtil.createButton( + 'leaflet-control-edit-cancel', + rightContainer, + DomUtil.add('span', '', null, translate('Cancel edits')), + () => this.umap.askForReset() + ) + DomEvent.on(controlEditCancel, 'mouseover', () => { + this.umap.tooltip.open({ + content: this.umap.help.displayLabel('CANCEL'), + anchor: controlEditCancel, + position: 'bottom', + delay: 500, + duration: 5000, + }) + }) + const controlEditDisable = DomUtil.createButton( + 'leaflet-control-edit-disable', + rightContainer, + DomUtil.add('span', '', null, translate('View')), + this.umap.disableEdit, + this.umap + ) + DomEvent.on(controlEditDisable, 'mouseover', () => { + this.umap.tooltip.open({ + content: this.umap.help.displayLabel('PREVIEW'), + anchor: controlEditDisable, + position: 'bottom', + delay: 500, + duration: 5000, + }) + }) + const controlEditSave = DomUtil.createButton( + 'leaflet-control-edit-save button', + rightContainer, + DomUtil.add('span', '', null, translate('Save')), + () => this.umap.saveAll() + ) + DomEvent.on(controlEditSave, 'mouseover', () => { + this.umap.tooltip.open({ + content: this.umap.help.displayLabel('SAVE'), + anchor: controlEditSave, + position: 'bottom', + delay: 500, + duration: 5000, + }) + }) + }, + + initCaptionBar: function () { + const container = DomUtil.create('div', 'umap-caption-bar', this._controlContainer) + const name = DomUtil.create('h3', 'map-name', container) + DomEvent.disableClickPropagation(container) + this.umap.addAuthorLink(container) + if (this.umap.getOption('captionMenus')) { + DomUtil.createButton( + 'umap-about-link flat', + container, + translate('Open caption'), + this.umap.openCaption, + this + ) + DomUtil.createButton( + 'umap-open-browser-link flat', + container, + translate('Browse data'), + () => this.openBrowser('data') + ) + if (this.options.facetKey) { + DomUtil.createButton( + 'umap-open-filter-link flat', + container, + translate('Filter data'), + () => this.openBrowser('filters') + ) + } + } + this.umap.onceDatalayersLoaded(function () { + this.slideshow.renderToolbox(container) + }) + }, +} + +const ManageTilelayerMixin = { + initTileLayers: function () { + this.tilelayers = [] + for (const props of this.options.tilelayers) { + const layer = this.createTileLayer(props) + this.tilelayers.push(layer) + if ( + this.options.tilelayer && + this.options.tilelayer.url_template === props.url_template + ) { + // Keep control over the displayed attribution for non custom tilelayers + this.options.tilelayer.attribution = props.attribution + } + } + if (this.options.tilelayer?.url_template && this.options.tilelayer.attribution) { + this.customTilelayer = this.createTileLayer(this.options.tilelayer) + this.selectTileLayer(this.customTilelayer) + } else { + this.selectTileLayer(this.tilelayers[0]) + } + if (this._controls) this._controls.tilelayers.setLayers() + }, + + createTileLayer: (tilelayer) => new L.TileLayer(tilelayer.url_template, tilelayer), + + selectTileLayer: function (tilelayer) { + if (tilelayer === this.selectedTilelayer) { + return + } + try { + this.addLayer(tilelayer) + this.fire('baselayerchange', { layer: tilelayer }) + if (this.selectedTilelayer) { + this.removeLayer(this.selectedTilelayer) + } + this.selectedTilelayer = tilelayer + if ( + !Number.isNaN(this.selectedTilelayer.options.minZoom) && + this.getZoom() < this.selectedTilelayer.options.minZoom + ) { + this.setZoom(this.selectedTilelayer.options.minZoom) + } + if ( + !Number.isNaN(this.selectedTilelayer.options.maxZoom) && + this.getZoom() > this.selectedTilelayer.options.maxZoom + ) { + this.setZoom(this.selectedTilelayer.options.maxZoom) + } + } catch (e) { + console.error(e) + this.removeLayer(tilelayer) + Alert.error(`${translate('Error in the tilelayer URL')}: ${tilelayer._url}`) + // Users can put tilelayer URLs by hand, and if they add wrong {variable}, + // Leaflet throw an error, and then the map is no more editable + } + this.setOverlay() + }, + + eachTileLayer: function (callback, context) { + const urls = [] + const callOne = (layer) => { + // Prevent adding a duplicate background, + // while adding selected/custom on top of the list + const url = layer.options.url_template + if (urls.indexOf(url) !== -1) return + callback.call(context, layer) + urls.push(url) + } + if (this.selectedTilelayer) callOne(this.selectedTilelayer) + if (this.customTilelayer) callOne(this.customTilelayer) + this.tilelayers.forEach(callOne) + }, + + setOverlay: function () { + if (!this.options.overlay || !this.options.overlay.url_template) return + const overlay = this.createTileLayer(this.options.overlay) + try { + this.addLayer(overlay) + if (this.overlay) this.removeLayer(this.overlay) + this.overlay = overlay + } catch (e) { + this.removeLayer(overlay) + console.error(e) + Alert.error(`${translate('Error in the overlay URL')}: ${overlay._url}`) + } + }, + + updateTileLayers: function () { + const callback = (tilelayer) => { + this.options.tilelayer = tilelayer.toJSON() + this.umap.isDirty = true + } + if (this._controls.tilelayersChooser) { + this._controls.tilelayersChooser.openSwitcher({ callback, edit: true }) + } + }, +} + +const EditMixin = { + startMarker: function () { + return this.editTools.startMarker() + }, + + startPolyline: function () { + return this.editTools.startPolyline() + }, + + startPolygon: function () { + return this.editTools.startPolygon() + }, + + initEditTools: function () { + this.editTools = new U.Editable(this.umap) + this.renderEditToolbar() + }, +} + +export const LeafletMap = BaseMap.extend({ + includes: [ControlsMixin, ManageTilelayerMixin, EditMixin], + initialize: function (umap, element) { + this.umap = umap + const options = this.umap.properties + + BaseMap.prototype.initialize.call(this, element, options) + + // After calling parent initialize, as we are doing initCenter our-selves + + this.loader = new Control.Loading() + this.loader.onAdd(this) + + if (!this.options.noControl) { + DomEvent.on(document.body, 'dataloading', (e) => this.fire('dataloading', e)) + DomEvent.on(document.body, 'dataload', (e) => this.fire('dataload', e)) + this.on('click', this.closeInplaceToolbar) + } + + this.on('baselayerchange', (e) => { + if (this._controls.miniMap) this._controls.miniMap.onMainMapBaseLayerChange(e) + }) + }, + + attachToDom: function () { + this.initControls() + // Needs locate control and hash to exist + this.initCenter() + this.initTileLayers() + // Needs tilelayer to exist for minimap + this.renderControls() + this.handleLimitBounds() + }, + + setOptions: function (options) { + setOptions(this, options) + }, + + closeInplaceToolbar: function () { + const toolbar = this._toolbars[L.Toolbar.Popup._toolbar_class_id] + if (toolbar) toolbar.remove() + }, + + latLng: (a, b, c) => { + // manage geojson case and call original method + if (!(a instanceof L.LatLng) && a.coordinates) { + // Guess it's a geojson + a = [a.coordinates[1], a.coordinates[0]] + } + return latLng(a, b, c) + }, + + _setDefaultCenter: function () { + this.options.center = this.latLng(this.options.center) + this.setView(this.options.center, this.options.zoom) + }, + + initCenter: function () { + this._setDefaultCenter() + if (this.options.hash) this.addHash() + if (this.options.hash && this._hash.parseHash(location.hash)) { + // FIXME An invalid hash will cause the load to fail + this._hash.update() + } else if (this.options.defaultView === 'locate' && !this.options.noControl) { + this._controls.locate.start() + } else if (this.options.defaultView === 'data') { + this.umap.onceDataLoaded(this.umap.fitDataBounds) + } else if (this.options.defaultView === 'latest') { + this.umap.onceDataLoaded(() => { + if (!this.umap.hasData()) return + const datalayer = this.umap.firstVisibleDatalayer() + let feature + if (datalayer) { + const feature = datalayer.getFeatureByIndex(-1) + if (feature) { + feature.zoomTo({ callback: this.options.noControl ? null : feature.view }) + return + } + } + }) + } + }, + + handleLimitBounds: function () { + const south = Number.parseFloat(this.options.limitBounds.south) + const west = Number.parseFloat(this.options.limitBounds.west) + const north = Number.parseFloat(this.options.limitBounds.north) + const east = Number.parseFloat(this.options.limitBounds.east) + if ( + !Number.isNaN(south) && + !Number.isNaN(west) && + !Number.isNaN(north) && + !Number.isNaN(east) + ) { + const bounds = latLngBounds([ + [south, west], + [north, east], + ]) + this.options.minZoom = this.getBoundsZoom(bounds, false) + try { + this.setMaxBounds(bounds) + } catch (e) { + // Unusable bounds, like -2 -2 -2 -2? + console.error('Error limiting bounds', e) + } + } else { + this.options.minZoom = 0 + this.setMaxBounds() + } + }, + + setMaxBounds: function (bounds) { + // Hack. Remove me when fix is released: + // https://github.com/Leaflet/Leaflet/pull/4494 + bounds = latLngBounds(bounds) + + if (!bounds.isValid()) { + this.options.maxBounds = null + return this.off('moveend', this._panInsideMaxBounds) + } + return BaseMap.prototype.setMaxBounds.call(this, bounds) + }, +}) diff --git a/umap/static/umap/js/modules/rendering/popup.js b/umap/static/umap/js/modules/rendering/popup.js index dc1e460a..b485c2c8 100644 --- a/umap/static/umap/js/modules/rendering/popup.js +++ b/umap/static/umap/js/modules/rendering/popup.js @@ -62,8 +62,8 @@ const Panel = Popup.extend({ }, onAdd: function (map) { - map.panel.setDefaultMode('expanded') - map.panel.open({ + map.umap.panel.setDefaultMode('expanded') + map.umap.panel.open({ content: this._content, actions: [Browser.backButton(map)], }) @@ -79,7 +79,7 @@ const Panel = Popup.extend({ }, onRemove: function (map) { - map.panel.close() + map.umap.panel.close() // fire events as in base class Popup.js:onRemove map.fire('popupclose', { popup: this }) diff --git a/umap/static/umap/js/modules/rendering/ui.js b/umap/static/umap/js/modules/rendering/ui.js index c01d309e..6e5a4d43 100644 --- a/umap/static/umap/js/modules/rendering/ui.js +++ b/umap/static/umap/js/modules/rendering/ui.js @@ -52,7 +52,7 @@ const FeatureMixin = { onClick: function (event) { if (this._map.measureTools?.enabled()) return this._popupHandlersAdded = true // Prevent leaflet from managing event - if (!this._map.editEnabled) { + if (!this._map.umap.editEnabled) { this.feature.view(event) } else if (!this.feature.isReadOnly()) { if (event.originalEvent.shiftKey) { @@ -96,8 +96,8 @@ const FeatureMixin = { DomEvent.stop(event) const items = this.feature .getContextMenuItems(event) - .concat(this._map.getContextMenuItems(event)) - this._map.contextmenu.open(event.originalEvent, items) + .concat(this._map.umap.getContextMenuItems(event)) + this._map.umap.contextmenu.open(event.originalEvent, items) }, onCommit: function () { @@ -134,7 +134,7 @@ const PointMixin = { _enableDragging: function () { // TODO: start dragging after 1 second on mouse down - if (this._map.editEnabled) { + if (this._map.umap.editEnabled) { if (!this.editEnabled()) this.enableEdit() // Enabling dragging on the marker override the Draggable._OnDown // event, which, as it stopPropagation, refrain the call of @@ -146,7 +146,7 @@ const PointMixin = { }, _disableDragging: function () { - if (this._map.editEnabled) { + if (this._map.umap.editEnabled) { if (this.editor?.drawing) return // when creating a new marker, the mouse can trigger the mouseover/mouseout event // do not listen to them this.disableEdit() @@ -253,21 +253,21 @@ export const LeafletMarker = Marker.extend({ const PathMixin = { _onMouseOver: function () { if (this._map.measureTools?.enabled()) { - this._map.tooltip.open({ content: this.getMeasure(), anchor: this }) - } else if (this._map.editEnabled && !this._map.editedFeature) { - this._map.tooltip.open({ content: translate('Click to edit'), anchor: this }) + this._map.umap.tooltip.open({ content: this.getMeasure(), anchor: this }) + } else if (this._map.umap.editEnabled && !this._map.umap.editedFeature) { + this._map.umap.tooltip.open({ content: translate('Click to edit'), anchor: this }) } }, makeGeometryEditable: function () { - if (this._map.editedFeature !== this.feature) { + if (this._map.umap.editedFeature !== this.feature) { this.disableEdit() return } this._map.once('moveend', this.makeGeometryEditable, this) const pointsCount = this._parts.reduce((acc, part) => acc + part.length, 0) if (pointsCount > 100 && this._map.getZoom() < this._map.getMaxZoom()) { - this._map.tooltip.open({ content: L._('Please zoom in to edit the geometry') }) + this._map.umap.tooltip.open({ content: L._('Please zoom in to edit the geometry') }) this.disableEdit() } else { this.enableEdit() diff --git a/umap/static/umap/js/modules/rules.js b/umap/static/umap/js/modules/rules.js index b584d0d8..21a0e06c 100644 --- a/umap/static/umap/js/modules/rules.js +++ b/umap/static/umap/js/modules/rules.js @@ -21,10 +21,10 @@ class Rule { set isDirty(status) { this._isDirty = status - if (status) this.map.isDirty = status + if (status) this.umap.isDirty = status } - constructor(map, condition = '', options = {}) { + constructor(umap, condition = '', options = {}) { // TODO make this public properties when browser coverage is ok // cf https://caniuse.com/?search=public%20class%20field this._condition = null @@ -37,14 +37,14 @@ class Rule { ['!=', this.not_equal], ['=', this.equal], ] - this.map = map + this.umap = umap this.active = true this.options = options this.condition = condition } render(fields) { - this.map.render(fields) + this.umap.render(fields) } equal(other) { @@ -102,7 +102,7 @@ class Rule { } getMap() { - return this.map + return this.umap } getOption(option) { @@ -136,7 +136,7 @@ class Rule { const defaultShapeProperties = DomUtil.add('div', '', container) defaultShapeProperties.appendChild(builder.build()) const autocomplete = new AutocompleteDatalist(builder.helpers.condition.input) - const properties = this.map.allProperties() + const properties = this.umap.allProperties() autocomplete.suggestions = properties autocomplete.input.addEventListener('input', (event) => { const value = event.target.value @@ -144,12 +144,12 @@ class Rule { autocomplete.suggestions = [`${value}=`, `${value}!=`, `${value}>`, `${value}<`] } else if (value.endsWith('=')) { const key = value.split('!')[0].split('=')[0] - autocomplete.suggestions = this.map + autocomplete.suggestions = this.umap .sortedValues(key) .map((str) => `${value}${str || ''}`) } }) - this.map.editPanel.open({ content: container }) + this.umap.editPanel.open({ content: container }) } renderToolbox(row) { @@ -176,7 +176,7 @@ class Rule { function () { if (!confirm(translate('Are you sure you want to delete this rule?'))) return this._delete() - this.map.editPanel.close() + this.umap.editPanel.close() }, this ) @@ -186,27 +186,27 @@ class Rule { DomEvent.on(toggle, 'click', () => { this.active = !this.active row.classList.toggle('off', !this.active) - this.map.render(['rules']) + this.umap.render(['rules']) }) } _delete() { - this.map.rules.rules = this.map.rules.rules.filter((rule) => rule !== this) + this.umap.rules.rules = this.umap.rules.rules.filter((rule) => rule !== this) } } export default class Rules { - constructor(map) { - this.map = map + constructor(umap) { + this.umap = umap this.rules = [] this.loadRules() } loadRules() { - if (!this.map.options.rules?.length) return - for (const { condition, options } of this.map.options.rules) { + if (!this.umap.properties.rules?.length) return + for (const { condition, options } of this.umap.properties.rules) { if (!condition) continue - this.rules.push(new Rule(this.map, condition, options)) + this.rules.push(new Rule(this.umap, condition, options)) } } @@ -225,7 +225,7 @@ export default class Rules { else newIdx = referenceIdx + 1 this.rules.splice(newIdx, 0, moved) moved.isDirty = true - this.map.render(['rules']) + this.umap.render(['rules']) } edit(container) { @@ -243,14 +243,14 @@ export default class Rules { } addRule() { - const rule = new Rule(this.map) + const rule = new Rule(this.umap) rule.isDirty = true this.rules.push(rule) rule.edit(map) } commit() { - this.map.options.rules = this.rules.map((rule) => { + this.umap.properties.rules = this.rules.map((rule) => { return { condition: rule.condition, options: rule.options, diff --git a/umap/static/umap/js/modules/saving.js b/umap/static/umap/js/modules/saving.js index 2b8b748a..98797633 100644 --- a/umap/static/umap/js/modules/saving.js +++ b/umap/static/umap/js/modules/saving.js @@ -25,7 +25,6 @@ export function has(obj) { } function _onUpdate() { - console.log(_queue) isDirty = Boolean(_queue.size) document.body.classList.toggle('umap-is-dirty', isDirty) } diff --git a/umap/static/umap/js/modules/share.js b/umap/static/umap/js/modules/share.js index 144c252f..5e2e8215 100644 --- a/umap/static/umap/js/modules/share.js +++ b/umap/static/umap/js/modules/share.js @@ -4,8 +4,8 @@ import { translate } from './i18n.js' import * as Utils from './utils.js' export default class Share { - constructor(map) { - this.map = map + constructor(umap) { + this.umap = umap } build() { @@ -22,11 +22,11 @@ export default class Share { window.location.protocol + Utils.getBaseUrl() ) - if (this.map.options.shortUrl) { + if (this.umap.properties.shortUrl) { DomUtil.createCopiableInput( this.container, translate('Short link'), - this.map.options.shortUrl + this.umap.properties.shortUrl ) } @@ -60,8 +60,8 @@ export default class Share { this.container, translate('All data and settings of the map') ) - const downloadUrl = Utils.template(this.map.options.urls.map_download, { - map_id: this.map.options.umap_id, + const downloadUrl = this.umap.urls.get('map_download', { + map_id: this.umap.properties.umap_id, }) const link = Utils.loadTemplate(`
@@ -115,10 +115,11 @@ export default class Share { 'queryString.captionBar', 'queryString.captionMenus', ] - for (let i = 0; i < this.map.HIDDABLE_CONTROLS.length; i++) { - UIFields.push(`queryString.${this.map.HIDDABLE_CONTROLS[i]}Control`) + // TODO: move HIDDABLE_CONTROLS to SCHEMA ? + for (const name of this.umap._leafletMap.HIDDABLE_CONTROLS) { + UIFields.push(`queryString.${name}Control`) } - const iframeExporter = new IframeExporter(this.map) + const iframeExporter = new IframeExporter(this.umap) const buildIframeCode = () => { iframe.textContent = iframeExporter.build() exportUrl.value = window.location.protocol + iframeExporter.buildUrl() @@ -136,13 +137,13 @@ export default class Share { open() { if (!this.container) this.build() - this.map.panel.open({ content: this.container }) + this.umap.panel.open({ content: this.container }) } async format(mode) { const type = EXPORT_FORMATS[mode] - const content = await type.formatter(this.map) - const filename = Utils.slugify(this.map.options.name) + type.ext + const content = await type.formatter(this.umap) + const filename = Utils.slugify(this.umap.properties.name) + type.ext return { content, filetype: type.filetype, filename } } @@ -161,8 +162,8 @@ export default class Share { } class IframeExporter { - constructor(map) { - this.map = map + constructor(umap) { + this.umap = umap this.baseUrl = Utils.getBaseUrl() this.options = { includeFullScreenLink: true, @@ -192,22 +193,18 @@ class IframeExporter { height: '300px', } // Use map default, not generic default - this.queryString.onLoadPanel = this.map.getOption('onLoadPanel') - } - - getMap() { - return this.map + this.queryString.onLoadPanel = this.umap.getOption('onLoadPanel') } buildUrl(options) { const datalayers = [] - if (this.options.viewCurrentFeature && this.map.currentFeature) { - this.queryString.feature = this.map.currentFeature.getSlug() + if (this.options.viewCurrentFeature && this.umap.currentFeature) { + this.queryString.feature = this.umap.currentFeature.getSlug() } else { delete this.queryString.feature } if (this.options.keepCurrentDatalayers) { - this.map.eachDataLayer((datalayer) => { + this.umap.eachDataLayer((datalayer) => { if (datalayer.isVisible() && datalayer.umap_id) { datalayers.push(datalayer.umap_id) } diff --git a/umap/static/umap/js/modules/slideshow.js b/umap/static/umap/js/modules/slideshow.js index ef73d4b0..6d3e8a67 100644 --- a/umap/static/umap/js/modules/slideshow.js +++ b/umap/static/umap/js/modules/slideshow.js @@ -13,20 +13,20 @@ const TOOLBOX_TEMPLATE = ` ` export default class Slideshow extends WithTemplate { - constructor(map, options) { + constructor(umap, options) { super() - this.map = map + this.umap = umap this._id = null this.CLASSNAME = 'umap-slideshow-active' this.setOptions(options) this._current = null if (this.options.autoplay) { - this.map.onceDataLoaded(function () { + this.umap.onceDataLoaded(function () { this.play() }, this) } - this.map.on( + this.umap._leafletMap.on( 'edit:enabled', function () { this.stop() @@ -65,7 +65,7 @@ export default class Slideshow extends WithTemplate { } defaultDatalayer() { - return this.map.findDataLayer((d) => d.canBrowse()) + return this.umap.findDataLayer((d) => d.canBrowse()) } startSpinner() { @@ -83,7 +83,7 @@ export default class Slideshow extends WithTemplate { play() { if (this._id) return - if (this.map.editEnabled || !this.map.options.slideshow.active) return + if (this.umap.editEnabled || !this.umap.options.slideshow.active) return L.DomUtil.addClass(document.body, this.CLASSNAME) this._id = window.setInterval(L.bind(this.loop, this), this.options.delay) this.startSpinner() diff --git a/umap/static/umap/js/modules/tableeditor.js b/umap/static/umap/js/modules/tableeditor.js index 79fb6c54..f04734d1 100644 --- a/umap/static/umap/js/modules/tableeditor.js +++ b/umap/static/umap/js/modules/tableeditor.js @@ -17,7 +17,7 @@ export default class TableEditor extends WithTemplate { constructor(datalayer) { super() this.datalayer = datalayer - this.map = this.datalayer.map + this.umap = this.datalayer.umap this.contextmenu = new ContextMenu({ className: 'dark' }) this.table = this.loadTemplate(TEMPLATE) if (!this.datalayer.isRemoteLayer()) { @@ -36,20 +36,20 @@ export default class TableEditor extends WithTemplate { openHeaderMenu(property) { const actions = [] let filterItem - if (this.map.facets.has(property)) { + if (this.umap.facets.has(property)) { filterItem = { label: translate('Remove filter for this column'), action: () => { - this.map.facets.remove(property) - this.map.browser.open('filters') + this.umap.facets.remove(property) + this.umap.browser.open('filters') }, } } else { filterItem = { label: translate('Add filter for this column'), action: () => { - this.map.facets.add(property) - this.map.browser.open('filters') + this.umap.facets.add(property) + this.umap.browser.open('filters') }, } } @@ -86,8 +86,8 @@ export default class TableEditor extends WithTemplate { } renderBody() { - const bounds = this.map.getBounds() - const inBbox = this.map.browser.options.inBbox + const bounds = this.umap._leafletMap.getBounds() + const inBbox = this.umap.browser.options.inBbox let html = '' this.datalayer.eachFeature((feature) => { if (feature.isFiltered()) return @@ -121,7 +121,7 @@ export default class TableEditor extends WithTemplate { } renameProperty(property) { - this.map.dialog + this.umap.dialog .prompt(translate('Please enter the new name of this property')) .then(({ prompt }) => { if (!prompt || !this.validateName(prompt)) return @@ -135,7 +135,7 @@ export default class TableEditor extends WithTemplate { } deleteProperty(property) { - this.map.dialog + this.umap.dialog .confirm( translate('Are you sure you want to delete this property on all the features?') ) @@ -150,7 +150,7 @@ export default class TableEditor extends WithTemplate { } addProperty() { - this.map.dialog + this.umap.dialog .prompt(translate('Please enter the name of the property')) .then(({ prompt }) => { if (!prompt || !this.validateName(prompt)) return @@ -187,10 +187,10 @@ export default class TableEditor extends WithTemplate { `) - filterButton.addEventListener('click', () => this.map.browser.open('filters')) + filterButton.addEventListener('click', () => this.umap.browser.open('filters')) actions.push(filterButton) - this.map.fullPanel.open({ + this.umap.fullPanel.open({ content: this.table, className: 'umap-table-editor', actions: actions, @@ -304,7 +304,7 @@ export default class TableEditor extends WithTemplate { deleteRows() { const selectedRows = this.getSelectedRows() if (!selectedRows.length) return - this.map.dialog + this.umap.dialog .confirm( translate('Found {count} rows. Are you sure you want to delete all?', { count: selectedRows.length, @@ -320,9 +320,9 @@ export default class TableEditor extends WithTemplate { this.datalayer.show() this.datalayer.dataChanged() this.renderBody() - if (this.map.browser.isOpen()) { - this.map.browser.resetFilters() - this.map.browser.open('filters') + if (this.umap.browser.isOpen()) { + this.umap.browser.resetFilters() + this.umap.browser.open('filters') } }) } diff --git a/umap/static/umap/js/modules/ui/panel.js b/umap/static/umap/js/modules/ui/panel.js index ec6dd530..6d0c14bd 100644 --- a/umap/static/umap/js/modules/ui/panel.js +++ b/umap/static/umap/js/modules/ui/panel.js @@ -2,9 +2,9 @@ import { DomEvent, DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js' import { translate } from '../i18n.js' export class Panel { - constructor(map) { - this.parent = map._controlContainer - this.map = map + constructor(umap) { + this.parent = umap._leafletMap._controlContainer + this.umap = umap this.container = DomUtil.create('div', '', this.parent) // This will be set once according to the panel configurated at load // or by using panels as popups @@ -80,26 +80,26 @@ export class Panel { onClose() { if (DomUtil.hasClass(this.container, 'on')) { DomUtil.removeClass(this.container, 'on') - this.map.invalidateSize({ pan: false }) + this.umap._leafletMap.invalidateSize({ pan: false }) } } } export class EditPanel extends Panel { - constructor(map) { - super(map) + constructor(umap) { + super(umap) this.className = 'right dark' } onClose() { super.onClose() - this.map.editedFeature = null + this.umap.editedFeature = null } } export class FullPanel extends Panel { - constructor(map) { - super(map) + constructor(umap) { + super(umap) this.className = 'full dark' this.mode = 'expanded' } diff --git a/umap/static/umap/js/modules/umap.js b/umap/static/umap/js/modules/umap.js new file mode 100644 index 00000000..11063595 --- /dev/null +++ b/umap/static/umap/js/modules/umap.js @@ -0,0 +1,1603 @@ +import { DomUtil, DomEvent } from '../../vendors/leaflet/leaflet-src.esm.js' +import { translate, setLocale, getLocale } from './i18n.js' +import * as Utils from './utils.js' +import { ServerStored } from './saving.js' +import * as SAVEMANAGER from './saving.js' +import { SyncEngine } from './sync/engine.js' +import { LeafletMap } from './rendering/map.js' +import URLs from './urls.js' +import { Panel, EditPanel, FullPanel } from './ui/panel.js' +import Dialog from './ui/dialog.js' +import Tooltip from './ui/tooltip.js' +import ContextMenu from './ui/contextmenu.js' +import { Request, ServerRequest } from './request.js' +import Help from './help.js' +import { Formatter } from './formatter.js' +import Slideshow from './slideshow.js' +import { MapPermissions } from './permissions.js' +import { SCHEMA } from './schema.js' +import { DataLayer } from './data/layer.js' +import Facets from './facets.js' +import Browser from './browser.js' +import Caption from './caption.js' +import Importer from './importer.js' +import Rules from './rules.js' +import Share from './share.js' +import { + uMapAlertCreation as AlertCreation, + uMapAlert as Alert, +} from '../components/alerts/alert.js' + +export default class Umap extends ServerStored { + constructor(element, geojson) { + super() + // We need to call async function in the init process, + // the init itself does not need to be awaited, but some calls + // in the process must be blocker + this.init(element, geojson) + } + + async init(element, geojson) { + this.properties = Object.assign( + { + enableMarkerDraw: true, + enablePolygonDraw: true, + enablePolylineDraw: true, + hash: true, + limitBounds: {}, + }, + geojson.properties + ) + this.searchParams = new URLSearchParams(window.location.search) + + this.sync_engine = new SyncEngine(this) + this.sync = this.sync_engine.proxy(this) + // Locale name (pt_PT, en_US…) + // To be used for Django localization + if (geojson.properties.locale) setLocale(geojson.properties.locale) + + // Language code (pt-pt, en-us…) + // To be used in javascript APIs + if (geojson.properties.lang) U.lang = geojson.properties.lang + + this.setPropertiesFromQueryString() + + // Needed for actions labels + this.help = new Help(this) + // Prevent default creation of controls + const zoomControl = this.properties.zoomControl + const fullscreenControl = this.properties.fullscreenControl + const center = geojson.geometry + this.properties.zoomControl = false + this.properties.fullscreenControl = false + + this._leafletMap = new LeafletMap(this, element) + + this.properties.zoomControl = zoomControl !== undefined ? zoomControl : true + this.properties.fullscreenControl = + fullscreenControl !== undefined ? fullscreenControl : true + + if (center) { + this._leafletMap.options.center = this._leafletMap.latLng(center) + } + this._leafletMap.attachToDom() + + if (geojson.properties.schema) this.overrideSchema(geojson.properties.schema) + + this.urls = new URLs(this.properties.urls) + + this.panel = new Panel(this) + this.dialog = new Dialog({ className: 'dark' }) + this.tooltip = new Tooltip(this._leafletMap._controlContainer) + this.contextmenu = new ContextMenu() + if (this.hasEditMode()) { + this.editPanel = new EditPanel(this) + this.fullPanel = new FullPanel(this) + } + this.server = new ServerRequest() + this.request = new Request() + this.facets = new Facets(this) + this.browser = new Browser(this) + this.caption = new Caption(this) + this.importer = new Importer(this) + this.share = new Share(this) + this.rules = new Rules(this) + + this.datalayersFromQueryString = this.searchParams.get('datalayers') + if (this.datalayersFromQueryString) { + this.datalayersFromQueryString = this.datalayersFromQueryString + .toString() + .split(',') + } + + // Retrocompat + if ( + this.properties.slideshow?.delay && + this.properties.slideshow.active === undefined + ) { + this.properties.slideshow.active = true + } + if (this.properties.advancedFilterKey) { + this.properties.facetKey = this.properties.advancedFilterKey + delete this.properties.advancedFilterKey + } + + // Global storage for retrieving datalayers and features. + this.datalayers = {} // All datalayers, including deleted. + this.datalayersIndex = [] // Datalayers actually on the map and ordered. + this.featuresIndex = {} + + this.formatter = new Formatter(this) + + this.initDataLayers() + + if (this.properties.displayCaptionOnLoad) { + // Retrocompat + if (!this.properties.onLoadPanel) { + this.properties.onLoadPanel = 'caption' + } + delete this.properties.displayCaptionOnLoad + } + if (this.properties.displayDataBrowserOnLoad) { + // Retrocompat + if (!this.properties.onLoadPanel) { + this.properties.onLoadPanel = 'databrowser' + } + delete this.properties.displayDataBrowserOnLoad + } + if (this.properties.datalayersControl === 'expanded') { + if (!this.properties.onLoadPanel) { + this.properties.onLoadPanel = 'datalayers' + } + delete this.properties.datalayersControl + } + if (this.properties.onLoadPanel === 'facet') { + this.properties.onLoadPanel = 'datafilters' + } + + // Creation mode + if (!this.properties.umap_id) { + if (!this.properties.preview) { + this.isDirty = true + this.enableEdit() + } + this._defaultExtent = true + this.properties.name = translate('Untitled map') + await this.loadDataFromQueryString() + } + + this.slideshow = new Slideshow(this, this.properties.slideshow) + this.permissions = new MapPermissions(this) + if (this.hasEditMode()) { + this._leafletMap.initEditTools() + } + + if (!this.properties.noControl) { + this.initShortcuts() + this._leafletMap.initCaptionBar() + this._leafletMap.on('contextmenu', this.onContextMenu) + this.onceDataLoaded(this.setViewFromQueryString) + this.propagate() + } + + window.onbeforeunload = () => (this.editEnabled && SAVEMANAGER.isDirty) || null + this.backup() + } + + get editedFeature() { + return this._editedFeature + } + + set editedFeature(feature) { + if (this._editedFeature && this._editedFeature !== feature) { + this._editedFeature.endEdit() + } + this._editedFeature = feature + this._leafletMap.fire('seteditedfeature') + } + + setPropertiesFromQueryString() { + const asBoolean = (key) => { + const value = this.searchParams.get(key) + if (value !== undefined && value !== null) { + this.properties[key] = value === '1' || value === 'true' + } + } + const asNullableBoolean = (key) => { + if (this.searchParams.has(key)) { + let value = this.searchParams.get(key) + if (value === 'null') value = null + else if (value === '0' || value === 'false') value = false + else value = true + this.properties[key] = value + } + } + const asNumber = (key) => { + const value = +this.searchParams.get(key) + if (!Number.isNaN(value)) this.properties[name] = value + } + // FIXME retrocompat + asBoolean('displayDataBrowserOnLoad') + asBoolean('displayCaptionOnLoad') + for (const [key, schema] of Object.entries(U.SCHEMA)) { + switch (schema.type) { + case Boolean: + if (schema.nullable) asNullableBoolean(key) + else asBoolean(key) + break + case Number: + asNumber(key) + break + case String: { + if (this.searchParams.has(key)) { + const value = this.searchParams.get(key) + if (value !== undefined) this.properties[key] = value + break + } + } + } + } + // Specific case for datalayersControl + // which accepts "expanded" value, on top of true/false/null + if (this.searchParams.get('datalayersControl') === 'expanded') { + if (!this.properties.onLoadPanel) { + this.properties.onLoadPanel = 'datalayers' + } + } + } + + async setViewFromQueryString() { + if (this.properties.noControl) return + // TODO: move to a "initPanel" function + if (this.searchParams.has('share')) { + this.share.open() + } else if (this.properties.onLoadPanel === 'databrowser') { + this.panel.setDefaultMode('expanded') + this.openBrowser('data') + } else if (this.properties.onLoadPanel === 'datalayers') { + this.panel.setDefaultMode('condensed') + this.openBrowser('layers') + } else if (this.properties.onLoadPanel === 'datafilters') { + this.panel.setDefaultMode('expanded') + this.openBrowser('filters') + } else if (this.properties.onLoadPanel === 'caption') { + this.panel.setDefaultMode('condensed') + this.openCaption() + } + // Comes after default panels, so if it opens in a panel it will + // take precedence. + const slug = this.searchParams.get('feature') + if (slug && this.featuresIndex[slug]) this.featuresIndex[slug].view() + if (this.searchParams.has('edit')) { + if (this.hasEditMode()) this.enableEdit() + // Sometimes users share the ?edit link by mistake, let's remove + // this search parameter from URL to prevent this + const url = new URL(window.location) + url.searchParams.delete('edit') + history.pushState({}, '', url) + } + if (this.searchParams.has('download')) { + const download_url = this.urls.get('map_download', { + map_id: this.properties.umap_id, + }) + window.location = download_url + } + } + + async loadDataFromQueryString() { + let data = this.searchParams.get('data') + const dataUrls = this.searchParams.getAll('dataUrl') + const dataFormat = this.searchParams.get('dataFormat') || 'geojson' + if (dataUrls.length) { + for (let dataUrl of dataUrls) { + dataUrl = decodeURIComponent(dataUrl) + dataUrl = this.localizeUrl(dataUrl) + dataUrl = this.proxyUrl(dataUrl) + const datalayer = this.createDataLayer() + await datalayer.importFromUrl(dataUrl, dataFormat) + } + } else if (data) { + data = decodeURIComponent(data) + const datalayer = this.createDataLayer() + await datalayer.importRaw(data, dataFormat) + } + } + + getOwnContextMenuItems(event) { + const items = [] + if (this.hasEditMode()) { + if (this.editEnabled) { + if (!SAVEMANAGER.isDirty) { + items.push({ + label: this.help.displayLabel('STOP_EDIT'), + action: () => this.disableEdit(), + }) + } + if (this.properties.enableMarkerDraw) { + items.push({ + label: this.help.displayLabel('DRAW_MARKER'), + action: () => this._leafletMap.startMarker(event), + }) + } + if (this.properties.enablePolylineDraw) { + items.push({ + label: this.help.displayLabel('DRAW_POLYGON'), + action: () => this._leafletMap.startPolygon(event), + }) + } + if (this.properties.enablePolygonDraw) { + items.push({ + label: this.help.displayLabel('DRAW_LINE'), + action: () => this._leafletMap.startPolyline(event), + }) + } + items.push('-') + items.push({ + label: translate('Help'), + action: () => this.help.show('edit'), + }) + } else { + items.push({ + label: this.help.displayLabel('TOGGLE_EDIT'), + action: () => this.enableEdit(), + }) + } + } + if (items.length) { + items.push('-') + } + items.push( + { + label: translate('Open browser'), + action: () => this.openBrowser('layers'), + }, + { + label: translate('Browse data'), + action: () => this.openBrowser('data'), + } + ) + if (this.properties.facetKey) { + items.push({ + label: translate('Filter data'), + action: () => this.openBrowser('filters'), + }) + } + items.push( + { + label: translate('Open caption'), + action: () => this.openCaption(), + }, + { + label: this.help.displayLabel('SEARCH'), + action: () => this.search(), + } + ) + return items + } + + getContextMenuItems(event) { + const items = [] + if (this.properties.urls.routing) { + items.push('-', { + label: translate('Directions from here'), + action: () => this.openExternalRouting(event), + }) + } + if (this.properties.urls.edit_in_osm) { + items.push('-', { + label: translate('Edit in OpenStreetMap'), + action: () => this.editInOSM(event), + }) + } + return items + } + + onContextMenu(event) { + const items = this.getOwnContextMenuItems(event).concat( + this.getContextMenuItems(event) + ) + this.contextmenu.open(event.originalEvent, items) + } + + // Merge the given schema with the default one + // Missing keys inside the schema are merged with the default ones. + overrideSchema(schema) { + for (const [key, extra] of Object.entries(schema)) { + SCHEMA[key] = Object.assign({}, SCHEMA[key], extra) + } + } + + hasEditMode() { + const editMode = this.properties.editMode + return editMode === 'simple' || editMode === 'advanced' + } + + getDefaultOption(key) { + return SCHEMA[key]?.default + } + + getOption(key, feature) { + if (feature) { + const value = this.rules.getOption(key, feature) + if (value !== undefined) return value + } + if (Utils.usableOption(this.properties, key)) return this.properties[key] + return this.getDefaultOption(key) + } + + getGeoContext() { + const bounds = this._leafletMap.getBounds() + const center = this._leafletMap.getCenter() + const context = { + bbox: bounds.toBBoxString(), + north: bounds.getNorthEast().lat, + east: bounds.getNorthEast().lng, + south: bounds.getSouthWest().lat, + west: bounds.getSouthWest().lng, + lat: center.lat, + lng: center.lng, + zoom: this._leafletMap.getZoom(), + } + context.left = context.west + context.bottom = context.south + context.right = context.east + context.top = context.north + return context + } + + localizeUrl(url) { + return Utils.greedyTemplate(url, this.getGeoContext(), true) + } + + initShortcuts() { + const globalShortcuts = function (e) { + if (e.key === 'Escape') { + if (this.importer.dialog.visible) { + this.importer.dialog.close() + } else if (this.editEnabled && this._leafletMap.editTools.drawing()) { + this._leafletMap.editTools.onEscape() + } else if (this._leafletMap.measureTools.enabled()) { + this._leafletMap.measureTools.stopDrawing() + } else if (this.fullPanel?.isOpen()) { + this.fullPanel?.close() + } else if (this.editPanel?.isOpen()) { + this.editPanel?.close() + } else if (this.panel.isOpen()) { + this.panel.close() + } + } + + // From now on, only ctrl/meta shortcut + if (!(e.ctrlKey || e.metaKey) || e.shiftKey) return + + if (e.key === 'f') { + L.DomEvent.stop(e) + this.search() + } + + /* Edit mode only shortcuts */ + if (!this.hasEditMode()) return + + // Edit mode Off + if (!this.editEnabled) { + switch (e.key) { + case 'e': + L.DomEvent.stop(e) + this.enableEdit() + break + } + return + } + + // Edit mode on + let used = true + switch (e.key) { + case 'e': + if (!SAVEMANAGER.isDirty) this.disableEdit() + break + case 's': + if (SAVEMANAGER.isDirty) this.saveAll() + break + case 'z': + if (SAVEMANAGER.isDirty) this.askForReset() + break + case 'm': + this._leafletMap.editTools.startMarker() + break + case 'p': + this._leafletMap.editTools.startPolygon() + break + case 'l': + this._leafletMap.editTools.startPolyline() + break + case 'i': + this._leafletMap.importer.open() + break + case 'o': + this._leafletMap.importer.openFiles() + break + case 'h': + this.help.show('edit') + break + default: + used = false + } + if (used) DomEvent.stop(e) + } + DomEvent.addListener(document, 'keydown', globalShortcuts, this) + } + + async initDataLayers(datalayers) { + datalayers = datalayers || this.properties.datalayers + for (const options of datalayers) { + // `false` to not propagate syncing elements served from uMap + this.createDataLayer(options, false) + } + this.datalayersLoaded = true + this._leafletMap.fire('datalayersloaded') + for (const datalayer of this.datalayersIndex) { + if (datalayer.showAtLoad()) await datalayer.show() + } + this.dataloaded = true + this._leafletMap.fire('dataloaded') + } + + createDataLayer(options = {}, sync = true) { + options.name = + options.name || `${translate('Layer')} ${this.datalayersIndex.length + 1}` + const datalayer = new DataLayer(this, options, sync) + + if (sync !== false) { + datalayer.sync.upsert(datalayer.options) + } + return datalayer + } + + newDataLayer() { + const datalayer = this.createDataLayer({}) + datalayer.edit() + } + + reindexDataLayers() { + this.eachDataLayer((datalayer) => datalayer.reindex()) + this.onDataLayersChanged() + } + + redrawVisibleDataLayers() { + this.eachVisibleDataLayer((datalayer) => { + datalayer.redraw() + }) + } + + indexDatalayers() { + const panes = this._leafletMap.getPane('overlayPane') + + this.datalayersIndex = [] + for (const pane of panes) { + if (!pane.dataset || !pane.dataset.id) continue + this.datalayersIndex.push(this.datalayers[pane.dataset.id]) + } + this.onDataLayersChanged() + } + + onceDatalayersLoaded(callback, context) { + // Once datalayers **metadata** have been loaded + if (this.datalayersLoaded) { + callback.call(context || this, this) + } else { + this._leafletMap.once('datalayersloaded', callback, context) + } + return this + } + + onceDataLoaded(callback, context) { + // Once datalayers **data** have been loaded + if (this.dataloaded) { + callback.call(context || this, this) + } else { + this._leafletMap.once('dataloaded', callback, context || this) + } + return this + } + + onDataLayersChanged() { + if (this.browser) this.browser.update() + this.caption.refresh() + } + + async saveAll() { + if (!SAVEMANAGER.isDirty) return + if (this._defaultExtent) this._setCenterAndZoom() + this.backup() + await SAVEMANAGER.save() + // Do a blind render for now, as we are not sure what could + // have changed, we'll be more subtil when we'll remove the + // save action + this.render(['name', 'user', 'permissions']) + this._leafletMap.fire('saved') + } + + propagate() { + let els = document.querySelectorAll('.map-name') + for (const el of els) { + el.textContent = this.getDisplayName() + } + const status = this.permissions.getShareStatusDisplay() + els = document.querySelectorAll('.share-status') + for (const el of els) { + if (status) { + el.textContent = translate('Visibility: {status}', { + status: status, + }) + } + } + } + + getDisplayName() { + 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.options) + } + + resetProperties() { + this.properties = Object.assign({}, this._backupProperties) + this.properties.tilelayer = Object.assign({}, this._backupProperties.tilelayer) + this.permissions.options = Object.assign({}, this._backupProperties.permissions) + } + + hasData() { + for (const datalayer of this.datalayersIndex) { + if (datalayer.hasData()) return true + } + } + + hasLayers() { + return Boolean(this.datalayersIndex.length) + } + + allProperties() { + return [].concat(...this.datalayersIndex.map((dl) => dl._propertiesIndex)) + } + + sortedValues(property) { + return [] + .concat(...this.datalayersIndex.map((dl) => dl.sortedValues(property))) + .filter((val, idx, arr) => arr.indexOf(val) === idx) + .sort(U.Utils.naturalSort) + } + + editCaption() { + if (!this.editEnabled) return + if (this.properties.editMode !== 'advanced') return + const container = DomUtil.create('div', 'umap-edit-container') + const metadataFields = ['properties.name', 'properties.description'] + + DomUtil.createTitle(container, translate('Edit map details'), 'icon-caption') + const builder = new U.FormBuilder(this, metadataFields, { + className: 'map-metadata', + umap: this, + }) + const form = builder.build() + container.appendChild(form) + + const credits = DomUtil.createFieldset(container, translate('Credits')) + const creditsFields = [ + 'properties.licence', + 'properties.shortCredit', + 'properties.longCredit', + 'properties.permanentCredit', + 'properties.permanentCreditBackground', + ] + const creditsBuilder = new U.FormBuilder(this, creditsFields, { umap: this }) + credits.appendChild(creditsBuilder.build()) + this.editPanel.open({ content: container }) + } + + _editControls(container) { + let UIFields = [] + for (const name of this._leafletMap.HIDDABLE_CONTROLS) { + UIFields.push(`properties.${name}Control`) + } + UIFields = UIFields.concat([ + 'properties.moreControl', + 'properties.scrollWheelZoom', + 'properties.miniMap', + 'properties.scaleControl', + 'properties.onLoadPanel', + 'properties.defaultView', + 'properties.displayPopupFooter', + 'properties.captionBar', + 'properties.captionMenus', + ]) + const builder = new U.FormBuilder(this, UIFields, { umap: this }) + const controlsOptions = DomUtil.createFieldset( + container, + translate('User interface options') + ) + controlsOptions.appendChild(builder.build()) + } + + _editShapeProperties(container) { + const shapeOptions = [ + 'properties.color', + 'properties.iconClass', + 'properties.iconUrl', + 'properties.iconOpacity', + 'properties.opacity', + 'properties.weight', + 'properties.fill', + 'properties.fillColor', + 'properties.fillOpacity', + 'properties.smoothFactor', + 'properties.dashArray', + ] + + const builder = new U.FormBuilder(this, shapeOptions, { umap: this }) + const defaultShapeProperties = DomUtil.createFieldset( + container, + translate('Default shape properties') + ) + defaultShapeProperties.appendChild(builder.build()) + } + + _editDefaultProperties(container) { + const optionsFields = [ + 'properties.zoomTo', + 'properties.easing', + 'properties.labelKey', + 'properties.sortKey', + 'properties.filterKey', + 'properties.facetKey', + 'properties.slugKey', + ] + + const builder = new U.FormBuilder(this, optionsFields, { umap: this }) + const defaultProperties = DomUtil.createFieldset( + container, + translate('Default properties') + ) + defaultProperties.appendChild(builder.build()) + } + + _editInteractionsProperties(container) { + const popupFields = [ + 'properties.popupShape', + 'properties.popupTemplate', + 'properties.popupContentTemplate', + 'properties.showLabel', + 'properties.labelDirection', + 'properties.labelInteractive', + 'properties.outlinkTarget', + ] + const builder = new U.FormBuilder(this, popupFields, { umap: this }) + const popupFieldset = DomUtil.createFieldset( + container, + translate('Default interaction options') + ) + popupFieldset.appendChild(builder.build()) + } + + _editTilelayer(container) { + if (!Utils.isObject(this.properties.tilelayer)) { + this.properties.tilelayer = {} + } + const tilelayerFields = [ + [ + 'properties.tilelayer.name', + { handler: 'BlurInput', placeholder: translate('display name') }, + ], + [ + 'properties.tilelayer.url_template', + { + handler: 'BlurInput', + helpText: `${translate('Supported scheme')}: http://{s}.domain.com/{z}/{x}/{y}.png`, + placeholder: 'url', + type: 'url', + }, + ], + [ + 'properties.tilelayer.maxZoom', + { + handler: 'BlurIntInput', + placeholder: translate('max zoom'), + min: 0, + max: this.properties.maxZoomLimit, + }, + ], + [ + 'properties.tilelayer.minZoom', + { + handler: 'BlurIntInput', + placeholder: translate('min zoom'), + min: 0, + max: this.properties.maxZoomLimit, + }, + ], + [ + 'properties.tilelayer.attribution', + { handler: 'BlurInput', placeholder: translate('attribution') }, + ], + [ + 'properties.tilelayer.tms', + { handler: 'Switch', label: translate('TMS format') }, + ], + ] + const customTilelayer = DomUtil.createFieldset( + container, + translate('Custom background') + ) + const builder = new U.FormBuilder(this, tilelayerFields, { umap: this }) + customTilelayer.appendChild(builder.build()) + } + + _editOverlay(container) { + if (!Utils.isObject(this.properties.overlay)) { + this.properties.overlay = {} + } + const overlayFields = [ + [ + 'properties.overlay.url_template', + { + handler: 'BlurInput', + helpText: `${translate('Supported scheme')}: http://{s}.domain.com/{z}/{x}/{y}.png`, + placeholder: 'url', + label: translate('Background overlay url'), + type: 'url', + }, + ], + [ + 'properties.overlay.maxZoom', + { + handler: 'BlurIntInput', + placeholder: translate('max zoom'), + min: 0, + max: this.properties.maxZoomLimit, + }, + ], + [ + 'properties.overlay.minZoom', + { + handler: 'BlurIntInput', + placeholder: translate('min zoom'), + min: 0, + max: this.properties.maxZoomLimit, + }, + ], + [ + 'properties.overlay.attribution', + { handler: 'BlurInput', placeholder: translate('attribution') }, + ], + [ + 'properties.overlay.opacity', + { handler: 'Range', min: 0, max: 1, step: 0.1, label: translate('Opacity') }, + ], + ['properties.overlay.tms', { handler: 'Switch', label: translate('TMS format') }], + ] + const overlay = DomUtil.createFieldset(container, translate('Custom overlay')) + const builder = new U.FormBuilder(this, overlayFields, { umap: this }) + overlay.appendChild(builder.build()) + } + + _editBounds(container) { + if (!Utils.isObject(this.properties.limitBounds)) { + this.properties.limitBounds = {} + } + const limitBounds = DomUtil.createFieldset(container, translate('Limit bounds')) + const boundsFields = [ + [ + 'properties.limitBounds.south', + { handler: 'BlurFloatInput', placeholder: translate('max South') }, + ], + [ + 'properties.limitBounds.west', + { handler: 'BlurFloatInput', placeholder: translate('max West') }, + ], + [ + 'properties.limitBounds.north', + { handler: 'BlurFloatInput', placeholder: translate('max North') }, + ], + [ + 'properties.limitBounds.east', + { handler: 'BlurFloatInput', placeholder: translate('max East') }, + ], + ] + const boundsBuilder = new U.FormBuilder(this, boundsFields, { umap: this }) + limitBounds.appendChild(boundsBuilder.build()) + const boundsButtons = DomUtil.create('div', 'button-bar half', limitBounds) + DomUtil.createButton( + 'button', + boundsButtons, + translate('Use current bounds'), + function () { + const bounds = this._leafletMap.getBounds() + this.properties.limitBounds.south = L.Util.formatNum(bounds.getSouth()) + this.properties.limitBounds.west = L.Util.formatNum(bounds.getWest()) + this.properties.limitBounds.north = L.Util.formatNum(bounds.getNorth()) + this.properties.limitBounds.east = L.Util.formatNum(bounds.getEast()) + boundsBuilder.fetchAll() + + this.sync.update(this, 'properties.limitBounds', this.properties.limitBounds) + this.isDirty = true + this._leafletMap.handleLimitBounds() + }, + this + ) + DomUtil.createButton( + 'button', + boundsButtons, + translate('Empty'), + function () { + this.properties.limitBounds.south = null + this.properties.limitBounds.west = null + this.properties.limitBounds.north = null + this.properties.limitBounds.east = null + boundsBuilder.fetchAll() + this.isDirty = true + this._leafletMap.handleLimitBounds() + }, + this + ) + } + + _editSlideshow(container) { + const slideshow = DomUtil.createFieldset(container, translate('Slideshow')) + const slideshowFields = [ + [ + 'properties.slideshow.active', + { handler: 'Switch', label: translate('Activate slideshow mode') }, + ], + [ + 'properties.slideshow.delay', + { + handler: 'SlideshowDelay', + helpText: translate('Delay between two transitions when in play mode'), + }, + ], + [ + 'properties.slideshow.easing', + { + handler: 'Switch', + label: translate('Animated transitions'), + inheritable: true, + }, + ], + [ + 'properties.slideshow.autoplay', + { handler: 'Switch', label: translate('Autostart when map is loaded') }, + ], + ] + const slideshowBuilder = new U.FormBuilder(this, slideshowFields, { + callback: () => this.slideshow.setOptions(this.properties.slideshow), + umap: this, + }) + slideshow.appendChild(slideshowBuilder.build()) + } + + _editSync(container) { + const sync = DomUtil.createFieldset(container, translate('Real-time collaboration')) + const builder = new U.FormBuilder(this, ['properties.syncEnabled'], { umap: this }) + sync.appendChild(builder.build()) + } + + _advancedActions(container) { + const advancedActions = DomUtil.createFieldset( + container, + translate('Advanced actions') + ) + const advancedButtons = DomUtil.create('div', 'button-bar half', advancedActions) + if (this.permissions.isOwner()) { + const deleteButton = Utils.loadTemplate(` + `) + deleteButton.addEventListener('click', () => this.del()) + advancedButtons.appendChild(deleteButton) + + DomUtil.createButton( + 'button umap-empty', + advancedButtons, + translate('Clear data'), + this.emptyDataLayers, + this + ) + DomUtil.createButton( + 'button umap-empty', + advancedButtons, + translate('Remove layers'), + this.removeDataLayers, + this + ) + } + DomUtil.createButton( + 'button umap-clone', + advancedButtons, + translate('Clone this map'), + this.clone, + this + ) + DomUtil.createButton( + 'button umap-download', + advancedButtons, + translate('Open share & download panel'), + this.share.open, + this.share + ) + } + + edit() { + if (!this.editEnabled) return + if (this.properties.editMode !== 'advanced') return + const container = DomUtil.create('div') + DomUtil.createTitle( + container, + translate('Map advanced properties'), + 'icon-settings' + ) + this._editControls(container) + this._editShapeProperties(container) + this._editDefaultProperties(container) + this._editInteractionsProperties(container) + this.rules.edit(container) + this._editTilelayer(container) + this._editOverlay(container) + this._editBounds(container) + this._editSlideshow(container) + if (this.properties.websocketEnabled) { + this._editSync(container) + } + this._advancedActions(container) + + this.editPanel.open({ content: container, className: 'dark' }) + } + + reset() { + if (this._leafletMap.editTools) this._leafletMap.editTools.stopDrawing() + this.resetProperties() + this.datalayersIndex = [].concat(this._datalayersIndex_bk) + // Iter over all datalayers, including deleted if any. + for (const datalayer of Object.values(this.datalayers)) { + if (datalayer.isDeleted) datalayer.connectToMap() + if (datalayer.isDirty) datalayer.reset() + } + this.ensurePanesOrder() + this._leafletMap.initTileLayers() + this.isDirty = false + this.onDataLayersChanged() + } + + async save() { + this.rules.commit() + const geojson = { + type: 'Feature', + geometry: this.geometry(), + properties: this.exportProperties(), + } + const formData = new FormData() + formData.append('name', this.properties.name) + formData.append('center', JSON.stringify(this.geometry())) + formData.append('settings', JSON.stringify(geojson)) + const uri = this.urls.get('map_save', { map_id: this.properties.umap_id }) + const [data, _, error] = await this.server.post(uri, {}, formData) + // FIXME: login_required response will not be an error, so it will not + // stop code while it should + if (error) { + return + } + // TOOD: map.save may not always be the first call during save process + // since SAVEMANAGER refactor + if (data.login_required) { + window.onLogin = () => this.saveAll() + window.open(data.login_required) + return + } + this.properties.user = data.user + if (!this.properties.umap_id) { + this.properties.umap_id = data.id + this.permissions.setOptions(data.permissions) + this.permissions.commit() + if (data.permissions?.anonymous_edit_url) { + this._leafletMap.once('saved', () => { + AlertCreation.info( + translate('Your map has been created with an anonymous account!'), + Number.Infinity, + data.permissions.anonymous_edit_url, + this.properties.urls.map_send_edit_link + ? this.sendEditLinkEmail.bind(this) + : null + ) + }) + } else { + this._leafletMap.once('saved', () => { + Alert.success(translate('Congratulations, your map has been created!')) + }) + } + } else { + if (!this.permissions.isDirty) { + // Do not override local changes to permissions, + // but update in case some other editors changed them in the meantime. + this.permissions.setOptions(data.permissions) + this.permissions.commit() + } + this._leafletMap.once('saved', () => { + Alert.success(data.info || translate('Map has been saved!')) + }) + } + // Update URL in case the name has changed. + if (history?.pushState) { + history.pushState({}, this.properties.name, data.url) + } else { + window.location = data.url + } + return true + } + + exportProperties() { + const properties = {} + for (const key of Object.keys(SCHEMA)) { + if (this.properties[key] !== undefined) { + properties[key] = this.properties[key] + } + } + return properties + } + + geometry() { + /* Return a GeoJSON geometry Object */ + const latlng = this._leafletMap.latLng( + this._leafletMap.options.center || this._leafletMap.getCenter() + ) + return { + type: 'Point', + coordinates: [latlng.lng, latlng.lat], + } + } + + toGeoJSON() { + let features = [] + this.eachDataLayer((datalayer) => { + if (datalayer.isVisible()) { + features = features.concat(datalayer.featuresToGeoJSON()) + } + }) + const geojson = { + type: 'FeatureCollection', + features: features, + } + return geojson + } + + enableEdit() { + document.body.classList.add('umap-edit-enabled') + this.editEnabled = true + this.drop.enable() + this._leafletMap.fire('edit:enabled') + this.initSyncEngine() + } + + disableEdit() { + if (this.isDirty) return + this.drop.disable() + document.body.classList.remove('umap-edit-enabled') + this.editedFeature = null + this.editEnabled = false + this._leafletMap.fire('edit:disabled') + this.editPanel.close() + this.fullPanel.close() + this.sync.stop() + this.closeInplaceToolbar() + } + + askForReset(e) { + this.dialog + .confirm(translate('Are you sure you want to cancel your changes?')) + .then(() => { + this.reset() + this.disableEdit() + }) + } + + async initSyncEngine() { + if (this.properties.websocketEnabled === false) return + if (this.properties.syncEnabled !== true) { + this.sync.stop() + } else { + const ws_token_uri = this.urls.get('map_websocket_auth_token', { + map_id: this.properties.umap_id, + }) + await this.sync.authenticate( + ws_token_uri, + this.properties.websocketURI, + this.server + ) + } + } + + getSyncMetadata() { + return { + engine: this.sync, + subject: 'map', + } + } + + render(fields) { + if (fields.includes('numberOfConnectedPeers')) { + this.renderEditToolbar() + this.propagate() + } + + const impacts = Utils.getImpactsFromSchema(fields) + for (const impact of impacts) { + switch (impact) { + case 'ui': + this._leafletMap.setOptions(this.properties) + this._leafletMap.initCaptionBar() + this._leafletMap.renderEditToolbar() + this._leafletMap.renderControls() + this.browser.redraw() + this.propagate() + break + case 'data': + this.redrawVisibleDataLayers() + break + case 'datalayer-index': + this.reindexDataLayers() + break + case 'background': + this._leafletMap.initTileLayers() + break + case 'bounds': + this._leafletMap.handleLimitBounds() + break + case 'sync': + this.initSyncEngine() + } + } + } + + // TODO: allow to control the default datalayer + // (edit and viewing) + // cf https://github.com/umap-project/umap/issues/585 + defaultEditDataLayer() { + let datalayer + let fallback + datalayer = this.lastUsedDataLayer + if ( + datalayer && + !datalayer.isDataReadOnly() && + datalayer.isBrowsable() && + datalayer.isVisible() + ) { + return datalayer + } + datalayer = this.findDataLayer((datalayer) => { + if (!datalayer.isDataReadOnly() && datalayer.isBrowsable()) { + fallback = datalayer + if (datalayer.isVisible()) return true + } + }) + if (datalayer) return datalayer + if (fallback) { + // No datalayer visible, let's force one + fallback.show() + return fallback + } + return this.createDataLayer() + } + + findDataLayer(method, context) { + for (let i = this.datalayersIndex.length - 1; i >= 0; i--) { + if (method.call(context, this.datalayersIndex[i])) { + 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() { + this.eachDataLayerReverse((datalayer) => { + datalayer._delete() + }) + } + + emptyDataLayers() { + this.eachDataLayerReverse((datalayer) => { + datalayer.empty() + }) + } + + editDatalayers() { + if (!this.editEnabled) return + const container = DomUtil.create('div') + DomUtil.createTitle(container, translate('Manage layers'), 'icon-layers') + const ul = DomUtil.create('ul', '', container) + this.eachDataLayerReverse((datalayer) => { + const row = DomUtil.create('li', 'orderable', ul) + DomUtil.createIcon(row, 'icon-drag', translate('Drag to reorder')) + datalayer.renderToolbox(row) + const title = DomUtil.add('span', '', row, datalayer.options.name) + row.classList.toggle('off', !datalayer.isVisible()) + title.textContent = datalayer.options.name + row.dataset.id = L.stamp(datalayer) + }) + const onReorder = (src, dst, initialIndex, finalIndex) => { + const layer = this.datalayers[src.dataset.id] + const other = this.datalayers[dst.dataset.id] + const minIndex = Math.min(layer.getRank(), other.getRank()) + const maxIndex = Math.max(layer.getRank(), other.getRank()) + if (finalIndex === 0) layer.bringToTop() + else if (finalIndex > initialIndex) layer.insertBefore(other) + else layer.insertAfter(other) + this.eachDataLayerReverse((datalayer) => { + if (datalayer.getRank() >= minIndex && datalayer.getRank() <= maxIndex) + datalayer.isDirty = true + }) + this.indexDatalayers() + } + const orderable = new U.Orderable(ul, onReorder) + + const bar = DomUtil.create('div', 'button-bar', container) + DomUtil.createButton( + 'show-on-edit block add-datalayer button', + bar, + translate('Add a layer'), + this.newDataLayer, + this + ) + + this.editPanel.open({ content: container }) + } + + getDataLayerByUmapId(umap_id) { + return this.findDataLayer((d) => d.umap_id === umap_id) + } + + firstVisibleDatalayer() { + return this.findDataLayer((datalayer) => { + if (datalayer.isVisible()) return true + }) + } + + ensurePanesOrder() { + this.eachDataLayer((datalayer) => { + datalayer.bringToTop() + }) + } + + openBrowser(mode) { + this.onceDatalayersLoaded(() => this.browser.open(mode)) + } + + openCaption() { + this.onceDatalayersLoaded(() => this.caption.open()) + } + + addAuthorLink(container) { + const author = this.properties.author + if (author?.name) { + const el = Utils.loadTemplate( + ` ${translate('by')} ${author.name}` + ) + container.appendChild(el) + } + } + + async star() { + if (!this.properties.umap_id) { + return Alert.error(translate('Please save the map first')) + } + const url = this.urls.get('map_star', { map_id: this.properties.umap_id }) + const [data, response, error] = await this.server.post(url) + if (error) { + return + } + this.properties.starred = data.starred + Alert.success( + data.starred + ? translate('Map has been starred') + : translate('Map has been unstarred') + ) + this.render(['starred']) + } + + processFileToImport(file, layer, type) { + type = type || Utils.detectFileType(file) + if (!type) { + U.Alert.error( + translate('Unable to detect format of file {filename}', { + filename: file.name, + }) + ) + return + } + if (type === 'umap') { + this.importFromFile(file, 'umap') + } else { + if (!layer) layer = this.createDataLayer({ name: file.name }) + layer.importFromFile(file, type) + } + } + + async importFromUrl(uri) { + const response = await this.request.get(uri) + if (response?.ok) { + this.importRaw(await response.text()) + } + } + + importRaw(rawData) { + const importedData = JSON.parse(rawData) + + let mustReindex = false + + for (const option of Object.keys(U.SCHEMA)) { + if (typeof importedData.properties[option] !== 'undefined') { + this.properties[option] = importedData.properties[option] + if (option === 'sortKey') mustReindex = true + } + } + + if (importedData.geometry) { + this.properties.center = this._leafletMap.latLng(importedData.geometry) + } + for (const geojson of importedData.layers) { + if (!geojson._umap_options && geojson._storage) { + geojson._umap_options = geojson._storage + delete geojson._storage + } + delete geojson._umap_options?.id // Never trust an id at this stage + const dataLayer = this.createDataLayer(geojson._umap_options) + dataLayer.fromUmapGeoJSON(geojson) + } + + // TODO: refactor with leafletMap init / render + this._leafletMap.setOptions(this.properties) + this._leafletMap.initTileLayers() + this._leafletMap.renderControls() + this._leafletMap.handleLimitBounds() + this.eachDataLayer((datalayer) => { + if (mustReindex) datalayer.reindex() + datalayer.redraw() + }) + this.propagate() + this._leafletMap.fire('postsync') + this.isDirty = true + } + + importFromFile(file) { + const reader = new FileReader() + reader.readAsText(file) + reader.onload = (e) => { + const rawData = e.target.result + try { + this.importRaw(rawData) + } catch (e) { + console.error('Error importing data', e) + U.Alert.error( + translate('Invalid umap data in {filename}', { filename: file.name }) + ) + } + } + } + + async del() { + this.dialog + .confirm(translate('Are you sure you want to delete this map?')) + .then(async () => { + const url = this.urls.get('map_delete', { map_id: this.properties.umap_id }) + const [data, response, error] = await this.server.post(url) + if (data.redirect) window.location = data.redirect + }) + } + + async clone() { + this.dialog + .confirm( + translate('Are you sure you want to clone this map and all its datalayers?') + ) + .then(async () => { + const url = this.urls.get('map_clone', { map_id: this.properties.umap_id }) + const [data, response, error] = await this.server.post(url) + if (data.redirect) window.location = data.redirect + }) + } + + async sendEditLinkEmail(formData) { + const sendLink = + this.properties.urls.map_send_edit_link && + this.urls.get('map_send_edit_link', { + map_id: this.properties.umap_id, + }) + await this.server.post(sendLink, {}, formData) + } + + getLayersBounds() { + const bounds = new L.latLngBounds() + this.eachBrowsableDataLayer((d) => { + if (d.isVisible()) bounds.extend(d.layer.getBounds()) + }) + return bounds + } + + fitDataBounds() { + const bounds = this.getLayersBounds() + if (!this.hasData() || !bounds.isValid()) return false + this._leafletMap.fitBounds(bounds) + } + + proxyUrl(url, ttl) { + if (this.properties.urls.ajax_proxy) { + url = Utils.greedyTemplate(this.properties.urls.ajax_proxy, { + url: encodeURIComponent(url), + ttl: ttl, + }) + } + return url + } + + openExternalRouting(event) { + const url = this.urls.get('routing', { + lat: event.latlng.lat, + lng: event.latlng.lng, + locale: getLocale(), + zoom: this._leafletMap.getZoom(), + }) + if (url) window.open(url) + } + + editInOSM(event) { + const url = this.urls.get('edit_in_osm', { + lat: event.latlng.lat, + lng: event.latlng.lng, + zoom: Math.max(this._leafletMap.getZoom(), 16), + }) + if (url) window.open(url) + } + + setCenterAndZoom() { + this._setCenterAndZoom() + Alert.success(L._('The zoom and center have been modified.')) + } + + _setCenterAndZoom() { + this._leafletMap.options.center = this._leafletMap.getCenter() + this._leafletMap.options.zoom = this._leafletMap.getZoom() + this.isDirty = true + this._defaultExtent = false + } +} diff --git a/umap/static/umap/js/modules/utils.js b/umap/static/umap/js/modules/utils.js index 3e451720..8ce52c99 100644 --- a/umap/static/umap/js/modules/utils.js +++ b/umap/static/umap/js/modules/utils.js @@ -41,7 +41,7 @@ export function getImpactsFromSchema(fields, schema) { // remove the option prefix for fields // And only keep the first part in case of a subfield // (e.g "options.limitBounds.foobar" will just return "limitBounds") - return field.replace('options.', '').split('.')[0] + return field.replace('options.', '').replace('properties.', '').split('.')[0] }) .reduce((acc, field) => { // retrieve the "impacts" field from the schema diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index e2494f9c..2b4602ac 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -2,7 +2,7 @@ U.BaseAction = L.ToolbarAction.extend({ initialize: function (map) { this.map = map if (this.options.label) { - this.options.tooltip = this.map.help.displayLabel( + this.options.tooltip = this.map.umap.help.displayLabel( this.options.label, (withKbdTag = false) ) @@ -25,7 +25,7 @@ U.ImportAction = U.BaseAction.extend({ }, addHooks: function () { - this.map.importer.open() + this.map.umap.importer.open() }, }) @@ -37,7 +37,7 @@ U.EditLayersAction = U.BaseAction.extend({ }, addHooks: function () { - this.map.editDatalayers() + this.map.umap.editDatalayers() }, }) @@ -49,7 +49,7 @@ U.EditCaptionAction = U.BaseAction.extend({ }, addHooks: function () { - this.map.editCaption() + this.map.umap.editCaption() }, }) @@ -61,7 +61,7 @@ U.EditPropertiesAction = U.BaseAction.extend({ }, addHooks: function () { - this.map.edit() + this.map.umap.edit() }, }) @@ -84,7 +84,7 @@ U.UpdateExtentAction = U.BaseAction.extend({ }, addHooks: function () { - this.map.setCenterAndZoom() + this.map.umap.setCenterAndZoom() }, }) @@ -95,7 +95,7 @@ U.UpdatePermsAction = U.BaseAction.extend({ }, addHooks: function () { - this.map.permissions.edit() + this.map.umap.permissions.edit() }, }) @@ -142,7 +142,8 @@ U.AddPolylineShapeAction = U.BaseAction.extend({ }, addHooks: function () { - this.map.editedFeature.ui.editor.newShape() + // FIXME: smells bad + this.map.umap.editedFeature.ui.editor.newShape() }, }) @@ -305,18 +306,24 @@ U.DrawToolbar = L.Toolbar.Control.extend({ appendToContainer: function (container) { this.options.actions = [] - if (this.map.options.enableMarkerDraw) { + if (this.map.umap.properties.enableMarkerDraw) { this.options.actions.push(U.DrawMarkerAction) } - if (this.map.options.enablePolylineDraw) { + if (this.map.umap.properties.enablePolylineDraw) { this.options.actions.push(U.DrawPolylineAction) - if (this.map.editedFeature && this.map.editedFeature instanceof U.LineString) { + if ( + this.map.umap.editedFeature && + this.map.umap.editedFeature instanceof U.LineString + ) { this.options.actions.push(U.AddPolylineShapeAction) } } - if (this.map.options.enablePolygonDraw) { + if (this.map.umap.properties.enablePolygonDraw) { this.options.actions.push(U.DrawPolygonAction) - if (this.map.editedFeature && this.map.editedFeature instanceof U.Polygon) { + if ( + this.map.umap.editedFeature && + this.map.umap.editedFeature instanceof U.Polygon + ) { this.options.actions.push(U.AddPolygonShapeAction) } } @@ -360,14 +367,14 @@ U.DropControl = L.Class.extend({ L.DomEvent.stop(e) }, - drop: function (e) { + drop: function (event) { this.map.scrollWheelZoom.enable() this.dropzone.classList.remove('umap-dragover') L.DomEvent.stop(e) - for (let i = 0, file; (file = e.dataTransfer.files[i]); i++) { - this.map.processFileToImport(file) + for (const file of event.dataTransfer.files) { + this.map.umap.processFileToImport(file) } - this.map.onceDataLoaded(this.map.fitDataBounds) + this.map.umap.onceDataLoaded(this.map.umap.fitDataBounds) }, dragleave: function () { @@ -387,15 +394,15 @@ U.EditControl = L.Control.extend({ '', container, L._('Edit'), - map.enableEdit, - map + map.umap.enableEdit, + map.umap ) L.DomEvent.on( enableEditing, 'mouseover', () => { - map.tooltip.open({ - content: map.help.displayLabel('TOGGLE_EDIT'), + map.umap.tooltip.open({ + content: map.umap.help.displayLabel('TOGGLE_EDIT'), anchor: enableEditing, position: 'bottom', delay: 750, @@ -476,8 +483,8 @@ U.PermanentCreditsControl = L.Control.extend({ }) L.Control.Button = L.Control.extend({ - initialize: function (map, options) { - this.map = map + initialize: function (umap, options) { + this.umap = umap L.Control.prototype.initialize.call(this, options) }, @@ -510,11 +517,11 @@ U.DataLayersControl = L.Control.Button.extend({ }, afterAdd: function (container) { - U.Utils.toggleBadge(container, this.map.browser.hasFilters()) + U.Utils.toggleBadge(container, this.umap.browser?.hasFilters()) }, onClick: function () { - this.map.openBrowser() + this.umap.openBrowser() }, }) @@ -526,7 +533,7 @@ U.CaptionControl = L.Control.Button.extend({ }, onClick: function () { - this.map.openCaption() + this.umap.openCaption() }, }) @@ -537,12 +544,13 @@ U.StarControl = L.Control.Button.extend({ }, getClassName: function () { - const status = this.map.options.starred ? ' starred' : '' + const status = this.umap.properties.starred ? ' starred' : '' return `leaflet-control-star umap-control${status}` }, onClick: function () { - this.map.star() + console.log(this.umap) + this.umap.star() }, }) @@ -554,248 +562,10 @@ L.Control.Embed = L.Control.Button.extend({ }, onClick: function () { - this.map.share.open() + this.umap.share.open() }, }) -const ControlsMixin = { - HIDDABLE_CONTROLS: [ - 'zoom', - 'search', - 'fullscreen', - 'embed', - 'datalayers', - 'caption', - 'locate', - 'measure', - 'editinosm', - 'star', - 'tilelayers', - ], - - renderEditToolbar: function () { - const className = 'umap-main-edit-toolbox' - const container = - document.querySelector(`.${className}`) || - L.DomUtil.create( - 'div', - `${className} with-transition dark`, - this._controlContainer - ) - container.innerHTML = '' - const leftContainer = L.DomUtil.create('div', 'umap-left-edit-toolbox', container) - const rightContainer = L.DomUtil.create('div', 'umap-right-edit-toolbox', container) - const logo = L.DomUtil.create('div', 'logo', leftContainer) - L.DomUtil.createLink('', logo, 'uMap', '/', null, L._('Go to the homepage')) - const nameButton = L.DomUtil.createButton('map-name', leftContainer, '') - L.DomEvent.on( - nameButton, - 'mouseover', - function () { - this.tooltip.open({ - content: L._('Edit the title of the map'), - anchor: nameButton, - position: 'bottom', - delay: 500, - duration: 5000, - }) - }, - this - ) - const shareStatusButton = L.DomUtil.createButton( - 'share-status', - leftContainer, - '', - this.permissions.edit, - this.permissions - ) - L.DomEvent.on( - shareStatusButton, - 'mouseover', - function () { - this.tooltip.open({ - content: L._('Update who can see and edit the map'), - anchor: shareStatusButton, - position: 'bottom', - delay: 500, - duration: 5000, - }) - }, - this - ) - if (this.options.editMode === 'advanced') { - L.DomEvent.on(nameButton, 'click', this.editCaption, this) - L.DomEvent.on(shareStatusButton, 'click', this.permissions.edit, this.permissions) - } - if (this.options.user?.id) { - const button = U.Utils.loadTemplate(` - - `) - rightContainer.appendChild(button) - const menu = new U.ContextMenu({ className: 'dark', fixed: true }) - const actions = [ - { - label: L._('New map'), - action: this.urls.get('map_new'), - }, - { - label: L._('My maps'), - action: this.urls.get('user_dashboard'), - }, - { - label: L._('My teams'), - action: this.urls.get('user_teams'), - }, - ] - if (this.urls.has('user_profile')) { - actions.push({ - label: L._('My profile'), - action: this.urls.get('user_profile'), - }) - } - button.addEventListener('click', () => { - menu.openBelow(button, actions) - }) - } - - const connectedPeers = this.sync.getNumberOfConnectedPeers() - if (connectedPeers !== 0) { - const connectedPeersCount = L.DomUtil.createButton( - 'leaflet-control-connected-peers', - rightContainer, - '' - ) - L.DomEvent.on(connectedPeersCount, 'mouseover', () => { - this.tooltip.open({ - content: L._('{connectedPeers} peer(s) currently connected to this map', { - connectedPeers: connectedPeers, - }), - anchor: connectedPeersCount, - position: 'bottom', - delay: 500, - duration: 5000, - }) - }) - - const updateConnectedPeersCount = () => { - connectedPeersCount.innerHTML = - '' + this.sync.getNumberOfConnectedPeers() + '' - } - updateConnectedPeersCount() - } - - this.help.getStartedLink(rightContainer) - const controlEditCancel = L.DomUtil.createButton( - 'leaflet-control-edit-cancel', - rightContainer, - L.DomUtil.add('span', '', null, L._('Cancel edits')), - this.askForReset, - this - ) - L.DomEvent.on( - controlEditCancel, - 'mouseover', - function () { - this.tooltip.open({ - content: this.help.displayLabel('CANCEL'), - anchor: controlEditCancel, - position: 'bottom', - delay: 500, - duration: 5000, - }) - }, - this - ) - const controlEditDisable = L.DomUtil.createButton( - 'leaflet-control-edit-disable', - rightContainer, - L.DomUtil.add('span', '', null, L._('View')), - this.disableEdit, - this - ) - L.DomEvent.on( - controlEditDisable, - 'mouseover', - function () { - this.tooltip.open({ - content: this.help.displayLabel('PREVIEW'), - anchor: controlEditDisable, - position: 'bottom', - delay: 500, - duration: 5000, - }) - }, - this - ) - const controlEditSave = L.DomUtil.createButton( - 'leaflet-control-edit-save button', - rightContainer, - L.DomUtil.add('span', '', null, L._('Save')), - this.saveAll, - this - ) - L.DomEvent.on( - controlEditSave, - 'mouseover', - function () { - this.tooltip.open({ - content: this.help.displayLabel('SAVE'), - anchor: controlEditSave, - position: 'bottom', - delay: 500, - duration: 5000, - }) - }, - this - ) - }, - - editDatalayers: function () { - if (!this.editEnabled) return - const container = L.DomUtil.create('div') - L.DomUtil.createTitle(container, L._('Manage layers'), 'icon-layers') - const ul = L.DomUtil.create('ul', '', container) - this.eachDataLayerReverse((datalayer) => { - const row = L.DomUtil.create('li', 'orderable', ul) - L.DomUtil.createIcon(row, 'icon-drag', L._('Drag to reorder')) - datalayer.renderToolbox(row) - const title = L.DomUtil.add('span', '', row, datalayer.options.name) - row.classList.toggle('off', !datalayer.isVisible()) - title.textContent = datalayer.options.name - row.dataset.id = L.stamp(datalayer) - }) - const onReorder = (src, dst, initialIndex, finalIndex) => { - const layer = this.datalayers[src.dataset.id] - const other = this.datalayers[dst.dataset.id] - const minIndex = Math.min(layer.getRank(), other.getRank()) - const maxIndex = Math.max(layer.getRank(), other.getRank()) - if (finalIndex === 0) layer.bringToTop() - else if (finalIndex > initialIndex) layer.insertBefore(other) - else layer.insertAfter(other) - this.eachDataLayerReverse((datalayer) => { - if (datalayer.getRank() >= minIndex && datalayer.getRank() <= maxIndex) - datalayer.isDirty = true - }) - this.indexDatalayers() - } - const orderable = new U.Orderable(ul, onReorder) - - const bar = L.DomUtil.create('div', 'button-bar', container) - L.DomUtil.createButton( - 'show-on-edit block add-datalayer button', - bar, - L._('Add a layer'), - this.newDataLayer, - this - ) - - this.editPanel.open({ content: container }) - }, -} - /* Used in view mode to define the current tilelayer */ U.TileLayerControl = L.Control.IconLayers.extend({ initialize: function (map, options) { @@ -819,7 +589,7 @@ U.TileLayerControl = L.Control.IconLayers.extend({ // Fixme when https://github.com/Leaflet/Leaflet/pull/9201 is released const icon = U.Utils.template( layer.options.url_template, - this.map.demoTileInfos + this.map.options.demoTileInfos ) layers.push({ title: layer.options.name, @@ -885,7 +655,7 @@ U.TileLayerChooser = L.Control.extend({ L.DomUtil.createTitle(container, L._('Change tilelayers'), 'icon-tilelayer') this._tilelayers_container = L.DomUtil.create('ul', '', container) this.buildList(options) - const panel = options.edit ? this.map.editPanel : this.map.panel + const panel = options.edit ? this.map.umap.editPanel : this.map.umap.panel panel.open({ content: container }) }, @@ -905,7 +675,7 @@ U.TileLayerChooser = L.Control.extend({ const el = L.DomUtil.create('li', selectedClass, this._tilelayers_container) const img = L.DomUtil.create('img', '', el) const name = L.DomUtil.create('div', '', el) - img.src = U.Utils.template(tilelayer.options.url_template, this.map.demoTileInfos) + img.src = U.Utils.template(tilelayer.options.url_template, this.map.options.demoTileInfos) img.loading = 'lazy' name.textContent = tilelayer.options.name L.DomEvent.on( @@ -935,8 +705,8 @@ U.AttributionControl = L.Control.Attribution.extend({ this._container.innerHTML = '' const container = L.DomUtil.create('div', 'attribution-container', this._container) container.innerHTML = credits - const shortCredit = this._map.getOption('shortCredit') - const captionMenus = this._map.getOption('captionMenus') + const shortCredit = this._map.umap.getOption('shortCredit') + const captionMenus = this._map.umap.getOption('captionMenus') if (shortCredit) { L.DomUtil.element({ tagName: 'span', @@ -947,7 +717,7 @@ U.AttributionControl = L.Control.Attribution.extend({ if (captionMenus) { const link = L.DomUtil.add('a', '', container, ` — ${L._('Open caption')}`) L.DomEvent.on(link, 'click', L.DomEvent.stop) - .on(link, 'click', this._map.openCaption, this._map) + .on(link, 'click', () => this._map.umap.openCaption()) .on(link, 'dblclick', L.DomEvent.stop) } if (window.top === window.self && captionMenus) { @@ -1139,7 +909,7 @@ U.SearchControl = L.Control.extend({ this.map.fire('dataload', { id: id }) }) this.search.resultsContainer = resultsContainer - this.map.panel.open({ content: container }).then(input.focus()) + this.map.umap.panel.open({ content: container }).then(input.focus()) }, }) @@ -1179,8 +949,9 @@ L.Control.Loading.include({ }) U.Editable = L.Editable.extend({ - initialize: function (map, options) { - L.Editable.prototype.initialize.call(this, map, options) + initialize: function (umap, options) { + this.umap = umap + L.Editable.prototype.initialize.call(this, umap._leafletMap, options) this.on('editable:drawing:click editable:drawing:move', this.drawingTooltip) // Layer for items added by users this.on('editable:drawing:cancel', (event) => { @@ -1188,7 +959,7 @@ U.Editable = L.Editable.extend({ }) this.on('editable:drawing:commit', function (event) { event.layer.feature.isDirty = true - if (this.map.editedFeature !== event.layer) event.layer.feature.edit(event) + if (this.umap.editedFeature !== event.layer) event.layer.feature.edit(event) }) this.on('editable:editing', (event) => { const feature = event.layer.feature @@ -1210,7 +981,7 @@ U.Editable = L.Editable.extend({ }, createPolyline: function (latlngs) { - const datalayer = this.map.defaultEditDataLayer() + const datalayer = this.umap.defaultEditDataLayer() const point = new U.LineString(datalayer, { geometry: { type: 'LineString', coordinates: [] }, }) @@ -1218,7 +989,7 @@ U.Editable = L.Editable.extend({ }, createPolygon: function (latlngs) { - const datalayer = this.map.defaultEditDataLayer() + const datalayer = this.umap.defaultEditDataLayer() const point = new U.Polygon(datalayer, { geometry: { type: 'Polygon', coordinates: [] }, }) @@ -1226,7 +997,7 @@ U.Editable = L.Editable.extend({ }, createMarker: function (latlng) { - const datalayer = this.map.defaultEditDataLayer() + const datalayer = this.umap.defaultEditDataLayer() const point = new U.Point(datalayer, { geometry: { type: 'Point', coordinates: [latlng.lng, latlng.lat] }, }) @@ -1235,15 +1006,15 @@ U.Editable = L.Editable.extend({ _getDefaultProperties: function () { const result = {} - if (this.map.options.featuresHaveOwner?.user) { - result.geojson = { properties: { owner: this.map.options.user.id } } + if (this.umap.properties.featuresHaveOwner?.user) { + result.geojson = { properties: { owner: this.umap.properties.user.id } } } return result }, connectCreatedToMap: function (layer) { // Overrided from Leaflet.Editable - const datalayer = this.map.defaultEditDataLayer() + const datalayer = this.umap.defaultEditDataLayer() datalayer.addFeature(layer.feature) layer.isDirty = true return layer @@ -1251,7 +1022,7 @@ U.Editable = L.Editable.extend({ drawingTooltip: function (e) { if (e.layer instanceof L.Marker && e.type === 'editable:drawing:start') { - this.map.tooltip.open({ content: L._('Click to add a marker') }) + this.umap.tooltip.open({ content: L._('Click to add a marker') }) } if (!(e.layer instanceof L.Polyline)) { // only continue with Polylines and Polygons @@ -1298,12 +1069,12 @@ U.Editable = L.Editable.extend({ } } if (content) { - this.map.tooltip.open({ content: content }) + this.umap.tooltip.open({ content: content }) } }, closeTooltip: function () { - this.map.ui.closeTooltip() + this.umap.closeTooltip() }, onVertexRawClick: (e) => { @@ -1314,7 +1085,7 @@ U.Editable = L.Editable.extend({ onEscape: function () { this.once('editable:drawing:end', (event) => { - this.map.tooltip.close() + this.umap.tooltip.close() // Leaflet.Editable will delete the drawn shape if invalid // (eg. line has only one drawn point) // So let's check if the layer has no more shape diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js index 5c42cde8..e7cf3cd5 100644 --- a/umap/static/umap/js/umap.core.js +++ b/umap/static/umap/js/umap.core.js @@ -25,48 +25,6 @@ L.Util.copyToClipboard = (textToCopy) => { } } -L.Util.queryString = (name, fallback) => { - const decode = (s) => decodeURIComponent(s.replace(/\+/g, ' ')) - const qs = window.location.search.slice(1).split('&') - const qa = {} - for (const i in qs) { - const key = qs[i].split('=') - if (!key) continue - qa[decode(key[0])] = key[1] ? decode(key[1]) : 1 - } - return qa[name] || fallback -} - -L.Util.booleanFromQueryString = (name) => { - const value = L.Util.queryString(name) - return value === '1' || value === 'true' -} - -L.Util.setFromQueryString = (options, name) => { - const value = L.Util.queryString(name) - if (typeof value !== 'undefined') options[name] = value -} - -L.Util.setBooleanFromQueryString = (options, name) => { - const value = L.Util.queryString(name) - if (typeof value !== 'undefined') options[name] = value === '1' || value === 'true' -} - -L.Util.setNumberFromQueryString = (options, name) => { - const value = +L.Util.queryString(name) - if (!Number.isNaN(value)) options[name] = value -} - -L.Util.setNullableBooleanFromQueryString = (options, name) => { - let value = L.Util.queryString(name) - if (typeof value !== 'undefined') { - if (value === 'null') value = null - else if (value === '0' || value === 'false') value = false - else value = true - options[name] = value - } -} - L.DomUtil.add = (tagName, className, container, content) => { const el = L.DomUtil.create(tagName, className, container) if (content) { diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index 92924b53..357113d5 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -221,14 +221,14 @@ L.FormBuilder.Element.include({ this.label = L.DomUtil.create('label', '', this.getLabelParent()) this.label.textContent = this.label.title = this.options.label if (this.options.helpEntries) { - this.builder.map.help.button(this.label, this.options.helpEntries) + this.builder.umap.help.button(this.label, this.options.helpEntries) } else if (this.options.helpTooltip) { const info = L.DomUtil.create('i', 'info', this.label) L.DomEvent.on( info, 'mouseover', function () { - this.builder.map.tooltip.open({ + this.builder.umap.tooltip.open({ anchor: info, content: this.options.helpTooltip, position: 'top', @@ -359,7 +359,7 @@ L.FormBuilder.SlideshowDelay = L.FormBuilder.IntSelect.extend({ L.FormBuilder.DataLayerSwitcher = L.FormBuilder.Select.extend({ getOptions: function () { const options = [] - this.builder.map.eachDataLayerReverse((datalayer) => { + this.builder.umap.eachDataLayerReverse((datalayer) => { if ( datalayer.isLoaded() && !datalayer.isDataReadOnly() && @@ -376,11 +376,11 @@ L.FormBuilder.DataLayerSwitcher = L.FormBuilder.Select.extend({ }, toJS: function () { - return this.builder.map.datalayers[this.value()] + return this.builder.umap.datalayers[this.value()] }, set: function () { - this.builder.map.lastUsedDataLayer = this.toJS() + this.builder.umap.lastUsedDataLayer = this.toJS() this.obj.changeDataLayer(this.toJS()) }, }) @@ -400,7 +400,7 @@ L.FormBuilder.DataFormat = L.FormBuilder.Select.extend({ L.FormBuilder.LicenceChooser = L.FormBuilder.Select.extend({ getOptions: function () { const licences = [] - const licencesList = this.builder.obj.options.licences + const licencesList = this.builder.obj.properties.licences let licence for (const i in licencesList) { licence = licencesList[i] @@ -414,7 +414,7 @@ L.FormBuilder.LicenceChooser = L.FormBuilder.Select.extend({ }, toJS: function () { - return this.builder.obj.options.licences[this.value()] + return this.builder.obj.properties.licences[this.value()] }, }) @@ -469,8 +469,8 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ onDefine: async function () { this.buttons.innerHTML = '' this.footer.innerHTML = '' - const [{ pictogram_list }, response, error] = await this.builder.map.server.get( - this.builder.map.options.urls.pictogram_list_json + const [{ pictogram_list }, response, error] = await this.builder.umap.server.get( + this.builder.umap.properties.urls.pictogram_list_json ) if (!error) this.pictogram_list = pictogram_list this.buildTabs() @@ -616,7 +616,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({ categories[category].push(props) } const sorted = Object.entries(categories).toSorted(([a], [b]) => - U.Utils.naturalSort(a, b, L.lang) + U.Utils.naturalSort(a, b, U.lang) ) for (const [name, items] of sorted) { this.addCategory(items, name) @@ -1167,8 +1167,8 @@ U.FormBuilder = L.FormBuilder.extend({ } }, - initialize: function (obj, fields, options) { - this.map = obj.map || obj.getMap() + initialize: function (obj, fields, options = {}) { + this.umap = obj.umap || options.umap this.computeDefaultOptions() L.FormBuilder.prototype.initialize.call(this, obj, fields, options) this.on('finish', this.finish) diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 893de510..708a5aea 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -1,7 +1,6 @@ L.Map.mergeOptions({ overlay: {}, datalayers: [], - hash: true, maxZoomLimit: 24, attributionControl: false, editMode: 'advanced', @@ -16,10 +15,6 @@ L.Map.mergeOptions({ demoTileInfos: { s: 'a', z: 9, x: 265, y: 181, '-y': 181, r: '' }, licences: [], licence: '', - enableMarkerDraw: true, - enablePolygonDraw: true, - enablePolylineDraw: true, - limitBounds: {}, slideshow: {}, clickable: true, permissions: {}, @@ -208,1626 +203,6 @@ U.Map = L.Map.extend({ this.backup() }, - initSyncEngine: async function () { - if (this.options.websocketEnabled === false) return - if (this.options.syncEnabled !== true) { - this.sync.stop() - } else { - const ws_token_uri = this.urls.get('map_websocket_auth_token', { - map_id: this.options.umap_id, - }) - await this.sync.authenticate(ws_token_uri, this.options.websocketURI, this.server) - } - }, - - getSyncMetadata: function () { - return { - engine: this.sync, - subject: 'map', - } - }, - - render: function (fields) { - if (fields.includes('numberOfConnectedPeers')) { - this.renderEditToolbar() - this.propagate() - } - - const impacts = U.Utils.getImpactsFromSchema(fields) - for (const impact of impacts) { - switch (impact) { - case 'ui': - this.initCaptionBar() - this.renderEditToolbar() - this.renderControls() - this.browser.redraw() - this.propagate() - break - case 'data': - this.redrawVisibleDataLayers() - break - case 'datalayer-index': - this.reindexDataLayers() - break - case 'background': - this.initTileLayers() - break - case 'bounds': - this.handleLimitBounds() - break - case 'sync': - this.initSyncEngine() - } - } - }, - - reindexDataLayers: function () { - this.eachDataLayer((datalayer) => datalayer.reindex()) - this.onDataLayersChanged() - }, - - redrawVisibleDataLayers: function () { - this.eachVisibleDataLayer((datalayer) => { - datalayer.redraw() - }) - }, - - setOptionsFromQueryString: (options) => { - // This is not an editable option - L.Util.setFromQueryString(options, 'editMode') - // FIXME retrocompat - L.Util.setBooleanFromQueryString(options, 'displayDataBrowserOnLoad') - L.Util.setBooleanFromQueryString(options, 'displayCaptionOnLoad') - for (const [key, schema] of Object.entries(U.SCHEMA)) { - switch (schema.type) { - case Boolean: - if (schema.nullable) L.Util.setNullableBooleanFromQueryString(options, key) - else L.Util.setBooleanFromQueryString(options, key) - break - case Number: - L.Util.setNumberFromQueryString(options, key) - break - case String: - L.Util.setFromQueryString(options, key) - break - } - } - // Specific case for datalayersControl - // which accepts "expanded" value, on top of true/false/null - if (L.Util.queryString('datalayersControl') === 'expanded') { - if (!options.onLoadPanel) { - options.onLoadPanel = 'datalayers' - } - } - }, - - loadDataFromQueryString: async function () { - let data = L.Util.queryString('data', null) - const url = new URL(window.location.href) - const dataUrls = new URLSearchParams(url.search).getAll('dataUrl') - const dataFormat = L.Util.queryString('dataFormat', 'geojson') - if (dataUrls.length) { - for (let dataUrl of dataUrls) { - dataUrl = decodeURIComponent(dataUrl) - dataUrl = this.localizeUrl(dataUrl) - dataUrl = this.proxyUrl(dataUrl) - const datalayer = this.createDataLayer() - await datalayer.importFromUrl(dataUrl, dataFormat) - } - } else if (data) { - data = decodeURIComponent(data) - const datalayer = this.createDataLayer() - await datalayer.importRaw(data, dataFormat) - } - }, - - setViewFromQueryString: async function () { - if (this.options.noControl) return - if (L.Util.queryString('share')) { - this.share.open() - } else if (this.options.onLoadPanel === 'databrowser') { - this.panel.setDefaultMode('expanded') - this.openBrowser('data') - } else if (this.options.onLoadPanel === 'datalayers') { - this.panel.setDefaultMode('condensed') - this.openBrowser('layers') - } else if (this.options.onLoadPanel === 'datafilters') { - this.panel.setDefaultMode('expanded') - this.openBrowser('filters') - } else if (this.options.onLoadPanel === 'caption') { - this.panel.setDefaultMode('condensed') - this.openCaption() - } - // Comes after default panels, so if it opens in a panel it will - // take precedence. - const slug = L.Util.queryString('feature') - if (slug && this.features_index[slug]) this.features_index[slug].view() - if (L.Util.queryString('edit')) { - if (this.hasEditMode()) this.enableEdit() - // Sometimes users share the ?edit link by mistake, let's remove - // this search parameter from URL to prevent this - const url = new URL(window.location) - url.searchParams.delete('edit') - history.pushState({}, '', url) - } - if (L.Util.queryString('download')) { - const download_url = this.urls.get('map_download', { - map_id: this.options.umap_id, - }) - window.location = download_url - } - }, - - // Merge the given schema with the default one - // Missing keys inside the schema are merged with the default ones. - overrideSchema: (schema) => { - for (const [key, extra] of Object.entries(schema)) { - U.SCHEMA[key] = L.extend({}, U.SCHEMA[key], extra) - } - }, - - initControls: function () { - this.helpMenuActions = {} - this._controls = {} - - if (this.hasEditMode() && !this.options.noControl) { - new U.EditControl(this).addTo(this) - - new U.DrawToolbar({ map: this }).addTo(this) - const editActions = [ - U.EditCaptionAction, - U.EditPropertiesAction, - U.EditLayersAction, - U.ChangeTileLayerAction, - U.UpdateExtentAction, - U.UpdatePermsAction, - U.ImportAction, - ] - if (this.options.editMode === 'advanced') { - new U.SettingsToolbar({ actions: editActions }).addTo(this) - } - } - this._controls.zoom = new L.Control.Zoom({ - zoomInTitle: L._('Zoom in'), - zoomOutTitle: L._('Zoom out'), - }) - this._controls.datalayers = new U.DataLayersControl(this) - this._controls.caption = new U.CaptionControl(this) - this._controls.locate = new U.Locate(this, { - strings: { - title: L._('Center map on your location'), - }, - showPopup: false, - // We style this control in our own CSS for consistency with other controls, - // but the control breaks if we don't specify a class here, so a fake class - // will do. - icon: 'umap-fake-class', - iconLoading: 'umap-fake-class', - flyTo: this.options.easing, - onLocationError: (err) => U.Alert.error(err.message), - }) - this._controls.fullscreen = new L.Control.Fullscreen({ - title: { false: L._('View Fullscreen'), true: L._('Exit Fullscreen') }, - }) - this._controls.search = new U.SearchControl() - this._controls.embed = new L.Control.Embed(this) - this._controls.tilelayersChooser = new U.TileLayerChooser(this) - if (this.options.user?.id) this._controls.star = new U.StarControl(this) - this._controls.editinosm = new L.Control.EditInOSM({ - position: 'topleft', - widgetOptions: { - helpText: L._( - 'Open this map extent in a map editor to provide more accurate data to OpenStreetMap' - ), - }, - }) - this._controls.measure = new L.MeasureControl().initHandler(this) - this._controls.more = new U.MoreControls() - this._controls.scale = L.control.scale() - this._controls.permanentCredit = new U.PermanentCreditsControl(this) - if (this.options.scrollWheelZoom) this.scrollWheelZoom.enable() - else this.scrollWheelZoom.disable() - this.browser = new U.Browser(this) - this.facets = new U.Facets(this) - this.caption = new U.Caption(this) - this.importer = new U.Importer(this) - this.drop = new U.DropControl(this) - this.share = new U.Share(this) - this.rules = new U.Rules(this) - this._controls.tilelayers = new U.TileLayerControl(this) - }, - - renderControls: function () { - const hasSlideshow = Boolean(this.options.slideshow?.active) - const barEnabled = this.options.captionBar || hasSlideshow - document.body.classList.toggle('umap-caption-bar-enabled', barEnabled) - document.body.classList.toggle('umap-slideshow-enabled', hasSlideshow) - for (const control of Object.values(this._controls)) { - this.removeControl(control) - } - if (this.options.noControl) return - - this._controls.attribution = new U.AttributionControl().addTo(this) - if (this.options.miniMap) { - this.whenReady(function () { - if (this.selected_tilelayer) { - this._controls.miniMap = new L.Control.MiniMap(this.selected_tilelayer, { - aimingRectOptions: { - color: this.getOption('color'), - fillColor: this.getOption('fillColor'), - stroke: this.getOption('stroke'), - fill: this.getOption('fill'), - weight: this.getOption('weight'), - opacity: this.getOption('opacity'), - fillOpacity: this.getOption('fillOpacity'), - }, - }).addTo(this) - this._controls.miniMap._miniMap.invalidateSize() - } - }) - } - let name - let status - let control - for (let i = 0; i < this.HIDDABLE_CONTROLS.length; i++) { - name = this.HIDDABLE_CONTROLS[i] - status = this.getOption(`${name}Control`) - if (status === false) continue - control = this._controls[name] - if (!control) continue - control.addTo(this) - if (status === undefined || status === null) - L.DomUtil.addClass(control._container, 'display-on-more') - else L.DomUtil.removeClass(control._container, 'display-on-more') - } - if (this.getOption('permanentCredit')) this._controls.permanentCredit.addTo(this) - if (this.getOption('moreControl')) this._controls.more.addTo(this) - if (this.getOption('scaleControl')) this._controls.scale.addTo(this) - this._controls.tilelayers.setLayers() - }, - - initDataLayers: async function (datalayers) { - datalayers = datalayers || this.options.datalayers - for (const options of datalayers) { - // `false` to not propagate syncing elements served from uMap - this.createDataLayer(options, false) - } - await this.loadDataLayers() - }, - - loadDataLayers: async function () { - this.datalayersLoaded = true - this.fire('datalayersloaded') - for (const datalayer of this.datalayers_index) { - if (datalayer.showAtLoad()) await datalayer.show() - } - this.dataloaded = true - this.fire('dataloaded') - }, - - indexDatalayers: function () { - const panes = this.getPane('overlayPane') - let pane - this.datalayers_index = [] - for (let i = 0; i < panes.children.length; i++) { - pane = panes.children[i] - if (!pane.dataset || !pane.dataset.id) continue - this.datalayers_index.push(this.datalayers[pane.dataset.id]) - } - this.onDataLayersChanged() - }, - - onDataLayersChanged: function () { - if (this.browser) this.browser.update() - this.caption.refresh() - }, - - ensurePanesOrder: function () { - this.eachDataLayer((datalayer) => { - datalayer.bringToTop() - }) - }, - - onceDatalayersLoaded: function (callback, context) { - // Once datalayers **metadata** have been loaded - if (this.datalayersLoaded) { - callback.call(context || this, this) - } else { - this.once('datalayersloaded', callback, context) - } - return this - }, - - onceDataLoaded: function (callback, context) { - // Once datalayers **data** have been loaded - if (this.dataloaded) { - callback.call(context || this, this) - } else { - this.once('dataloaded', callback, context) - } - return this - }, - - backupOptions: function () { - this._backupOptions = L.extend({}, this.options) - this._backupOptions.tilelayer = L.extend({}, this.options.tilelayer) - this._backupOptions.limitBounds = L.extend({}, this.options.limitBounds) - this._backupOptions.permissions = L.extend({}, this.permissions.options) - }, - - resetOptions: function () { - this.options = L.extend({}, this._backupOptions) - this.options.tilelayer = L.extend({}, this._backupOptions.tilelayer) - this.permissions.options = L.extend({}, this._backupOptions.permissions) - }, - - initShortcuts: function () { - const globalShortcuts = function (e) { - if (e.key === 'Escape') { - if (this.importer.dialog.visible) { - this.importer.dialog.close() - } else if (this.editEnabled && this.editTools.drawing()) { - this.editTools.onEscape() - } else if (this.measureTools.enabled()) { - this.measureTools.stopDrawing() - } else if (this.fullPanel?.isOpen()) { - this.fullPanel?.close() - } else if (this.editPanel?.isOpen()) { - this.editPanel?.close() - } else if (this.panel.isOpen()) { - this.panel.close() - } - } - - // From now on, only ctrl/meta shortcut - if (!(e.ctrlKey || e.metaKey) || e.shiftKey) return - - if (e.key === 'f') { - L.DomEvent.stop(e) - this.search() - } - - /* Edit mode only shortcuts */ - if (!this.hasEditMode()) return - - // Edit mode Off - if (!this.editEnabled) { - switch (e.key) { - case 'e': - L.DomEvent.stop(e) - this.enableEdit() - break - } - return - } - - // Edit mode on - let used = true - switch (e.key) { - case 'e': - if (!U.SAVEMANAGER.isDirty) this.disableEdit() - break - case 's': - if (U.SAVEMANAGER.isDirty) this.saveAll() - break - case 'z': - if (U.SAVEMANAGER.isDirty) this.askForReset() - break - case 'm': - this.editTools.startMarker() - break - case 'p': - this.editTools.startPolygon() - break - case 'l': - this.editTools.startPolyline() - break - case 'i': - this.importer.open() - break - case 'o': - this.importer.openFiles() - break - case 'h': - this.help.show('edit') - break - default: - used = false - } - if (used) L.DomEvent.stop(e) - } - L.DomEvent.addListener(document, 'keydown', globalShortcuts, this) - }, - - initTileLayers: function () { - this.tilelayers = [] - for (const props of this.options.tilelayers) { - const layer = this.createTileLayer(props) - this.tilelayers.push(layer) - if ( - this.options.tilelayer && - this.options.tilelayer.url_template === props.url_template - ) { - // Keep control over the displayed attribution for non custom tilelayers - this.options.tilelayer.attribution = props.attribution - } - } - if (this.options.tilelayer?.url_template && this.options.tilelayer.attribution) { - this.customTilelayer = this.createTileLayer(this.options.tilelayer) - this.selectTileLayer(this.customTilelayer) - } else { - this.selectTileLayer(this.tilelayers[0]) - } - if (this._controls) this._controls.tilelayers.setLayers() - }, - - createTileLayer: (tilelayer) => new L.TileLayer(tilelayer.url_template, tilelayer), - - selectTileLayer: function (tilelayer) { - if (tilelayer === this.selected_tilelayer) { - return - } - try { - this.addLayer(tilelayer) - this.fire('baselayerchange', { layer: tilelayer }) - if (this.selected_tilelayer) { - this.removeLayer(this.selected_tilelayer) - } - this.selected_tilelayer = tilelayer - if ( - !Number.isNaN(this.selected_tilelayer.options.minZoom) && - this.getZoom() < this.selected_tilelayer.options.minZoom - ) { - this.setZoom(this.selected_tilelayer.options.minZoom) - } - if ( - !Number.isNaN(this.selected_tilelayer.options.maxZoom) && - this.getZoom() > this.selected_tilelayer.options.maxZoom - ) { - this.setZoom(this.selected_tilelayer.options.maxZoom) - } - } catch (e) { - console.error(e) - this.removeLayer(tilelayer) - U.Alert.error(`${L._('Error in the tilelayer URL')}: ${tilelayer._url}`) - // Users can put tilelayer URLs by hand, and if they add wrong {variable}, - // Leaflet throw an error, and then the map is no more editable - } - this.setOverlay() - }, - - eachTileLayer: function (callback, context) { - const urls = [] - const callOne = (layer) => { - // Prevent adding a duplicate background, - // while adding selected/custom on top of the list - const url = layer.options.url_template - if (urls.indexOf(url) !== -1) return - callback.call(context, layer) - urls.push(url) - } - if (this.selected_tilelayer) callOne(this.selected_tilelayer) - if (this.customTilelayer) callOne(this.customTilelayer) - this.tilelayers.forEach(callOne) - }, - - setOverlay: function () { - if (!this.options.overlay || !this.options.overlay.url_template) return - const overlay = this.createTileLayer(this.options.overlay) - try { - this.addLayer(overlay) - if (this.overlay) this.removeLayer(this.overlay) - this.overlay = overlay - } catch (e) { - this.removeLayer(overlay) - console.error(e) - U.Alert.error(`${L._('Error in the overlay URL')}: ${overlay._url}`) - } - }, - - _setDefaultCenter: function () { - this.options.center = this.latLng(this.options.center) - this.setView(this.options.center, this.options.zoom) - }, - - hasData: function () { - for (const datalayer of this.datalayers_index) { - if (datalayer.hasData()) return true - } - }, - - hasLayers: function () { - return Boolean(this.datalayers_index.length) - }, - - fitDataBounds: function () { - const bounds = this.getLayersBounds() - if (!this.hasData() || !bounds.isValid()) return false - this.fitBounds(bounds) - }, - - initCenter: function () { - this._setDefaultCenter() - if (this.options.hash) this.addHash() - if (this.options.hash && this._hash.parseHash(location.hash)) { - // FIXME An invalid hash will cause the load to fail - this._hash.update() - } else if (this.options.defaultView === 'locate' && !this.options.noControl) { - this._controls.locate.start() - } else if (this.options.defaultView === 'data') { - this.onceDataLoaded(this.fitDataBounds) - } else if (this.options.defaultView === 'latest') { - this.onceDataLoaded(() => { - if (!this.hasData()) return - const datalayer = this.firstVisibleDatalayer() - let feature - if (datalayer) { - const feature = datalayer.getFeatureByIndex(-1) - if (feature) { - feature.zoomTo({ callback: this.options.noControl ? null : feature.view }) - return - } - } - }) - } - }, - - latLng: (a, b, c) => { - // manage geojson case and call original method - if (!(a instanceof L.LatLng) && a.coordinates) { - // Guess it's a geojson - a = [a.coordinates[1], a.coordinates[0]] - } - return L.latLng(a, b, c) - }, - - handleLimitBounds: function () { - const south = Number.parseFloat(this.options.limitBounds.south) - const west = Number.parseFloat(this.options.limitBounds.west) - const north = Number.parseFloat(this.options.limitBounds.north) - const east = Number.parseFloat(this.options.limitBounds.east) - if ( - !Number.isNaN(south) && - !Number.isNaN(west) && - !Number.isNaN(north) && - !Number.isNaN(east) - ) { - const bounds = L.latLngBounds([ - [south, west], - [north, east], - ]) - this.options.minZoom = this.getBoundsZoom(bounds, false) - try { - this.setMaxBounds(bounds) - } catch (e) { - // Unusable bounds, like -2 -2 -2 -2? - console.error('Error limiting bounds', e) - } - } else { - this.options.minZoom = 0 - this.setMaxBounds() - } - }, - - setMaxBounds: function (bounds) { - // Hack. Remove me when fix is released: - // https://github.com/Leaflet/Leaflet/pull/4494 - bounds = L.latLngBounds(bounds) - - if (!bounds.isValid()) { - this.options.maxBounds = null - return this.off('moveend', this._panInsideMaxBounds) - } - return L.Map.prototype.setMaxBounds.call(this, bounds) - }, - - createDataLayer: function (options = {}, sync = true) { - options.name = options.name || `${L._('Layer')} ${this.datalayers_index.length + 1}` - const datalayer = new U.DataLayer(this, options, sync) - - if (sync !== false) { - datalayer.sync.upsert(datalayer.options) - } - return datalayer - }, - - newDataLayer: function () { - const datalayer = this.createDataLayer({}) - datalayer.edit() - }, - - getDefaultOption: (option) => U.SCHEMA[option]?.default, - - getOption: function (option, feature) { - if (feature) { - const value = this.rules.getOption(option, feature) - if (value !== undefined) return value - } - if (U.Utils.usableOption(this.options, option)) return this.options[option] - return this.getDefaultOption(option) - }, - - setCenterAndZoom: function () { - this._setCenterAndZoom() - U.Alert.success(L._('The zoom and center have been modified.')) - }, - - _setCenterAndZoom: function () { - this.options.center = this.getCenter() - this.options.zoom = this.getZoom() - this.isDirty = true - this._default_extent = false - }, - - updateTileLayers: function () { - const callback = (tilelayer) => { - this.options.tilelayer = tilelayer.toJSON() - this.isDirty = true - } - if (this._controls.tilelayersChooser) { - this._controls.tilelayersChooser.openSwitcher({ callback, edit: true }) - } - }, - - toGeoJSON: function () { - let features = [] - this.eachDataLayer((datalayer) => { - if (datalayer.isVisible()) { - features = features.concat(datalayer.featuresToGeoJSON()) - } - }) - const geojson = { - type: 'FeatureCollection', - features: features, - } - return geojson - }, - - eachFeature: function (callback, context) { - this.eachBrowsableDataLayer((datalayer) => { - if (datalayer.isVisible()) datalayer.eachFeature(callback, context) - }) - }, - - processFileToImport: function (file, layer, type) { - type = type || U.Utils.detectFileType(file) - if (!type) { - U.Alert.error( - L._('Unable to detect format of file {filename}', { - filename: file.name, - }) - ) - return - } - if (type === 'umap') { - this.importFromFile(file, 'umap') - } else { - if (!layer) layer = this.createDataLayer({ name: file.name }) - layer.importFromFile(file, type) - } - }, - - importFromUrl: async function (uri) { - const response = await this.request.get(uri) - if (response?.ok) { - this.importRaw(await response.text()) - } - }, - - importRaw: function (rawData) { - const importedData = JSON.parse(rawData) - - let mustReindex = false - - for (const option of Object.keys(U.SCHEMA)) { - if (typeof importedData.properties[option] !== 'undefined') { - this.options[option] = importedData.properties[option] - if (option === 'sortKey') mustReindex = true - } - } - - if (importedData.geometry) this.options.center = this.latLng(importedData.geometry) - importedData.layers.forEach((geojson) => { - if (!geojson._umap_options && geojson._storage) { - geojson._umap_options = geojson._storage - delete geojson._storage - } - delete geojson._umap_options?.id // Never trust an id at this stage - const dataLayer = this.createDataLayer(geojson._umap_options) - dataLayer.fromUmapGeoJSON(geojson) - }) - - this.initTileLayers() - this.renderControls() - this.handleLimitBounds() - this.eachDataLayer((datalayer) => { - if (mustReindex) datalayer.reindex() - datalayer.redraw() - }) - this.propagate() - this.fire('postsync') - this.isDirty = true - }, - - importFromFile: function (file) { - const reader = new FileReader() - reader.readAsText(file) - reader.onload = (e) => { - const rawData = e.target.result - try { - this.importRaw(rawData) - } catch (e) { - console.error('Error importing data', e) - U.Alert.error(L._('Invalid umap data in {filename}', { filename: file.name })) - } - } - }, - - openBrowser: function (mode) { - this.onceDatalayersLoaded(() => this.browser.open(mode)) - }, - - openCaption: function () { - this.onceDatalayersLoaded(() => this.caption.open()) - }, - - eachDataLayer: function (method, context) { - for (let i = 0; i < this.datalayers_index.length; i++) { - method.call(context, this.datalayers_index[i]) - } - }, - - eachDataLayerReverse: function (method, context, filter) { - for (let i = this.datalayers_index.length - 1; i >= 0; i--) { - if (filter && !filter.call(context, this.datalayers_index[i])) continue - method.call(context, this.datalayers_index[i]) - } - }, - - eachBrowsableDataLayer: function (method, context) { - this.eachDataLayerReverse(method, context, (d) => d.allowBrowse()) - }, - - eachVisibleDataLayer: function (method, context) { - this.eachDataLayerReverse(method, context, (d) => d.isVisible()) - }, - - findDataLayer: function (method, context) { - for (let i = this.datalayers_index.length - 1; i >= 0; i--) { - if (method.call(context, this.datalayers_index[i])) - return this.datalayers_index[i] - } - }, - - backup: function () { - this.backupOptions() - this._datalayers_index_bk = [].concat(this.datalayers_index) - }, - - reset: function () { - if (this.editTools) this.editTools.stopDrawing() - this.resetOptions() - this.datalayers_index = [].concat(this._datalayers_index_bk) - // Iter over all datalayers, including deleted if any. - for (const datalayer of Object.values(this.datalayers)) { - if (datalayer.isDeleted) datalayer.connectToMap() - if (datalayer.isDirty) datalayer.reset() - } - this.ensurePanesOrder() - this.initTileLayers() - this.isDirty = false - this.onDataLayersChanged() - }, - - exportOptions: function () { - const properties = {} - for (const option of Object.keys(U.SCHEMA)) { - if (typeof this.options[option] !== 'undefined') { - properties[option] = this.options[option] - } - } - return properties - }, - - save: async function () { - this.rules.commit() - const geojson = { - type: 'Feature', - geometry: this.geometry(), - properties: this.exportOptions(), - } - const formData = new FormData() - formData.append('name', this.options.name) - formData.append('center', JSON.stringify(this.geometry())) - formData.append('settings', JSON.stringify(geojson)) - const uri = this.urls.get('map_save', { map_id: this.options.umap_id }) - const [data, _, error] = await this.server.post(uri, {}, formData) - // FIXME: login_required response will not be an error, so it will not - // stop code while it should - if (error) { - return - } - if (data.login_required) { - window.onLogin = () => this.saveAll() - window.open(data.login_required) - return - } - this.options.user = data.user - if (!this.options.umap_id) { - this.options.umap_id = data.id - this.permissions.setOptions(data.permissions) - this.permissions.commit() - if (data.permissions?.anonymous_edit_url) { - this.once('saved', () => { - U.AlertCreation.info( - L._('Your map has been created with an anonymous account!'), - Number.Infinity, - data.permissions.anonymous_edit_url, - this.options.urls.map_send_edit_link - ? this.sendEditLinkEmail.bind(this) - : null - ) - }) - } else { - this.once('saved', () => { - U.Alert.success(L._('Congratulations, your map has been created!')) - }) - } - } else { - if (!this.permissions.isDirty) { - // Do not override local changes to permissions, - // but update in case some other editors changed them in the meantime. - this.permissions.setOptions(data.permissions) - this.permissions.commit() - } - this.once('saved', () => { - U.Alert.success(data.info || L._('Map has been saved!')) - }) - } - // Update URL in case the name has changed. - if (history?.pushState) { - history.pushState({}, this.options.name, data.url) - } else { - window.location = data.url - } - return true - }, - - saveAll: async function () { - if (!U.SAVEMANAGER.isDirty) return - if (this._default_extent) this._setCenterAndZoom() - this.backup() - await U.SAVEMANAGER.save() - // Do a blind render for now, as we are not sure what could - // have changed, we'll be more subtil when we'll remove the - // save action - this.render(['name', 'user', 'permissions']) - this.fire('saved') - }, - - propagate: function () { - let els = document.querySelectorAll('.map-name') - for (const el of els) { - el.textContent = this.getDisplayName() - } - const status = this.permissions.getShareStatusDisplay() - els = document.querySelectorAll('.share-status') - for (const el of els) { - if (status) { - el.textContent = L._('Visibility: {status}', { - status: status, - }) - } - } - }, - - star: async function () { - if (!this.options.umap_id) { - return U.Alert.error(L._('Please save the map first')) - } - const url = this.urls.get('map_star', { map_id: this.options.umap_id }) - const [data, response, error] = await this.server.post(url) - if (error) { - return - } - this.options.starred = data.starred - U.Alert.success( - data.starred ? L._('Map has been starred') : L._('Map has been unstarred') - ) - this.renderControls() - }, - - geometry: function () { - /* Return a GeoJSON geometry Object */ - const latlng = this.latLng(this.options.center || this.getCenter()) - return { - type: 'Point', - coordinates: [latlng.lng, latlng.lat], - } - }, - - firstVisibleDatalayer: function () { - return this.findDataLayer((datalayer) => { - if (datalayer.isVisible()) return true - }) - }, - - // TODO: allow to control the default datalayer - // (edit and viewing) - // cf https://github.com/umap-project/umap/issues/585 - defaultEditDataLayer: function () { - let datalayer - let fallback - datalayer = this.lastUsedDataLayer - if ( - datalayer && - !datalayer.isDataReadOnly() && - datalayer.isBrowsable() && - datalayer.isVisible() - ) { - return datalayer - } - datalayer = this.findDataLayer((datalayer) => { - if (!datalayer.isDataReadOnly() && datalayer.isBrowsable()) { - fallback = datalayer - if (datalayer.isVisible()) return true - } - }) - if (datalayer) return datalayer - if (fallback) { - // No datalayer visible, let's force one - fallback.show() - return fallback - } - return this.createDataLayer() - }, - - getDataLayerByUmapId: function (umap_id) { - return this.findDataLayer((d) => d.umap_id === umap_id) - }, - - _editControls: function (container) { - let UIFields = [] - for (let i = 0; i < this.HIDDABLE_CONTROLS.length; i++) { - UIFields.push(`options.${this.HIDDABLE_CONTROLS[i]}Control`) - } - UIFields = UIFields.concat([ - 'options.moreControl', - 'options.scrollWheelZoom', - 'options.miniMap', - 'options.scaleControl', - 'options.onLoadPanel', - 'options.defaultView', - 'options.displayPopupFooter', - 'options.captionBar', - 'options.captionMenus', - ]) - builder = new U.FormBuilder(this, UIFields) - const controlsOptions = L.DomUtil.createFieldset( - container, - L._('User interface options') - ) - controlsOptions.appendChild(builder.build()) - }, - - _editShapeProperties: function (container) { - const shapeOptions = [ - 'options.color', - 'options.iconClass', - 'options.iconUrl', - 'options.iconOpacity', - 'options.opacity', - 'options.weight', - 'options.fill', - 'options.fillColor', - 'options.fillOpacity', - 'options.smoothFactor', - 'options.dashArray', - ] - - builder = new U.FormBuilder(this, shapeOptions) - const defaultShapeProperties = L.DomUtil.createFieldset( - container, - L._('Default shape properties') - ) - defaultShapeProperties.appendChild(builder.build()) - }, - - _editDefaultProperties: function (container) { - const optionsFields = [ - 'options.zoomTo', - 'options.easing', - 'options.labelKey', - 'options.sortKey', - 'options.filterKey', - 'options.facetKey', - 'options.slugKey', - ] - - builder = new U.FormBuilder(this, optionsFields) - const defaultProperties = L.DomUtil.createFieldset( - container, - L._('Default properties') - ) - defaultProperties.appendChild(builder.build()) - }, - - _editInteractionsProperties: function (container) { - const popupFields = [ - 'options.popupShape', - 'options.popupTemplate', - 'options.popupContentTemplate', - 'options.showLabel', - 'options.labelDirection', - 'options.labelInteractive', - 'options.outlinkTarget', - ] - builder = new U.FormBuilder(this, popupFields) - const popupFieldset = L.DomUtil.createFieldset( - container, - L._('Default interaction options') - ) - popupFieldset.appendChild(builder.build()) - }, - - _editTilelayer: function (container) { - if (!U.Utils.isObject(this.options.tilelayer)) { - this.options.tilelayer = {} - } - const tilelayerFields = [ - [ - 'options.tilelayer.name', - { handler: 'BlurInput', placeholder: L._('display name') }, - ], - [ - 'options.tilelayer.url_template', - { - handler: 'BlurInput', - helpText: `${L._('Supported scheme')}: http://{s}.domain.com/{z}/{x}/{y}.png`, - placeholder: 'url', - type: 'url', - }, - ], - [ - 'options.tilelayer.maxZoom', - { - handler: 'BlurIntInput', - placeholder: L._('max zoom'), - min: 0, - max: this.options.maxZoomLimit, - }, - ], - [ - 'options.tilelayer.minZoom', - { - handler: 'BlurIntInput', - placeholder: L._('min zoom'), - min: 0, - max: this.options.maxZoomLimit, - }, - ], - [ - 'options.tilelayer.attribution', - { handler: 'BlurInput', placeholder: L._('attribution') }, - ], - ['options.tilelayer.tms', { handler: 'Switch', label: L._('TMS format') }], - ] - const customTilelayer = L.DomUtil.createFieldset( - container, - L._('Custom background') - ) - builder = new U.FormBuilder(this, tilelayerFields) - customTilelayer.appendChild(builder.build()) - }, - - _editOverlay: function (container) { - if (!U.Utils.isObject(this.options.overlay)) { - this.options.overlay = {} - } - const overlayFields = [ - [ - 'options.overlay.url_template', - { - handler: 'BlurInput', - helpText: `${L._('Supported scheme')}: http://{s}.domain.com/{z}/{x}/{y}.png`, - placeholder: 'url', - label: L._('Background overlay url'), - type: 'url', - }, - ], - [ - 'options.overlay.maxZoom', - { - handler: 'BlurIntInput', - placeholder: L._('max zoom'), - min: 0, - max: this.options.maxZoomLimit, - }, - ], - [ - 'options.overlay.minZoom', - { - handler: 'BlurIntInput', - placeholder: L._('min zoom'), - min: 0, - max: this.options.maxZoomLimit, - }, - ], - [ - 'options.overlay.attribution', - { handler: 'BlurInput', placeholder: L._('attribution') }, - ], - [ - 'options.overlay.opacity', - { handler: 'Range', min: 0, max: 1, step: 0.1, label: L._('Opacity') }, - ], - ['options.overlay.tms', { handler: 'Switch', label: L._('TMS format') }], - ] - const overlay = L.DomUtil.createFieldset(container, L._('Custom overlay')) - builder = new U.FormBuilder(this, overlayFields) - overlay.appendChild(builder.build()) - }, - - _editBounds: function (container) { - if (!U.Utils.isObject(this.options.limitBounds)) { - this.options.limitBounds = {} - } - const limitBounds = L.DomUtil.createFieldset(container, L._('Limit bounds')) - const boundsFields = [ - [ - 'options.limitBounds.south', - { handler: 'BlurFloatInput', placeholder: L._('max South') }, - ], - [ - 'options.limitBounds.west', - { handler: 'BlurFloatInput', placeholder: L._('max West') }, - ], - [ - 'options.limitBounds.north', - { handler: 'BlurFloatInput', placeholder: L._('max North') }, - ], - [ - 'options.limitBounds.east', - { handler: 'BlurFloatInput', placeholder: L._('max East') }, - ], - ] - const boundsBuilder = new U.FormBuilder(this, boundsFields) - limitBounds.appendChild(boundsBuilder.build()) - const boundsButtons = L.DomUtil.create('div', 'button-bar half', limitBounds) - L.DomUtil.createButton( - 'button', - boundsButtons, - L._('Use current bounds'), - function () { - const bounds = this.getBounds() - this.options.limitBounds.south = L.Util.formatNum(bounds.getSouth()) - this.options.limitBounds.west = L.Util.formatNum(bounds.getWest()) - this.options.limitBounds.north = L.Util.formatNum(bounds.getNorth()) - this.options.limitBounds.east = L.Util.formatNum(bounds.getEast()) - boundsBuilder.fetchAll() - - this.sync.update(this, 'options.limitBounds', this.options.limitBounds) - this.isDirty = true - this.handleLimitBounds() - }, - this - ) - L.DomUtil.createButton( - 'button', - boundsButtons, - L._('Empty'), - function () { - this.options.limitBounds.south = null - this.options.limitBounds.west = null - this.options.limitBounds.north = null - this.options.limitBounds.east = null - boundsBuilder.fetchAll() - this.isDirty = true - this.handleLimitBounds() - }, - this - ) - }, - - _editSlideshow: function (container) { - const slideshow = L.DomUtil.createFieldset(container, L._('Slideshow')) - const slideshowFields = [ - [ - 'options.slideshow.active', - { handler: 'Switch', label: L._('Activate slideshow mode') }, - ], - [ - 'options.slideshow.delay', - { - handler: 'SlideshowDelay', - helpText: L._('Delay between two transitions when in play mode'), - }, - ], - [ - 'options.slideshow.easing', - { handler: 'Switch', label: L._('Animated transitions'), inheritable: true }, - ], - [ - 'options.slideshow.autoplay', - { handler: 'Switch', label: L._('Autostart when map is loaded') }, - ], - ] - const slideshowBuilder = new U.FormBuilder(this, slideshowFields, { - callback: () => this.slideshow.setOptions(this.options.slideshow), - }) - slideshow.appendChild(slideshowBuilder.build()) - }, - - _editSync: function (container) { - const sync = L.DomUtil.createFieldset(container, L._('Real-time collaboration')) - const builder = new U.FormBuilder(this, ['options.syncEnabled']) - sync.appendChild(builder.build()) - }, - - _advancedActions: function (container) { - const advancedActions = L.DomUtil.createFieldset(container, L._('Advanced actions')) - const advancedButtons = L.DomUtil.create('div', 'button-bar half', advancedActions) - if (this.permissions.isOwner()) { - const deleteButton = U.Utils.loadTemplate(` - `) - deleteButton.addEventListener('click', () => this.del()) - advancedButtons.appendChild(deleteButton) - - L.DomUtil.createButton( - 'button umap-empty', - advancedButtons, - L._('Clear data'), - this.emptyDataLayers, - this - ) - L.DomUtil.createButton( - 'button umap-empty', - advancedButtons, - L._('Remove layers'), - this.removeDataLayers, - this - ) - } - L.DomUtil.createButton( - 'button umap-clone', - advancedButtons, - L._('Clone this map'), - this.clone, - this - ) - L.DomUtil.createButton( - 'button umap-download', - advancedButtons, - L._('Open share & download panel'), - this.share.open, - this.share - ) - }, - - editCaption: function () { - if (!this.editEnabled) return - if (this.options.editMode !== 'advanced') return - const container = L.DomUtil.create('div', 'umap-edit-container') - const metadataFields = ['options.name', 'options.description'] - - L.DomUtil.createTitle(container, L._('Edit map details'), 'icon-caption') - const builder = new U.FormBuilder(this, metadataFields, { - className: 'map-metadata', - }) - const form = builder.build() - container.appendChild(form) - - const credits = L.DomUtil.createFieldset(container, L._('Credits')) - const creditsFields = [ - 'options.licence', - 'options.shortCredit', - 'options.longCredit', - 'options.permanentCredit', - 'options.permanentCreditBackground', - ] - const creditsBuilder = new U.FormBuilder(this, creditsFields) - credits.appendChild(creditsBuilder.build()) - this.editPanel.open({ content: container }) - }, - - edit: function () { - if (!this.editEnabled) return - if (this.options.editMode !== 'advanced') return - const container = L.DomUtil.create('div') - L.DomUtil.createTitle(container, L._('Map advanced properties'), 'icon-settings') - this._editControls(container) - this._editShapeProperties(container) - this._editDefaultProperties(container) - this._editInteractionsProperties(container) - this.rules.edit(container) - this._editTilelayer(container) - this._editOverlay(container) - this._editBounds(container) - this._editSlideshow(container) - if (this.options.websocketEnabled) { - this._editSync(container) - } - this._advancedActions(container) - - this.editPanel.open({ content: container, className: 'dark' }) - }, - - enableEdit: function () { - L.DomUtil.addClass(document.body, 'umap-edit-enabled') - this.editEnabled = true - this.drop.enable() - this.fire('edit:enabled') - this.initSyncEngine() - }, - - disableEdit: function () { - if (this.isDirty) return - this.drop.disable() - L.DomUtil.removeClass(document.body, 'umap-edit-enabled') - this.editedFeature = null - this.editEnabled = false - this.fire('edit:disabled') - this.editPanel.close() - this.fullPanel.close() - this.sync.stop() - this.closeInplaceToolbar() - }, - - hasEditMode: function () { - return this.options.editMode === 'simple' || this.options.editMode === 'advanced' - }, - - getDisplayName: function () { - return this.options.name || L._('Untitled map') - }, - - initCaptionBar: function () { - const container = L.DomUtil.create( - 'div', - 'umap-caption-bar', - this._controlContainer - ) - const name = L.DomUtil.create('h3', 'map-name', container) - L.DomEvent.disableClickPropagation(container) - this.addAuthorLink('span', container) - if (this.getOption('captionMenus')) { - L.DomUtil.createButton( - 'umap-about-link flat', - container, - L._('Open caption'), - this.openCaption, - this - ) - L.DomUtil.createButton( - 'umap-open-browser-link flat', - container, - L._('Browse data'), - () => this.openBrowser('data') - ) - if (this.options.facetKey) { - L.DomUtil.createButton( - 'umap-open-filter-link flat', - container, - L._('Filter data'), - () => this.openBrowser('filters') - ) - } - } - this.onceDatalayersLoaded(function () { - this.slideshow.renderToolbox(container) - }) - }, - - askForReset: function (e) { - this.dialog - .confirm(L._('Are you sure you want to cancel your changes?')) - .then(() => { - this.reset() - this.disableEdit() - }) - }, - - startMarker: function () { - return this.editTools.startMarker() - }, - - startPolyline: function () { - return this.editTools.startPolyline() - }, - - startPolygon: function () { - return this.editTools.startPolygon() - }, - - del: async function () { - this.dialog - .confirm(L._('Are you sure you want to delete this map?')) - .then(async () => { - const url = this.urls.get('map_delete', { map_id: this.options.umap_id }) - const [data, response, error] = await this.server.post(url) - if (data.redirect) window.location = data.redirect - }) - }, - - clone: async function () { - this.dialog - .confirm(L._('Are you sure you want to clone this map and all its datalayers?')) - .then(async () => { - const url = this.urls.get('map_clone', { map_id: this.options.umap_id }) - const [data, response, error] = await this.server.post(url) - if (data.redirect) window.location = data.redirect - }) - }, - - removeDataLayers: function () { - this.eachDataLayerReverse((datalayer) => { - datalayer._delete() - }) - }, - - emptyDataLayers: function () { - this.eachDataLayerReverse((datalayer) => { - datalayer.empty() - }) - }, - - initLoader: function () { - this.loader = new L.Control.Loading() - this.loader.onAdd(this) - }, - - getOwnContextMenuItems: function (event) { - const items = [] - if (this.hasEditMode()) { - if (this.editEnabled) { - if (!this.isDirty) { - items.push({ - label: this.help.displayLabel('STOP_EDIT'), - action: () => this.disableEdit(), - }) - } - if (this.options.enableMarkerDraw) { - items.push({ - label: this.help.displayLabel('DRAW_MARKER'), - action: () => this.startMarker(event), - }) - } - if (this.options.enablePolylineDraw) { - items.push({ - label: this.help.displayLabel('DRAW_POLYGON'), - action: () => this.startPolygon(event), - }) - } - if (this.options.enablePolygonDraw) { - items.push({ - label: this.help.displayLabel('DRAW_LINE'), - action: () => this.startPolyline(event), - }) - } - items.push('-') - items.push({ - label: L._('Help'), - action: () => this.help.show('edit'), - }) - } else { - items.push({ - label: this.help.displayLabel('TOGGLE_EDIT'), - action: () => this.enableEdit(), - }) - } - } - if (items.length) { - items.push('-') - } - items.push( - { - label: L._('Open browser'), - action: () => this.openBrowser('layers'), - }, - { - label: L._('Browse data'), - action: () => this.openBrowser('data'), - } - ) - if (this.options.facetKey) { - items.push({ - label: L._('Filter data'), - action: () => this.openBrowser('filters'), - }) - } - items.push( - { - label: L._('Open caption'), - action: () => this.openCaption(), - }, - { - label: this.help.displayLabel('SEARCH'), - action: () => this.search(), - } - ) - return items - }, - - getContextMenuItems: function (event) { - const items = [] - if (this.options.urls.routing) { - items.push('-', { - label: L._('Directions from here'), - action: () => this.openExternalRouting(event), - }) - } - if (this.options.urls.edit_in_osm) { - items.push('-', { - label: L._('Edit in OpenStreetMap'), - action: () => this.editInOSM(event), - }) - } - return items - }, - - onContextMenu: function (event) { - const items = this.getOwnContextMenuItems(event).concat( - this.getContextMenuItems(event) - ) - this.contextmenu.open(event.originalEvent, items) - }, - - editInOSM: function (e) { - const url = this.urls.get('edit_in_osm', { - lat: e.latlng.lat, - lng: e.latlng.lng, - zoom: Math.max(this.getZoom(), 16), - }) - if (url) window.open(url) - }, - - openExternalRouting: function (e) { - const url = this.urls.get('routing', { - lat: e.latlng.lat, - lng: e.latlng.lng, - locale: L.getLocale(), - zoom: this.getZoom(), - }) - if (url) window.open(url) - }, - - getMap: function () { - return this - }, - - getGeoContext: function () { - const context = { - bbox: this.getBounds().toBBoxString(), - north: this.getBounds().getNorthEast().lat, - east: this.getBounds().getNorthEast().lng, - south: this.getBounds().getSouthWest().lat, - west: this.getBounds().getSouthWest().lng, - lat: this.getCenter().lat, - lng: this.getCenter().lng, - zoom: this.getZoom(), - } - context.left = context.west - context.bottom = context.south - context.right = context.east - context.top = context.north - return context - }, - - localizeUrl: function (url) { - return U.Utils.greedyTemplate(url, this.getGeoContext(), true) - }, - - proxyUrl: function (url, ttl) { - if (this.options.urls.ajax_proxy) { - url = U.Utils.greedyTemplate(this.options.urls.ajax_proxy, { - url: encodeURIComponent(url), - ttl: ttl, - }) - } - return url - }, getFeatureById: function (id) { let feature @@ -1837,57 +212,8 @@ U.Map = L.Map.extend({ } }, - closeInplaceToolbar: function () { - const toolbar = this._toolbars[L.Toolbar.Popup._toolbar_class_id] - if (toolbar) toolbar.remove() - }, - search: function () { if (this._controls.search) this._controls.search.open() }, - getLayersBounds: function () { - const bounds = new L.latLngBounds() - this.eachBrowsableDataLayer((d) => { - if (d.isVisible()) bounds.extend(d.layer.getBounds()) - }) - return bounds - }, - - sendEditLinkEmail: async function (formData) { - const sendLink = - this.options.urls.map_send_edit_link && - this.urls.get('map_send_edit_link', { - map_id: this.options.umap_id, - }) - await this.server.post(sendLink, {}, formData) - }, - - allProperties: function () { - return [].concat(...this.datalayers_index.map((dl) => dl._propertiesIndex)) - }, - - sortedValues: function (property) { - return [] - .concat(...this.datalayers_index.map((dl) => dl.sortedValues(property))) - .filter((val, idx, arr) => arr.indexOf(val) === idx) - .sort(U.Utils.naturalSort) - }, - - addAuthorLink: function (element, container) { - if (this.options.author?.name) { - const authorContainer = L.DomUtil.add( - element, - 'umap-map-author', - container, - ` ${L._('by')} ` - ) - L.DomUtil.createLink( - '', - authorContainer, - this.options.author.name, - this.options.author.url - ) - } - }, }) diff --git a/umap/static/umap/vendors/formbuilder/Leaflet.FormBuilder.js b/umap/static/umap/vendors/formbuilder/Leaflet.FormBuilder.js index 0d819651..6f814904 100644 --- a/umap/static/umap/vendors/formbuilder/Leaflet.FormBuilder.js +++ b/umap/static/umap/vendors/formbuilder/Leaflet.FormBuilder.js @@ -63,7 +63,11 @@ L.FormBuilder = L.Evented.extend({ const path = field.split('.') let value = this.obj for (const sub of path) { - value = value[sub] + try { + value = value[sub] + } catch { + console.log(field) + } } return value }, diff --git a/umap/templates/umap/js.html b/umap/templates/umap/js.html index 928ed264..7faf49b8 100644 --- a/umap/templates/umap/js.html +++ b/umap/templates/umap/js.html @@ -41,5 +41,4 @@ - - + diff --git a/umap/templates/umap/map_init.html b/umap/templates/umap/map_init.html index e8d4d739..3d970105 100644 --- a/umap/templates/umap/map_init.html +++ b/umap/templates/umap/map_init.html @@ -1,12 +1,11 @@ -{% load umap_tags %} +{% load umap_tags static %} {% include "umap/messages.html" %}
- diff --git a/umap/templates/umap/user_dashboard.html b/umap/templates/umap/user_dashboard.html index 48cbf408..672eb9c1 100644 --- a/umap/templates/umap/user_dashboard.html +++ b/umap/templates/umap/user_dashboard.html @@ -1,6 +1,6 @@ {% extends "umap/content.html" %} -{% load i18n %} +{% load i18n static %} {% block head_title %} {{ SITE_NAME }} - {% trans "My Dashboard" %} @@ -45,23 +45,22 @@ {% endblock maincontent %} {% block bottom_js %} {{ block.super }} - {% endblock bottom_js %} diff --git a/umap/tests/integration/test_import.py b/umap/tests/integration/test_import.py index 31de9fdd..ad0a42eb 100644 --- a/umap/tests/integration/test_import.py +++ b/umap/tests/integration/test_import.py @@ -93,10 +93,10 @@ def test_umap_import_from_textarea(live_server, tilelayer, page, settings): page.locator('img[src="https://tile.openstreetmap.fr/hot/6/32/21.png"]') ).to_be_visible() # Should not have imported umap_id, while in the file options - assert not page.evaluate("U.MAP.options.umap_id") + assert not page.evaluate("U.MAP.properties.umap_id") with page.expect_response(re.compile(r".*/datalayer/create/.*")): page.get_by_role("button", name="Save").click() - assert page.evaluate("U.MAP.options.umap_id") + assert page.evaluate("U.MAP.properties.umap_id") def test_import_geojson_from_textarea(tilelayer, live_server, page):