mirror of
https://github.com/umap-project/umap.git
synced 2025-04-28 19:42:36 +02:00
chore: move layers to modules/
This is a small step in the direction of spliting the data part and the rendering part. Basically in modules/data relies the data part, and in modules/rendering the rendering part, which at some point in the history should be the only place where we use and inherit from Leaflet, including utils and such.
This commit is contained in:
parent
54266c7d34
commit
abbd0e4803
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