Compare commits

..

11 commits

Author SHA1 Message Date
Yohan Boniface
8c2a0eca28
chore: split umap.js in two modules (#2257)
Some checks failed
Test & Docs / docs (push) Has been cancelled
Test & Docs / tests (postgresql, 3.10) (push) Has been cancelled
Test & Docs / tests (postgresql, 3.12) (push) Has been cancelled
Test & Docs / lint (push) Has been cancelled
Long awaited step! Very intrusive change!

The `umap.js` old papa js style is now moved to modules, and split in
two, as we did for layers and features: one module is for the data part,
and the other is for the rendering part, and this one inherits from
Leaflet.

Sadly, the split between those two modules is not as clear as I'd like
it to be, as some functions my be interpreted or not as rendering.

What is now moved to the rendering module is all elements that inherit
from Leaflet, so what concerns the map itself, but also controls in of
the Leaflet world (inheriting from L.Control). In the other hand, UI
elements that does not inherit from Leaflet are kept on the `umap.js`
module (panels, tooltip, contextmenu…).

Also, `Umap` as a `properties` key, to follow geojson spec, and
distinguish from `LeafletMap.options`.

This is a first step, that will need more work, but as tests pass, I'd
suggest to merge if we agree on the choices and continue working with
other (smaller) PRs (and I'll take care of rebasing current other PRs).

Some specific points I've in mind that does not smell good:
- umap.js module still uses some Leaflet utils
- Umap and LeafletMap have a reference to each other
- umap.js module still need Leaflet events
- layers and feature still need to have reference to both Umap and
LeafletMap
2024-11-13 14:52:33 +01:00
Yohan Boniface
378e0f3ad3 chore: review with Alexis 2024-11-13 14:27:39 +01:00
Yohan Boniface
51e41b7bce chore: remove unused imports from global.js 2024-11-12 15:28:05 +01:00
Yohan Boniface
b49f3d7633 chore: remove old umap.js file 2024-11-12 15:06:34 +01:00
Yohan Boniface
0dbac92853 chore: remove 'postsync' event 2024-11-12 14:50:08 +01:00
Yohan Boniface
6d56bbb5de chore: refactore LeafletMap.setup/update 2024-11-12 14:36:59 +01:00
Yohan Boniface
e1a24b6180 chore: fix updaters to use umap instead of map and properties instead of options 2024-11-12 13:27:29 +01:00
Yohan Boniface
b0eb263d93 chore: remove use of L. in umap.js 2024-11-12 11:38:38 +01:00
Yohan Boniface
491d0515cd chore: fix help module not using umap module 2024-11-12 11:31:14 +01:00
Yohan Boniface
55f04a2f10 chore: remove DomEvent import from umap.js 2024-11-12 11:23:18 +01:00
Yohan Boniface
c952fed96a chore: split umap.js in two modules 2024-11-12 10:17:12 +01:00
38 changed files with 2768 additions and 2784 deletions

View file

@ -63,15 +63,3 @@ When the data layers are initialized, they can have two states:
To mark what needs to be synced with the server, uMap currently mark objects as "dirty". Something marked as "dirty" has changed on the client, but is not yet saved on the server.
Each map, datalayer and permission objects can be marked as "dirty". When a change is made on an object, it will mark its parent as "dirty" as well, so it can be updated accordingly.
### Saving data to the server with `umap.save()`
Here is what's being done when you call `map.save()`:
1. `map.saveSelf()`, posting `name`, `center` and `settings` to the server, and then
2. calls `permission.save()`, which will post the permissions to the server, and then call back
3. `map.continueSaving()`, which will take the first dirtyLayer and call
4. `datalayer.save()` on it. It does the following:
1. Post the data (`name`, `displayOnLoad`, `rank`, `settings`, and `geojson`)
2. Calls `permission.save()`, posting `edit_status` to the server, and then calling `map.continue_saving()` and remove the datalayer from `dirtyDatalayers`.
5. When the `dirtyDatalayers` list is empty, we are done.

View file

@ -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))
}
}

View file

