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:
Yohan Boniface 2024-07-11 16:02:03 +02:00
parent 54266c7d34
commit abbd0e4803
13 changed files with 2054 additions and 1965 deletions

2
.gitignore vendored
View file

@ -9,7 +9,7 @@ site/*
.pytest_cache/
node_modules
umap.conf
data
./data
./static
### Python ###

View file

@ -43,16 +43,14 @@ export default class Caption {
const p = DomUtil.create('p', 'datalayer-legend', container)
const legend = DomUtil.create('span', '', p)
const headline = DomUtil.create('strong', '', p)
datalayer.onceLoaded(() => {
datalayer.renderLegend(legend)
if (datalayer.options.description) {
DomUtil.element({
tagName: 'span',
parent: p,
safeHTML: Utils.toHTML(datalayer.options.description),
})
}
})
datalayer.renderLegend(legend)
if (datalayer.options.description) {
DomUtil.element({
tagName: 'span',
parent: p,
safeHTML: Utils.toHTML(datalayer.options.description),
})
}
datalayer.renderToolbox(headline)
DomUtil.add('span', '', headline, `${datalayer.options.name} `)
}

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,12 @@
import {
uMapAlert as Alert,
uMapAlertConflict as AlertConflict,
uMapAlertCreation as AlertCreation,
} 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 Caption from './caption.js'
import Facets from './facets.js'
@ -23,6 +26,7 @@ import TableEditor from './tableeditor.js'
import Tooltip from './ui/tooltip.js'
import URLs from './urls.js'
import * as Utils from './utils.js'
import { DataLayer, LAYER_TYPES } from './data/layer.js'
// Import modules and export them to the global scope.
// For the not yet module-compatible JS out there.
@ -31,12 +35,12 @@ import * as Utils from './utils.js'
window.U = {
Alert,
AlertCreation,
AlertConflict,
AjaxAutocomplete,
AjaxAutocompleteMultiple,
AutocompleteDatalist,
Browser,
Caption,
DataLayer,
Dialog,
EditPanel,
Facets,
@ -45,6 +49,7 @@ window.U = {
Help,
HTTPError,
Importer,
LAYER_TYPES,
NOKError,
Orderable,
Panel,

View 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)
},
})

View 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()
}
},
})

View 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
},
})

View 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]]
})
},
})

View file

@ -318,7 +318,7 @@ export default class TableEditor extends WithTemplate {
feature.del()
}
this.datalayer.show()
this.datalayer.fire('datachanged')
this.datalayer.dataChanged()
this.renderBody()
if (this.map.browser.isOpen()) {
this.map.browser.resetFilters()

View file

@ -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 = {
HIDDABLE_CONTROLS: [
'zoom',

View file

@ -342,14 +342,7 @@ L.FormBuilder.TextColorPicker = L.FormBuilder.ColorPicker.extend({
L.FormBuilder.LayerTypeChooser = L.FormBuilder.Select.extend({
getOptions: () => {
const layer_classes = [
U.Layer.Default,
U.Layer.Cluster,
U.Layer.Heat,
U.Layer.Choropleth,
U.Layer.Categorized,
]
return layer_classes.map((class_) => [class_.TYPE, class_.NAME])
return U.LAYER_TYPES.map((class_) => [class_.TYPE, class_.NAME])
},
})
@ -367,6 +360,7 @@ L.FormBuilder.DataLayerSwitcher = L.FormBuilder.Select.extend({
getOptions: function () {
const options = []
this.builder.map.eachDataLayerReverse((datalayer) => {
console.log(datalayer.isLoaded(), datalayer.isDataReadOnly(), datalayer.isBrowsable())
if (
datalayer.isLoaded() &&
!datalayer.isDataReadOnly() &&

File diff suppressed because it is too large Load diff

View file

@ -5,6 +5,9 @@
<script type="module"
src="{% static 'umap/js/modules/leaflet-configure.js' %}"
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 %}
{% with "umap/locale/"|add:locale|add:".js" as path %}
<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/osmtogeojson/osmtogeojson.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' %}"
defer></script>
<script src="{% static 'umap/vendors/photon/leaflet.photon.js' %}" defer></script>
<script src="{% static 'umap/vendors/georsstogeojson/GeoRSSToGeoJSON.js' %}"
defer></script>
<script src="{% static 'umap/vendors/heat/leaflet-heat.js' %}" defer></script>
<script src="{% static 'umap/vendors/fullscreen/Leaflet.fullscreen.min.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.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.js' %}" defer></script>
<script src="{% static 'umap/js/components/fragment.js' %}" defer></script>