mirror of
https://github.com/umap-project/umap.git
synced 2025-04-29 11:52:38 +02:00
Merge pull request #1989 from umap-project/layer-to-modules
chore: move layers to modules/
This commit is contained in:
commit
ab34765c30
13 changed files with 2054 additions and 1965 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -9,7 +9,7 @@ site/*
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
node_modules
|
node_modules
|
||||||
umap.conf
|
umap.conf
|
||||||
data
|
./data
|
||||||
./static
|
./static
|
||||||
|
|
||||||
### Python ###
|
### Python ###
|
||||||
|
|
|
@ -43,7 +43,6 @@ export default class Caption {
|
||||||
const p = DomUtil.create('p', 'datalayer-legend', container)
|
const p = DomUtil.create('p', 'datalayer-legend', container)
|
||||||
const legend = DomUtil.create('span', '', p)
|
const legend = DomUtil.create('span', '', p)
|
||||||
const headline = DomUtil.create('strong', '', p)
|
const headline = DomUtil.create('strong', '', p)
|
||||||
datalayer.onceLoaded(() => {
|
|
||||||
datalayer.renderLegend(legend)
|
datalayer.renderLegend(legend)
|
||||||
if (datalayer.options.description) {
|
if (datalayer.options.description) {
|
||||||
DomUtil.element({
|
DomUtil.element({
|
||||||
|
@ -52,7 +51,6 @@ export default class Caption {
|
||||||
safeHTML: Utils.toHTML(datalayer.options.description),
|
safeHTML: Utils.toHTML(datalayer.options.description),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
|
||||||
datalayer.renderToolbox(headline)
|
datalayer.renderToolbox(headline)
|
||||||
DomUtil.add('span', '', headline, `${datalayer.options.name} `)
|
DomUtil.add('span', '', headline, `${datalayer.options.name} `)
|
||||||
}
|
}
|
||||||
|
|
1283
umap/static/umap/js/modules/data/layer.js
Normal file
1283
umap/static/umap/js/modules/data/layer.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,9 +1,12 @@
|
||||||
import {
|
import {
|
||||||
uMapAlert as Alert,
|
uMapAlert as Alert,
|
||||||
uMapAlertConflict as AlertConflict,
|
|
||||||
uMapAlertCreation as AlertCreation,
|
uMapAlertCreation as AlertCreation,
|
||||||
} from '../components/alerts/alert.js'
|
} from '../components/alerts/alert.js'
|
||||||
import { AjaxAutocomplete, AjaxAutocompleteMultiple, AutocompleteDatalist } from './autocomplete.js'
|
import {
|
||||||
|
AjaxAutocomplete,
|
||||||
|
AjaxAutocompleteMultiple,
|
||||||
|
AutocompleteDatalist,
|
||||||
|
} from './autocomplete.js'
|
||||||
import Browser from './browser.js'
|
import Browser from './browser.js'
|
||||||
import Caption from './caption.js'
|
import Caption from './caption.js'
|
||||||
import Facets from './facets.js'
|
import Facets from './facets.js'
|
||||||
|
@ -23,6 +26,7 @@ import TableEditor from './tableeditor.js'
|
||||||
import Tooltip from './ui/tooltip.js'
|
import Tooltip from './ui/tooltip.js'
|
||||||
import URLs from './urls.js'
|
import URLs from './urls.js'
|
||||||
import * as Utils from './utils.js'
|
import * as Utils from './utils.js'
|
||||||
|
import { DataLayer, LAYER_TYPES } from './data/layer.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.
|
||||||
|
@ -31,12 +35,12 @@ import * as Utils from './utils.js'
|
||||||
window.U = {
|
window.U = {
|
||||||
Alert,
|
Alert,
|
||||||
AlertCreation,
|
AlertCreation,
|
||||||
AlertConflict,
|
|
||||||
AjaxAutocomplete,
|
AjaxAutocomplete,
|
||||||
AjaxAutocompleteMultiple,
|
AjaxAutocompleteMultiple,
|
||||||
AutocompleteDatalist,
|
AutocompleteDatalist,
|
||||||
Browser,
|
Browser,
|
||||||
Caption,
|
Caption,
|
||||||
|
DataLayer,
|
||||||
Dialog,
|
Dialog,
|
||||||
EditPanel,
|
EditPanel,
|
||||||
Facets,
|
Facets,
|
||||||
|
@ -45,6 +49,7 @@ window.U = {
|
||||||
Help,
|
Help,
|
||||||
HTTPError,
|
HTTPError,
|
||||||
Importer,
|
Importer,
|
||||||
|
LAYER_TYPES,
|
||||||
NOKError,
|
NOKError,
|
||||||
Orderable,
|
Orderable,
|
||||||
Panel,
|
Panel,
|
||||||
|
|
105
umap/static/umap/js/modules/rendering/layers/base.js
Normal file
105
umap/static/umap/js/modules/rendering/layers/base.js
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import { FeatureGroup, TileLayer } from '../../../../vendors/leaflet/leaflet-src.esm.js'
|
||||||
|
import { translate } from '../../i18n.js'
|
||||||
|
import * as Utils from '../../utils.js'
|
||||||
|
|
||||||
|
export const LayerMixin = {
|
||||||
|
browsable: true,
|
||||||
|
|
||||||
|
onInit: function (map) {
|
||||||
|
if (this.datalayer.autoLoaded()) map.on('zoomend', this.onZoomEnd, this)
|
||||||
|
},
|
||||||
|
|
||||||
|
onDelete: function (map) {
|
||||||
|
map.off('zoomend', this.onZoomEnd, this)
|
||||||
|
},
|
||||||
|
|
||||||
|
onAdd: function (map) {
|
||||||
|
map.on('moveend', this.onMoveEnd, this)
|
||||||
|
},
|
||||||
|
|
||||||
|
onRemove: function (map) {
|
||||||
|
map.off('moveend', this.onMoveEnd, this)
|
||||||
|
},
|
||||||
|
|
||||||
|
getType: function () {
|
||||||
|
const proto = Object.getPrototypeOf(this)
|
||||||
|
return proto.constructor.TYPE
|
||||||
|
},
|
||||||
|
|
||||||
|
getName: function () {
|
||||||
|
const proto = Object.getPrototypeOf(this)
|
||||||
|
return proto.constructor.NAME
|
||||||
|
},
|
||||||
|
|
||||||
|
getFeatures: function () {
|
||||||
|
return this._layers
|
||||||
|
},
|
||||||
|
|
||||||
|
getEditableOptions: () => [],
|
||||||
|
|
||||||
|
onEdit: () => {},
|
||||||
|
|
||||||
|
hasDataVisible: function () {
|
||||||
|
return !!Object.keys(this._layers).length
|
||||||
|
},
|
||||||
|
|
||||||
|
// Called when data changed on the datalayer
|
||||||
|
dataChanged: () => {},
|
||||||
|
|
||||||
|
onMoveEnd: function () {
|
||||||
|
if (this.datalayer.isRemoteLayer() && this.datalayer.showAtZoom()) {
|
||||||
|
this.datalayer.fetchRemoteData()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onZoomEnd() {
|
||||||
|
if (this.datalayer._forcedVisibility) return
|
||||||
|
if (!this.datalayer.showAtZoom() && this.datalayer.isVisible()) {
|
||||||
|
this.datalayer.hide()
|
||||||
|
}
|
||||||
|
if (this.datalayer.showAtZoom() && !this.datalayer.isVisible()) {
|
||||||
|
this.datalayer.show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Default = FeatureGroup.extend({
|
||||||
|
statics: {
|
||||||
|
NAME: translate('Default'),
|
||||||
|
TYPE: 'Default',
|
||||||
|
},
|
||||||
|
includes: [LayerMixin],
|
||||||
|
|
||||||
|
initialize: function (datalayer) {
|
||||||
|
this.datalayer = datalayer
|
||||||
|
FeatureGroup.prototype.initialize.call(this)
|
||||||
|
LayerMixin.onInit.call(this, this.datalayer.map)
|
||||||
|
},
|
||||||
|
|
||||||
|
onAdd: function (map) {
|
||||||
|
LayerMixin.onAdd.call(this, map)
|
||||||
|
return FeatureGroup.prototype.onAdd.call(this, map)
|
||||||
|
},
|
||||||
|
|
||||||
|
onRemove: function (map) {
|
||||||
|
LayerMixin.onRemove.call(this, map)
|
||||||
|
return FeatureGroup.prototype.onRemove.call(this, map)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
TileLayer.include({
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
minZoom: this.options.minZoom,
|
||||||
|
maxZoom: this.options.maxZoom,
|
||||||
|
attribution: this.options.attribution,
|
||||||
|
url_template: this._url,
|
||||||
|
name: this.options.name,
|
||||||
|
tms: this.options.tms,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getAttribution() {
|
||||||
|
return Utils.toHTML(this.options.attribution)
|
||||||
|
},
|
||||||
|
})
|
103
umap/static/umap/js/modules/rendering/layers/cluster.js
Normal file
103
umap/static/umap/js/modules/rendering/layers/cluster.js
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
// WARNING must be loaded dynamically, or at least after leaflet.markercluster
|
||||||
|
// Uses global L.MarkerCluster and L.MarkerClusterGroup, not exposed as ESM
|
||||||
|
// Uses global U.Icon not yet a module
|
||||||
|
import { translate } from '../../i18n.js'
|
||||||
|
import { LayerMixin } from './base.js'
|
||||||
|
import * as Utils from '../../utils.js'
|
||||||
|
|
||||||
|
const MarkerCluster = L.MarkerCluster.extend({
|
||||||
|
// Custom class so we can call computeTextColor
|
||||||
|
// when element is already on the DOM.
|
||||||
|
|
||||||
|
_initIcon: function () {
|
||||||
|
L.MarkerCluster.prototype._initIcon.call(this)
|
||||||
|
const div = this._icon.querySelector('div')
|
||||||
|
// Compute text color only when icon is added to the DOM.
|
||||||
|
div.style.color = this._iconObj.computeTextColor(div)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Cluster = L.MarkerClusterGroup.extend({
|
||||||
|
statics: {
|
||||||
|
NAME: translate('Clustered'),
|
||||||
|
TYPE: 'Cluster',
|
||||||
|
},
|
||||||
|
includes: [LayerMixin],
|
||||||
|
|
||||||
|
initialize: function (datalayer) {
|
||||||
|
this.datalayer = datalayer
|
||||||
|
if (!Utils.isObject(this.datalayer.options.cluster)) {
|
||||||
|
this.datalayer.options.cluster = {}
|
||||||
|
}
|
||||||
|
const options = {
|
||||||
|
polygonOptions: {
|
||||||
|
color: this.datalayer.getColor(),
|
||||||
|
},
|
||||||
|
iconCreateFunction: (cluster) => new U.Icon.Cluster(datalayer, cluster),
|
||||||
|
}
|
||||||
|
if (this.datalayer.options.cluster?.radius) {
|
||||||
|
options.maxClusterRadius = this.datalayer.options.cluster.radius
|
||||||
|
}
|
||||||
|
L.MarkerClusterGroup.prototype.initialize.call(this, options)
|
||||||
|
LayerMixin.onInit.call(this, this.datalayer.map)
|
||||||
|
this._markerCluster = MarkerCluster
|
||||||
|
this._layers = []
|
||||||
|
},
|
||||||
|
|
||||||
|
onAdd: function (map) {
|
||||||
|
LayerMixin.onAdd.call(this, map)
|
||||||
|
return L.MarkerClusterGroup.prototype.onAdd.call(this, map)
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
onRemove: function (map) {
|
||||||
|
// In some situation, the onRemove is called before the layer is really
|
||||||
|
// added to the map: basically when combining a defaultView=data + max/minZoom
|
||||||
|
// and loading the map at a zoom outside of that zoom range.
|
||||||
|
// FIXME: move this upstream (_unbindEvents should accept a map parameter
|
||||||
|
// instead of relying on this._map)
|
||||||
|
this._map = map
|
||||||
|
LayerMixin.onRemove.call(this, map)
|
||||||
|
return L.MarkerClusterGroup.prototype.onRemove.call(this, map)
|
||||||
|
},
|
||||||
|
|
||||||
|
addLayer: function (layer) {
|
||||||
|
this._layers.push(layer)
|
||||||
|
return L.MarkerClusterGroup.prototype.addLayer.call(this, layer)
|
||||||
|
},
|
||||||
|
|
||||||
|
removeLayer: function (layer) {
|
||||||
|
this._layers.splice(this._layers.indexOf(layer), 1)
|
||||||
|
return L.MarkerClusterGroup.prototype.removeLayer.call(this, layer)
|
||||||
|
},
|
||||||
|
|
||||||
|
getEditableOptions: () => [
|
||||||
|
[
|
||||||
|
'options.cluster.radius',
|
||||||
|
{
|
||||||
|
handler: 'BlurIntInput',
|
||||||
|
placeholder: translate('Clustering radius'),
|
||||||
|
helpText: translate('Override clustering radius (default 80)'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'options.cluster.textColor',
|
||||||
|
{
|
||||||
|
handler: 'TextColorPicker',
|
||||||
|
placeholder: translate('Auto'),
|
||||||
|
helpText: translate('Text color for the cluster label'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
onEdit: function (field, builder) {
|
||||||
|
if (field === 'options.cluster.radius') {
|
||||||
|
// No way to reset radius of an already instanciated MarkerClusterGroup...
|
||||||
|
this.datalayer.resetLayer(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (field === 'options.color') {
|
||||||
|
this.options.polygonOptions.color = this.datalayer.getColor()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
176
umap/static/umap/js/modules/rendering/layers/heat.js
Normal file
176
umap/static/umap/js/modules/rendering/layers/heat.js
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
// Uses global L.HeatLayer, not exposed as ESM
|
||||||
|
import { Marker, LatLng, latLngBounds, Bounds, point } from '../../../../vendors/leaflet/leaflet-src.esm.js'
|
||||||
|
import { LayerMixin } from './base.js'
|
||||||
|
import * as Utils from '../../utils.js'
|
||||||
|
import { translate } from '../../i18n.js'
|
||||||
|
|
||||||
|
export const Heat = L.HeatLayer.extend({
|
||||||
|
statics: {
|
||||||
|
NAME: translate('Heatmap'),
|
||||||
|
TYPE: 'Heat',
|
||||||
|
},
|
||||||
|
includes: [LayerMixin],
|
||||||
|
browsable: false,
|
||||||
|
|
||||||
|
initialize: function (datalayer) {
|
||||||
|
this.datalayer = datalayer
|
||||||
|
L.HeatLayer.prototype.initialize.call(this, [], this.datalayer.options.heat)
|
||||||
|
LayerMixin.onInit.call(this, this.datalayer.map)
|
||||||
|
if (!Utils.isObject(this.datalayer.options.heat)) {
|
||||||
|
this.datalayer.options.heat = {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addLayer: function (layer) {
|
||||||
|
if (layer instanceof Marker) {
|
||||||
|
let latlng = layer.getLatLng()
|
||||||
|
let alt
|
||||||
|
if (this.datalayer.options.heat?.intensityProperty) {
|
||||||
|
alt = Number.parseFloat(
|
||||||
|
layer.properties[this.datalayer.options.heat.intensityProperty || 0]
|
||||||
|
)
|
||||||
|
latlng = new LatLng(latlng.lat, latlng.lng, alt)
|
||||||
|
}
|
||||||
|
this.addLatLng(latlng)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onAdd: function (map) {
|
||||||
|
LayerMixin.onAdd.call(this, map)
|
||||||
|
return L.HeatLayer.prototype.onAdd.call(this, map)
|
||||||
|
},
|
||||||
|
|
||||||
|
onRemove: function (map) {
|
||||||
|
LayerMixin.onRemove.call(this, map)
|
||||||
|
return L.HeatLayer.prototype.onRemove.call(this, map)
|
||||||
|
},
|
||||||
|
|
||||||
|
clearLayers: function () {
|
||||||
|
this.setLatLngs([])
|
||||||
|
},
|
||||||
|
|
||||||
|
getFeatures: () => ({}),
|
||||||
|
|
||||||
|
getBounds: function () {
|
||||||
|
return latLngBounds(this._latlngs)
|
||||||
|
},
|
||||||
|
|
||||||
|
getEditableOptions: () => [
|
||||||
|
[
|
||||||
|
'options.heat.radius',
|
||||||
|
{
|
||||||
|
handler: 'Range',
|
||||||
|
min: 10,
|
||||||
|
max: 100,
|
||||||
|
step: 5,
|
||||||
|
label: translate('Heatmap radius'),
|
||||||
|
helpText: translate('Override heatmap radius (default 25)'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'options.heat.intensityProperty',
|
||||||
|
{
|
||||||
|
handler: 'BlurInput',
|
||||||
|
placeholder: translate('Heatmap intensity property'),
|
||||||
|
helpText: translate('Optional intensity property for heatmap'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
onEdit: function (field, builder) {
|
||||||
|
if (field === 'options.heat.intensityProperty') {
|
||||||
|
this.datalayer.resetLayer(true) // We need to repopulate the latlngs
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (field === 'options.heat.radius') {
|
||||||
|
this.options.radius = this.datalayer.options.heat.radius
|
||||||
|
}
|
||||||
|
this._updateOptions()
|
||||||
|
},
|
||||||
|
|
||||||
|
redraw: function () {
|
||||||
|
// setlalngs call _redraw through setAnimFrame, thus async, so this
|
||||||
|
// can ends with race condition if we remove the layer very faslty after.
|
||||||
|
// TODO: PR in upstream Leaflet.heat
|
||||||
|
if (!this._map) return
|
||||||
|
L.HeatLayer.prototype.redraw.call(this)
|
||||||
|
},
|
||||||
|
|
||||||
|
_redraw: function () {
|
||||||
|
// Import patch from https://github.com/Leaflet/Leaflet.heat/pull/78
|
||||||
|
// Remove me when this get merged and released.
|
||||||
|
if (!this._map) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const data = []
|
||||||
|
const r = this._heat._r
|
||||||
|
const size = this._map.getSize()
|
||||||
|
const bounds = new Bounds(point([-r, -r]), size.add([r, r]))
|
||||||
|
const cellSize = r / 2
|
||||||
|
const grid = []
|
||||||
|
const panePos = this._map._getMapPanePos()
|
||||||
|
const offsetX = panePos.x % cellSize
|
||||||
|
const offsetY = panePos.y % cellSize
|
||||||
|
let i
|
||||||
|
let len
|
||||||
|
let p
|
||||||
|
let cell
|
||||||
|
let x
|
||||||
|
let y
|
||||||
|
let j
|
||||||
|
let len2
|
||||||
|
|
||||||
|
this._max = 1
|
||||||
|
|
||||||
|
for (i = 0, len = this._latlngs.length; i < len; i++) {
|
||||||
|
p = this._map.latLngToContainerPoint(this._latlngs[i])
|
||||||
|
x = Math.floor((p.x - offsetX) / cellSize) + 2
|
||||||
|
y = Math.floor((p.y - offsetY) / cellSize) + 2
|
||||||
|
|
||||||
|
const alt =
|
||||||
|
this._latlngs[i].alt !== undefined
|
||||||
|
? this._latlngs[i].alt
|
||||||
|
: this._latlngs[i][2] !== undefined
|
||||||
|
? +this._latlngs[i][2]
|
||||||
|
: 1
|
||||||
|
|
||||||
|
grid[y] = grid[y] || []
|
||||||
|
cell = grid[y][x]
|
||||||
|
|
||||||
|
if (!cell) {
|
||||||
|
cell = grid[y][x] = [p.x, p.y, alt]
|
||||||
|
cell.p = p
|
||||||
|
} else {
|
||||||
|
cell[0] = (cell[0] * cell[2] + p.x * alt) / (cell[2] + alt) // x
|
||||||
|
cell[1] = (cell[1] * cell[2] + p.y * alt) / (cell[2] + alt) // y
|
||||||
|
cell[2] += alt // cumulated intensity value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the max for the current zoom level
|
||||||
|
if (cell[2] > this._max) {
|
||||||
|
this._max = cell[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._heat.max(this._max)
|
||||||
|
|
||||||
|
for (i = 0, len = grid.length; i < len; i++) {
|
||||||
|
if (grid[i]) {
|
||||||
|
for (j = 0, len2 = grid[i].length; j < len2; j++) {
|
||||||
|
cell = grid[i][j]
|
||||||
|
if (cell && bounds.contains(cell.p)) {
|
||||||
|
data.push([
|
||||||
|
Math.round(cell[0]),
|
||||||
|
Math.round(cell[1]),
|
||||||
|
Math.min(cell[2], this._max),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._heat.data(data).draw(this.options.minOpacity)
|
||||||
|
|
||||||
|
this._frame = null
|
||||||
|
},
|
||||||
|
})
|
364
umap/static/umap/js/modules/rendering/layers/relative.js
Normal file
364
umap/static/umap/js/modules/rendering/layers/relative.js
Normal file
|
@ -0,0 +1,364 @@
|
||||||
|
import { FeatureGroup, DomUtil } from '../../../../vendors/leaflet/leaflet-src.esm.js'
|
||||||
|
import { translate } from '../../i18n.js'
|
||||||
|
import { LayerMixin } from './base.js'
|
||||||
|
import * as Utils from '../../utils.js'
|
||||||
|
|
||||||
|
// Layer where each feature color is relative to the others,
|
||||||
|
// so we need all features before behing able to set one
|
||||||
|
// feature layer
|
||||||
|
const RelativeColorLayerMixin = {
|
||||||
|
initialize: function (datalayer) {
|
||||||
|
this.datalayer = datalayer
|
||||||
|
this.colorSchemes = Object.keys(colorbrewer)
|
||||||
|
.filter((k) => k !== 'schemeGroups')
|
||||||
|
.sort()
|
||||||
|
const key = this.getType().toLowerCase()
|
||||||
|
if (!Utils.isObject(this.datalayer.options[key])) {
|
||||||
|
this.datalayer.options[key] = {}
|
||||||
|
}
|
||||||
|
FeatureGroup.prototype.initialize.call(this, [], this.datalayer.options[key])
|
||||||
|
LayerMixin.onInit.call(this, this.datalayer.map)
|
||||||
|
},
|
||||||
|
|
||||||
|
dataChanged: function () {
|
||||||
|
this.redraw()
|
||||||
|
},
|
||||||
|
|
||||||
|
redraw: function () {
|
||||||
|
this.compute()
|
||||||
|
if (this._map) this.eachLayer(this._map.addLayer, this._map)
|
||||||
|
},
|
||||||
|
|
||||||
|
getOption: function (option, feature) {
|
||||||
|
if (feature && option === feature.staticOptions.mainColor) {
|
||||||
|
return this.getColor(feature)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addLayer: function (layer) {
|
||||||
|
// Do not add yet the layer to the map
|
||||||
|
// wait for datachanged event, so we can compute breaks only once
|
||||||
|
const id = this.getLayerId(layer)
|
||||||
|
this._layers[id] = layer
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
onAdd: function (map) {
|
||||||
|
this.compute()
|
||||||
|
LayerMixin.onAdd.call(this, map)
|
||||||
|
return FeatureGroup.prototype.onAdd.call(this, map)
|
||||||
|
},
|
||||||
|
|
||||||
|
onRemove: function (map) {
|
||||||
|
LayerMixin.onRemove.call(this, map)
|
||||||
|
return FeatureGroup.prototype.onRemove.call(this, map)
|
||||||
|
},
|
||||||
|
|
||||||
|
getValues: function () {
|
||||||
|
const values = []
|
||||||
|
this.datalayer.eachLayer((layer) => {
|
||||||
|
const value = this._getValue(layer)
|
||||||
|
if (value !== undefined) values.push(value)
|
||||||
|
})
|
||||||
|
return values
|
||||||
|
},
|
||||||
|
|
||||||
|
renderLegend: function (container) {
|
||||||
|
const parent = DomUtil.create('ul', '', container)
|
||||||
|
const items = this.getLegendItems()
|
||||||
|
for (const [color, label] of items) {
|
||||||
|
const li = DomUtil.create('li', '', parent)
|
||||||
|
const colorEl = DomUtil.create('span', 'datalayer-color', li)
|
||||||
|
colorEl.style.backgroundColor = color
|
||||||
|
const labelEl = DomUtil.create('span', '', li)
|
||||||
|
labelEl.textContent = label
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getColorSchemes: function (classes) {
|
||||||
|
return this.colorSchemes.filter((scheme) => Boolean(colorbrewer[scheme][classes]))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Choropleth = FeatureGroup.extend({
|
||||||
|
statics: {
|
||||||
|
NAME: translate('Choropleth'),
|
||||||
|
TYPE: 'Choropleth',
|
||||||
|
},
|
||||||
|
includes: [LayerMixin, RelativeColorLayerMixin],
|
||||||
|
// Have defaults that better suit the choropleth mode.
|
||||||
|
defaults: {
|
||||||
|
color: 'white',
|
||||||
|
fillColor: 'red',
|
||||||
|
fillOpacity: 0.7,
|
||||||
|
weight: 2,
|
||||||
|
},
|
||||||
|
MODES: {
|
||||||
|
kmeans: translate('K-means'),
|
||||||
|
equidistant: translate('Equidistant'),
|
||||||
|
jenks: translate('Jenks-Fisher'),
|
||||||
|
quantiles: translate('Quantiles'),
|
||||||
|
manual: translate('Manual'),
|
||||||
|
},
|
||||||
|
|
||||||
|
_getValue: function (feature) {
|
||||||
|
const key = this.datalayer.options.choropleth.property || 'value'
|
||||||
|
const value = +feature.properties[key]
|
||||||
|
if (!Number.isNaN(value)) return value
|
||||||
|
},
|
||||||
|
|
||||||
|
compute: function () {
|
||||||
|
const values = this.getValues()
|
||||||
|
|
||||||
|
if (!values.length) {
|
||||||
|
this.options.breaks = []
|
||||||
|
this.options.colors = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const mode = this.datalayer.options.choropleth.mode
|
||||||
|
let classes = +this.datalayer.options.choropleth.classes || 5
|
||||||
|
let breaks
|
||||||
|
classes = Math.min(classes, values.length)
|
||||||
|
if (mode === 'manual') {
|
||||||
|
const manualBreaks = this.datalayer.options.choropleth.breaks
|
||||||
|
if (manualBreaks) {
|
||||||
|
breaks = manualBreaks
|
||||||
|
.split(',')
|
||||||
|
.map((b) => +b)
|
||||||
|
.filter((b) => !Number.isNaN(b))
|
||||||
|
}
|
||||||
|
} else if (mode === 'equidistant') {
|
||||||
|
breaks = ss.equalIntervalBreaks(values, classes)
|
||||||
|
} else if (mode === 'jenks') {
|
||||||
|
breaks = ss.jenks(values, classes)
|
||||||
|
} else if (mode === 'quantiles') {
|
||||||
|
const quantiles = [...Array(classes)].map((e, i) => i / classes).concat(1)
|
||||||
|
breaks = ss.quantile(values, quantiles)
|
||||||
|
} else {
|
||||||
|
breaks = ss.ckmeans(values, classes).map((cluster) => cluster[0])
|
||||||
|
breaks.push(ss.max(values)) // Needed for computing the legend
|
||||||
|
}
|
||||||
|
this.options.breaks = breaks || []
|
||||||
|
this.datalayer.options.choropleth.breaks = this.options.breaks
|
||||||
|
.map((b) => +b.toFixed(2))
|
||||||
|
.join(',')
|
||||||
|
const fillColor = this.datalayer.getOption('fillColor') || this.defaults.fillColor
|
||||||
|
let colorScheme = this.datalayer.options.choropleth.brewer
|
||||||
|
if (!colorbrewer[colorScheme]) colorScheme = 'Blues'
|
||||||
|
this.options.colors = colorbrewer[colorScheme][this.options.breaks.length - 1] || []
|
||||||
|
},
|
||||||
|
|
||||||
|
getColor: function (feature) {
|
||||||
|
if (!feature) return // FIXME should not happen
|
||||||
|
const featureValue = this._getValue(feature)
|
||||||
|
// Find the bucket/step/limit that this value is less than and give it that color
|
||||||
|
for (let i = 1; i < this.options.breaks.length; i++) {
|
||||||
|
if (featureValue <= this.options.breaks[i]) {
|
||||||
|
return this.options.colors[i - 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onEdit: function (field, builder) {
|
||||||
|
// Only compute the breaks if we're dealing with choropleth
|
||||||
|
if (!field.startsWith('options.choropleth')) return
|
||||||
|
// If user touches the breaks, then force manual mode
|
||||||
|
if (field === 'options.choropleth.breaks') {
|
||||||
|
this.datalayer.options.choropleth.mode = 'manual'
|
||||||
|
if (builder) builder.helpers['options.choropleth.mode'].fetch()
|
||||||
|
}
|
||||||
|
this.compute()
|
||||||
|
// If user changes the mode or the number of classes,
|
||||||
|
// then update the breaks input value
|
||||||
|
if (field === 'options.choropleth.mode' || field === 'options.choropleth.classes') {
|
||||||
|
if (builder) builder.helpers['options.choropleth.breaks'].fetch()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getEditableOptions: function () {
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'options.choropleth.property',
|
||||||
|
{
|
||||||
|
handler: 'Select',
|
||||||
|
selectOptions: this.datalayer._propertiesIndex,
|
||||||
|
label: translate('Choropleth property value'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'options.choropleth.brewer',
|
||||||
|
{
|
||||||
|
handler: 'Select',
|
||||||
|
label: translate('Choropleth color palette'),
|
||||||
|
selectOptions: this.colorSchemes,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'options.choropleth.classes',
|
||||||
|
{
|
||||||
|
handler: 'Range',
|
||||||
|
min: 3,
|
||||||
|
max: 9,
|
||||||
|
step: 1,
|
||||||
|
label: translate('Choropleth classes'),
|
||||||
|
helpText: translate('Number of desired classes (default 5)'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'options.choropleth.breaks',
|
||||||
|
{
|
||||||
|
handler: 'BlurInput',
|
||||||
|
label: translate('Choropleth breakpoints'),
|
||||||
|
helpText: translate(
|
||||||
|
'Comma separated list of numbers, including min and max values.'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'options.choropleth.mode',
|
||||||
|
{
|
||||||
|
handler: 'MultiChoice',
|
||||||
|
default: 'kmeans',
|
||||||
|
choices: Object.entries(this.MODES),
|
||||||
|
label: translate('Choropleth mode'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
getLegendItems: function () {
|
||||||
|
return this.options.breaks.slice(0, -1).map((el, index) => {
|
||||||
|
const from = +this.options.breaks[index].toFixed(1)
|
||||||
|
const to = +this.options.breaks[index + 1].toFixed(1)
|
||||||
|
return [this.options.colors[index], `${from} - ${to}`]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Categorized = FeatureGroup.extend({
|
||||||
|
statics: {
|
||||||
|
NAME: translate('Categorized'),
|
||||||
|
TYPE: 'Categorized',
|
||||||
|
},
|
||||||
|
includes: [LayerMixin, RelativeColorLayerMixin],
|
||||||
|
MODES: {
|
||||||
|
manual: translate('Manual'),
|
||||||
|
alpha: translate('Alphabetical'),
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
color: 'white',
|
||||||
|
fillColor: 'red',
|
||||||
|
fillOpacity: 0.7,
|
||||||
|
weight: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
_getValue: function (feature) {
|
||||||
|
const key =
|
||||||
|
this.datalayer.options.categorized.property || this.datalayer._propertiesIndex[0]
|
||||||
|
return feature.properties[key]
|
||||||
|
},
|
||||||
|
|
||||||
|
getColor: function (feature) {
|
||||||
|
if (!feature) return // FIXME should not happen
|
||||||
|
const featureValue = this._getValue(feature)
|
||||||
|
for (let i = 0; i < this.options.categories.length; i++) {
|
||||||
|
if (featureValue === this.options.categories[i]) {
|
||||||
|
return this.options.colors[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
compute: function () {
|
||||||
|
const values = this.getValues()
|
||||||
|
|
||||||
|
if (!values.length) {
|
||||||
|
this.options.categories = []
|
||||||
|
this.options.colors = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const mode = this.datalayer.options.categorized.mode
|
||||||
|
let categories = []
|
||||||
|
if (mode === 'manual') {
|
||||||
|
const manualCategories = this.datalayer.options.categorized.categories
|
||||||
|
if (manualCategories) {
|
||||||
|
categories = manualCategories.split(',')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
categories = values
|
||||||
|
.filter((val, idx, arr) => arr.indexOf(val) === idx)
|
||||||
|
.sort(Utils.naturalSort)
|
||||||
|
}
|
||||||
|
this.options.categories = categories
|
||||||
|
this.datalayer.options.categorized.categories = this.options.categories.join(',')
|
||||||
|
const fillColor = this.datalayer.getOption('fillColor') || this.defaults.fillColor
|
||||||
|
const colorScheme = this.datalayer.options.categorized.brewer
|
||||||
|
this._classes = this.options.categories.length
|
||||||
|
if (colorbrewer[colorScheme]?.[this._classes]) {
|
||||||
|
this.options.colors = colorbrewer[colorScheme][this._classes]
|
||||||
|
} else {
|
||||||
|
this.options.colors = colorbrewer?.Accent[this._classes]
|
||||||
|
? colorbrewer?.Accent[this._classes]
|
||||||
|
: U.COLORS // Fixme: move COLORS to modules/
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getEditableOptions: function () {
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'options.categorized.property',
|
||||||
|
{
|
||||||
|
handler: 'Select',
|
||||||
|
selectOptions: this.datalayer._propertiesIndex,
|
||||||
|
label: translate('Category property'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'options.categorized.brewer',
|
||||||
|
{
|
||||||
|
handler: 'Select',
|
||||||
|
label: translate('Color palette'),
|
||||||
|
selectOptions: this.getColorSchemes(this._classes),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'options.categorized.categories',
|
||||||
|
{
|
||||||
|
handler: 'BlurInput',
|
||||||
|
label: translate('Categories'),
|
||||||
|
helpText: translate('Comma separated list of categories.'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'options.categorized.mode',
|
||||||
|
{
|
||||||
|
handler: 'MultiChoice',
|
||||||
|
default: 'alpha',
|
||||||
|
choices: Object.entries(this.MODES),
|
||||||
|
label: translate('Categories mode'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
onEdit: function (field, builder) {
|
||||||
|
// Only compute the categories if we're dealing with categorized
|
||||||
|
if (!field.startsWith('options.categorized')) return
|
||||||
|
// If user touches the categories, then force manual mode
|
||||||
|
if (field === 'options.categorized.categories') {
|
||||||
|
this.datalayer.options.categorized.mode = 'manual'
|
||||||
|
if (builder) builder.helpers['options.categorized.mode'].fetch()
|
||||||
|
}
|
||||||
|
this.compute()
|
||||||
|
// If user changes the mode
|
||||||
|
// then update the categories input value
|
||||||
|
if (field === 'options.categorized.mode') {
|
||||||
|
if (builder) builder.helpers['options.categorized.categories'].fetch()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getLegendItems: function () {
|
||||||
|
return this.options.categories.map((limit, index) => {
|
||||||
|
return [this.options.colors[index], this.options.categories[index]]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
|
@ -318,7 +318,7 @@ export default class TableEditor extends WithTemplate {
|
||||||
feature.del()
|
feature.del()
|
||||||
}
|
}
|
||||||
this.datalayer.show()
|
this.datalayer.show()
|
||||||
this.datalayer.fire('datachanged')
|
this.datalayer.dataChanged()
|
||||||
this.renderBody()
|
this.renderBody()
|
||||||
if (this.map.browser.isOpen()) {
|
if (this.map.browser.isOpen()) {
|
||||||
this.map.browser.resetFilters()
|
this.map.browser.resetFilters()
|
||||||
|
|
|
@ -562,107 +562,6 @@ L.Control.Embed = L.Control.Button.extend({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
U.DataLayer.include({
|
|
||||||
renderLegend: function (container) {
|
|
||||||
if (this.layer.renderLegend) return this.layer.renderLegend(container)
|
|
||||||
const color = L.DomUtil.create('span', 'datalayer-color', container)
|
|
||||||
color.style.backgroundColor = this.getColor()
|
|
||||||
},
|
|
||||||
|
|
||||||
renderToolbox: function (container) {
|
|
||||||
const toggle = L.DomUtil.createButtonIcon(
|
|
||||||
container,
|
|
||||||
'icon-eye',
|
|
||||||
L._('Show/hide layer')
|
|
||||||
)
|
|
||||||
const zoomTo = L.DomUtil.createButtonIcon(
|
|
||||||
container,
|
|
||||||
'icon-zoom',
|
|
||||||
L._('Zoom to layer extent')
|
|
||||||
)
|
|
||||||
const edit = L.DomUtil.createButtonIcon(
|
|
||||||
container,
|
|
||||||
'icon-edit show-on-edit',
|
|
||||||
L._('Edit')
|
|
||||||
)
|
|
||||||
const table = L.DomUtil.createButtonIcon(
|
|
||||||
container,
|
|
||||||
'icon-table show-on-edit',
|
|
||||||
L._('Edit properties in a table')
|
|
||||||
)
|
|
||||||
const remove = L.DomUtil.createButtonIcon(
|
|
||||||
container,
|
|
||||||
'icon-delete show-on-edit',
|
|
||||||
L._('Delete layer')
|
|
||||||
)
|
|
||||||
if (this.isReadOnly()) {
|
|
||||||
L.DomUtil.addClass(container, 'readonly')
|
|
||||||
} else {
|
|
||||||
L.DomEvent.on(edit, 'click', this.edit, this)
|
|
||||||
L.DomEvent.on(table, 'click', this.tableEdit, this)
|
|
||||||
L.DomEvent.on(
|
|
||||||
remove,
|
|
||||||
'click',
|
|
||||||
function () {
|
|
||||||
if (!this.isVisible()) return
|
|
||||||
if (!confirm(L._('Are you sure you want to delete this layer?'))) return
|
|
||||||
this._delete()
|
|
||||||
},
|
|
||||||
this
|
|
||||||
)
|
|
||||||
}
|
|
||||||
L.DomEvent.on(toggle, 'click', this.toggle, this)
|
|
||||||
L.DomEvent.on(zoomTo, 'click', this.zoomTo, this)
|
|
||||||
container.classList.add(this.getHidableClass())
|
|
||||||
container.classList.toggle('off', !this.isVisible())
|
|
||||||
},
|
|
||||||
|
|
||||||
getHidableElements: function () {
|
|
||||||
return document.querySelectorAll(`.${this.getHidableClass()}`)
|
|
||||||
},
|
|
||||||
|
|
||||||
getHidableClass: function () {
|
|
||||||
return `show_with_datalayer_${L.stamp(this)}`
|
|
||||||
},
|
|
||||||
|
|
||||||
propagateDelete: function () {
|
|
||||||
const els = this.getHidableElements()
|
|
||||||
for (const el of els) {
|
|
||||||
L.DomUtil.remove(el)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
propagateRemote: function () {
|
|
||||||
const els = this.getHidableElements()
|
|
||||||
for (const el of els) {
|
|
||||||
el.classList.toggle('remotelayer', this.isRemoteLayer())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
propagateHide: function () {
|
|
||||||
const els = this.getHidableElements()
|
|
||||||
for (let i = 0; i < els.length; i++) {
|
|
||||||
L.DomUtil.addClass(els[i], 'off')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
propagateShow: function () {
|
|
||||||
this.onceLoaded(function () {
|
|
||||||
const els = this.getHidableElements()
|
|
||||||
for (let i = 0; i < els.length; i++) {
|
|
||||||
L.DomUtil.removeClass(els[i], 'off')
|
|
||||||
}
|
|
||||||
}, this)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
U.DataLayer.addInitHook(function () {
|
|
||||||
this.on('hide', this.propagateHide)
|
|
||||||
this.on('show', this.propagateShow)
|
|
||||||
this.on('erase', this.propagateDelete)
|
|
||||||
if (this.isVisible()) this.propagateShow()
|
|
||||||
})
|
|
||||||
|
|
||||||
const ControlsMixin = {
|
const ControlsMixin = {
|
||||||
HIDDABLE_CONTROLS: [
|
HIDDABLE_CONTROLS: [
|
||||||
'zoom',
|
'zoom',
|
||||||
|
|
|
@ -342,14 +342,7 @@ L.FormBuilder.TextColorPicker = L.FormBuilder.ColorPicker.extend({
|
||||||
|
|
||||||
L.FormBuilder.LayerTypeChooser = L.FormBuilder.Select.extend({
|
L.FormBuilder.LayerTypeChooser = L.FormBuilder.Select.extend({
|
||||||
getOptions: () => {
|
getOptions: () => {
|
||||||
const layer_classes = [
|
return U.LAYER_TYPES.map((class_) => [class_.TYPE, class_.NAME])
|
||||||
U.Layer.Default,
|
|
||||||
U.Layer.Cluster,
|
|
||||||
U.Layer.Heat,
|
|
||||||
U.Layer.Choropleth,
|
|
||||||
U.Layer.Categorized,
|
|
||||||
]
|
|
||||||
return layer_classes.map((class_) => [class_.TYPE, class_.NAME])
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -367,6 +360,7 @@ L.FormBuilder.DataLayerSwitcher = L.FormBuilder.Select.extend({
|
||||||
getOptions: function () {
|
getOptions: function () {
|
||||||
const options = []
|
const options = []
|
||||||
this.builder.map.eachDataLayerReverse((datalayer) => {
|
this.builder.map.eachDataLayerReverse((datalayer) => {
|
||||||
|
console.log(datalayer.isLoaded(), datalayer.isDataReadOnly(), datalayer.isBrowsable())
|
||||||
if (
|
if (
|
||||||
datalayer.isLoaded() &&
|
datalayer.isLoaded() &&
|
||||||
!datalayer.isDataReadOnly() &&
|
!datalayer.isDataReadOnly() &&
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -5,6 +5,9 @@
|
||||||
<script type="module"
|
<script type="module"
|
||||||
src="{% static 'umap/js/modules/leaflet-configure.js' %}"
|
src="{% static 'umap/js/modules/leaflet-configure.js' %}"
|
||||||
defer></script>
|
defer></script>
|
||||||
|
<script src="{% static 'umap/vendors/markercluster/leaflet.markercluster.js' %}"
|
||||||
|
defer></script>
|
||||||
|
<script src="{% static 'umap/vendors/heat/leaflet-heat.js' %}" defer></script>
|
||||||
{% if locale %}
|
{% if locale %}
|
||||||
{% with "umap/locale/"|add:locale|add:".js" as path %}
|
{% with "umap/locale/"|add:locale|add:".js" as path %}
|
||||||
<script src="{% static path %}" defer></script>
|
<script src="{% static path %}" defer></script>
|
||||||
|
@ -21,14 +24,11 @@
|
||||||
<script src="{% static 'umap/vendors/csv2geojson/csv2geojson.js' %}" defer></script>
|
<script src="{% static 'umap/vendors/csv2geojson/csv2geojson.js' %}" defer></script>
|
||||||
<script src="{% static 'umap/vendors/osmtogeojson/osmtogeojson.js' %}" defer></script>
|
<script src="{% static 'umap/vendors/osmtogeojson/osmtogeojson.js' %}" defer></script>
|
||||||
<script src="{% static 'umap/vendors/loading/Control.Loading.js' %}" defer></script>
|
<script src="{% static 'umap/vendors/loading/Control.Loading.js' %}" defer></script>
|
||||||
<script src="{% static 'umap/vendors/markercluster/leaflet.markercluster.js' %}"
|
|
||||||
defer></script>
|
|
||||||
<script src="{% static 'umap/vendors/contextmenu/leaflet.contextmenu.min.js' %}"
|
<script src="{% static 'umap/vendors/contextmenu/leaflet.contextmenu.min.js' %}"
|
||||||
defer></script>
|
defer></script>
|
||||||
<script src="{% static 'umap/vendors/photon/leaflet.photon.js' %}" defer></script>
|
<script src="{% static 'umap/vendors/photon/leaflet.photon.js' %}" defer></script>
|
||||||
<script src="{% static 'umap/vendors/georsstogeojson/GeoRSSToGeoJSON.js' %}"
|
<script src="{% static 'umap/vendors/georsstogeojson/GeoRSSToGeoJSON.js' %}"
|
||||||
defer></script>
|
defer></script>
|
||||||
<script src="{% static 'umap/vendors/heat/leaflet-heat.js' %}" defer></script>
|
|
||||||
<script src="{% static 'umap/vendors/fullscreen/Leaflet.fullscreen.min.js' %}"
|
<script src="{% static 'umap/vendors/fullscreen/Leaflet.fullscreen.min.js' %}"
|
||||||
defer></script>
|
defer></script>
|
||||||
<script src="{% static 'umap/vendors/toolbar/leaflet.toolbar.js' %}" defer></script>
|
<script src="{% static 'umap/vendors/toolbar/leaflet.toolbar.js' %}" defer></script>
|
||||||
|
@ -49,7 +49,6 @@
|
||||||
<script src="{% static 'umap/js/umap.features.js' %}" defer></script>
|
<script src="{% static 'umap/js/umap.features.js' %}" defer></script>
|
||||||
<script src="{% static 'umap/js/umap.permissions.js' %}" defer></script>
|
<script src="{% static 'umap/js/umap.permissions.js' %}" defer></script>
|
||||||
<script src="{% static 'umap/js/umap.datalayer.permissions.js' %}" defer></script>
|
<script src="{% static 'umap/js/umap.datalayer.permissions.js' %}" defer></script>
|
||||||
<script src="{% static 'umap/js/umap.layer.js' %}" defer></script>
|
|
||||||
<script src="{% static 'umap/js/umap.controls.js' %}" defer></script>
|
<script src="{% static 'umap/js/umap.controls.js' %}" defer></script>
|
||||||
<script src="{% static 'umap/js/umap.js' %}" defer></script>
|
<script src="{% static 'umap/js/umap.js' %}" defer></script>
|
||||||
<script src="{% static 'umap/js/components/fragment.js' %}" defer></script>
|
<script src="{% static 'umap/js/components/fragment.js' %}" defer></script>
|
||||||
|
|
Loading…
Reference in a new issue