Merge pull request #1661 from umap-project/schema-i18n

Add minimal schema module
This commit is contained in:
Yohan Boniface 2024-03-05 17:57:40 +01:00 committed by GitHub
commit 6396ee5e58
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 557 additions and 470 deletions

View file

@ -10,6 +10,7 @@ from django.core.files.base import File
from django.core.signing import Signer from django.core.signing import Signer
from django.template.defaultfilters import slugify from django.template.defaultfilters import slugify
from django.urls import reverse from django.urls import reverse
from django.utils.functional import classproperty
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .managers import PublicManager from .managers import PublicManager
@ -218,7 +219,7 @@ class Map(NamedModel):
"umap_id": self.pk, "umap_id": self.pk,
"onLoadPanel": "none", "onLoadPanel": "none",
"captionBar": False, "captionBar": False,
"default_iconUrl": "%sumap/img/marker.svg" % settings.STATIC_URL, "schema": self.extra_schema,
"slideshow": {}, "slideshow": {},
} }
) )
@ -329,6 +330,14 @@ class Map(NamedModel):
datalayer.clone(map_inst=new) datalayer.clone(map_inst=new)
return new return new
@classproperty
def extra_schema(self):
return {
"iconUrl": {
"default": "%sumap/img/marker.svg" % settings.STATIC_URL,
}
}
class Pictogram(NamedModel): class Pictogram(NamedModel):
""" """

View file

@ -30,7 +30,7 @@ export default class Browser {
title.textContent = feature.getDisplayName() || '—' title.textContent = feature.getDisplayName() || '—'
const bgcolor = feature.getDynamicOption('color') const bgcolor = feature.getDynamicOption('color')
colorBox.style.backgroundColor = bgcolor 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) const icon = U.Icon.makeIconElement(symbol, colorBox)
U.Icon.setIconContrast(icon, colorBox, symbol, bgcolor) U.Icon.setIconContrast(icon, colorBox, symbol, bgcolor)
} }
@ -131,7 +131,7 @@ export default class Browser {
'h3', 'h3',
'umap-browse-title', 'umap-browse-title',
container, container,
this.map.options.name this.map.getOption('name')
) )
const formContainer = DomUtil.create('div', '', container) const formContainer = DomUtil.create('div', '', container)

View file

@ -1,14 +1,12 @@
import * as L from '../../vendors/leaflet/leaflet-src.esm.js'
import URLs from './urls.js' import URLs from './urls.js'
import Browser from './browser.js' import Browser from './browser.js'
import * as Utils from './utils.js' import * as Utils from './utils.js'
import {SCHEMA} from './schema.js'
import { Request, ServerRequest, RequestError, HTTPError, NOKError } from './request.js' import { Request, ServerRequest, RequestError, HTTPError, NOKError } from './request.js'
// Import modules and export them to the global scope. // Import modules and export them to the global scope.
// For the not yet module-compatible JS out there. // 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 = { window.U = {
URLs, URLs,
Request, Request,
@ -18,4 +16,5 @@ window.U = {
NOKError, NOKError,
Browser, Browser,
Utils, Utils,
SCHEMA,
} }

View file

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

View file

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

View file

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

View file

