diff --git a/umap/models.py b/umap/models.py
index aa60b334..c61401ff 100644
--- a/umap/models.py
+++ b/umap/models.py
@@ -10,6 +10,7 @@ from django.core.files.base import File
from django.core.signing import Signer
from django.template.defaultfilters import slugify
from django.urls import reverse
+from django.utils.functional import classproperty
from django.utils.translation import gettext_lazy as _
from .managers import PublicManager
@@ -218,7 +219,7 @@ class Map(NamedModel):
"umap_id": self.pk,
"onLoadPanel": "none",
"captionBar": False,
- "default_iconUrl": "%sumap/img/marker.svg" % settings.STATIC_URL,
+ "schema": self.extra_schema,
"slideshow": {},
}
)
@@ -329,6 +330,14 @@ class Map(NamedModel):
datalayer.clone(map_inst=new)
return new
+ @classproperty
+ def extra_schema(self):
+ return {
+ "iconUrl": {
+ "default": "%sumap/img/marker.svg" % settings.STATIC_URL,
+ }
+ }
+
class Pictogram(NamedModel):
"""
diff --git a/umap/static/umap/js/modules/browser.js b/umap/static/umap/js/modules/browser.js
index c3370acf..80c7f358 100644
--- a/umap/static/umap/js/modules/browser.js
+++ b/umap/static/umap/js/modules/browser.js
@@ -30,7 +30,7 @@ export default class Browser {
title.textContent = feature.getDisplayName() || '—'
const bgcolor = feature.getDynamicOption('color')
colorBox.style.backgroundColor = bgcolor
- if (symbol && symbol !== this.map.options.default_iconUrl) {
+ if (symbol && symbol !== U.SCHEMA.iconUrl.default) {
const icon = U.Icon.makeIconElement(symbol, colorBox)
U.Icon.setIconContrast(icon, colorBox, symbol, bgcolor)
}
@@ -131,7 +131,7 @@ export default class Browser {
'h3',
'umap-browse-title',
container,
- this.map.options.name
+ this.map.getOption('name')
)
const formContainer = DomUtil.create('div', '', container)
diff --git a/umap/static/umap/js/modules/global.js b/umap/static/umap/js/modules/global.js
index 2d579187..25ee3505 100644
--- a/umap/static/umap/js/modules/global.js
+++ b/umap/static/umap/js/modules/global.js
@@ -1,14 +1,12 @@
-import * as L from '../../vendors/leaflet/leaflet-src.esm.js'
import URLs from './urls.js'
import Browser from './browser.js'
import * as Utils from './utils.js'
+import {SCHEMA} from './schema.js'
import { Request, ServerRequest, RequestError, HTTPError, NOKError } from './request.js'
// Import modules and export them to the global scope.
// For the not yet module-compatible JS out there.
-// Copy the leaflet module, it's expected by leaflet plugins to be writeable.
-window.L = { ...L }
window.U = {
URLs,
Request,
@@ -18,4 +16,5 @@ window.U = {
NOKError,
Browser,
Utils,
+ SCHEMA,
}
diff --git a/umap/static/umap/js/modules/i18n.js b/umap/static/umap/js/modules/i18n.js
new file mode 100644
index 00000000..02c90997
--- /dev/null
+++ b/umap/static/umap/js/modules/i18n.js
@@ -0,0 +1,35 @@
+// Comes from https://github.com/Leaflet/Leaflet/pull/9281
+import { Util } from '../../vendors/leaflet/leaflet-src.esm.js'
+
+export const locales = {}
+
+// @property locale: String
+// The current locale code, that will be used when translating strings.
+export let locale = null
+
+// @function registerLocale(code: String, locale?: Object): String
+// Define localized strings for a given locale, defined by `code`.
+export function registerLocale(code, locale) {
+ locales[code] = Util.extend({}, locales[code], locale)
+}
+// @function setLocale(code: String): undefined
+// Define or change the locale code to be used when translating strings.
+export function setLocale(code) {
+ locale = code
+}
+// @function translate(string: String, data?: Object): String
+// Actually try to translate the `string`, with optionnal variable passed in `data`.
+export function translate(string, data = {}) {
+ if (locale && locales[locale] && locales[locale][string] !== undefined) {
+ string = locales[locale][string]
+ }
+ try {
+ // Do not fail if some data is missing
+ // a bad translation should not break the app
+ string = Util.template(string, data)
+ } catch (err) {
+ console.error(err)
+ }
+
+ return string
+}
diff --git a/umap/static/umap/js/modules/leaflet-configure.js b/umap/static/umap/js/modules/leaflet-configure.js
new file mode 100644
index 00000000..b7b502ff
--- /dev/null
+++ b/umap/static/umap/js/modules/leaflet-configure.js
@@ -0,0 +1,7 @@
+import * as L from '../../vendors/leaflet/leaflet-src.esm.js'
+// Comes from https://github.com/Leaflet/Leaflet/pull/9281
+// TODELETE once it's merged!
+import * as i18n from './i18n.js'
+
+window.L = { ...L, ...i18n }
+window.L._ = i18n.translate
diff --git a/umap/static/umap/js/modules/schema.js b/umap/static/umap/js/modules/schema.js
new file mode 100644
index 00000000..d34f8ffc
--- /dev/null
+++ b/umap/static/umap/js/modules/schema.js
@@ -0,0 +1,388 @@
+import { translate } from './i18n.js'
+
+export const SCHEMA = {
+ zoom: {
+ type: Number,
+ },
+ scrollWheelZoom: {
+ type: Boolean,
+ label: translate('Allow scroll wheel zoom?'),
+ },
+ scaleControl: {
+ type: Boolean,
+ label: translate('Do you want to display the scale control?'),
+ default: true,
+ },
+ moreControl: {
+ type: Boolean,
+ label: translate('Do you want to display the «more» control?'),
+ default: true,
+ },
+ miniMap: {
+ type: Boolean,
+ label: translate('Do you want to display a minimap?'),
+ default: false,
+ },
+ displayPopupFooter: {
+ type: Boolean,
+ label: translate('Do you want to display popup footer?'),
+ default: false,
+ },
+ onLoadPanel: {
+ type: String,
+ label: translate('Do you want to display a panel on load?'),
+ choices: [
+ ['none', translate('None')],
+ ['caption', translate('Caption')],
+ ['databrowser', translate('Data browser')],
+ ['facet', translate('Facet search')],
+ ],
+ default: 'none',
+ },
+ defaultView: {
+ type: String,
+ label: translate('Default view'),
+ choices: [
+ ['center', translate('Saved center and zoom')],
+ ['data', translate('Fit all data')],
+ ['latest', translate('Latest feature')],
+ ['locate', translate('User location')],
+ ],
+ default: 'center',
+ },
+ name: {
+ type: String,
+ label: translate('name'),
+ },
+ description: {
+ label: translate('description'),
+ type: 'Text',
+ helpEntries: 'textFormatting',
+ },
+ licence: {
+ type: String,
+ label: translate('licence'),
+ },
+ tilelayer: {
+ type: Object,
+ },
+ overlay: {
+ type: Object,
+ },
+ limitBounds: {
+ type: Object,
+ },
+ color: {
+ type: String,
+ handler: 'ColorPicker',
+ label: translate('color'),
+ helpEntries: 'colorValue',
+ inheritable: true,
+ default: 'DarkBlue',
+ },
+ iconClass: {
+ type: String,
+ label: translate('Icon shape'),
+ inheritable: true,
+ choices: [
+ ['Default', translate('Default')],
+ ['Circle', translate('Circle')],
+ ['Drop', translate('Drop')],
+ ['Ball', translate('Ball')],
+ ],
+ default: 'Default',
+ },
+ iconUrl: {
+ type: String,
+ handler: 'IconUrl',
+ label: translate('Icon symbol'),
+ inheritable: true,
+ // helpText: translate(
+ // 'Symbol can be either a unicode character or an URL. You can use feature properties as variables: ex.: with "http://myserver.org/images/{name}.png", the {name} variable will be replaced by the "name" value of each marker.'
+ // ),
+ },
+ smoothFactor: {
+ type: Number,
+ min: 0,
+ max: 10,
+ step: 0.5,
+ label: translate('Simplify'),
+ helpEntries: 'smoothFactor',
+ inheritable: true,
+ default: 1.0,
+ },
+ iconOpacity: {
+ type: Number,
+ min: 0.1,
+ max: 1,
+ step: 0.1,
+ label: translate('icon opacity'),
+ inheritable: true,
+ default: 1,
+ },
+ opacity: {
+ type: Number,
+ min: 0.1,
+ max: 1,
+ step: 0.1,
+ label: translate('opacity'),
+ inheritable: true,
+ default: 0.5,
+ },
+ weight: {
+ type: Number,
+ min: 1,
+ max: 20,
+ step: 1,
+ label: translate('weight'),
+ inheritable: true,
+ default: 3,
+ },
+ fill: {
+ type: Boolean,
+ label: translate('fill'),
+ helpEntries: 'fill',
+ inheritable: true,
+ default: true,
+ },
+ fillColor: {
+ type: String,
+ handler: 'ColorPicker',
+ label: translate('fill color'),
+ helpEntries: 'fillColor',
+ inheritable: true,
+ },
+ fillOpacity: {
+ type: Number,
+ min: 0.1,
+ max: 1,
+ step: 0.1,
+ label: translate('fill opacity'),
+ inheritable: true,
+ default: 0.3,
+ },
+ dashArray: {
+ type: String,
+ label: translate('dash array'),
+ helpEntries: 'dashArray',
+ inheritable: true,
+ },
+ popupShape: {
+ type: String,
+ label: translate('Popup shape'),
+ inheritable: true,
+ choices: [
+ ['Default', translate('Popup')],
+ ['Large', translate('Popup (large)')],
+ ['Panel', translate('Side panel')],
+ ],
+ default: 'Default',
+ },
+ popupTemplate: {
+ type: String,
+ label: translate('Popup content style'),
+ inheritable: true,
+ choices: [
+ ['Default', translate('Default')],
+ ['Table', translate('Table')],
+ ['GeoRSSImage', translate('GeoRSS (title + image)')],
+ ['GeoRSSLink', translate('GeoRSS (only link)')],
+ ['OSM', translate('OpenStreetMap')],
+ ],
+ default: 'Default',
+ },
+ popupContentTemplate: {
+ type: 'Text',
+ label: translate('Popup content template'),
+ helpEntries: ['dynamicProperties', 'textFormatting'],
+ placeholder: '# {name}',
+ inheritable: true,
+ default: '# {name}\n{description}',
+ },
+ zoomTo: {
+ type: Number,
+ placeholder: translate('Inherit'),
+ helpEntries: 'zoomTo',
+ label: translate('Default zoom level'),
+ inheritable: true,
+ },
+ captionBar: {
+ type: Boolean,
+ label: translate('Do you want to display a caption bar?'),
+ default: false,
+ },
+ captionMenus: {
+ type: Boolean,
+ label: translate('Do you want to display caption menus?'),
+ default: true,
+ },
+ slideshow: {
+ type: Object,
+ },
+ sortKey: {
+ type: String,
+ },
+ labelKey: {
+ type: String,
+ helpEntries: 'labelKey',
+ placeholder: translate('Default: name'),
+ label: translate('Label key'),
+ inheritable: true,
+ },
+ filterKey: {
+ type: String,
+ },
+ facetKey: {
+ type: String,
+ },
+ slugKey: {
+ type: String,
+ },
+ showLabel: {
+ type: Boolean,
+ nullable: true,
+ label: translate('Display label'),
+ inheritable: true,
+ default: false,
+ },
+ labelDirection: {
+ type: String,
+ label: translate('Label direction'),
+ inheritable: true,
+ choices: [
+ ['auto', translate('Automatic')],
+ ['left', translate('On the left')],
+ ['right', translate('On the right')],
+ ['top', translate('On the top')],
+ ['bottom', translate('On the bottom')],
+ ],
+ default: 'auto',
+ },
+ labelInteractive: {
+ type: Boolean,
+ label: translate('Labels are clickable'),
+ inheritable: true,
+ },
+ outlinkTarget: {
+ type: String,
+ label: translate('Open link in…'),
+ inheritable: true,
+ default: 'blank',
+ choices: [
+ ['blank', translate('new window')],
+ ['self', translate('iframe')],
+ ['parent', translate('parent window')],
+ ],
+ },
+ shortCredit: {
+ type: String,
+ label: translate('Short credits'),
+ helpEntries: ['shortCredit', 'textFormatting'],
+ },
+ longCredit: {
+ type: 'Text',
+ label: translate('Long credits'),
+ helpEntries: ['longCredit', 'textFormatting'],
+ },
+ permanentCredit: {
+ type: 'Text',
+ label: translate('Permanent credits'),
+ helpEntries: ['permanentCredit', 'textFormatting'],
+ },
+ permanentCreditBackground: {
+ type: Boolean,
+ label: translate('Permanent credits background'),
+ default: true,
+ },
+ zoomControl: {
+ type: Boolean,
+ nullable: true,
+ label: translate('Display the zoom control'),
+ default: true,
+ },
+ datalayersControl: {
+ type: Boolean,
+ nullable: true,
+ handler: 'DataLayersControl',
+ label: translate('Display the data layers control'),
+ default: true,
+ },
+ searchControl: {
+ type: Boolean,
+ nullable: true,
+ label: translate('Display the search control'),
+ default: true,
+ },
+ locateControl: {
+ type: Boolean,
+ nullable: true,
+ label: translate('Display the locate control'),
+ },
+ fullscreenControl: {
+ type: Boolean,
+ nullable: true,
+ label: translate('Display the fullscreen control'),
+ default: true,
+ },
+ editinosmControl: {
+ type: Boolean,
+ nullable: true,
+ label: translate('Display the control to open OpenStreetMap editor'),
+ default: null,
+ },
+ embedControl: {
+ type: Boolean,
+ nullable: true,
+ label: translate('Display the embed control'),
+ default: true,
+ },
+ measureControl: {
+ type: Boolean,
+ nullable: true,
+ label: translate('Display the measure control'),
+ },
+ tilelayersControl: {
+ type: Boolean,
+ nullable: true,
+ label: translate('Display the tile layers control'),
+ },
+ starControl: {
+ type: Boolean,
+ nullable: true,
+ label: translate('Display the star map button'),
+ },
+ easing: {
+ type: Boolean,
+ default: false,
+ },
+ interactive: {
+ type: Boolean,
+ label: translate('Allow interactions'),
+ helpEntries: 'interactive',
+ inheritable: true,
+ default: true,
+ },
+ fromZoom: {
+ type: Number,
+ label: translate('From zoom'),
+ helpText: translate('Optional.'),
+ },
+ toZoom: {
+ type: Number,
+ label: translate('To zoom'),
+ helpText: translate('Optional.'),
+ },
+ stroke: {
+ type: Boolean,
+ label: translate('stroke'),
+ helpEntries: 'stroke',
+ inheritable: true,
+ default: true,
+ },
+ outlink: {
+ label: translate('Link to…'),
+ helpEntries: 'outlink',
+ placeholder: 'http://...',
+ inheritable: true,
+ },
+}
diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js
index b76daccc..cfbd78bf 100644
--- a/umap/static/umap/js/umap.controls.js
+++ b/umap/static/umap/js/umap.controls.js
@@ -1184,25 +1184,22 @@ U.AttributionControl = L.Control.Attribution.extend({
this._container,
credits
)
- if (this._map.options.shortCredit) {
- L.DomUtil.add(
- 'span',
- '',
- container,
- ` — ${L.Util.toHTML(this._map.options.shortCredit)}`
- )
+ const shortCredit = this._map.getOption('shortCredit'),
+ captionMenus = this._map.getOption('captionMenus')
+ if (shortCredit) {
+ L.DomUtil.add('span', '', container, ` — ${L.Util.toHTML(shortCredit)}`)
}
- if (this._map.options.captionMenus) {
+ if (captionMenus) {
const link = L.DomUtil.add('a', '', container, ` — ${L._('About')}`)
L.DomEvent.on(link, 'click', L.DomEvent.stop)
.on(link, 'click', this._map.displayCaption, this._map)
.on(link, 'dblclick', L.DomEvent.stop)
}
- if (window.top === window.self && this._map.options.captionMenus) {
+ if (window.top === window.self && captionMenus) {
// We are not in iframe mode
L.DomUtil.createLink('', container, ` — ${L._('Home')}`, '/')
}
- if (this._map.options.captionMenus) {
+ if (captionMenus) {
L.DomUtil.createLink(
'',
container,
diff --git a/umap/static/umap/js/umap.core.js b/umap/static/umap/js/umap.core.js
index 1dcda4b2..a7cabbc3 100644
--- a/umap/static/umap/js/umap.core.js
+++ b/umap/static/umap/js/umap.core.js
@@ -415,7 +415,10 @@ L.DomUtil.TextColorFromBackgroundColor = (el, bgcolor) => {
L.DomUtil.contrastWCAG21 = (rgb) => {
const [r, g, b] = rgb
// luminance of inputted colour
- const lum = 0.2126 * L.DomUtil.colourMod(r) + 0.7152 * L.DomUtil.colourMod(g) + 0.0722 * L.DomUtil.colourMod(b)
+ const lum =
+ 0.2126 * L.DomUtil.colourMod(r) +
+ 0.7152 * L.DomUtil.colourMod(g) +
+ 0.0722 * L.DomUtil.colourMod(b)
// white has a luminance of 1
const whiteLum = 1
const contrast = (whiteLum + 0.05) / (lum + 0.05)
@@ -729,9 +732,6 @@ U.Help = L.Class.extend({
formatURL: `${L._(
'Supported variables that will be dynamically replaced'
)}: {bbox}, {lat}, {lng}, {zoom}, {east}, {north}..., {left}, {top}..., locale, lang`,
- formatIconSymbol: L._(
- 'Symbol can be either a unicode character or an URL. You can use feature properties as variables: ex.: with "http://myserver.org/images/{name}.png", the {name} variable will be replaced by the "name" value of each marker.'
- ),
colorValue: L._('Must be a valid CSS value (eg.: DarkBlue or #123456)'),
smoothFactor: L._(
'How much to simplify the polyline on each zoom level (more = better performance and smoother look, less = more accurate)'
@@ -741,7 +741,7 @@ U.Help = L.Class.extend({
),
zoomTo: L._('Zoom level for automatic zooms'),
labelKey: L._(
- 'The name of the property to use as feature label (eg.: "nom"). You can also use properties inside brackets to use more than one or mix with static content (eg.: "{name} in {place}")'
+ 'The name of the property to use as feature label (eg.: "nom"). You can also use properties inside brackets to use more than one or mix with static content (eg.: "{name} in {place}")'
),
stroke: L._('Whether to display or not polygons paths.'),
fill: L._('Whether to fill polygons with color.'),
diff --git a/umap/static/umap/js/umap.features.js b/umap/static/umap/js/umap.features.js
index 57caecce..94958370 100644
--- a/umap/static/umap/js/umap.features.js
+++ b/umap/static/umap/js/umap.features.js
@@ -53,7 +53,7 @@ U.FeatureMixin = {
},
getSlug: function () {
- return this.properties[this.map.options.slugKey || 'name'] || ''
+ return this.properties[this.map.getOption('slugKey') || 'name'] || ''
},
getPermalink: function () {
@@ -103,11 +103,15 @@ U.FeatureMixin = {
L._('Feature properties')
)
- let builder = new U.FormBuilder(this, ['datalayer'], {
- callback: function () {
- this.edit(e)
- }, // removeLayer step will close the edit panel, let's reopen it
- })
+ let builder = new U.FormBuilder(
+ this,
+ [['datalayer', { handler: 'DataLayerSwitcher' }]],
+ {
+ callback: function () {
+ this.edit(e)
+ }, // removeLayer step will close the edit panel, let's reopen it
+ }
+ )
container.appendChild(builder.build())
const properties = []
@@ -209,7 +213,7 @@ U.FeatureMixin = {
if (L.Browser.ielt9) return false
if (this.datalayer.isRemoteLayer() && this.datalayer.options.remoteData.dynamic)
return false
- return this.map.options.displayPopupFooter
+ return this.map.getOption('displayPopupFooter')
},
getPopupClass: function () {
@@ -309,7 +313,7 @@ U.FeatureMixin = {
zoomTo: function (e) {
e = e || {}
- const easing = e.easing !== undefined ? e.easing : this.map.options.easing
+ const easing = e.easing !== undefined ? e.easing : this.map.getOption('easing')
if (easing) {
this.map.flyTo(this.getCenter(), this.getBestZoom())
} else {
@@ -975,7 +979,7 @@ U.PathMixin = {
zoomTo: function (e) {
// Use bounds instead of centroid for paths.
e = e || {}
- const easing = e.easing !== undefined ? e.easing : this.map.options.easing
+ const easing = e.easing !== undefined ? e.easing : this.map.getOption('easing')
if (easing) {
this.map.flyToBounds(this.getBounds(), this.getBestZoom())
} else {
diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js
index 2d28a026..5031e152 100644
--- a/umap/static/umap/js/umap.forms.js
+++ b/umap/static/umap/js/umap.forms.js
@@ -340,15 +340,6 @@ L.FormBuilder.TextColorPicker = L.FormBuilder.ColorPicker.extend({
],
})
-L.FormBuilder.IconClassSwitcher = L.FormBuilder.Select.extend({
- selectOptions: [
- ['Default', L._('Default')],
- ['Circle', L._('Circle')],
- ['Drop', L._('Drop')],
- ['Ball', L._('Ball')],
- ],
-})
-
L.FormBuilder.ProxyTTLSelect = L.FormBuilder.Select.extend({
selectOptions: [
[undefined, L._('No cache')],
@@ -358,24 +349,6 @@ L.FormBuilder.ProxyTTLSelect = L.FormBuilder.Select.extend({
],
})
-L.FormBuilder.PopupShape = L.FormBuilder.Select.extend({
- selectOptions: [
- ['Default', L._('Popup')],
- ['Large', L._('Popup (large)')],
- ['Panel', L._('Side panel')],
- ],
-})
-
-L.FormBuilder.PopupContent = L.FormBuilder.Select.extend({
- selectOptions: [
- ['Default', L._('Default')],
- ['Table', L._('Table')],
- ['GeoRSSImage', L._('GeoRSS (title + image)')],
- ['GeoRSSLink', L._('GeoRSS (only link)')],
- ['OSM', L._('OpenStreetMap')],
- ],
-})
-
L.FormBuilder.LayerTypeChooser = L.FormBuilder.Select.extend({
getOptions: function () {
const layer_classes = [
@@ -427,24 +400,6 @@ L.FormBuilder.DataLayerSwitcher = L.FormBuilder.Select.extend({
},
})
-L.FormBuilder.DefaultView = L.FormBuilder.Select.extend({
- selectOptions: [
- ['center', L._('Saved center and zoom')],
- ['data', L._('Fit all data')],
- ['latest', L._('Latest feature')],
- ['locate', L._('User location')],
- ],
-})
-
-L.FormBuilder.OnLoadPanel = L.FormBuilder.Select.extend({
- selectOptions: [
- ['none', L._('None')],
- ['caption', L._('Caption')],
- ['databrowser', L._('Data browser')],
- ['facet', L._('Facet search')],
- ],
-})
-
L.FormBuilder.DataFormat = L.FormBuilder.Select.extend({
selectOptions: [
[undefined, L._('Choose the data format')],
@@ -457,16 +412,6 @@ L.FormBuilder.DataFormat = L.FormBuilder.Select.extend({
],
})
-L.FormBuilder.LabelDirection = L.FormBuilder.Select.extend({
- selectOptions: [
- ['auto', L._('Automatic')],
- ['left', L._('On the left')],
- ['right', L._('On the right')],
- ['top', L._('On the top')],
- ['bottom', L._('On the bottom')],
- ],
-})
-
L.FormBuilder.LicenceChooser = L.FormBuilder.Select.extend({
getOptions: function () {
const licences = []
@@ -708,7 +653,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({
},
isDefault: function () {
- return !this.value() || this.value() === U.DEFAULT_ICON_URL
+ return !this.value() || this.value() === U.SCHEMA.iconUrl.default
},
addGrid: function (onSearch) {
@@ -905,7 +850,7 @@ L.FormBuilder.TernaryChoices = L.FormBuilder.MultiChoice.extend({
},
})
-L.FormBuilder.ControlChoice = L.FormBuilder.TernaryChoices.extend({
+L.FormBuilder.NullableChoices = L.FormBuilder.TernaryChoices.extend({
choices: [
[true, L._('always')],
[false, L._('never')],
@@ -913,17 +858,7 @@ L.FormBuilder.ControlChoice = L.FormBuilder.TernaryChoices.extend({
],
})
-L.FormBuilder.LabelChoice = L.FormBuilder.TernaryChoices.extend({
- default: false,
-
- choices: [
- [true, L._('always')],
- [false, L._('never')],
- ['null', L._('on hover')],
- ],
-})
-
-L.FormBuilder.DataLayersControl = L.FormBuilder.ControlChoice.extend({
+L.FormBuilder.DataLayersControl = L.FormBuilder.TernaryChoices.extend({
choices: [
[true, L._('collapsed')],
['expanded', L._('expanded')],
@@ -934,21 +869,11 @@ L.FormBuilder.DataLayersControl = L.FormBuilder.ControlChoice.extend({
toJS: function () {
let value = this.value()
if (value !== 'expanded')
- value = L.FormBuilder.ControlChoice.prototype.toJS.call(this)
+ value = L.FormBuilder.TernaryChoices.prototype.toJS.call(this)
return value
},
})
-L.FormBuilder.OutlinkTarget = L.FormBuilder.MultiChoice.extend({
- default: 'blank',
-
- choices: [
- ['blank', L._('new window')],
- ['self', L._('iframe')],
- ['parent', L._('parent window')],
- ],
-})
-
L.FormBuilder.Range = L.FormBuilder.FloatInput.extend({
type: function () {
return 'range'
@@ -1052,230 +977,55 @@ U.FormBuilder = L.FormBuilder.extend({
className: 'umap-form',
},
- defaultOptions: {
- name: { label: L._('name') },
- description: {
- label: L._('description'),
- handler: 'Textarea',
- helpEntries: 'textFormatting',
- },
- color: {
- handler: 'ColorPicker',
- label: L._('color'),
- helpEntries: 'colorValue',
- inheritable: true,
- },
- iconOpacity: {
- handler: 'Range',
- min: 0.1,
- max: 1,
- step: 0.1,
- label: L._('icon opacity'),
- inheritable: true,
- },
- opacity: {
- handler: 'Range',
- min: 0.1,
- max: 1,
- step: 0.1,
- label: L._('opacity'),
- inheritable: true,
- },
- stroke: {
- handler: 'Switch',
- label: L._('stroke'),
- helpEntries: 'stroke',
- inheritable: true,
- },
- weight: {
- handler: 'Range',
- min: 1,
- max: 20,
- step: 1,
- label: L._('weight'),
- inheritable: true,
- },
- fill: {
- handler: 'Switch',
- label: L._('fill'),
- helpEntries: 'fill',
- inheritable: true,
- },
- fillColor: {
- handler: 'ColorPicker',
- label: L._('fill color'),
- helpEntries: 'fillColor',
- inheritable: true,
- },
- fillOpacity: {
- handler: 'Range',
- min: 0.1,
- max: 1,
- step: 0.1,
- label: L._('fill opacity'),
- inheritable: true,
- },
- smoothFactor: {
- handler: 'Range',
- min: 0,
- max: 10,
- step: 0.5,
- label: L._('Simplify'),
- helpEntries: 'smoothFactor',
- inheritable: true,
- },
- dashArray: {
- label: L._('dash array'),
- helpEntries: 'dashArray',
- inheritable: true,
- },
- iconClass: {
- handler: 'IconClassSwitcher',
- label: L._('Icon shape'),
- inheritable: true,
- },
- iconUrl: {
- handler: 'IconUrl',
- label: L._('Icon symbol'),
- inheritable: true,
- helpText: U.Help.formatIconSymbol,
- },
- popupShape: { handler: 'PopupShape', label: L._('Popup shape'), inheritable: true },
- popupTemplate: {
- handler: 'PopupContent',
- label: L._('Popup content style'),
- inheritable: true,
- },
- popupContentTemplate: {
- label: L._('Popup content template'),
- handler: 'Textarea',
- helpEntries: ['dynamicProperties', 'textFormatting'],
- placeholder: '# {name}',
- inheritable: true,
- },
- datalayer: {
- handler: 'DataLayerSwitcher',
- label: L._('Choose the layer of the feature'),
- },
- moreControl: {
- handler: 'Switch',
- label: L._('Do you want to display the «more» control?'),
- },
- scrollWheelZoom: { handler: 'Switch', label: L._('Allow scroll wheel zoom?') },
- miniMap: { handler: 'Switch', label: L._('Do you want to display a minimap?') },
- scaleControl: {
- handler: 'Switch',
- label: L._('Do you want to display the scale control?'),
- },
- onLoadPanel: {
- handler: 'OnLoadPanel',
- label: L._('Do you want to display a panel on load?'),
- },
- defaultView: {
- handler: 'DefaultView',
- label: L._('Default view'),
- },
- displayPopupFooter: {
- handler: 'Switch',
- label: L._('Do you want to display popup footer?'),
- },
- captionBar: {
- handler: 'Switch',
- label: L._('Do you want to display a caption bar?'),
- },
- captionMenus: {
- handler: 'Switch',
- label: L._('Do you want to display caption menus?'),
- },
- zoomTo: {
- handler: 'IntInput',
- placeholder: L._('Inherit'),
- helpEntries: 'zoomTo',
- label: L._('Default zoom level'),
- inheritable: true,
- },
- showLabel: {
- handler: 'LabelChoice',
- label: L._('Display label'),
- inheritable: true,
- },
- labelDirection: {
- handler: 'LabelDirection',
- label: L._('Label direction'),
- inheritable: true,
- },
- labelInteractive: {
- handler: 'Switch',
- label: L._('Labels are clickable'),
- inheritable: true,
- },
- outlink: {
- label: L._('Link to…'),
- helpEntries: 'outlink',
- placeholder: 'http://...',
- inheritable: true,
- },
- outlinkTarget: {
- handler: 'OutlinkTarget',
- label: L._('Open link in…'),
- inheritable: true,
- },
- labelKey: {
- helpEntries: 'labelKey',
- placeholder: L._('Default: name'),
- label: L._('Label key'),
- inheritable: true,
- },
- zoomControl: { handler: 'ControlChoice', label: L._('Display the zoom control') },
- searchControl: {
- handler: 'ControlChoice',
- label: L._('Display the search control'),
- },
- fullscreenControl: {
- handler: 'ControlChoice',
- label: L._('Display the fullscreen control'),
- },
- embedControl: { handler: 'ControlChoice', label: L._('Display the embed control') },
- locateControl: {
- handler: 'ControlChoice',
- label: L._('Display the locate control'),
- },
- measureControl: {
- handler: 'ControlChoice',
- label: L._('Display the measure control'),
- },
- tilelayersControl: {
- handler: 'ControlChoice',
- label: L._('Display the tile layers control'),
- },
- editinosmControl: {
- handler: 'ControlChoice',
- label: L._('Display the control to open OpenStreetMap editor'),
- },
- datalayersControl: {
- handler: 'DataLayersControl',
- label: L._('Display the data layers control'),
- },
- starControl: {
- handler: 'ControlChoice',
- label: L._('Display the star map button'),
- },
- fromZoom: {
- handler: 'IntInput',
- label: L._('From zoom'),
- helpText: L._('Optional.'),
- },
- toZoom: { handler: 'IntInput', label: L._('To zoom'), helpText: L._('Optional.') },
- interactive: {
- handler: 'Switch',
- label: L._('Allow interactions'),
- helpEntries: 'interactive',
- inheritable: true,
- },
+ computeDefaultOptions: function () {
+ for (let [key, schema] of Object.entries(U.SCHEMA)) {
+ if (schema.type === Boolean) {
+ if (schema.nullable) schema.handler = 'NullableChoices'
+ else schema.handler = 'Switch'
+ } else if (schema.type === 'Text') {
+ schema.handler = 'Textarea'
+ } else if (schema.type === Number) {
+ if (schema.step) schema.handler = 'Range'
+ else schema.handler = 'IntInput'
+ } else if (schema.choices) {
+ const text_length = schema.choices.reduce(
+ (acc, [value, label]) => acc + label.length,
+ 0
+ )
+ // Try to be smart and use MultiChoice only
+ // for choices where labels are shorts…
+ if (text_length < 40) {
+ schema.handler = 'MultiChoice'
+ } else {
+ schema.handler = 'Select'
+ schema.selectOptions = schema.choices
+ }
+ } else {
+ switch (key) {
+ case 'color':
+ case 'fillColor':
+ schema.handler = 'ColorPicker'
+ break
+ case 'iconUrl':
+ schema.handler = 'IconUrl'
+ break
+ case 'datalayersControl':
+ schema.handler = 'DataLayersControl'
+ break
+ case 'licence':
+ schema.handler = 'LicenceChooser'
+ break
+ }
+ }
+ // FormBuilder use this key for the input type itself
+ delete schema.type
+ this.defaultOptions[key] = schema
+ }
},
initialize: function (obj, fields, options) {
this.map = obj.map || obj.getMap()
+ this.computeDefaultOptions()
L.FormBuilder.prototype.initialize.call(this, obj, fields, options)
this.on('finish', this.finish)
},
diff --git a/umap/static/umap/js/umap.icon.js b/umap/static/umap/js/umap.icon.js
index 5a0189b3..7eae6575 100644
--- a/umap/static/umap/js/umap.icon.js
+++ b/umap/static/umap/js/umap.icon.js
@@ -19,7 +19,7 @@ U.Icon = L.DivIcon.extend({
_setRecent: function (url) {
if (L.Util.hasVar(url)) return
- if (url === U.DEFAULT_ICON_URL) return
+ if (url === U.SCHEMA.iconUrl.default) return
if (U.Icon.RECENT.indexOf(url) === -1) {
U.Icon.RECENT.push(url)
}
@@ -236,7 +236,7 @@ U.Icon.setIconContrast = function (icon, parent, src, bgcolor) {
if (L.DomUtil.contrastedColor(parent, bgcolor)) {
// Decide whether to switch svg to white or not, but do it
// only for internal SVG, as invert could do weird things
- if (L.Util.isPath(src) && src.endsWith('.svg') && src !== U.DEFAULT_ICON_URL) {
+ if (L.Util.isPath(src) && src.endsWith('.svg') && src !== U.SCHEMA.iconUrl.default) {
// Must be called after icon container is added to the DOM
// An image
icon.style.filter = 'invert(1)'
diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js
index bc73784b..b4df88cf 100644
--- a/umap/static/umap/js/umap.js
+++ b/umap/static/umap/js/umap.js
@@ -2,33 +2,12 @@ L.Map.mergeOptions({
overlay: null,
datalayers: [],
hash: true,
- default_color: 'DarkBlue',
- default_smoothFactor: 1.0,
- default_opacity: 0.5,
- default_fillOpacity: 0.3,
- default_stroke: true,
- default_fill: true,
- default_weight: 3,
- default_iconOpacity: 1,
- default_iconClass: 'Default',
- default_popupContentTemplate: '# {name}\n{description}',
- default_interactive: true,
- default_labelDirection: 'auto',
maxZoomLimit: 24,
attributionControl: false,
editMode: 'advanced',
- embedControl: true,
- zoomControl: true,
- datalayersControl: true,
- searchControl: true,
- editInOSMControl: false,
- editInOSMControlOptions: false,
- scaleControl: true,
noControl: false, // Do not render any control.
- miniMap: false,
name: '',
description: '',
- displayPopupFooter: false,
// When a TileLayer is in TMS mode, it needs -y instead of y.
// This is usually handled by the TileLayer instance itself, but
// we cannot rely on this because of the y is overriden by Leaflet
@@ -44,77 +23,14 @@ L.Map.mergeOptions({
importPresets: [
// {url: 'http://localhost:8019/en/datalayer/1502/', label: 'Simplified World Countries', format: 'geojson'}
],
- moreControl: true,
- captionBar: false,
- captionMenus: true,
slideshow: {},
clickable: true,
- easing: false,
permissions: {},
- permanentCreditBackground: true,
featuresHaveOwner: false,
})
U.Map = L.Map.extend({
includes: [ControlsMixin],
- editableOptions: {
- zoom: undefined,
- scrollWheelZoom: Boolean,
- scaleControl: Boolean,
- moreControl: Boolean,
- miniMap: Boolean,
- displayPopupFooter: undefined,
- onLoadPanel: String,
- defaultView: String,
- name: String,
- description: String,
- licence: undefined,
- tilelayer: undefined,
- overlay: undefined,
- limitBounds: undefined,
- color: String,
- iconClass: String,
- iconUrl: String,
- smoothFactor: undefined,
- iconOpacity: undefined,
- opacity: undefined,
- weight: undefined,
- fill: undefined,
- fillColor: undefined,
- fillOpacity: undefined,
- dashArray: undefined,
- popupShape: String,
- popupTemplate: String,
- popupContentTemplate: String,
- zoomTo: Number,
- captionBar: Boolean,
- captionMenus: Boolean,
- slideshow: undefined,
- sortKey: undefined,
- labelKey: String,
- filterKey: undefined,
- facetKey: undefined,
- slugKey: undefined,
- showLabel: 'NullableBoolean',
- labelDirection: undefined,
- labelInteractive: undefined,
- outlinkTarget: undefined,
- shortCredit: undefined,
- longCredit: undefined,
- permanentCredit: undefined,
- permanentCreditBackground: undefined,
- zoomControl: 'NullableBoolean',
- datalayersControl: 'NullableBoolean',
- searchControl: 'NullableBoolean',
- locateControl: 'NullableBoolean',
- fullscreenControl: 'NullableBoolean',
- editinosmControl: 'NullableBoolean',
- embedControl: 'NullableBoolean',
- measureControl: 'NullableBoolean',
- tilelayersControl: 'NullableBoolean',
- starControl: 'NullableBoolean',
- easing: undefined,
- },
initialize: function (el, geojson) {
// Locale name (pt_PT, en_US…)
@@ -133,7 +49,8 @@ U.Map = L.Map.extend({
geojson.properties.fullscreenControl = false
L.Map.prototype.initialize.call(this, el, geojson.properties)
- U.DEFAULT_ICON_URL = this.options.default_iconUrl
+
+ if (geojson.properties.schema) this.overrideSchema(geojson.properties.schema)
// After calling parent initialize, as we are doing initCenter our-selves
if (geojson.geometry) this.options.center = this.latLng(geojson.geometry)
@@ -329,13 +246,11 @@ U.Map = L.Map.extend({
// FIXME retrocompat
L.Util.setBooleanFromQueryString(options, 'displayDataBrowserOnLoad')
L.Util.setBooleanFromQueryString(options, 'displayCaptionOnLoad')
- for (const [key, type] of Object.entries(this.editableOptions)) {
- switch (type) {
+ for (const [key, schema] of Object.entries(U.SCHEMA)) {
+ switch (schema.type) {
case Boolean:
- L.Util.setBooleanFromQueryString(options, key)
- break
- case 'NullableBoolean':
- L.Util.setNullableBooleanFromQueryString(options, key)
+ if (schema.nullable) L.Util.setNullableBooleanFromQueryString(options, key)
+ else L.Util.setBooleanFromQueryString(options, key)
break
case Number:
L.Util.setNumberFromQueryString(options, key)
@@ -352,6 +267,12 @@ U.Map = L.Map.extend({
}
},
+ overrideSchema: function (schema) {
+ for (const [key, extra] of Object.entries(schema)) {
+ U.SCHEMA[key] = L.extend({}, U.SCHEMA[key], extra)
+ }
+ },
+
initControls: function () {
this.helpMenuActions = {}
this._controls = {}
@@ -393,7 +314,7 @@ U.Map = L.Map.extend({
title: { false: L._('View Fullscreen'), true: L._('Exit Fullscreen') },
})
this._controls.search = new U.SearchControl()
- this._controls.embed = new L.Control.Embed(this, this.options.embedOptions)
+ this._controls.embed = new L.Control.Embed(this)
this._controls.tilelayersChooser = new U.TileLayerChooser(this)
if (this.options.user) this._controls.star = new U.StarControl(this)
this._controls.editinosm = new L.Control.EditInOSM({
@@ -459,7 +380,7 @@ U.Map = L.Map.extend({
let name, status, control
for (let i = 0; i < this.HIDDABLE_CONTROLS.length; i++) {
name = this.HIDDABLE_CONTROLS[i]
- status = this.options[`${name}Control`]
+ status = this.getOption(`${name}Control`)
if (status === false) continue
control = this._controls[name]
if (!control) continue
@@ -468,9 +389,9 @@ U.Map = L.Map.extend({
L.DomUtil.addClass(control._container, 'display-on-more')
else L.DomUtil.removeClass(control._container, 'display-on-more')
}
- if (this.options.permanentCredit) this._controls.permanentCredit.addTo(this)
- if (this.options.moreControl) this._controls.more.addTo(this)
- if (this.options.scaleControl) this._controls.scale.addTo(this)
+ if (this.getOption('permanentCredit')) this._controls.permanentCredit.addTo(this)
+ if (this.getOption('moreControl')) this._controls.more.addTo(this)
+ if (this.getOption('scaleControl')) this._controls.scale.addTo(this)
},
initDataLayers: async function (datalayers) {
@@ -820,7 +741,7 @@ U.Map = L.Map.extend({
},
getDefaultOption: function (option) {
- return this.options[`default_${option}`]
+ return U.SCHEMA[option] && U.SCHEMA[option].default
},
getOption: function (option) {
@@ -904,7 +825,7 @@ U.Map = L.Map.extend({
let mustReindex = false
- for (const option of Object.keys(this.editableOptions)) {
+ for (const option of Object.keys(U.SCHEMA)) {
if (typeof importedData.properties[option] !== 'undefined') {
this.options[option] = importedData.properties[option]
if (option === 'sortKey') mustReindex = true
@@ -1033,7 +954,7 @@ U.Map = L.Map.extend({
exportOptions: function () {
const properties = {}
- for (const option of Object.keys(this.editableOptions)) {
+ for (const option of Object.keys(U.SCHEMA)) {
if (typeof this.options[option] !== 'undefined') {
properties[option] = this.options[option]
}
@@ -1555,35 +1476,11 @@ U.Map = L.Map.extend({
_editCredits: function (container) {
const credits = L.DomUtil.createFieldset(container, L._('Credits'))
const creditsFields = [
- ['options.licence', { handler: 'LicenceChooser', label: L._('licence') }],
- [
- 'options.shortCredit',
- {
- handler: 'Input',
- label: L._('Short credits'),
- helpEntries: ['shortCredit', 'textFormatting'],
- },
- ],
- [
- 'options.longCredit',
- {
- handler: 'Textarea',
- label: L._('Long credits'),
- helpEntries: ['longCredit', 'textFormatting'],
- },
- ],
- [
- 'options.permanentCredit',
- {
- handler: 'Textarea',
- label: L._('Permanent credits'),
- helpEntries: ['permanentCredit', 'textFormatting'],
- },
- ],
- [
- 'options.permanentCreditBackground',
- { handler: 'Switch', label: L._('Permanent credits background') },
- ],
+ 'options.licence',
+ 'options.shortCredit',
+ 'options.longCredit',
+ 'options.permanentCredit',
+ 'options.permanentCreditBackground',
]
const creditsBuilder = new U.FormBuilder(this, creditsFields, {
callback: this.renderControls,
@@ -1691,7 +1588,7 @@ U.Map = L.Map.extend({
name = L.DomUtil.create('h3', '', container)
L.DomEvent.disableClickPropagation(container)
this.permissions.addOwnerLink('span', container)
- if (this.options.captionMenus) {
+ if (this.getOption('captionMenus')) {
L.DomUtil.createButton(
'umap-about-link flat',
container,
diff --git a/umap/static/umap/js/umap.share.js b/umap/static/umap/js/umap.share.js
index 167f73d1..ad7c7b14 100644
--- a/umap/static/umap/js/umap.share.js
+++ b/umap/static/umap/js/umap.share.js
@@ -215,7 +215,7 @@ U.IframeExporter = L.Evented.extend({
this.map = map
this.baseUrl = L.Util.getBaseUrl()
// Use map default, not generic default
- this.queryString.onLoadPanel = this.map.options.onLoadPanel
+ this.queryString.onLoadPanel = this.map.getOption('onLoadPanel')
},
getMap: function () {
diff --git a/umap/static/umap/test/index.html b/umap/static/umap/test/index.html
index 9d6da7d4..e01794a3 100644
--- a/umap/static/umap/test/index.html
+++ b/umap/static/umap/test/index.html
@@ -8,7 +8,6 @@
-
diff --git a/umap/templates/umap/js.html b/umap/templates/umap/js.html
index 95e4a675..86ea502e 100644
--- a/umap/templates/umap/js.html
+++ b/umap/templates/umap/js.html
@@ -2,11 +2,16 @@
+
+{% if locale %}
+ {% with "umap/locale/"|add:locale|add:".js" as path %}
+
+ {% endwith %}
+{% endif %}
-
-{% if locale %}
- {% with "umap/locale/"|add:locale|add:".js" as path %}
-
- {% endwith %}
-{% endif %}
+
diff --git a/umap/tests/integration/test_owned_map.py b/umap/tests/integration/test_owned_map.py
index f6b84584..6316940b 100644
--- a/umap/tests/integration/test_owned_map.py
+++ b/umap/tests/integration/test_owned_map.py
@@ -210,7 +210,8 @@ def test_can_change_owner(map, live_server, login, user):
close = page.locator(".umap-field-owner .close")
close.click()
input = page.locator("input.edit-owner")
- input.type(user.username)
+ with page.expect_response(re.compile(r".*/agnocomplete/.*")):
+ input.type(user.username)
input.press("Tab")
save = page.get_by_role("button", name="Save")
expect(save).to_be_visible()
diff --git a/umap/views.py b/umap/views.py
index be6a900a..05187268 100644
--- a/umap/views.py
+++ b/umap/views.py
@@ -495,7 +495,7 @@ class MapDetailMixin:
"urls": _urls_for_js(),
"tilelayers": TileLayer.get_list(),
"editMode": self.edit_mode,
- "default_iconUrl": "%sumap/img/marker.svg" % settings.STATIC_URL, # noqa
+ "schema": Map.extra_schema,
"umap_id": self.get_umap_id(),
"starred": self.is_starred(),
"licences": dict((l.name, l.json) for l in Licence.objects.all()),