@ -6,9 +6,10 @@ 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, leafletMap) {
this._umap = umap
this._leafletMap = leafletMap
this._leafletMap.on('moveend', this.onMoveEnd, this)
this.options = {
filter: '',
inBbox: false,
@ -82,7 +83,7 @@ export default class Browser {
updateDatalayer(datalayer) {
// Compute once, but use it for each feature later.
this.bounds = this.map.getBounds()
this.bounds = this._leafletMap.getBounds()
const parent = DomUtil.get(this.datalayerId(datalayer))
// Panel is not open
if (!parent) return
@ -115,10 +116,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 +133,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 +148,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 +187,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 +207,7 @@ export default class Browser {
textContent: translate('Reset all'),
})
this.map.panel.open({
this._umap.panel.open({
content: container,
className: 'umap-browser',
})
@ -230,7 +231,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 +241,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 +251,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 {
@ -262,7 +263,7 @@ export default class Browser {
})
}
static backButton(map) {
static backButton(umap) {
const button = DomUtil.createButtonIcon(
DomUtil.create('li', '', undefined),
'icon-back',
@ -271,7 +272,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', () => umap.openBrowser())
return button
}
}

View file

@ -3,8 +3,9 @@ import { translate } from './i18n.js'
import * as Utils from './utils.js'
export default class Caption {
constructor(map) {
this.map = map
constructor(umap, leafletMap) {
this._umap = umap
this._leafletMap = leafletMap
}
isOpen() {
@ -21,38 +22,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('<h4></h4>')
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 +68,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 +87,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 +99,19 @@ export default class Caption {
DomUtil.element({
tagName: 'strong',
parent: tilelayerCredit,
textContent: `${this.map.selected_tilelayer.options.name} `,
textContent: `${this._leafletMap.selectedTilelayer.options.name} `,
})
DomUtil.element({
tagName: 'span',
parent: tilelayerCredit,
safeHTML: this.map.selected_tilelayer.getAttribution(),
safeHTML: this._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(
`

View file

@ -18,8 +18,9 @@ import {
import loadPopup from '../rendering/popup.js'
class Feature {
constructor(datalayer, geojson = {}, id = null) {
this.sync = datalayer.map.sync_engine.proxy(this)
constructor(umap, datalayer, geojson = {}, id = null) {
this._umap = umap
this.sync = umap.sync_engine.proxy(this)
this._marked_for_deletion = false
this._isDirty = false
this._ui = null
@ -69,10 +70,6 @@ class Feature {
return this._ui
}
get map() {
return this.datalayer?.map
}
get center() {
return this.ui.getCenter()
}
@ -168,7 +165,7 @@ class Feature {
}
getSlug() {
return this.properties[this.map.getOption('slugKey') || 'name'] || ''
return this.properties[this._umap.getProperty('slugKey') || 'name'] || ''
}
getPermalink() {
@ -196,10 +193,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 +206,7 @@ class Feature {
return field.startsWith('properties.')
})
if (impactData) {
if (this.map.currentFeature === this) {
if (this._umap.currentFeature === this) {
this.view()
}
}
@ -217,7 +214,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 +253,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 +267,7 @@ class Feature {
<i class="icon icon-24 icon-delete"></i>${translate('Delete')}
</button>`)
button.addEventListener('click', () => {
this.confirmDelete().then(() => this.map.editPanel.close())
this.confirmDelete().then(() => this._umap.editPanel.close())
})
container.appendChild(button)
}
@ -320,20 +317,25 @@ class Feature {
endEdit() {}
getDisplayName(fallback) {
if (fallback === undefined) fallback = this.datalayer.getName()
const key = this.getOption('labelKey') || 'name'
// Variables mode.
if (U.Utils.hasVar(key))
return U.Utils.greedyTemplate(key, this.extendedProperties())
if (Utils.hasVar(key)) {
return Utils.greedyTemplate(key, this.extendedProperties())
}
// Simple mode.
return this.properties[key] || this.properties.title || fallback
return (
this.properties[key] ||
this.properties.title ||
fallback ||
this.datalayer.getName()
)
}
hasPopupFooter() {
if (this.datalayer.isRemoteLayer() && this.datalayer.options.remoteData.dynamic) {
return false
}
return this.map.getOption('displayPopupFooter')
return this._umap.getProperty('displayPopupFooter')
}
getPopupClass() {
@ -347,7 +349,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 +361,7 @@ class Feature {
del(sync) {
this.isDirty = true
this.map.closePopup()
this._umap._leafletMap.closePopup()
if (this.datalayer) {
this.datalayer.removeFeature(this, sync)
}
@ -417,34 +419,37 @@ class Feature {
let value = fallback
if (typeof this.staticOptions[option] !== 'undefined') {
value = this.staticOptions[option]
} else if (U.Utils.usableOption(this.properties._umap_options, option)) {
} else if (Utils.usableOption(this.properties._umap_options, option)) {
value = this.properties._umap_options[option]
} else if (this.datalayer) {
value = this.datalayer.getOption(option, this)
} else {
value = this.map.getOption(option)
value = this._umap.getProperty(option)
}
return value
}
getDynamicOption(option, fallback) {
let value = this.getOption(option, fallback)
getDynamicOption(key, fallback) {
let value = this.getOption(key, fallback)
// 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 (Utils.hasVar(value)) {
value = Utils.greedyTemplate(value, this.properties, true)
if (Utils.hasVar(value)) value = SCHEMA[key]?.default
}
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.getProperty('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()
)
}
}
@ -494,13 +499,9 @@ class Feature {
return [U.ToggleEditAction, U.DeleteFeatureAction]
}
getMap() {
return this.map
}
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 +526,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 +563,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 +613,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 +624,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 +632,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(),
},
{
@ -648,8 +649,8 @@ class Feature {
}
export class Point extends Feature {
constructor(datalayer, geojson, id) {
super(datalayer, geojson, id)
constructor(umap, datalayer, geojson, id) {
super(umap, datalayer, geojson, id)
this.staticOptions = {
mainColor: 'color',
className: 'marker',
@ -750,17 +751,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 +787,10 @@ 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 +829,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.getProperty('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 +847,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 +878,14 @@ class Path extends Feature {
getContextMenuEditItems(event) {
const items = super.getContextMenuEditItems(event)
if (this.map?.editedFeature !== this && this.isSameClass(this.map.editedFeature)) {
if (
this._umap?.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)
},
})
}
@ -892,8 +902,8 @@ class Path extends Feature {
}
export class LineString extends Path {
constructor(datalayer, geojson, id) {
super(datalayer, geojson, id)
constructor(umap, datalayer, geojson, id) {
super(umap, datalayer, geojson, id)
this.staticOptions = {
stroke: true,
fill: false,
@ -977,8 +987,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 +1032,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(),
})
}
@ -1041,8 +1051,8 @@ export class LineString extends Path {
}
export class Polygon extends Path {
constructor(datalayer, geojson, id) {
super(datalayer, geojson, id)
constructor(umap, datalayer, geojson, id) {
super(umap, datalayer, geojson, id)
this.staticOptions = {
mainColor: 'fillColor',
className: 'polygon',

View file

@ -21,6 +21,7 @@ import { DataLayerPermissions } from '../permissions.js'
import { Point, LineString, Polygon } from './features.js'
import TableEditor from '../tableeditor.js'
import { ServerStored } from '../saving.js'
import * as Schema from '../schema.js'
export const LAYER_TYPES = [
DefaultLayer,
@ -37,10 +38,10 @@ const LAYER_MAP = LAYER_TYPES.reduce((acc, klass) => {
}, {})
export class DataLayer extends ServerStored {
constructor(map, data) {
constructor(umap, leafletMap, 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 +49,12 @@ 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._leafletMap = leafletMap
this.parentPane = this._leafletMap.getPane('overlayPane')
this.pane = this._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 })
@ -78,7 +83,7 @@ export class DataLayer extends ServerStored {
}
this.backupOptions()
this.connectToMap()
this.permissions = new DataLayerPermissions(this)
this.permissions = new DataLayerPermissions(this._umap, this)
if (!this.umap_id) {
if (this.showAtLoad()) this.show()
this.isDirty = true
@ -128,7 +133,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 +158,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 +197,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._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 +218,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 +239,7 @@ export class DataLayer extends ServerStored {
dataChanged() {
if (!this.hasDataLoaded()) return
this.map.onDataLayersChanged()
this._umap.onDataLayersChanged()
this.layer.dataChanged()
}
@ -275,14 +280,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.getProperty('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._leafletMap.getZoom()
return !((!Number.isNaN(from) && zoom < from) || (!Number.isNaN(to) && zoom > to))
}
@ -294,14 +299,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.renderUrl(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 +346,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 +389,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 +398,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 +449,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.getProperty('sortKey'), U.lang)
for (const feature of collection) {
this.makeFeature(feature, sync)
}
@ -460,15 +464,15 @@ export class DataLayer extends ServerStored {
switch (geometry.type) {
case 'Point':
// FIXME: deal with MultiPoint
feature = new Point(this, geojson, id)
feature = new Point(this._umap, this, geojson, id)
break
case 'MultiLineString':
case 'LineString':
feature = new LineString(this, geojson, id)
feature = new LineString(this._umap, this, geojson, id)
break
case 'MultiPolygon':
case 'Polygon':
feature = new Polygon(this, geojson, id)
feature = new Polygon(this._umap, this, geojson, id)
break
default:
console.log(geojson)
@ -486,7 +490,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 +511,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.renderUrl(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.getProperty('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 +560,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._leafletMap)
this.propagateDelete()
this._leaflet_events_bk = this._leaflet_events
this.clear()
@ -597,7 +601,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 +635,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 +746,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 +772,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 +786,7 @@ export class DataLayer extends ServerStored {
</button>`)
deleteButton.addEventListener('click', () => {
this._delete()
this.map.editPanel.close()
this._umap.editPanel.close()
})
advancedButtons.appendChild(deleteButton)
@ -820,9 +825,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 +848,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.getProperty(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 +869,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 +879,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 +904,13 @@ export class DataLayer extends ServerStored {
}
async show() {
this.map.addLayer(this.layer)
this._leafletMap.addLayer(this.layer)
if (!this.isLoaded()) await this.fetchData()
this.propagateShow()
}
hide() {
this.map.removeLayer(this.layer)
this._leafletMap.removeLayer(this.layer)
this.propagateHide()
}
@ -922,7 +927,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._leafletMap.fitBounds(bounds, options)
}
}
@ -953,7 +958,7 @@ export class DataLayer extends ServerStored {
}
isVisible() {
return Boolean(this.layer && this.map.hasLayer(this.layer))
return Boolean(this.layer && this._leafletMap.hasLayer(this.layer))
}
getFeatureByIndex(index) {
@ -990,7 +995,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 +1005,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 +1021,7 @@ export class DataLayer extends ServerStored {
}
getRank() {
return this.map.datalayers_index.indexOf(this)
return this._umap.datalayersIndex.indexOf(this)
}
isReadOnly() {
@ -1043,8 +1048,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 +1061,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 +1077,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,23 +1106,19 @@ 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
}
getMap() {
return this.map
}
getName() {
return this.options.name || translate('Untitled layer')
}
tableEdit() {
if (!this.isVisible()) return
const editor = new TableEditor(this)
const editor = new TableEditor(this._umap, this, this._leafletMap)
editor.open()
}
@ -1125,9 +1126,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 +1179,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()

View file

@ -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
}
}

View file

@ -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

View file

@ -1,37 +1,13 @@
import {
uMapAlert as Alert,
uMapAlertCreation as AlertCreation,
} from '../components/alerts/alert.js'
import {
AjaxAutocomplete,
AjaxAutocompleteMultiple,
AutocompleteDatalist,
} from './autocomplete.js'
import Browser from './browser.js'
import Caption from './caption.js'
import ContextMenu from './ui/contextmenu.js'
import Facets from './facets.js'
import { Formatter } from './formatter.js'
import { uMapAlert as Alert } from '../components/alerts/alert.js'
import { AjaxAutocomplete, AjaxAutocompleteMultiple } from './autocomplete.js'
import Help from './help.js'
import Importer from './importer.js'
import Orderable from './orderable.js'
import { HTTPError, NOKError, Request, RequestError, ServerRequest } from './request.js'
import Rules from './rules.js'
import { ServerRequest } from './request.js'
import { SCHEMA } from './schema.js'
import Share from './share.js'
import Slideshow from './slideshow.js'
import { SyncEngine } from './sync/engine.js'
import Dialog from './ui/dialog.js'
import { EditPanel, FullPanel, Panel } from './ui/panel.js'
import Tooltip from './ui/tooltip.js'
import URLs from './urls.js'
import * as Utils from './utils.js'
import * as Icon from './rendering/icon.js'
import { DataLayer, LAYER_TYPES } from './data/layer.js'
import { DataLayerPermissions, MapPermissions } from './permissions.js'
import { LAYER_TYPES } from './data/layer.js'
import { Point, LineString, Polygon } from './data/features.js'
import { LeafletMarker, LeafletPolyline, LeafletPolygon } from './rendering/ui.js'
import * as SAVEMANAGER from './saving.js'
// Import modules and export them to the global scope.
// For the not yet module-compatible JS out there.
@ -39,45 +15,18 @@ import * as SAVEMANAGER from './saving.js'
// By alphabetic order
window.U = {
Alert,
AlertCreation,
AjaxAutocomplete,
AjaxAutocompleteMultiple,
AutocompleteDatalist,
Browser,
Caption,
ContextMenu,
DataLayer,
DataLayerPermissions,
Dialog,
EditPanel,
Facets,
Formatter,
FullPanel,
Help,
HTTPError,
Icon,
Importer,
LAYER_TYPES,
LeafletMarker,
LeafletPolygon,
LeafletPolyline,
LineString,
MapPermissions,
NOKError,
Orderable,
Panel,
Point,
Polygon,
Request,
RequestError,
Rules,
SAVEMANAGER,
SCHEMA,
ServerRequest,
Share,
Slideshow,
SyncEngine,
Tooltip,
URLs,
Utils,
}

View file

@ -1,5 +1,7 @@
import { DomEvent, DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
import { translate } from './i18n.js'
import * as Utils from './utils.js'
import Dialog from './ui/dialog.js'
const SHORTCUTS = {
DRAW_MARKER: {
@ -163,9 +165,9 @@ const ENTRIES = {
}
export default class Help {
constructor(map) {
this.map = map
this.dialog = new U.Dialog()
constructor(umap) {
this.umap = umap
this.dialog = new Dialog({ className: 'dark', accept: false, cancel: false })
this.isMacOS = /mac/i.test(
// eslint-disable-next-line compat/compat -- Fallback available.
navigator.userAgentData ? navigator.userAgentData.platform : navigator.platform
@ -199,7 +201,7 @@ export default class Help {
innerHTML: ENTRIES[name],
})
}
this.dialog.open({ template: container, className: 'dark', cancel: false, accept: false })
this.dialog.open({ template: container })
}
// Special dynamic case. Do we still think this dialog is useful?
@ -211,7 +213,7 @@ export default class Help {
className: 'umap-help-entry',
parent: container,
}).appendChild(this._buildEditEntry())
this.map.dialog.open({ content: container, className: 'dark' })
this.dialog.open({ template: container })
}
button(container, entries) {
@ -247,9 +249,11 @@ export default class Help {
DomEvent.on(actionContainer, 'click', action.addHooks, action)
DomEvent.on(actionContainer, 'click', this.dialog.close, this.dialog)
}
for (const id in this.map.helpMenuActions) {
addAction(this.map.helpMenuActions[id])
for (const action of Object.values(Help.MENU_ACTIONS)) {
addAction(action)
}
return container
}
}
Help.MENU_ACTIONS = {}

View file

@ -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,15 @@ export default class Importer {
}
full() {
this.map.once('postsync', this.map._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 +281,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 +299,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)

View file

@ -7,15 +7,15 @@ 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.setProperties(umap.properties.permissions)
this._umap = umap
this._isDirty = false
}
setOptions(options) {
this.options = Object.assign(
setProperties(properties) {
this.properties = Object.assign(
{
owner: null,
team: null,
@ -23,42 +23,42 @@ export class MapPermissions extends ServerStored {
share_status: null,
edit_status: null,
},
options
properties
)
}
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) {
const fields = []
if (this.isOwner()) {
fields.push([
'options.edit_status',
'properties.edit_status',
{
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)
const form = builder.build()
container.appendChild(form)
if (this.options.anonymous_edit_url) {
if (this.properties.anonymous_edit_url) {
DomUtil.createCopiableInput(
container,
translate('Secret edit link:'),
this.options.anonymous_edit_url
this.properties.anonymous_edit_url
)
}
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(
@ -86,38 +86,38 @@ export class MapPermissions extends ServerStored {
container.appendChild(fieldset)
if (this.isOwner()) {
topFields.push([
'options.edit_status',
'properties.edit_status',
{
handler: 'IntSelect',
label: translate('Who can edit'),
selectOptions: this.map.options.edit_statuses,
selectOptions: this._umap.properties.edit_statuses,
},
])
topFields.push([
'options.share_status',
'properties.share_status',
{
handler: 'IntSelect',
label: translate('Who can view'),
selectOptions: this.map.options.share_statuses,
selectOptions: this._umap.properties.share_statuses,
},
])
collaboratorsFields.push([
'options.owner',
'properties.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',
'properties.team',
{
handler: 'ManageTeam',
label: translate('Attach map to a team'),
teams: this.map.options.user.teams,
teams: this._umap.properties.user.teams,
},
])
}
}
collaboratorsFields.push([
'options.editors',
'properties.editors',
{ handler: 'ManageEditors', label: translate("Map's editors") },
])
@ -136,20 +136,20 @@ export class MapPermissions extends ServerStored {
}
_editDatalayers(container) {
if (this.map.hasLayers()) {
if (this._umap.hasLayers()) {
const fieldset = Utils.loadTemplate(
`<fieldset class="separator"><legend>${translate('Datalayers')}</legend></fieldset>`
)
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,78 +158,79 @@ 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.properties.owner = this._umap.properties.user
Alert.success(translate('Map has been attached to your account'))
this.map.editPanel.close()
this._umap.editPanel.close()
}
}
async save() {
if (!this.isDirty) return
const formData = new FormData()
if (!this.isAnonymousMap() && this.options.editors) {
const editors = this.options.editors.map((u) => u.id)
for (let i = 0; i < this.options.editors.length; i++)
formData.append('editors', this.options.editors[i].id)
if (!this.isAnonymousMap() && this.properties.editors) {
const editors = this.properties.editors.map((u) => u.id)
for (let i = 0; i < this.properties.editors.length; i++)
formData.append('editors', this.properties.editors[i].id)
}
if (this.isOwner() || this.isAnonymousMap()) {
formData.append('edit_status', this.options.edit_status)
formData.append('edit_status', this.properties.edit_status)
}
if (this.isOwner()) {
formData.append('owner', this.options.owner?.id)
formData.append('team', this.options.team?.id || '')
formData.append('share_status', this.options.share_status)
formData.append('owner', this.properties.owner?.id)
formData.append('team', this.properties.team?.id || '')
formData.append('share_status', this.properties.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')
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.options
this._umap.properties.permissions = Object.assign(
{},
this._umap.properties.permissions,
this.properties
)
}
getShareStatusDisplay() {
if (this.map.options.share_statuses) {
return Object.fromEntries(this.map.options.share_statuses)[
this.options.share_status
if (this._umap.properties.share_statuses) {
return Object.fromEntries(this._umap.properties.share_statuses)[
this.properties.share_status
]
}
}
}
export class DataLayerPermissions extends ServerStored {
constructor(datalayer) {
constructor(umap, datalayer) {
super()
this.options = Object.assign(
this._umap = umap
this.properties = Object.assign(
{
edit_status: null,
},
@ -239,20 +240,16 @@ export class DataLayerPermissions extends ServerStored {
this.datalayer = datalayer
}
get map() {
return this.datalayer.map
}
edit(container) {
const fields = [
[
'options.edit_status',
'properties.edit_status',
{
handler: 'IntSelect',
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 +261,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,
})
}
@ -273,8 +270,8 @@ export class DataLayerPermissions extends ServerStored {
async save() {
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(
formData.append('edit_status', this.properties.edit_status)
const [data, response, error] = await this._umap.server.post(
this.getUrl(),
{},
formData
@ -289,7 +286,7 @@ export class DataLayerPermissions extends ServerStored {
this.datalayer.options.permissions = Object.assign(
{},
this.datalayer.options.permissions,
this.options
this.properties
)
}
}

View file

@ -5,20 +5,20 @@ import * as Utils from '../../utils.js'
export const LayerMixin = {
browsable: true,
onInit: function (map) {
if (this.datalayer.autoLoaded()) map.on('zoomend', this.onZoomEnd, this)
onInit: function (leafletMap) {
if (this.datalayer.autoLoaded()) leafletMap.on('zoomend', this.onZoomEnd, this)
},
onDelete: function (map) {
map.off('zoomend', this.onZoomEnd, this)
onDelete: function (leafletMap) {
leafletMap.off('zoomend', this.onZoomEnd, this)
},
onAdd: function (map) {
map.on('moveend', this.onMoveEnd, this)
onAdd: function (leafletMap) {
leafletMap.on('moveend', this.onMoveEnd, this)
},
onRemove: function (map) {
map.off('moveend', this.onMoveEnd, this)
onRemove: function (leafletMap) {
leafletMap.off('moveend', this.onMoveEnd, this)
},
getType: function () {
@ -73,17 +73,17 @@ 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._leafletMap)
},
onAdd: function (map) {
LayerMixin.onAdd.call(this, map)
return FeatureGroup.prototype.onAdd.call(this, map)
onAdd: function (leafletMap) {
LayerMixin.onAdd.call(this, leafletMap)
return FeatureGroup.prototype.onAdd.call(this, leafletMap)
},
onRemove: function (map) {
LayerMixin.onRemove.call(this, map)
return FeatureGroup.prototype.onRemove.call(this, map)
onRemove: function (leafletMap) {
LayerMixin.onRemove.call(this, leafletMap)
return FeatureGroup.prototype.onRemove.call(this, leafletMap)
},
})

View file

@ -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._leafletMap)
},
ensureOptions: () => {},

View file

@ -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._leafletMap)
this._markerCluster = MarkerCluster
this._layers = []
},

View file

@ -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._leafletMap)
if (!Utils.isObject(this.datalayer.options.heat)) {
this.datalayer.options.heat = {}
}

View file

@ -0,0 +1,581 @@
// 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'
import ContextMenu from '../ui/contextmenu.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._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.getProperty('color'),
fillColor: this._umap.getProperty('fillColor'),
stroke: this._umap.getProperty('stroke'),
fill: this._umap.getProperty('fill'),
weight: this._umap.getProperty('weight'),
opacity: this._umap.getProperty('opacity'),
fillOpacity: this._umap.getProperty('fillOpacity'),
},
}).addTo(this)
this._controls.miniMap._miniMap.invalidateSize()
}
})
}
for (const name of this.HIDDABLE_CONTROLS) {
const status = this._umap.getProperty(`${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.getProperty('permanentCredit'))
this._controls.permanentCredit.addTo(this)
if (this._umap.getProperty('moreControl')) this._controls.more.addTo(this)
if (this._umap.getProperty('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(`
<button class="umap-user flat" type="button">
<i class="icon icon-16 icon-profile"></i>
<span>${this.options.user.name}</span>
</button>
`)
rightContainer.appendChild(button)
const menu = new 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._umap.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,
})
})
},
renderCaptionBar: function () {
if (this.options.noControl) return
const container =
this._controlContainer.querySelector('.umap-caption-bar') ||
DomUtil.create('div', 'umap-caption-bar', this._controlContainer)
container.innerHTML = ''
const name = DomUtil.create('h3', 'map-name', container)
DomEvent.disableClickPropagation(container)
this._umap.addAuthorLink(container)
if (this._umap.getProperty('captionMenus')) {
DomUtil.createButton(
'umap-about-link flat',
container,
translate('Open caption'),
() => this._umap.openCaption()
)
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(() => {
this._umap.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 })
}
},
}
export const LeafletMap = BaseMap.extend({
includes: [ControlsMixin, ManageTilelayerMixin],
// The initialize and the setup method might seem similar, but they
// serve two different purposes:
// initialize is for Leaflet internal, when we do "new LeafletMap",
// while setup is the public API for the LeafletMap to actually
// render to the DOM.
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)
})
},
setup: function () {
this.initControls()
// Needs locate control and hash to exist
this.initCenter()
this.renderUI()
},
renderUI: function () {
setOptions(this, this._umap.properties)
this.initTileLayers()
this.renderCaptionBar()
this.renderEditToolbar()
// Needs tilelayer to exist for minimap
this.renderControls()
this.handleLimitBounds()
},
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)
},
initEditTools: function () {
this.editTools = new U.Editable(this._umap)
this.renderEditToolbar()
},
})

View file

@ -61,15 +61,15 @@ const Panel = Popup.extend({
zoomAnimation: false,
},
onAdd: function (map) {
map.panel.setDefaultMode('expanded')
map.panel.open({
onAdd: function (leafletMap) {
leafletMap._umap.panel.setDefaultMode('expanded')
leafletMap._umap.panel.open({
content: this._content,
actions: [Browser.backButton(map)],
actions: [Browser.backButton(leafletMap._umap)],
})
// fire events as in base class Popup.js:onAdd
map.fire('popupopen', { popup: this })
leafletMap.fire('popupopen', { popup: this })
if (this._source) {
this._source.fire('popupopen', { popup: this }, true)
if (!(this._source instanceof Path)) {
@ -78,11 +78,11 @@ const Panel = Popup.extend({
}
},
onRemove: function (map) {
map.panel.close()
onRemove: function (leafletMap) {
leafletMap._umap.panel.close()
// fire events as in base class Popup.js:onRemove
map.fire('popupclose', { popup: this })
leafletMap.fire('popupclose', { popup: this })
if (this._source) {
this._source.fire('popupclose', { popup: this }, true)
if (!(this._source instanceof Path)) {

View file

@ -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.getSharedContextMenuItems(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()

View file

@ -2,6 +2,7 @@ import { DomEvent, DomUtil, stamp } from '../../vendors/leaflet/leaflet-src.esm.
import { translate } from './i18n.js'
import * as Utils from './utils.js'
import { AutocompleteDatalist } from './autocomplete.js'
import Orderable from './orderable.js'
const EMPTY_VALUES = ['', undefined, null]
@ -21,10 +22,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 +38,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) {
@ -101,10 +102,6 @@ class Rule {
return this.operator(this.cast(props[this.key]))
}
getMap() {
return this.map
}
getOption(option) {
return this.options[option]
}
@ -136,7 +133,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 +141,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 +173,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 +183,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 +222,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) {
@ -236,21 +233,21 @@ export default class Rules {
rule.renderToolbox(DomUtil.create('li', 'orderable', ul))
}
const orderable = new U.Orderable(ul, this.onReorder.bind(this))
const orderable = new Orderable(ul, this.onReorder.bind(this))
}
DomUtil.createButton('umap-add', body, translate('Add rule'), this.addRule, this)
}
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,

View file

@ -10,22 +10,21 @@ export async function save() {
}
}
export function add(obj) {
function add(obj) {
_queue.add(obj)
_onUpdate()
onUpdate()
}
export function remove(obj) {
function remove(obj) {
_queue.delete(obj)
_onUpdate()
onUpdate()
}
export function has(obj) {
function has(obj) {
return _queue.has(obj)
}
function _onUpdate() {
console.log(_queue)
function onUpdate() {
isDirty = Boolean(_queue.size)
document.body.classList.toggle('umap-is-dirty', isDirty)
}

View file

@ -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(`
<div>
@ -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.getProperty('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)
}

View file

@ -13,20 +13,20 @@ const TOOLBOX_TEMPLATE = `
`
export default class Slideshow extends WithTemplate {
constructor(map, options) {
constructor(umap, leafletMap, properties) {
super()
this.map = map
this._umap = umap
this._id = null
this.CLASSNAME = 'umap-slideshow-active'
this.setOptions(options)
this.setProperties(properties)
this._current = null
if (this.options.autoplay) {
this.map.onceDataLoaded(function () {
if (this.properties.autoplay) {
this._umap.onceDataLoaded(function () {
this.play()
}, this)
}
this.map.on(
leafletMap.on(
'edit:enabled',
function () {
this.stop()
@ -54,22 +54,22 @@ export default class Slideshow extends WithTemplate {
return this.current.getNext()
}
setOptions(options) {
this.options = Object.assign(
setProperties(properties) {
this.properties = Object.assign(
{
delay: 5000,
autoplay: false,
},
options
properties
)
}
defaultDatalayer() {
return this.map.findDataLayer((d) => d.canBrowse())
return this._umap.findDataLayer((d) => d.canBrowse())
}
startSpinner() {
const time = Number.parseInt(this.options.delay, 10)
const time = Number.parseInt(this.properties.delay, 10)
if (!time) return
const css = `rotation ${time / 1000}s infinite linear`
const spinner = document.querySelector('.umap-slideshow-toolbox .play .spinner')
@ -83,9 +83,9 @@ 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.properties.slideshow.active) return
L.DomUtil.addClass(document.body, this.CLASSNAME)
this._id = window.setInterval(L.bind(this.loop, this), this.options.delay)
this._id = window.setInterval(L.bind(this.loop, this), this.properties.delay)
this.startSpinner()
this.loop()
}
@ -123,7 +123,7 @@ export default class Slideshow extends WithTemplate {
step() {
if (!this.current) return this.stop()
this.current.zoomTo({ easing: this.options.easing })
this.current.zoomTo({ easing: this.properties.easing })
this.current.view()
}

View file

@ -6,8 +6,8 @@ import { fieldInSchema } from '../utils.js'
*/
class BaseUpdater {
constructor(map) {
this.map = map
constructor(umap) {
this._umap = umap
}
updateObjectValue(obj, key, value) {
@ -32,8 +32,8 @@ class BaseUpdater {
}
getDataLayerFromID(layerId) {
if (layerId) return this.map.getDataLayerByUmapId(layerId)
return this.map.defaultEditDataLayer()
if (layerId) return this._umap.getDataLayerByUmapId(layerId)
return this._umap.defaultEditDataLayer()
}
applyMessage(payload) {
@ -45,18 +45,18 @@ class BaseUpdater {
export class MapUpdater extends BaseUpdater {
update({ key, value }) {
if (fieldInSchema(key)) {
this.updateObjectValue(this.map, key, value)
this.updateObjectValue(this._umap, key, value)
}
this.map.render([key])
this._umap.render([key])
}
}
export class DataLayerUpdater extends BaseUpdater {
upsert({ value }) {
// Inserts does not happen (we use multiple updates instead).
this.map.createDataLayer(value, false)
this.map.render([])
this._umap.createDataLayer(value, false)
this._umap.render([])
}
update({ key, metadata, value }) {

View file

@ -14,10 +14,11 @@ const TEMPLATE = `
`
export default class TableEditor extends WithTemplate {
constructor(datalayer) {
constructor(umap, datalayer, leafletMap) {
super()
this.datalayer = datalayer
this.map = this.datalayer.map
this._umap = umap
this._leafletMap = leafletMap
this.contextmenu = new ContextMenu({ className: 'dark' })
this.table = this.loadTemplate(TEMPLATE)
if (!this.datalayer.isRemoteLayer()) {
@ -36,20 +37,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 +87,8 @@ export default class TableEditor extends WithTemplate {
}
renderBody() {
const bounds = this.map.getBounds()
const inBbox = this.map.browser.options.inBbox
const bounds = this._leafletMap.getBounds()
const inBbox = this._umap.browser.options.inBbox
let html = ''
this.datalayer.eachFeature((feature) => {
if (feature.isFiltered()) return
@ -121,7 +122,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 +136,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 +151,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 +188,10 @@ export default class TableEditor extends WithTemplate {
<button class="flat" type="button" data-ref="filters">
<i class="icon icon-16 icon-filters"></i>${translate('Filter data')}
</button>`)
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 +305,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 +321,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')
}
})
}

View file

@ -2,9 +2,10 @@ 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, leafletMap) {
this.parent = leafletMap._controlContainer
this._umap = umap
this._leafletMap = leafletMap
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 +81,26 @@ export class Panel {
onClose() {
if (DomUtil.hasClass(this.container, 'on')) {
DomUtil.removeClass(this.container, 'on')
this.map.invalidateSize({ pan: false })
this._leafletMap.invalidateSize({ pan: false })
}
}
}
export class EditPanel extends Panel {
constructor(map) {
super(map)
constructor(umap, leafletMap) {
super(umap, leafletMap)
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, leafletMap) {
super(umap, leafletMap)
this.className = 'full dark'
this.mode = 'expanded'
}

File diff suppressed because it is too large Load diff

View file

@ -25,6 +25,16 @@ export function checkId(string) {
return /^[A-Za-z0-9]{5}$/.test(string)
}
function _getPropertyName(field) {
const filtered_field = ['options.', 'properties.'].reduce(
(acc, prefix) => acc.replace(prefix, ''),
field
)
return filtered_field.split('.')[0]
}
/**
* Compute the impacts for a given list of fields.
*
@ -41,7 +51,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 _getPropertyName(field)
})
.reduce((acc, field) => {
// retrieve the "impacts" field from the schema
@ -66,7 +76,7 @@ export function getImpactsFromSchema(fields, schema) {
export function fieldInSchema(field, schema) {
const current_schema = schema || U.SCHEMA
if (typeof field !== 'string') return false
const field_name = field.replace('options.', '').split('.')[0]
const field_name = _getPropertyName(field)
return current_schema[field_name] !== undefined
}

View file

@ -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)
)
@ -12,8 +12,8 @@ U.BaseAction = L.ToolbarAction.extend({
tooltip: this.options.tooltip,
}
L.ToolbarAction.prototype.initialize.call(this)
if (this.options.helpMenu && !this.map.helpMenuActions[this.options.className])
this.map.helpMenuActions[this.options.className] = this
if (this.options.helpMenu && !U.Help.MENU_ACTIONS[this.options.className])
U.Help.MENU_ACTIONS[this.options.className] = this
},
})
@ -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()
},
})
@ -107,7 +107,7 @@ U.DrawMarkerAction = U.BaseAction.extend({
},
addHooks: function () {
this.map.startMarker()
this.map.editTools.startMarker()
},
})
@ -119,7 +119,7 @@ U.DrawPolylineAction = U.BaseAction.extend({
},
addHooks: function () {
this.map.startPolyline()
this.map.editTools.startPolyline()
},
})
@ -131,7 +131,7 @@ U.DrawPolygonAction = U.BaseAction.extend({
},
addHooks: function () {
this.map.startPolygon()
this.map.editTools.startPolygon()
},
})
@ -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,12 @@ 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()
this._umap.star()
},
})
@ -554,248 +561,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(`
<button class="umap-user flat" type="button">
<i class="icon icon-16 icon-profile"></i>
<span>${this.options.user.name}</span>
</button>
`)
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 =
'<span>' + this.sync.getNumberOfConnectedPeers() + '</span>'
}
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 +588,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 +654,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 +674,10 @@ 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 +707,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.getProperty('shortCredit')
const captionMenus = this._map._umap.getProperty('captionMenus')
if (shortCredit) {
L.DomUtil.element({
tagName: 'span',
@ -947,7 +719,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 +911,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 +951,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 +961,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,24 +983,24 @@ U.Editable = L.Editable.extend({
},
createPolyline: function (latlngs) {
const datalayer = this.map.defaultEditDataLayer()
const point = new U.LineString(datalayer, {
const datalayer = this._umap.defaultEditDataLayer()
const point = new U.LineString(this._umap, datalayer, {
geometry: { type: 'LineString', coordinates: [] },
})
return point.ui
},
createPolygon: function (latlngs) {
const datalayer = this.map.defaultEditDataLayer()
const point = new U.Polygon(datalayer, {
const datalayer = this._umap.defaultEditDataLayer()
const point = new U.Polygon(this._umap, datalayer, {
geometry: { type: 'Polygon', coordinates: [] },
})
return point.ui
},
createMarker: function (latlng) {
const datalayer = this.map.defaultEditDataLayer()
const point = new U.Point(datalayer, {
const datalayer = this._umap.defaultEditDataLayer()
const point = new U.Point(this._umap, datalayer, {
geometry: { type: 'Point', coordinates: [latlng.lng, latlng.lat] },
})
return point.ui
@ -1235,15 +1008,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 +1024,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 +1071,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 +1087,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

View file

@ -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) {

View file

@ -221,21 +221,16 @@ 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({
L.DomEvent.on(info, 'mouseover', () => {
this.builder._umap.tooltip.open({
anchor: info,
content: this.options.helpTooltip,
position: 'top',
})
},
this
)
})
}
}
},
@ -359,7 +354,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 +371,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 +395,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 +409,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 +464,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 +611,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 +1162,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)

File diff suppressed because it is too large Load diff

View file

@ -63,7 +63,11 @@ L.FormBuilder = L.Evented.extend({
const path = field.split('.')
let value = this.obj
for (const sub of path) {
try {
value = value[sub]
} catch {
console.log(field)
}
}
return value
},

View file

@ -41,5 +41,4 @@
<script src="{% static 'umap/js/umap.core.js' %}" defer></script>
<script src="{% static 'umap/js/umap.forms.js' %}" defer></script>
<script src="{% static 'umap/js/umap.controls.js' %}" defer></script>
<script src="{% static 'umap/js/umap.js' %}" defer></script>
<script src="{% static 'umap/js/components/fragment.js' %}" defer></script>
<script type="module" src="{% static 'umap/js/components/fragment.js' %}" defer></script>

View file

@ -1,12 +1,11 @@
{% load umap_tags %}
{% load umap_tags static %}
{% include "umap/messages.html" %}
<div id="map">
</div>
<!-- djlint:off -->
<script defer type="text/javascript">
window.addEventListener('DOMContentLoaded', (event) => {
U.MAP = new U.Map("map", {{ map_settings|notag|safe }})
})
<script defer type="module">
import Umap from '{% static "umap/js/modules/umap.js" %}'
U.MAP = new Umap("map", {{ map_settings|notag|safe }})
</script>
<!-- djlint:on -->

View file

@ -1,6 +1,6 @@
{% extends "umap/content.html" %}
{% load i18n %}
{% load i18n static %}
{% block head_title %}
{{ SITE_NAME }} - {% trans "My Dashboard" %}
@ -45,8 +45,8 @@
{% endblock maincontent %}
{% block bottom_js %}
{{ block.super }}
<script type="text/javascript">
!(function () {
<script type="module">
import Umap from '{% static "umap/js/modules/umap.js" %}'
const CACHE = {}
for (const mapOpener of document.querySelectorAll("button.map-opener")) {
mapOpener.addEventListener('click', (event) => {
@ -55,13 +55,12 @@
const mapId = button.dataset.mapId
if (!document.querySelector(`#${mapId}_target`).children.length) {
const previewSettings = JSON.parse(document.getElementById(mapId).textContent)
const map = new U.Map(`${mapId}_target`, previewSettings)
const map = new Umap(`${mapId}_target`, previewSettings)
CACHE[mapId] = map
} else {
CACHE[mapId].invalidateSize()
}
})
}
})()
</script>
{% endblock bottom_js %}

View file

@ -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):

View file

@ -161,7 +161,7 @@ def test_websocket_connection_can_sync_polygons(
@pytest.mark.xdist_group(name="websockets")
def test_websocket_connection_can_sync_map_properties(
context, live_server, websocket_server, tilelayer
new_page, live_server, websocket_server, tilelayer
):
map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
map.settings["properties"]["syncEnabled"] = True
@ -169,9 +169,9 @@ def test_websocket_connection_can_sync_map_properties(
DataLayerFactory(map=map, data={})
# Create two tabs
peerA = context.new_page()
peerA = new_page()
peerA.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
peerB = context.new_page()
peerB = new_page()
peerB.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
# Name change is synced
@ -193,7 +193,7 @@ def test_websocket_connection_can_sync_map_properties(
@pytest.mark.xdist_group(name="websockets")
def test_websocket_connection_can_sync_datalayer_properties(
context, live_server, websocket_server, tilelayer
new_page, live_server, websocket_server, tilelayer
):
map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
map.settings["properties"]["syncEnabled"] = True
@ -201,9 +201,9 @@ def test_websocket_connection_can_sync_datalayer_properties(
DataLayerFactory(map=map, data={})
# Create two tabs
peerA = context.new_page()
peerA = new_page()
peerA.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
peerB = context.new_page()
peerB = new_page()
peerB.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
# Layer addition, name and type are synced
@ -215,7 +215,7 @@ def test_websocket_connection_can_sync_datalayer_properties(
peerA.locator("body").press("Escape")
peerB.get_by_role("link", name="Manage layers").click()
peerB.get_by_role("button", name="Edit").first.click()
peerB.locator(".panel.right").get_by_role("button", name="Edit").first.click()
expect(peerB.locator('input[name="name"]')).to_have_value("synced layer!")
expect(peerB.get_by_role("combobox")).to_have_value("Choropleth")