@ -1184,25 +1184,22 @@ U.AttributionControl = L.Control.Attribution.extend({
this._container, this._container,
credits credits
) )
if (this._map.options.shortCredit) { const shortCredit = this._map.getOption('shortCredit'),
L.DomUtil.add( captionMenus = this._map.getOption('captionMenus')
'span', if (shortCredit) {
'', L.DomUtil.add('span', '', container, `${L.Util.toHTML(shortCredit)}`)
container,
`${L.Util.toHTML(this._map.options.shortCredit)}`
)
} }
if (this._map.options.captionMenus) { if (captionMenus) {
const link = L.DomUtil.add('a', '', container, `${L._('About')}`) const link = L.DomUtil.add('a', '', container, `${L._('About')}`)
L.DomEvent.on(link, 'click', L.DomEvent.stop) L.DomEvent.on(link, 'click', L.DomEvent.stop)
.on(link, 'click', this._map.displayCaption, this._map) .on(link, 'click', this._map.displayCaption, this._map)
.on(link, 'dblclick', L.DomEvent.stop) .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 // We are not in iframe mode
L.DomUtil.createLink('', container, `${L._('Home')}`, '/') L.DomUtil.createLink('', container, `${L._('Home')}`, '/')
} }
if (this._map.options.captionMenus) { if (captionMenus) {
L.DomUtil.createLink( L.DomUtil.createLink(
'', '',
container, container,

View file

@ -415,7 +415,10 @@ L.DomUtil.TextColorFromBackgroundColor = (el, bgcolor) => {
L.DomUtil.contrastWCAG21 = (rgb) => { L.DomUtil.contrastWCAG21 = (rgb) => {
const [r, g, b] = rgb const [r, g, b] = rgb
// luminance of inputted colour // 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 // white has a luminance of 1
const whiteLum = 1 const whiteLum = 1
const contrast = (whiteLum + 0.05) / (lum + 0.05) const contrast = (whiteLum + 0.05) / (lum + 0.05)
@ -729,9 +732,6 @@ U.Help = L.Class.extend({
formatURL: `${L._( formatURL: `${L._(
'Supported variables that will be dynamically replaced' 'Supported variables that will be dynamically replaced'
)}: {bbox}, {lat}, {lng}, {zoom}, {east}, {north}..., {left}, {top}..., locale, lang`, )}: {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)'), colorValue: L._('Must be a valid CSS value (eg.: DarkBlue or #123456)'),
smoothFactor: L._( smoothFactor: L._(
'How much to simplify the polyline on each zoom level (more = better performance and smoother look, less = more accurate)' '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'), zoomTo: L._('Zoom level for automatic zooms'),
labelKey: L._( 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.'), stroke: L._('Whether to display or not polygons paths.'),
fill: L._('Whether to fill polygons with color.'), fill: L._('Whether to fill polygons with color.'),

View file

@ -53,7 +53,7 @@ U.FeatureMixin = {
}, },
getSlug: function () { getSlug: function () {
return this.properties[this.map.options.slugKey || 'name'] || '' return this.properties[this.map.getOption('slugKey') || 'name'] || ''
}, },
getPermalink: function () { getPermalink: function () {
@ -103,11 +103,15 @@ U.FeatureMixin = {
L._('Feature properties') L._('Feature properties')
) )
let builder = new U.FormBuilder(this, ['datalayer'], { let builder = new U.FormBuilder(
this,
[['datalayer', { handler: 'DataLayerSwitcher' }]],
{
callback: function () { callback: function () {
this.edit(e) this.edit(e)
}, // removeLayer step will close the edit panel, let's reopen it }, // removeLayer step will close the edit panel, let's reopen it
}) }
)
container.appendChild(builder.build()) container.appendChild(builder.build())
const properties = [] const properties = []
@ -209,7 +213,7 @@ U.FeatureMixin = {
if (L.Browser.ielt9) return false if (L.Browser.ielt9) return false
if (this.datalayer.isRemoteLayer() && this.datalayer.options.remoteData.dynamic) if (this.datalayer.isRemoteLayer() && this.datalayer.options.remoteData.dynamic)
return false return false
return this.map.options.displayPopupFooter return this.map.getOption('displayPopupFooter')
}, },
getPopupClass: function () { getPopupClass: function () {
@ -309,7 +313,7 @@ U.FeatureMixin = {
zoomTo: function (e) { zoomTo: function (e) {
e = 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) { if (easing) {
this.map.flyTo(this.getCenter(), this.getBestZoom()) this.map.flyTo(this.getCenter(), this.getBestZoom())
} else { } else {
@ -975,7 +979,7 @@ U.PathMixin = {
zoomTo: function (e) { zoomTo: function (e) {
// Use bounds instead of centroid for paths. // Use bounds instead of centroid for paths.
e = 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) { if (easing) {
this.map.flyToBounds(this.getBounds(), this.getBestZoom()) this.map.flyToBounds(this.getBounds(), this.getBestZoom())
} else { } else {

View file

@ -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({ L.FormBuilder.ProxyTTLSelect = L.FormBuilder.Select.extend({
selectOptions: [ selectOptions: [
[undefined, L._('No cache')], [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({ L.FormBuilder.LayerTypeChooser = L.FormBuilder.Select.extend({
getOptions: function () { getOptions: function () {
const layer_classes = [ 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({ L.FormBuilder.DataFormat = L.FormBuilder.Select.extend({
selectOptions: [ selectOptions: [
[undefined, L._('Choose the data format')], [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({ L.FormBuilder.LicenceChooser = L.FormBuilder.Select.extend({
getOptions: function () { getOptions: function () {
const licences = [] const licences = []
@ -708,7 +653,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({
}, },
isDefault: function () { isDefault: function () {
return !this.value() || this.value() === U.DEFAULT_ICON_URL return !this.value() || this.value() === U.SCHEMA.iconUrl.default
}, },
addGrid: function (onSearch) { 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: [ choices: [
[true, L._('always')], [true, L._('always')],
[false, L._('never')], [false, L._('never')],
@ -913,17 +858,7 @@ L.FormBuilder.ControlChoice = L.FormBuilder.TernaryChoices.extend({
], ],
}) })
L.FormBuilder.LabelChoice = L.FormBuilder.TernaryChoices.extend({ L.FormBuilder.DataLayersControl = L.FormBuilder.TernaryChoices.extend({
default: false,
choices: [
[true, L._('always')],
[false, L._('never')],
['null', L._('on hover')],
],
})
L.FormBuilder.DataLayersControl = L.FormBuilder.ControlChoice.extend({
choices: [ choices: [
[true, L._('collapsed')], [true, L._('collapsed')],
['expanded', L._('expanded')], ['expanded', L._('expanded')],
@ -934,21 +869,11 @@ L.FormBuilder.DataLayersControl = L.FormBuilder.ControlChoice.extend({
toJS: function () { toJS: function () {
let value = this.value() let value = this.value()
if (value !== 'expanded') if (value !== 'expanded')
value = L.FormBuilder.ControlChoice.prototype.toJS.call(this) value = L.FormBuilder.TernaryChoices.prototype.toJS.call(this)
return value 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({ L.FormBuilder.Range = L.FormBuilder.FloatInput.extend({
type: function () { type: function () {
return 'range' return 'range'
@ -1052,230 +977,55 @@ U.FormBuilder = L.FormBuilder.extend({
className: 'umap-form', className: 'umap-form',
}, },
defaultOptions: { computeDefaultOptions: function () {
name: { label: L._('name') }, for (let [key, schema] of Object.entries(U.SCHEMA)) {
description: { if (schema.type === Boolean) {
label: L._('description'), if (schema.nullable) schema.handler = 'NullableChoices'
handler: 'Textarea', else schema.handler = 'Switch'
helpEntries: 'textFormatting', } else if (schema.type === 'Text') {
}, schema.handler = 'Textarea'
color: { } else if (schema.type === Number) {
handler: 'ColorPicker', if (schema.step) schema.handler = 'Range'
label: L._('color'), else schema.handler = 'IntInput'
helpEntries: 'colorValue', } else if (schema.choices) {
inheritable: true, const text_length = schema.choices.reduce(
}, (acc, [value, label]) => acc + label.length,
iconOpacity: { 0
handler: 'Range', )
min: 0.1, // Try to be smart and use MultiChoice only
max: 1, // for choices where labels are shorts…
step: 0.1, if (text_length < 40) {
label: L._('icon opacity'), schema.handler = 'MultiChoice'
inheritable: true, } else {
}, schema.handler = 'Select'
opacity: { schema.selectOptions = schema.choices
handler: 'Range', }
min: 0.1, } else {
max: 1, switch (key) {
step: 0.1, case 'color':
label: L._('opacity'), case 'fillColor':
inheritable: true, schema.handler = 'ColorPicker'
}, break
stroke: { case 'iconUrl':
handler: 'Switch', schema.handler = 'IconUrl'
label: L._('stroke'), break
helpEntries: 'stroke', case 'datalayersControl':
inheritable: true, schema.handler = 'DataLayersControl'
}, break
weight: { case 'licence':
handler: 'Range', schema.handler = 'LicenceChooser'
min: 1, break
max: 20, }
step: 1, }
label: L._('weight'), // FormBuilder use this key for the input type itself
inheritable: true, delete schema.type
}, this.defaultOptions[key] = schema
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,
},
}, },
initialize: function (obj, fields, options) { initialize: function (obj, fields, options) {
this.map = obj.map || obj.getMap() this.map = obj.map || obj.getMap()
this.computeDefaultOptions()
L.FormBuilder.prototype.initialize.call(this, obj, fields, options) L.FormBuilder.prototype.initialize.call(this, obj, fields, options)
this.on('finish', this.finish) this.on('finish', this.finish)
}, },

View file

@ -19,7 +19,7 @@ U.Icon = L.DivIcon.extend({
_setRecent: function (url) { _setRecent: function (url) {
if (L.Util.hasVar(url)) return 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) { if (U.Icon.RECENT.indexOf(url) === -1) {
U.Icon.RECENT.push(url) U.Icon.RECENT.push(url)
} }
@ -236,7 +236,7 @@ U.Icon.setIconContrast = function (icon, parent, src, bgcolor) {
if (L.DomUtil.contrastedColor(parent, bgcolor)) { if (L.DomUtil.contrastedColor(parent, bgcolor)) {
// Decide whether to switch svg to white or not, but do it // Decide whether to switch svg to white or not, but do it
// only for internal SVG, as invert could do weird things // 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 // Must be called after icon container is added to the DOM
// An image // An image
icon.style.filter = 'invert(1)' icon.style.filter = 'invert(1)'

View file

@ -2,33 +2,12 @@ L.Map.mergeOptions({
overlay: null, overlay: null,
datalayers: [], datalayers: [],
hash: true, 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, maxZoomLimit: 24,
attributionControl: false, attributionControl: false,
editMode: 'advanced', editMode: 'advanced',
embedControl: true,
zoomControl: true,
datalayersControl: true,
searchControl: true,
editInOSMControl: false,
editInOSMControlOptions: false,
scaleControl: true,
noControl: false, // Do not render any control. noControl: false, // Do not render any control.
miniMap: false,
name: '', name: '',
description: '', description: '',
displayPopupFooter: false,
// When a TileLayer is in TMS mode, it needs -y instead of y. // When a TileLayer is in TMS mode, it needs -y instead of y.
// This is usually handled by the TileLayer instance itself, but // This is usually handled by the TileLayer instance itself, but
// we cannot rely on this because of the y is overriden by Leaflet // we cannot rely on this because of the y is overriden by Leaflet
@ -44,77 +23,14 @@ L.Map.mergeOptions({
importPresets: [ importPresets: [
// {url: 'http://localhost:8019/en/datalayer/1502/', label: 'Simplified World Countries', format: 'geojson'} // {url: 'http://localhost:8019/en/datalayer/1502/', label: 'Simplified World Countries', format: 'geojson'}
], ],
moreControl: true,
captionBar: false,
captionMenus: true,
slideshow: {}, slideshow: {},
clickable: true, clickable: true,
easing: false,
permissions: {}, permissions: {},
permanentCreditBackground: true,
featuresHaveOwner: false, featuresHaveOwner: false,
}) })
U.Map = L.Map.extend({ U.Map = L.Map.extend({
includes: [ControlsMixin], 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) { initialize: function (el, geojson) {
// Locale name (pt_PT, en_US…) // Locale name (pt_PT, en_US…)
@ -133,7 +49,8 @@ U.Map = L.Map.extend({
geojson.properties.fullscreenControl = false geojson.properties.fullscreenControl = false
L.Map.prototype.initialize.call(this, el, geojson.properties) 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 // After calling parent initialize, as we are doing initCenter our-selves
if (geojson.geometry) this.options.center = this.latLng(geojson.geometry) if (geojson.geometry) this.options.center = this.latLng(geojson.geometry)
@ -329,13 +246,11 @@ U.Map = L.Map.extend({
// FIXME retrocompat // FIXME retrocompat
L.Util.setBooleanFromQueryString(options, 'displayDataBrowserOnLoad') L.Util.setBooleanFromQueryString(options, 'displayDataBrowserOnLoad')
L.Util.setBooleanFromQueryString(options, 'displayCaptionOnLoad') L.Util.setBooleanFromQueryString(options, 'displayCaptionOnLoad')
for (const [key, type] of Object.entries(this.editableOptions)) { for (const [key, schema] of Object.entries(U.SCHEMA)) {
switch (type) { switch (schema.type) {
case Boolean: case Boolean:
L.Util.setBooleanFromQueryString(options, key) if (schema.nullable) L.Util.setNullableBooleanFromQueryString(options, key)
break else L.Util.setBooleanFromQueryString(options, key)
case 'NullableBoolean':
L.Util.setNullableBooleanFromQueryString(options, key)
break break
case Number: case Number:
L.Util.setNumberFromQueryString(options, key) 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 () { initControls: function () {
this.helpMenuActions = {} this.helpMenuActions = {}
this._controls = {} this._controls = {}
@ -393,7 +314,7 @@ U.Map = L.Map.extend({
title: { false: L._('View Fullscreen'), true: L._('Exit Fullscreen') }, title: { false: L._('View Fullscreen'), true: L._('Exit Fullscreen') },
}) })
this._controls.search = new U.SearchControl() 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) this._controls.tilelayersChooser = new U.TileLayerChooser(this)
if (this.options.user) this._controls.star = new U.StarControl(this) if (this.options.user) this._controls.star = new U.StarControl(this)
this._controls.editinosm = new L.Control.EditInOSM({ this._controls.editinosm = new L.Control.EditInOSM({
@ -459,7 +380,7 @@ U.Map = L.Map.extend({
let name, status, control let name, status, control
for (let i = 0; i < this.HIDDABLE_CONTROLS.length; i++) { for (let i = 0; i < this.HIDDABLE_CONTROLS.length; i++) {
name = this.HIDDABLE_CONTROLS[i] name = this.HIDDABLE_CONTROLS[i]
status = this.options[`${name}Control`] status = this.getOption(`${name}Control`)
if (status === false) continue if (status === false) continue
control = this._controls[name] control = this._controls[name]
if (!control) continue if (!control) continue
@ -468,9 +389,9 @@ U.Map = L.Map.extend({
L.DomUtil.addClass(control._container, 'display-on-more') L.DomUtil.addClass(control._container, 'display-on-more')
else L.DomUtil.removeClass(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.getOption('permanentCredit')) this._controls.permanentCredit.addTo(this)
if (this.options.moreControl) this._controls.more.addTo(this) if (this.getOption('moreControl')) this._controls.more.addTo(this)
if (this.options.scaleControl) this._controls.scale.addTo(this) if (this.getOption('scaleControl')) this._controls.scale.addTo(this)
}, },
initDataLayers: async function (datalayers) { initDataLayers: async function (datalayers) {
@ -820,7 +741,7 @@ U.Map = L.Map.extend({
}, },
getDefaultOption: function (option) { getDefaultOption: function (option) {
return this.options[`default_${option}`] return U.SCHEMA[option] && U.SCHEMA[option].default
}, },
getOption: function (option) { getOption: function (option) {
@ -904,7 +825,7 @@ U.Map = L.Map.extend({
let mustReindex = false 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') { if (typeof importedData.properties[option] !== 'undefined') {
this.options[option] = importedData.properties[option] this.options[option] = importedData.properties[option]
if (option === 'sortKey') mustReindex = true if (option === 'sortKey') mustReindex = true
@ -1033,7 +954,7 @@ U.Map = L.Map.extend({
exportOptions: function () { exportOptions: function () {
const properties = {} const properties = {}
for (const option of Object.keys(this.editableOptions)) { for (const option of Object.keys(U.SCHEMA)) {
if (typeof this.options[option] !== 'undefined') { if (typeof this.options[option] !== 'undefined') {
properties[option] = this.options[option] properties[option] = this.options[option]
} }
@ -1555,35 +1476,11 @@ U.Map = L.Map.extend({
_editCredits: function (container) { _editCredits: function (container) {
const credits = L.DomUtil.createFieldset(container, L._('Credits')) const credits = L.DomUtil.createFieldset(container, L._('Credits'))
const creditsFields = [ const creditsFields = [
['options.licence', { handler: 'LicenceChooser', label: L._('licence') }], 'options.licence',
[
'options.shortCredit', 'options.shortCredit',
{
handler: 'Input',
label: L._('Short credits'),
helpEntries: ['shortCredit', 'textFormatting'],
},
],
[
'options.longCredit', 'options.longCredit',
{
handler: 'Textarea',
label: L._('Long credits'),
helpEntries: ['longCredit', 'textFormatting'],
},
],
[
'options.permanentCredit', 'options.permanentCredit',
{
handler: 'Textarea',
label: L._('Permanent credits'),
helpEntries: ['permanentCredit', 'textFormatting'],
},
],
[
'options.permanentCreditBackground', 'options.permanentCreditBackground',
{ handler: 'Switch', label: L._('Permanent credits background') },
],
] ]
const creditsBuilder = new U.FormBuilder(this, creditsFields, { const creditsBuilder = new U.FormBuilder(this, creditsFields, {
callback: this.renderControls, callback: this.renderControls,
@ -1691,7 +1588,7 @@ U.Map = L.Map.extend({
name = L.DomUtil.create('h3', '', container) name = L.DomUtil.create('h3', '', container)
L.DomEvent.disableClickPropagation(container) L.DomEvent.disableClickPropagation(container)
this.permissions.addOwnerLink('span', container) this.permissions.addOwnerLink('span', container)
if (this.options.captionMenus) { if (this.getOption('captionMenus')) {
L.DomUtil.createButton( L.DomUtil.createButton(
'umap-about-link flat', 'umap-about-link flat',
container, container,

View file

@ -215,7 +215,7 @@ U.IframeExporter = L.Evented.extend({
this.map = map this.map = map
this.baseUrl = L.Util.getBaseUrl() this.baseUrl = L.Util.getBaseUrl()
// Use map default, not generic default // Use map default, not generic default
this.queryString.onLoadPanel = this.map.options.onLoadPanel this.queryString.onLoadPanel = this.map.getOption('onLoadPanel')
}, },
getMap: function () { getMap: function () {

View file

@ -8,7 +8,6 @@
<script src="../vendors/editable/Path.Drag.js" defer></script> <script src="../vendors/editable/Path.Drag.js" defer></script>
<script src="../vendors/editable/Leaflet.Editable.js" defer></script> <script src="../vendors/editable/Leaflet.Editable.js" defer></script>
<script src="../vendors/hash/leaflet-hash.js" defer></script> <script src="../vendors/hash/leaflet-hash.js" defer></script>
<script src="../vendors/i18n/Leaflet.i18n.js" defer></script>
<script src="../vendors/editinosm/Leaflet.EditInOSM.js" defer></script> <script src="../vendors/editinosm/Leaflet.EditInOSM.js" defer></script>
<script src="../vendors/minimap/Control.MiniMap.min.js" defer></script> <script src="../vendors/minimap/Control.MiniMap.min.js" defer></script>
<script src="../vendors/csv2geojson/csv2geojson.js" defer></script> <script src="../vendors/csv2geojson/csv2geojson.js" defer></script>

View file

@ -2,11 +2,16 @@
<script type="module" <script type="module"
src="{% static 'umap/vendors/leaflet/leaflet-src.esm.js' %}" src="{% static 'umap/vendors/leaflet/leaflet-src.esm.js' %}"
defer></script> defer></script>
<script type="module" src="{% static 'umap/js/modules/leaflet-configure.js' %}" defer></script>
{% if locale %}
{% with "umap/locale/"|add:locale|add:".js" as path %}
<script src="{% static path %}" defer></script>
{% endwith %}
{% endif %}
<script type="module" src="{% static 'umap/js/modules/global.js' %}" defer></script> <script type="module" src="{% static 'umap/js/modules/global.js' %}" defer></script>
<script src="{% static 'umap/vendors/editable/Path.Drag.js' %}" defer></script> <script src="{% static 'umap/vendors/editable/Path.Drag.js' %}" defer></script>
<script src="{% static 'umap/vendors/editable/Leaflet.Editable.js' %}" defer></script> <script src="{% static 'umap/vendors/editable/Leaflet.Editable.js' %}" defer></script>
<script src="{% static 'umap/vendors/hash/leaflet-hash.js' %}" defer></script> <script src="{% static 'umap/vendors/hash/leaflet-hash.js' %}" defer></script>
<script src="{% static 'umap/vendors/i18n/Leaflet.i18n.js' %}" defer></script>
<script src="{% static 'umap/vendors/editinosm/Leaflet.EditInOSM.js' %}" <script src="{% static 'umap/vendors/editinosm/Leaflet.EditInOSM.js' %}"
defer></script> defer></script>
<script src="{% static 'umap/vendors/minimap/Control.MiniMap.min.js' %}" <script src="{% static 'umap/vendors/minimap/Control.MiniMap.min.js' %}"
@ -39,11 +44,7 @@
<script src="{% static 'umap/vendors/colorbrewer/colorbrewer.js' %}" defer></script> <script src="{% static 'umap/vendors/colorbrewer/colorbrewer.js' %}" defer></script>
<script src="{% static 'umap/vendors/simple-statistics/simple-statistics.min.js' %}" <script src="{% static 'umap/vendors/simple-statistics/simple-statistics.min.js' %}"
defer></script> defer></script>
{% if locale %}
{% with "umap/locale/"|add:locale|add:".js" as path %}
<script src="{% static path %}" defer></script>
{% endwith %}
{% endif %}
<script src="{% static 'umap/js/umap.core.js' %}" defer></script> <script src="{% static 'umap/js/umap.core.js' %}" defer></script>
<script src="{% static 'umap/js/umap.autocomplete.js' %}" defer></script> <script src="{% static 'umap/js/umap.autocomplete.js' %}" defer></script>
<script src="{% static 'umap/js/umap.popup.js' %}" defer></script> <script src="{% static 'umap/js/umap.popup.js' %}" defer></script>

View file

@ -210,6 +210,7 @@ def test_can_change_owner(map, live_server, login, user):
close = page.locator(".umap-field-owner .close") close = page.locator(".umap-field-owner .close")
close.click() close.click()
input = page.locator("input.edit-owner") input = page.locator("input.edit-owner")
with page.expect_response(re.compile(r".*/agnocomplete/.*")):
input.type(user.username) input.type(user.username)
input.press("Tab") input.press("Tab")
save = page.get_by_role("button", name="Save") save = page.get_by_role("button", name="Save")

View file

@ -495,7 +495,7 @@ class MapDetailMixin:
"urls": _urls_for_js(), "urls": _urls_for_js(),
"tilelayers": TileLayer.get_list(), "tilelayers": TileLayer.get_list(),
"editMode": self.edit_mode, "editMode": self.edit_mode,
"default_iconUrl": "%sumap/img/marker.svg" % settings.STATIC_URL, # noqa "schema": Map.extra_schema,
"umap_id": self.get_umap_id(), "umap_id": self.get_umap_id(),
"starred": self.is_starred(), "starred": self.is_starred(),
"licences": dict((l.name, l.json) for l in Licence.objects.all()), "licences": dict((l.name, l.json) for l in Licence.objects.all()),