[chore] move umap utils to a module

Allow the tests to be run from inside a cli, without requiring a browser.
This commit is contained in:
Alexis Métaireau 2024-03-28 17:02:11 +01:00
parent a28db94f72
commit c4e527bf8e
20 changed files with 419 additions and 383 deletions

View file

@ -40,8 +40,9 @@
"@tmcw/togeojson": "^5.8.0",
"colorbrewer": "^1.5.6",
"csv2geojson": "5.1.1",
"dompurify": "^3.0.3",
"dompurify": "^3.0.11",
"georsstogeojson": "^0.1.0",
"jsdom": "^24.0.0",
"leaflet": "1.9.4",
"leaflet-contextmenu": "^1.4.0",
"leaflet-editable": "^1.2.0",

View file

@ -25,7 +25,7 @@ mkdir -p umap/static/umap/vendors/georsstogeojson/ && cp -r node_modules/georsst
mkdir -p umap/static/umap/vendors/togpx/ && cp -r node_modules/togpx/togpx.js umap/static/umap/vendors/togpx/
mkdir -p umap/static/umap/vendors/tokml && cp -r node_modules/tokml/tokml.js umap/static/umap/vendors/tokml
mkdir -p umap/static/umap/vendors/locatecontrol/ && cp -r node_modules/leaflet.locatecontrol/dist/L.Control.Locate.min.* umap/static/umap/vendors/locatecontrol/
mkdir -p umap/static/umap/vendors/dompurify/ && cp -r node_modules/dompurify/dist/purify.min.* umap/static/umap/vendors/dompurify/
mkdir -p umap/static/umap/vendors/dompurify/ && cp -r node_modules/dompurify/dist/*.mjs umap/static/umap/vendors/dompurify/
mkdir -p umap/static/umap/vendors/colorbrewer/ && cp node_modules/colorbrewer/index.js umap/static/umap/vendors/colorbrewer/colorbrewer.js
mkdir -p umap/static/umap/vendors/simple-statistics/ && cp node_modules/simple-statistics/dist/simple-statistics.min.* umap/static/umap/vendors/simple-statistics/
mkdir -p umap/static/umap/vendors/iconlayers/ && cp node_modules/leaflet-iconlayers/dist/* umap/static/umap/vendors/iconlayers/

View file

@ -1,7 +1,7 @@
import URLs from './urls.js'
import Browser from './browser.js'
import * as Utils from './utils.js'
import {SCHEMA} from './schema.js'
import { SCHEMA } from './schema.js'
import { Request, ServerRequest, RequestError, HTTPError, NOKError } from './request.js'
// Import modules and export them to the global scope.

View file

@ -1,19 +1,4 @@
// Vendorized from leaflet.utils
// https://github.com/Leaflet/Leaflet/blob/108c6717b70f57c63645498f9bd66b6677758786/src/core/Util.js#L132-L151
var templateRe = /\{ *([\w_ -]+) *\}/g
function template(str, data) {
return str.replace(templateRe, function (str, key) {
var value = data[key]
if (value === undefined) {
throw new Error('No value provided for variable ' + str)
} else if (typeof value === 'function') {
value = value(data)
}
return value
})
}
import { template } from "./utils.js"
export default class URLs {
constructor(serverUrls) {

View file

@ -1,3 +1,5 @@
import { default as DOMPurifyInitializer } from '../../vendors/dompurify/purify.es.mjs'
/**
* Generate a pseudo-unique identifier (5 chars long, mixed-case alphanumeric)
*
@ -22,3 +24,284 @@ export function checkId(string) {
if (typeof string !== 'string') return false
return /^[A-Za-z0-9]{5}$/.test(string)
}
/**
* Import DOM purify, and initialize it.
*
* If the context is a node server, uses jsdom to provide
* DOM APIs
*/
export default function getPurify() {
if (typeof window === 'undefined') {
return DOMPurifyInitializer(new global.JSDOM('').window)
} else {
return DOMPurifyInitializer(window)
}
}
export function escapeHTML(s) {
s = s ? s.toString() : ''
s = getPurify().sanitize(s, {
USE_PROFILES: { html: true },
ADD_TAGS: ['iframe'],
ALLOWED_TAGS: [
'h3',
'h4',
'h5',
'hr',
'strong',
'em',
'ul',
'li',
'a',
'div',
'iframe',
'img',
'br',
],
ADD_ATTR: ['target', 'allow', 'allowfullscreen', 'frameborder', 'scrolling'],
ALLOWED_ATTR: ['href', 'src', 'width', 'height'],
// Added: `geo:` URL scheme as defined in RFC5870:
// https://www.rfc-editor.org/rfc/rfc5870.html
// The base RegExp comes from:
// https://github.com/cure53/DOMPurify/blob/main/src/regexp.js#L10
ALLOWED_URI_REGEXP:
/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|geo):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
})
return s
}
export function toHTML(r, options) {
if (!r) return ''
const target = (options && options.target) || 'blank'
let ii
// detect newline format
const newline = r.indexOf('\r\n') != -1 ? '\r\n' : r.indexOf('\n') != -1 ? '\n' : ''
// headings and hr
r = r.replace(/^### (.*)/gm, '<h5>$1</h5>')
r = r.replace(/^## (.*)/gm, '<h4>$1</h4>')
r = r.replace(/^# (.*)/gm, '<h3>$1</h3>')
r = r.replace(/^---/gm, '<hr>')
// bold, italics
r = r.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
r = r.replace(/\*(.*?)\*/g, '<em>$1</em>')
// unordered lists
r = r.replace(/^\*\* (.*)/gm, '<ul><ul><li>$1</li></ul></ul>')
r = r.replace(/^\* (.*)/gm, '<ul><li>$1</li></ul>')
for (ii = 0; ii < 3; ii++)
r = r.replace(new RegExp(`</ul>${newline}<ul>`, 'g'), newline)
// links
r = r.replace(/(\[\[http)/g, '[[h_t_t_p') // Escape for avoiding clash between [[http://xxx]] and http://xxx
r = r.replace(/({{http)/g, '{{h_t_t_p')
r = r.replace(/(=http)/g, '=h_t_t_p') // http://xxx as query string, see https://github.com/umap-project/umap/issues/607
r = r.replace(/(https?:[^ \<)\n]*)/g, `<a target="_${target}" href="$1">$1</a>`)
r = r.replace(
/\[\[(h_t_t_ps?:[^\]|]*?)\]\]/g,
`<a target="_${target}" href="$1">$1</a>`
)
r = r.replace(
/\[\[(h_t_t_ps?:[^|]*?)\|(.*?)\]\]/g,
`<a target="_${target}" href="$1">$2</a>`
)
r = r.replace(/\[\[([^\]|]*?)\]\]/g, `<a target="_${target}" href="$1">$1</a>`)
r = r.replace(/\[\[([^|]*?)\|(.*?)\]\]/g, `<a target="_${target}" href="$1">$2</a>`)
// iframe
r = r.replace(
/{{{(h_t_t_ps?[^ |{]*)}}}/g,
'<div><iframe frameborder="0" src="$1" width="100%" height="300px"></iframe></div>'
)
r = r.replace(
/{{{(h_t_t_ps?[^ |{]*)\|(\d*)(px)?}}}/g,
'<div><iframe frameborder="0" src="$1" width="100%" height="$2px"></iframe></div>'
)
r = r.replace(
/{{{(h_t_t_ps?[^ |{]*)\|(\d*)(px)?\*(\d*)(px)?}}}/g,
'<div><iframe frameborder="0" src="$1" width="$4px" height="$2px"></iframe></div>'
)
// images
r = r.replace(/{{([^\]|]*?)}}/g, '<img src="$1">')
r = r.replace(
/{{([^|]*?)\|(\d*?)(px)?}}/g,
'<img src="$1" style="width:$2px;min-width:$2px;">'
)
//Unescape http
r = r.replace(/(h_t_t_p)/g, 'http')
// Preserver line breaks
if (newline) r = r.replace(new RegExp(`${newline}(?=[^]+)`, 'g'), `<br>${newline}`)
r = escapeHTML(r)
return r
}
export function isObject(what) {
return typeof what === 'object' && what !== null
}
export function CopyJSON(geojson) {
return JSON.parse(JSON.stringify(geojson))
}
export function detectFileType(f) {
const filename = f.name ? escape(f.name.toLowerCase()) : ''
function ext(_) {
return filename.indexOf(_) !== -1
}
if (f.type === 'application/vnd.google-earth.kml+xml' || ext('.kml')) {
return 'kml'
}
if (ext('.gpx')) return 'gpx'
if (ext('.geojson') || ext('.json')) return 'geojson'
if (f.type === 'text/csv' || ext('.csv') || ext('.tsv') || ext('.dsv')) {
return 'csv'
}
if (ext('.xml') || ext('.osm')) return 'osm'
if (ext('.umap')) return 'umap'
}
export function usableOption(options, option) {
return options[option] !== undefined && options[option] !== ''
}
export function greedyTemplate(str, data, ignore) {
function getValue(data, path) {
let value = data
for (let i = 0; i < path.length; i++) {
value = value[path[i]]
if (value === undefined) break
}
return value
}
if (typeof str !== 'string') return ''
return str.replace(
/\{ *([^\{\}/\-]+)(?:\|("[^"]*"))? *\}/g,
(str, key, staticFallback) => {
const vars = key.split('|')
let value
let path
if (staticFallback !== undefined) {
vars.push(staticFallback)
}
for (let i = 0; i < vars.length; i++) {
path = vars[i]
if (path.startsWith('"') && path.endsWith('"'))
value = path.substring(1, path.length - 1) // static default value.
else value = getValue(data, path.split('.'))
if (value !== undefined) break
}
if (value === undefined) {
if (ignore) value = str
else value = ''
}
return value
}
)
}
export function naturalSort(a, b, lang) {
return a
.toString()
.toLowerCase()
.localeCompare(b.toString().toLowerCase(), lang || 'en', {
sensitivity: 'base',
numeric: true,
})
}
export function sortFeatures(features, sortKey, lang) {
const sortKeys = (sortKey || 'name').split(',')
const sort = (a, b, i) => {
let sortKey = sortKeys[i],
reverse = 1
if (sortKey[0] === '-') {
reverse = -1
sortKey = sortKey.substring(1)
}
let score
const valA = a.properties[sortKey] || ''
const valB = b.properties[sortKey] || ''
if (!valA) score = -1
else if (!valB) score = 1
else score = naturalSort(valA, valB, lang)
if (score === 0 && sortKeys[i + 1]) return sort(a, b, i + 1)
return score * reverse
}
features.sort((a, b) => {
if (!a.properties || !b.properties) {
return 0
}
return sort(a, b, 0)
})
return features
}
export function flattenCoordinates(coords) {
while (coords[0] && typeof coords[0][0] !== 'number') coords = coords[0]
return coords
}
export function buildQueryString(params) {
const query_string = []
for (const key in params) {
query_string.push(`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
}
return query_string.join('&')
}
export function getBaseUrl() {
return `//${window.location.host}${window.location.pathname}`
}
export function hasVar(value) {
return typeof value === 'string' && value.indexOf('{') != -1
}
export function isPath(value) {
return value && value.length && value.startsWith('/')
}
export function isRemoteUrl(value) {
return value && value.length && value.startsWith('http')
}
export function isDataImage(value) {
return value && value.length && value.startsWith('data:image')
}
export function normalize(s) {
return (s || '')
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
}
// Vendorized from leaflet.utils
// https://github.com/Leaflet/Leaflet/blob/108c6717b70f57c63645498f9bd66b6677758786/src/core/Util.js#L132-L151
var templateRe = /\{ *([\w_ -]+) *\}/g
export function template(str, data) {
return str.replace(templateRe, function (str, key) {
var value = data[key]
if (value === undefined) {
throw new Error('No value provided for variable ' + str)
} else if (typeof value === 'function') {
value = value(data)
}
return value
})
}

View file

@ -153,7 +153,7 @@ U.AutoComplete = L.Class.extend({
this.displaySelected(choice)
this.hide()
if (this.options.callback) {
L.Util.bind(this.options.callback, this)(choice)
U.Utils.bind(this.options.callback, this)(choice)
}
}
},

View file

@ -476,7 +476,7 @@ U.PermanentCreditsControl = L.Control.extend({
},
setCredits: function () {
this.paragraphContainer.innerHTML = L.Util.toHTML(this.map.options.permanentCredit)
this.paragraphContainer.innerHTML = U.Utils.toHTML(this.map.options.permanentCredit)
},
setBackground: function () {
@ -820,7 +820,7 @@ const ControlsMixin = {
this.permissions.addOwnerLink('h5', container)
if (this.options.description) {
const description = L.DomUtil.create('div', 'umap-map-description', container)
description.innerHTML = L.Util.toHTML(this.options.description)
description.innerHTML = U.Utils.toHTML(this.options.description)
}
const datalayerContainer = L.DomUtil.create('div', 'datalayer-container', container)
this.eachVisibleDataLayer((datalayer) => {
@ -832,7 +832,7 @@ const ControlsMixin = {
datalayer.onceLoaded(function () {
datalayer.renderLegend(legend)
if (datalayer.options.description) {
description.innerHTML = L.Util.toHTML(datalayer.options.description)
description.innerHTML = U.Utils.toHTML(datalayer.options.description)
}
})
datalayer.renderToolbox(headline)
@ -846,7 +846,7 @@ const ControlsMixin = {
'p',
'',
credits,
L.Util.toHTML(this.options.longCredit || this.options.shortCredit)
U.Utils.toHTML(this.options.longCredit || this.options.shortCredit)
)
}
if (this.options.licence) {
@ -1077,7 +1077,7 @@ U.TileLayerControl = L.Control.IconLayers.extend({
// when the tilelayer is actually added to the map (needs this._tileZoom
// to be defined)
// Fixme when https://github.com/Leaflet/Leaflet/pull/9201 is released
const icon = L.Util.template(
const icon = U.Utils.template(
layer.options.url_template,
this.map.demoTileInfos
)
@ -1150,7 +1150,7 @@ U.TileLayerChooser = L.Control.extend({
el = L.DomUtil.create('li', selectedClass, this._tilelayers_container),
img = L.DomUtil.create('img', '', el),
name = L.DomUtil.create('div', '', el)
img.src = L.Util.template(tilelayer.options.url_template, this.map.demoTileInfos)
img.src = U.Utils.template(tilelayer.options.url_template, this.map.demoTileInfos)
img.loading = 'lazy'
name.textContent = tilelayer.options.name
L.DomEvent.on(
@ -1187,7 +1187,7 @@ U.AttributionControl = L.Control.Attribution.extend({
const shortCredit = this._map.getOption('shortCredit'),
captionMenus = this._map.getOption('captionMenus')
if (shortCredit) {
L.DomUtil.add('span', '', container, `${L.Util.toHTML(shortCredit)}`)
L.DomUtil.add('span', '', container, `${U.Utils.toHTML(shortCredit)}`)
}
if (captionMenus) {
const link = L.DomUtil.add('a', '', container, `${L._('About')}`)

View file

@ -1,277 +1,3 @@
/*
* Utils
*/
L.Util.queryString = (name, fallback) => {
const decode = (s) => decodeURIComponent(s.replace(/\+/g, ' '))
const qs = window.location.search.slice(1).split('&'),
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 (!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.Util.escapeHTML = (s) => {
s = s ? s.toString() : ''
s = DOMPurify.sanitize(s, {
USE_PROFILES: { html: true },
ADD_TAGS: ['iframe'],
ALLOWED_TAGS: [
'h3',
'h4',
'h5',
'hr',
'strong',
'em',
'ul',
'li',
'a',
'div',
'iframe',
'img',
'br',
],
ADD_ATTR: ['target', 'allow', 'allowfullscreen', 'frameborder', 'scrolling'],
ALLOWED_ATTR: ['href', 'src', 'width', 'height'],
// Added: `geo:` URL scheme as defined in RFC5870:
// https://www.rfc-editor.org/rfc/rfc5870.html
// The base RegExp comes from:
// https://github.com/cure53/DOMPurify/blob/main/src/regexp.js#L10
ALLOWED_URI_REGEXP:
/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|geo):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
})
return s
}
L.Util.toHTML = (r, options) => {
if (!r) return ''
const target = (options && options.target) || 'blank'
let ii
// detect newline format
const newline = r.indexOf('\r\n') != -1 ? '\r\n' : r.indexOf('\n') != -1 ? '\n' : ''
// headings and hr
r = r.replace(/^### (.*)/gm, '<h5>$1</h5>')
r = r.replace(/^## (.*)/gm, '<h4>$1</h4>')
r = r.replace(/^# (.*)/gm, '<h3>$1</h3>')
r = r.replace(/^---/gm, '<hr>')
// bold, italics
r = r.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
r = r.replace(/\*(.*?)\*/g, '<em>$1</em>')
// unordered lists
r = r.replace(/^\*\* (.*)/gm, '<ul><ul><li>$1</li></ul></ul>')
r = r.replace(/^\* (.*)/gm, '<ul><li>$1</li></ul>')
for (ii = 0; ii < 3; ii++)
r = r.replace(new RegExp(`</ul>${newline}<ul>`, 'g'), newline)
// links
r = r.replace(/(\[\[http)/g, '[[h_t_t_p') // Escape for avoiding clash between [[http://xxx]] and http://xxx
r = r.replace(/({{http)/g, '{{h_t_t_p')
r = r.replace(/(=http)/g, '=h_t_t_p') // http://xxx as query string, see https://github.com/umap-project/umap/issues/607
r = r.replace(/(https?:[^ \<)\n]*)/g, `<a target="_${target}" href="$1">$1</a>`)
r = r.replace(
/\[\[(h_t_t_ps?:[^\]|]*?)\]\]/g,
`<a target="_${target}" href="$1">$1</a>`
)
r = r.replace(
/\[\[(h_t_t_ps?:[^|]*?)\|(.*?)\]\]/g,
`<a target="_${target}" href="$1">$2</a>`
)
r = r.replace(/\[\[([^\]|]*?)\]\]/g, `<a target="_${target}" href="$1">$1</a>`)
r = r.replace(/\[\[([^|]*?)\|(.*?)\]\]/g, `<a target="_${target}" href="$1">$2</a>`)
// iframe
r = r.replace(
/{{{(h_t_t_ps?[^ |{]*)}}}/g,
'<div><iframe frameborder="0" src="$1" width="100%" height="300px"></iframe></div>'
)
r = r.replace(
/{{{(h_t_t_ps?[^ |{]*)\|(\d*)(px)?}}}/g,
'<div><iframe frameborder="0" src="$1" width="100%" height="$2px"></iframe></div>'
)
r = r.replace(
/{{{(h_t_t_ps?[^ |{]*)\|(\d*)(px)?\*(\d*)(px)?}}}/g,
'<div><iframe frameborder="0" src="$1" width="$4px" height="$2px"></iframe></div>'
)
// images
r = r.replace(/{{([^\]|]*?)}}/g, '<img src="$1">')
r = r.replace(
/{{([^|]*?)\|(\d*?)(px)?}}/g,
'<img src="$1" style="width:$2px;min-width:$2px;">'
)
//Unescape http
r = r.replace(/(h_t_t_p)/g, 'http')
// Preserver line breaks
if (newline) r = r.replace(new RegExp(`${newline}(?=[^]+)`, 'g'), `<br>${newline}`)
r = L.Util.escapeHTML(r)
return r
}
L.Util.isObject = (what) => typeof what === 'object' && what !== null
L.Util.CopyJSON = (geojson) => JSON.parse(JSON.stringify(geojson))
L.Util.detectFileType = (f) => {
const filename = f.name ? escape(f.name.toLowerCase()) : ''
function ext(_) {
return filename.indexOf(_) !== -1
}
if (f.type === 'application/vnd.google-earth.kml+xml' || ext('.kml')) {
return 'kml'
}
if (ext('.gpx')) return 'gpx'
if (ext('.geojson') || ext('.json')) return 'geojson'
if (f.type === 'text/csv' || ext('.csv') || ext('.tsv') || ext('.dsv')) {
return 'csv'
}
if (ext('.xml') || ext('.osm')) return 'osm'
if (ext('.umap')) return 'umap'
}
L.Util.usableOption = (options, option) =>
options[option] !== undefined && options[option] !== ''
L.Util.greedyTemplate = (str, data, ignore) => {
function getValue(data, path) {
let value = data
for (let i = 0; i < path.length; i++) {
value = value[path[i]]
if (value === undefined) break
}
return value
}
if (typeof str !== 'string') return ''
return str.replace(
/\{ *([^\{\}/\-]+)(?:\|("[^"]*"))? *\}/g,
(str, key, staticFallback) => {
const vars = key.split('|')
let value
let path
if (staticFallback !== undefined) {
vars.push(staticFallback)
}
for (let i = 0; i < vars.length; i++) {
path = vars[i]
if (path.startsWith('"') && path.endsWith('"'))
value = path.substring(1, path.length - 1) // static default value.
else value = getValue(data, path.split('.'))
if (value !== undefined) break
}
if (value === undefined) {
if (ignore) value = str
else value = ''
}
return value
}
)
}
L.Util.naturalSort = (a, b) => {
return a
.toString()
.toLowerCase()
.localeCompare(b.toString().toLowerCase(), L.lang || 'en', {
sensitivity: 'base',
numeric: true,
})
}
L.Util.sortFeatures = (features, sortKey) => {
const sortKeys = (sortKey || 'name').split(',')
const sort = (a, b, i) => {
let sortKey = sortKeys[i],
reverse = 1
if (sortKey[0] === '-') {
reverse = -1
sortKey = sortKey.substring(1)
}
let score
const valA = a.properties[sortKey] || ''
const valB = b.properties[sortKey] || ''
if (!valA) score = -1
else if (!valB) score = 1
else score = L.Util.naturalSort(valA, valB)
if (score === 0 && sortKeys[i + 1]) return sort(a, b, i + 1)
return score * reverse
}
features.sort((a, b) => {
if (!a.properties || !b.properties) {
return 0
}
return sort(a, b, 0)
})
return features
}
L.Util.flattenCoordinates = (coords) => {
while (coords[0] && typeof coords[0][0] !== 'number') coords = coords[0]
return coords
}
L.Util.buildQueryString = (params) => {
const query_string = []
for (const key in params) {
query_string.push(`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
}
return query_string.join('&')
}
L.Util.getBaseUrl = () => `//${window.location.host}${window.location.pathname}`
L.Util.hasVar = (value) => {
return typeof value === 'string' && value.indexOf('{') != -1
}
L.Util.isPath = function (value) {
return value && value.length && value.startsWith('/')
}
L.Util.isRemoteUrl = function (value) {
return value && value.length && value.startsWith('http')
}
L.Util.isDataImage = function (value) {
return value && value.length && value.startsWith('data:image')
}
L.Util.copyToClipboard = function (textToCopy) {
// https://stackoverflow.com/a/65996386
// Navigator clipboard api needs a secure context (https)
@ -299,11 +25,46 @@ L.Util.copyToClipboard = function (textToCopy) {
}
}
L.Util.normalize = function (s) {
return (s || '')
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
L.Util.queryString = function (name, fallback) {
const decode = (s) => decodeURIComponent(s.replace(/\+/g, ' '))
const qs = window.location.search.slice(1).split('&'),
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 = function (name) {
const value = L.Util.queryString(name)
return value === '1' || value === 'true'
}
L.Util.setFromQueryString = function (options, name) {
const value = L.Util.queryString(name)
if (typeof value !== 'undefined') options[name] = value
}
L.Util.setBooleanFromQueryString = function (options, name) {
const value = L.Util.queryString(name)
if (typeof value !== 'undefined') options[name] = value == '1' || value == 'true'
}
L.Util.setNumberFromQueryString = function (options, name) {
const value = +L.Util.queryString(name)
if (!isNaN(value)) options[name] = value
}
L.Util.setNullableBooleanFromQueryString = function (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) => {
@ -367,7 +128,7 @@ L.DomUtil.createCopiableInput = (parent, label, value) => {
'',
wrapper,
'',
() => L.Util.copyToClipboard(input.value),
() => U.Utils.copyToClipboard(input.value),
this
)
button.title = L._('copy')

View file

@ -46,7 +46,7 @@ U.DataLayerPermissions = L.Class.extend({
},
getUrl: function () {
return L.Util.template(this.datalayer.map.options.urls.datalayer_permissions, {
return U.Utils.template(this.datalayer.map.options.urls.datalayer_permissions, {
map_id: this.datalayer.map.options.umap_id,
pk: this.datalayer.umap_id,
})

View file

@ -59,7 +59,7 @@ U.FeatureMixin = {
getPermalink: function () {
const slug = this.getSlug()
if (slug)
return `${L.Util.getBaseUrl()}?${L.Util.buildQueryString({ feature: slug })}${
return `${U.Utils.getBaseUrl()}?${U.Utils.buildQueryString({ feature: slug })}${
window.location.hash
}`
},
@ -204,7 +204,8 @@ U.FeatureMixin = {
if (fallback === undefined) fallback = this.datalayer.options.name
const key = this.getOption('labelKey') || 'name'
// Variables mode.
if (L.Util.hasVar(key)) return L.Util.greedyTemplate(key, this.extendedProperties())
if (U.Utils.hasVar(key))
return U.Utils.greedyTemplate(key, this.extendedProperties())
// Simple mode.
return this.properties[key] || this.properties.title || fallback
},
@ -291,7 +292,7 @@ U.FeatureMixin = {
let value = fallback
if (typeof this.staticOptions[option] !== 'undefined') {
value = this.staticOptions[option]
} else if (L.Util.usableOption(this.properties._umap_options, option)) {
} else if (U.Utils.usableOption(this.properties._umap_options, option)) {
value = this.properties._umap_options[option]
} else if (this.datalayer) {
value = this.datalayer.getOption(option, this)
@ -304,9 +305,9 @@ U.FeatureMixin = {
getDynamicOption: function (option, fallback) {
let value = this.getOption(option, fallback)
// There is a variable inside.
if (L.Util.hasVar(value)) {
value = L.Util.greedyTemplate(value, this.properties, true)
if (L.Util.hasVar(value)) value = this.map.getDefaultOption(option)
if (U.Utils.hasVar(value)) {
value = U.Utils.greedyTemplate(value, this.properties, true)
if (U.Utils.hasVar(value)) value = this.map.getDefaultOption(option)
}
return value
},
@ -485,7 +486,7 @@ U.FeatureMixin = {
options.permanent = showLabel === true
this.unbindTooltip()
if ((showLabel === true || showLabel === null) && displayName)
this.bindTooltip(L.Util.escapeHTML(displayName), options)
this.bindTooltip(U.Utils.escapeHTML(displayName), options)
},
matchFilter: function (filter, keys) {
@ -1059,7 +1060,7 @@ U.Polyline = L.Polyline.extend({
const geojson = this.toGeoJSON()
geojson.geometry.type = 'Polygon'
geojson.geometry.coordinates = [
L.Util.flattenCoordinates(geojson.geometry.coordinates),
U.Utils.flattenCoordinates(geojson.geometry.coordinates),
]
const polygon = this.datalayer.geojsonToFeatures(geojson)
polygon.edit()
@ -1199,7 +1200,7 @@ U.Polygon = L.Polygon.extend({
toPolyline: function () {
const geojson = this.toGeoJSON()
geojson.geometry.type = 'LineString'
geojson.geometry.coordinates = L.Util.flattenCoordinates(
geojson.geometry.coordinates = U.Utils.flattenCoordinates(
geojson.geometry.coordinates
)
const polyline = this.datalayer.geojsonToFeatures(geojson)

View file

@ -492,8 +492,8 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({
this.buildTabs()
const value = this.value()
if (U.Icon.RECENT.length) this.showRecentTab()
else if (!value || L.Util.isPath(value)) this.showSymbolsTab()
else if (L.Util.isRemoteUrl(value) || L.Util.isDataImage(value)) this.showURLTab()
else if (!value || U.Utils.isPath(value)) this.showSymbolsTab()
else if (U.Utils.isRemoteUrl(value) || U.Utils.isDataImage(value)) this.showURLTab()
else this.showCharsTab()
const closeButton = L.DomUtil.createButton(
'button action-button',
@ -567,7 +567,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({
updatePreview: function () {
this.buttons.innerHTML = ''
if (this.isDefault()) return
if (!L.Util.hasVar(this.value())) {
if (!U.Utils.hasVar(this.value())) {
// Do not try to render URL with variables
const box = L.DomUtil.create('div', 'umap-pictogram-choice', this.buttons)
L.DomEvent.on(box, 'click', this.onDefine, this)
@ -585,11 +585,11 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({
addIconPreview: function (pictogram, parent) {
const baseClass = 'umap-pictogram-choice',
value = pictogram.src,
search = L.Util.normalize(this.searchInput.value),
search = U.Utils.normalize(this.searchInput.value),
title = pictogram.attribution
? `${pictogram.name} — © ${pictogram.attribution}`
: pictogram.name || pictogram.src
if (search && L.Util.normalize(title).indexOf(search) === -1) return
if (search && U.Utils.normalize(title).indexOf(search) === -1) return
const className = value === this.value() ? `${baseClass} selected` : baseClass,
container = L.DomUtil.create('div', className, parent)
U.Icon.makeIconElement(value, container)
@ -637,7 +637,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({
categories[category].push(props)
}
const sorted = Object.entries(categories).toSorted(([a], [b]) =>
L.Util.naturalSort(a, b)
U.Utils.naturalSort(a, b, L.lang)
)
for (let [name, items] of sorted) {
this.addCategory(items, name)
@ -688,7 +688,7 @@ L.FormBuilder.IconUrl = L.FormBuilder.BlurInput.extend({
showURLTab: function () {
this.openTab('url')
const value =
L.Util.isRemoteUrl(this.value()) || L.Util.isDataImage(this.value())
U.Utils.isRemoteUrl(this.value()) || U.Utils.isDataImage(this.value())
? this.value()
: null
const input = this.buildInput(this.body, value)

View file

@ -18,7 +18,7 @@ U.Icon = L.DivIcon.extend({
},
_setRecent: function (url) {
if (L.Util.hasVar(url)) return
if (U.Utils.hasVar(url)) return
if (url === U.SCHEMA.iconUrl.default) return
if (U.Icon.RECENT.indexOf(url) === -1) {
U.Icon.RECENT.push(url)
@ -50,7 +50,10 @@ U.Icon = L.DivIcon.extend({
},
formatUrl: function (url, feature) {
return L.Util.greedyTemplate(url || '', feature ? feature.extendedProperties() : {})
return U.Utils.greedyTemplate(
url || '',
feature ? feature.extendedProperties() : {}
)
},
onAdd: function () {},
@ -206,7 +209,7 @@ U.Icon.Cluster = L.DivIcon.extend({
})
U.Icon.isImg = function (src) {
return L.Util.isPath(src) || L.Util.isRemoteUrl(src) || L.Util.isDataImage(src)
return U.Utils.isPath(src) || U.Utils.isRemoteUrl(src) || U.Utils.isDataImage(src)
}
U.Icon.makeIconElement = function (src, parent) {
@ -236,7 +239,11 @@ 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.SCHEMA.iconUrl.default) {
if (
U.Utils.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)'

View file

@ -102,7 +102,7 @@ U.Importer = L.Class.extend({
let type = '',
newType
for (let i = 0; i < e.target.files.length; i++) {
newType = L.Util.detectFileType(e.target.files[i])
newType = U.Utils.detectFileType(e.target.files[i])
if (!type && newType) type = newType
if (type && newType !== type) {
type = ''

View file

@ -76,6 +76,7 @@ U.Map = L.Map.extend({
.split(',')
}
let editedFeature = null
const self = this
try {
@ -732,7 +733,7 @@ U.Map = L.Map.extend({
},
getOption: function (option) {
if (L.Util.usableOption(this.options, option)) return this.options[option]
if (U.Utils.usableOption(this.options, option)) return this.options[option]
return this.getDefaultOption(option)
},
@ -789,7 +790,7 @@ U.Map = L.Map.extend({
},
processFileToImport: function (file, layer, type) {
type = type || L.Util.detectFileType(file)
type = type || U.Utils.detectFileType(file)
if (!type) {
this.ui.alert({
content: L._('Unable to detect format of file {filename}', {
@ -992,7 +993,7 @@ U.Map = L.Map.extend({
{
label: L._('Copy link'),
callback: () => {
L.Util.copyToClipboard(data.permissions.anonymous_edit_url)
U.Utils.copyToClipboard(data.permissions.anonymous_edit_url)
this.ui.alert({
content: L._('Secret edit link copied to clipboard!'),
level: 'info',
@ -1263,7 +1264,7 @@ U.Map = L.Map.extend({
},
_editTilelayer: function (container) {
if (!L.Util.isObject(this.options.tilelayer)) {
if (!U.Utils.isObject(this.options.tilelayer)) {
this.options.tilelayer = {}
}
const tilelayerFields = [
@ -1316,7 +1317,7 @@ U.Map = L.Map.extend({
},
_editOverlay: function (container) {
if (!L.Util.isObject(this.options.overlay)) {
if (!U.Utils.isObject(this.options.overlay)) {
this.options.overlay = {}
}
const overlayFields = [
@ -1367,7 +1368,7 @@ U.Map = L.Map.extend({
},
_editBounds: function (container) {
if (!L.Util.isObject(this.options.limitBounds)) {
if (!U.Utils.isObject(this.options.limitBounds)) {
this.options.limitBounds = {}
}
const limitBounds = L.DomUtil.createFieldset(container, L._('Limit bounds'))
@ -1401,10 +1402,10 @@ U.Map = L.Map.extend({
L._('Use current bounds'),
function () {
const bounds = this.getBounds()
this.options.limitBounds.south = L.Util.formatNum(bounds.getSouth())
this.options.limitBounds.west = L.Util.formatNum(bounds.getWest())
this.options.limitBounds.north = L.Util.formatNum(bounds.getNorth())
this.options.limitBounds.east = L.Util.formatNum(bounds.getEast())
this.options.limitBounds.south = U.Utils.formatNum(bounds.getSouth())
this.options.limitBounds.west = U.Utils.formatNum(bounds.getWest())
this.options.limitBounds.north = U.Utils.formatNum(bounds.getNorth())
this.options.limitBounds.east = U.Utils.formatNum(bounds.getEast())
boundsBuilder.fetchAll()
this.isDirty = true
this.handleLimitBounds()
@ -1796,12 +1797,12 @@ U.Map = L.Map.extend({
},
localizeUrl: function (url) {
return L.Util.greedyTemplate(url, this.getGeoContext(), true)
return U.Utils.greedyTemplate(url, this.getGeoContext(), true)
},
proxyUrl: function (url, ttl) {
if (this.options.urls.ajax_proxy) {
url = L.Util.greedyTemplate(this.options.urls.ajax_proxy, {
url = U.Utils.greedyTemplate(this.options.urls.ajax_proxy, {
url: encodeURIComponent(url),
ttl: ttl,
})

View file

@ -97,7 +97,7 @@ U.Layer.Cluster = L.MarkerClusterGroup.extend({
},
getEditableOptions: function () {
if (!L.Util.isObject(this.datalayer.options.cluster)) {
if (!U.Utils.isObject(this.datalayer.options.cluster)) {
this.datalayer.options.cluster = {}
}
return [
@ -155,7 +155,7 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
initialize: function (datalayer) {
this.datalayer = datalayer
if (!L.Util.isObject(this.datalayer.options.choropleth)) {
if (!U.Utils.isObject(this.datalayer.options.choropleth)) {
this.datalayer.options.choropleth = {}
}
L.FeatureGroup.prototype.initialize.call(
@ -381,7 +381,7 @@ U.Layer.Heat = L.HeatLayer.extend({
},
getEditableOptions: function () {
if (!L.Util.isObject(this.datalayer.options.heat)) {
if (!U.Utils.isObject(this.datalayer.options.heat)) {
this.datalayer.options.heat = {}
}
return [
@ -724,16 +724,16 @@ U.DataLayer = L.Evented.extend({
},
backupData: function () {
this._geojson_bk = L.Util.CopyJSON(this._geojson)
this._geojson_bk = U.Utils.CopyJSON(this._geojson)
},
reindex: function () {
const features = []
this.eachFeature((feature) => features.push(feature))
L.Util.sortFeatures(features, this.map.getOption('sortKey'))
U.Utils.sortFeatures(features, this.map.getOption('sortKey'), L.lang)
this._index = []
for (let i = 0; i < features.length; i++) {
this._index.push(L.Util.stamp(features[i]))
this._index.push(U.Utils.stamp(features[i]))
}
},
@ -793,16 +793,16 @@ U.DataLayer = L.Evented.extend({
},
backupOptions: function () {
this._backupOptions = L.Util.CopyJSON(this.options)
this._backupOptions = U.Utils.CopyJSON(this.options)
},
resetOptions: function () {
this.options = L.Util.CopyJSON(this._backupOptions)
this.options = U.Utils.CopyJSON(this._backupOptions)
},
setOptions: function (options) {
delete options.geojson
this.options = L.Util.CopyJSON(U.DataLayer.prototype.options) // Start from fresh.
this.options = U.Utils.CopyJSON(U.DataLayer.prototype.options) // Start from fresh.
this.updateOptions(options)
},
@ -824,7 +824,7 @@ U.DataLayer = L.Evented.extend({
_dataUrl: function () {
const template = this.map.options.urls.datalayer_view
let url = L.Util.template(template, {
let url = U.Utils.template(template, {
pk: this.umap_id,
map_id: this.map.options.umap_id,
})
@ -971,7 +971,7 @@ U.DataLayer = L.Evented.extend({
let latlngs
if (features) {
L.Util.sortFeatures(features, this.map.getOption('sortKey'))
U.Utils.sortFeatures(features, this.map.getOption('sortKey'), L.lang)
for (i = 0, len = features.length; i < len; i++) {
this.geojsonToFeatures(features[i])
}
@ -1061,7 +1061,7 @@ U.DataLayer = L.Evented.extend({
importFromFile: function (f, type) {
const reader = new FileReader()
type = type || L.Util.detectFileType(f)
type = type || U.Utils.detectFileType(f)
reader.readAsText(f)
reader.onload = (e) => this.importRaw(e.target.result, type)
},
@ -1079,21 +1079,21 @@ U.DataLayer = L.Evented.extend({
},
getDeleteUrl: function () {
return L.Util.template(this.map.options.urls.datalayer_delete, {
return U.Utils.template(this.map.options.urls.datalayer_delete, {
pk: this.umap_id,
map_id: this.map.options.umap_id,
})
},
getVersionsUrl: function () {
return L.Util.template(this.map.options.urls.datalayer_versions, {
return U.Utils.template(this.map.options.urls.datalayer_versions, {
pk: this.umap_id,
map_id: this.map.options.umap_id,
})
},
getVersionUrl: function (name) {
return L.Util.template(this.map.options.urls.datalayer_version, {
return U.Utils.template(this.map.options.urls.datalayer_version, {
pk: this.umap_id,
map_id: this.map.options.umap_id,
name: name,
@ -1112,10 +1112,10 @@ U.DataLayer = L.Evented.extend({
},
clone: function () {
const options = L.Util.CopyJSON(this.options)
const options = U.Utils.CopyJSON(this.options)
options.name = L._('Clone of {name}', { name: this.options.name })
delete options.id
const geojson = L.Util.CopyJSON(this._geojson),
const geojson = U.Utils.CopyJSON(this._geojson),
datalayer = this.map.createDataLayer(options)
datalayer.fromGeoJSON(geojson)
return datalayer
@ -1276,7 +1276,7 @@ U.DataLayer = L.Evented.extend({
)
popupFieldset.appendChild(builder.build())
if (!L.Util.isObject(this.options.remoteData)) {
if (!U.Utils.isObject(this.options.remoteData)) {
this.options.remoteData = {}
}
const remoteDataFields = [
@ -1371,7 +1371,7 @@ U.DataLayer = L.Evented.extend({
},
getOwnOption: function (option) {
if (L.Util.usableOption(this.options, option)) return this.options[option]
if (U.Utils.usableOption(this.options, option)) return this.options[option]
},
getOption: function (option, feature) {
@ -1668,6 +1668,6 @@ L.TileLayer.include({
},
getAttribution: function () {
return L.Util.toHTML(this.options.attribution)
return U.Utils.toHTML(this.options.attribution)
},
})

View file

@ -62,8 +62,7 @@ U.MapPermissions = L.Class.extend({
title = L.DomUtil.create('h3', '', container)
if (this.isAnonymousMap()) {
if (this.options.anonymous_edit_url) {
const helpText = `${L._('Secret edit link:')}<br>${
this.options.anonymous_edit_url
const helpText = `${L._('Secret edit link:')}<br>${this.options.anonymous_edit_url
}`
L.DomUtil.add('p', 'help-text', container, helpText)
fields.push([
@ -171,13 +170,13 @@ U.MapPermissions = L.Class.extend({
},
getUrl: function () {
return L.Util.template(this.map.options.urls.map_update_permissions, {
return U.Utils.template(this.map.options.urls.map_update_permissions, {
map_id: this.map.options.umap_id,
})
},
getAttachUrl: function () {
return L.Util.template(this.map.options.urls.map_attach_owner, {
return U.Utils.template(this.map.options.urls.map_attach_owner, {
map_id: this.map.options.umap_id,
})
},

View file

@ -124,12 +124,12 @@ U.PopupTemplate.Default = L.Class.extend({
let center
properties = this.feature.extendedProperties()
// Resolve properties inside description
properties.description = L.Util.greedyTemplate(
properties.description = U.Utils.greedyTemplate(
this.feature.properties.description || '',
properties
)
content = L.Util.greedyTemplate(template, properties)
content = L.Util.toHTML(content, { target: target })
content = U.Utils.greedyTemplate(template, properties)
content = U.Utils.toHTML(content, { target: target })
container.innerHTML = content
return container
},
@ -211,7 +211,7 @@ U.PopupTemplate.Table = U.PopupTemplate.BaseWithTitle.extend({
for (const key in this.feature.properties) {
if (typeof this.feature.properties[key] === 'object' || key === 'name') continue
// TODO, manage links (url, mailto, wikipedia...)
this.addRow(table, key, L.Util.escapeHTML(this.feature.properties[key]).trim())
this.addRow(table, key, U.Utils.escapeHTML(this.feature.properties[key]).trim())
}
return table
},

View file

@ -51,7 +51,7 @@ U.Share = L.Class.extend({
L.DomUtil.createCopiableInput(
this.container,
L._('Link to view the map'),
window.location.protocol + L.Util.getBaseUrl()
window.location.protocol + U.Utils.getBaseUrl()
)
if (this.map.options.shortUrl) {
@ -84,7 +84,7 @@ U.Share = L.Class.extend({
this.container,
L._('All data and settings of the map')
)
const downloadUrl = L.Util.template(this.map.options.urls.map_download, {
const downloadUrl = U.Utils.template(this.map.options.urls.map_download, {
map_id: this.map.options.umap_id,
})
const link = L.DomUtil.createLink(
@ -213,7 +213,7 @@ U.IframeExporter = L.Evented.extend({
initialize: function (map) {
this.map = map
this.baseUrl = L.Util.getBaseUrl()
this.baseUrl = U.Utils.getBaseUrl()
// Use map default, not generic default
this.queryString.onLoadPanel = this.map.getOption('onLoadPanel')
},
@ -241,7 +241,7 @@ U.IframeExporter = L.Evented.extend({
}
const currentView = this.options.currentView ? window.location.hash : ''
const queryString = L.extend({}, this.queryString, options)
return `${this.baseUrl}?${L.Util.buildQueryString(queryString)}${currentView}`
return `${this.baseUrl}?${U.Utils.buildQueryString(queryString)}${currentView}`
},
build: function () {

View file

@ -28,7 +28,6 @@
<script src="../vendors/iconlayers/iconLayers.js" defer></script>
<script src="../vendors/tokml/tokml.js" defer></script>
<script src="../vendors/locatecontrol/L.Control.Locate.min.js" defer></script>
<script src="../vendors/dompurify/purify.min.js" defer></script>
<script src="../vendors/colorbrewer/colorbrewer.js" defer></script>
<script src="../vendors/simple-statistics/simple-statistics.min.js" defer></script>

View file

@ -42,7 +42,6 @@
<script src="{% static 'umap/vendors/tokml/tokml.js' %}" defer></script>
<script src="{% static 'umap/vendors/locatecontrol/L.Control.Locate.min.js' %}"
defer></script>
<script src="{% static 'umap/vendors/dompurify/purify.min.js' %}" defer></script>
<script src="{% static 'umap/vendors/colorbrewer/colorbrewer.js' %}" defer></script>
<script src="{% static 'umap/vendors/simple-statistics/simple-statistics.min.js' %}"
defer></script>