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/
|
||||
node_modules
|
||||
umap.conf
|
||||
data
|
||||
./data
|
||||
./static
|
||||
|
||||
### Python ###
|
||||
|
|
|
@ -43,7 +43,6 @@ 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({
|
||||
|
@ -52,7 +51,6 @@ export default class Caption {
|
|||
safeHTML: Utils.toHTML(datalayer.options.description),
|
||||
})
|
||||
}
|
||||
})
|
||||
datalayer.renderToolbox(headline)
|
||||
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 {
|
||||
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,
|
||||
|
|
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()
|
||||
}
|
||||
this.datalayer.show()
|
||||
this.datalayer.fire('datachanged')
|
||||
this.datalayer.dataChanged()
|
||||
this.renderBody()
|
||||
if (this.map.browser.isOpen()) {
|
||||
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 = {
|
||||
HIDDABLE_CONTROLS: [
|
||||
'zoom',
|
||||
|
|
|
@ -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
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue