umap-project 2.5.1__py3-none-any.whl → 2.6.0b0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of umap-project might be problematic. Click here for more details.
- umap/__init__.py +1 -1
- umap/admin.py +6 -1
- umap/context_processors.py +2 -1
- umap/decorators.py +13 -2
- umap/forms.py +26 -2
- umap/locale/br/LC_MESSAGES/django.mo +0 -0
- umap/locale/br/LC_MESSAGES/django.po +252 -146
- umap/locale/ca/LC_MESSAGES/django.mo +0 -0
- umap/locale/ca/LC_MESSAGES/django.po +274 -162
- umap/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
- umap/locale/cs_CZ/LC_MESSAGES/django.po +261 -150
- umap/locale/de/LC_MESSAGES/django.mo +0 -0
- umap/locale/de/LC_MESSAGES/django.po +299 -187
- umap/locale/el/LC_MESSAGES/django.mo +0 -0
- umap/locale/el/LC_MESSAGES/django.po +215 -159
- umap/locale/en/LC_MESSAGES/django.po +211 -155
- umap/locale/es/LC_MESSAGES/django.mo +0 -0
- umap/locale/es/LC_MESSAGES/django.po +255 -144
- umap/locale/eu/LC_MESSAGES/django.mo +0 -0
- umap/locale/eu/LC_MESSAGES/django.po +254 -198
- umap/locale/fa_IR/LC_MESSAGES/django.mo +0 -0
- umap/locale/fa_IR/LC_MESSAGES/django.po +346 -234
- umap/locale/fr/LC_MESSAGES/django.mo +0 -0
- umap/locale/fr/LC_MESSAGES/django.po +216 -160
- umap/locale/hu/LC_MESSAGES/django.mo +0 -0
- umap/locale/hu/LC_MESSAGES/django.po +215 -159
- umap/locale/it/LC_MESSAGES/django.mo +0 -0
- umap/locale/it/LC_MESSAGES/django.po +252 -146
- umap/locale/ms/LC_MESSAGES/django.mo +0 -0
- umap/locale/ms/LC_MESSAGES/django.po +252 -146
- umap/locale/pl/LC_MESSAGES/django.mo +0 -0
- umap/locale/pl/LC_MESSAGES/django.po +254 -148
- umap/locale/pt/LC_MESSAGES/django.mo +0 -0
- umap/locale/pt/LC_MESSAGES/django.po +215 -159
- umap/locale/sv/LC_MESSAGES/django.mo +0 -0
- umap/locale/sv/LC_MESSAGES/django.po +254 -143
- umap/locale/th_TH/LC_MESSAGES/django.mo +0 -0
- umap/locale/th_TH/LC_MESSAGES/django.po +125 -70
- umap/locale/zh_TW/LC_MESSAGES/django.mo +0 -0
- umap/locale/zh_TW/LC_MESSAGES/django.po +256 -145
- umap/migrations/0022_add_team.py +94 -0
- umap/models.py +45 -10
- umap/settings/__init__.py +2 -0
- umap/settings/base.py +3 -2
- umap/static/umap/base.css +32 -41
- umap/static/umap/content.css +19 -25
- umap/static/umap/css/icon.css +63 -37
- umap/static/umap/css/importers.css +1 -1
- umap/static/umap/css/slideshow.css +7 -5
- umap/static/umap/css/tableeditor.css +4 -3
- umap/static/umap/img/16-white.svg +1 -4
- umap/static/umap/img/16.svg +2 -6
- umap/static/umap/img/24-white.svg +4 -4
- umap/static/umap/img/24.svg +6 -6
- umap/static/umap/img/source/16-white.svg +2 -5
- umap/static/umap/img/source/16.svg +3 -7
- umap/static/umap/img/source/24-white.svg +7 -14
- umap/static/umap/img/source/24.svg +10 -17
- umap/static/umap/js/components/alerts/alert.css +20 -8
- umap/static/umap/js/modules/autocomplete.js +3 -3
- umap/static/umap/js/modules/browser.js +4 -3
- umap/static/umap/js/modules/caption.js +9 -11
- umap/static/umap/js/modules/data/features.js +994 -0
- umap/static/umap/js/modules/data/layer.js +1210 -0
- umap/static/umap/js/modules/formatter.js +12 -3
- umap/static/umap/js/modules/global.js +21 -5
- umap/static/umap/js/modules/permissions.js +280 -0
- umap/static/umap/js/{umap.icon.js → modules/rendering/icon.js} +77 -56
- umap/static/umap/js/modules/rendering/layers/base.js +105 -0
- umap/static/umap/js/modules/rendering/layers/classified.js +484 -0
- umap/static/umap/js/modules/rendering/layers/cluster.js +103 -0
- umap/static/umap/js/modules/rendering/layers/heat.js +182 -0
- umap/static/umap/js/modules/rendering/popup.js +99 -0
- umap/static/umap/js/modules/rendering/template.js +217 -0
- umap/static/umap/js/modules/rendering/ui.js +573 -0
- umap/static/umap/js/modules/schema.js +24 -0
- umap/static/umap/js/modules/share.js +66 -45
- umap/static/umap/js/modules/sync/updaters.js +9 -10
- umap/static/umap/js/modules/tableeditor.js +7 -7
- umap/static/umap/js/modules/ui/dialog.js +8 -4
- umap/static/umap/js/modules/utils.js +22 -13
- umap/static/umap/js/umap.controls.js +79 -146
- umap/static/umap/js/umap.core.js +9 -9
- umap/static/umap/js/umap.forms.js +32 -12
- umap/static/umap/js/umap.js +65 -63
- umap/static/umap/locale/br.js +35 -35
- umap/static/umap/locale/br.json +35 -35
- umap/static/umap/locale/ca.js +50 -50
- umap/static/umap/locale/ca.json +50 -50
- umap/static/umap/locale/de.js +136 -136
- umap/static/umap/locale/de.json +136 -136
- umap/static/umap/locale/el.js +47 -47
- umap/static/umap/locale/el.json +47 -47
- umap/static/umap/locale/en.js +7 -1
- umap/static/umap/locale/en.json +7 -1
- umap/static/umap/locale/fa_IR.js +44 -44
- umap/static/umap/locale/fa_IR.json +44 -44
- umap/static/umap/locale/fr.js +8 -2
- umap/static/umap/locale/fr.json +8 -2
- umap/static/umap/locale/pt.js +17 -17
- umap/static/umap/locale/pt.json +17 -17
- umap/static/umap/locale/pt_PT.js +207 -207
- umap/static/umap/locale/pt_PT.json +207 -207
- umap/static/umap/locale/th_TH.js +25 -25
- umap/static/umap/locale/th_TH.json +25 -25
- umap/static/umap/map.css +107 -104
- umap/static/umap/nav.css +19 -10
- umap/static/umap/unittests/utils.js +230 -107
- umap/static/umap/vendors/csv2geojson/csv2geojson.js +62 -40
- umap/static/umap/vendors/markercluster/MarkerCluster.Default.css +1 -1
- umap/storage.py +1 -0
- umap/templates/404.html +5 -1
- umap/templates/500.html +3 -1
- umap/templates/auth/user_detail.html +8 -2
- umap/templates/auth/user_form.html +19 -10
- umap/templates/auth/user_stars.html +8 -2
- umap/templates/base.html +1 -0
- umap/templates/registration/login.html +18 -3
- umap/templates/umap/about.html +1 -0
- umap/templates/umap/about_summary.html +22 -7
- umap/templates/umap/components/alerts/alert.html +42 -21
- umap/templates/umap/content.html +2 -0
- umap/templates/umap/content_footer.html +6 -2
- umap/templates/umap/css.html +1 -0
- umap/templates/umap/dashboard_menu.html +15 -0
- umap/templates/umap/home.html +14 -4
- umap/templates/umap/js.html +4 -9
- umap/templates/umap/login_popup_end.html +10 -4
- umap/templates/umap/map_detail.html +8 -2
- umap/templates/umap/map_fragment.html +3 -1
- umap/templates/umap/map_init.html +2 -1
- umap/templates/umap/map_list.html +4 -3
- umap/templates/umap/map_table.html +36 -12
- umap/templates/umap/messages.html +0 -1
- umap/templates/umap/navigation.html +2 -1
- umap/templates/umap/password_change.html +5 -1
- umap/templates/umap/password_change_done.html +8 -2
- umap/templates/umap/search.html +8 -2
- umap/templates/umap/search_bar.html +1 -0
- umap/templates/umap/team_confirm_delete.html +19 -0
- umap/templates/umap/team_detail.html +27 -0
- umap/templates/umap/team_form.html +60 -0
- umap/templates/umap/user_dashboard.html +7 -9
- umap/templates/umap/user_teams.html +51 -0
- umap/tests/base.py +8 -1
- umap/tests/conftest.py +6 -0
- umap/tests/fixtures/test_circles_layer.geojson +219 -0
- umap/tests/fixtures/test_upload_georss.xml +20 -0
- umap/tests/integration/conftest.py +18 -4
- umap/tests/integration/helpers.py +12 -0
- umap/tests/integration/test_anonymous_owned_map.py +23 -0
- umap/tests/integration/test_basics.py +29 -0
- umap/tests/integration/test_caption.py +20 -0
- umap/tests/integration/test_circles_layer.py +69 -0
- umap/tests/integration/test_draw_polygon.py +110 -13
- umap/tests/integration/test_draw_polyline.py +8 -18
- umap/tests/integration/test_edit_datalayer.py +1 -1
- umap/tests/integration/test_import.py +64 -5
- umap/tests/integration/test_owned_map.py +21 -13
- umap/tests/integration/test_team.py +47 -0
- umap/tests/integration/test_tilelayer.py +19 -2
- umap/tests/integration/test_view_marker.py +28 -1
- umap/tests/integration/test_websocket_sync.py +5 -5
- umap/tests/test_datalayer.py +32 -7
- umap/tests/test_datalayer_views.py +1 -1
- umap/tests/test_map.py +30 -4
- umap/tests/test_map_views.py +2 -2
- umap/tests/test_statics.py +40 -0
- umap/tests/test_team_views.py +131 -0
- umap/tests/test_views.py +15 -1
- umap/urls.py +23 -13
- umap/views.py +116 -10
- {umap_project-2.5.1.dist-info → umap_project-2.6.0b0.dist-info}/METADATA +9 -9
- {umap_project-2.5.1.dist-info → umap_project-2.6.0b0.dist-info}/RECORD +177 -170
- umap/static/umap/js/umap.datalayer.permissions.js +0 -70
- umap/static/umap/js/umap.features.js +0 -1290
- umap/static/umap/js/umap.layer.js +0 -1837
- umap/static/umap/js/umap.permissions.js +0 -208
- umap/static/umap/js/umap.popup.js +0 -341
- umap/static/umap/test/TableEditor.js +0 -104
- umap/static/umap/vendors/leaflet/leaflet-src.js +0 -14512
- umap/static/umap/vendors/leaflet/leaflet-src.js.map +0 -1
- umap/static/umap/vendors/leaflet/leaflet.js +0 -6
- umap/static/umap/vendors/leaflet/leaflet.js.map +0 -1
- umap/static/umap/vendors/markercluster/WhereAreTheJavascriptFiles.txt +0 -5
- umap/static/umap/vendors/markercluster/leaflet.markercluster-src.js +0 -2718
- umap/static/umap/vendors/markercluster/leaflet.markercluster-src.js.map +0 -1
- umap/static/umap/vendors/toolbar/leaflet.toolbar-src.css +0 -117
- umap/static/umap/vendors/toolbar/leaflet.toolbar-src.js +0 -365
- umap/tests/integration/test_statics.py +0 -47
- {umap_project-2.5.1.dist-info → umap_project-2.6.0b0.dist-info}/WHEEL +0 -0
- {umap_project-2.5.1.dist-info → umap_project-2.6.0b0.dist-info}/entry_points.txt +0 -0
- {umap_project-2.5.1.dist-info → umap_project-2.6.0b0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { FeatureGroup, TileLayer } from '../../../../vendors/leaflet/leaflet-src.esm.js'
|
|
2
|
+
import { translate } from '../../i18n.js'
|
|
3
|
+
import * as Utils from '../../utils.js'
|
|
4
|
+
|
|
5
|
+
export const LayerMixin = {
|
|
6
|
+
browsable: true,
|
|
7
|
+
|
|
8
|
+
onInit: function (map) {
|
|
9
|
+
if (this.datalayer.autoLoaded()) map.on('zoomend', this.onZoomEnd, this)
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
onDelete: function (map) {
|
|
13
|
+
map.off('zoomend', this.onZoomEnd, this)
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
onAdd: function (map) {
|
|
17
|
+
map.on('moveend', this.onMoveEnd, this)
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
onRemove: function (map) {
|
|
21
|
+
map.off('moveend', this.onMoveEnd, this)
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
getType: function () {
|
|
25
|
+
const proto = Object.getPrototypeOf(this)
|
|
26
|
+
return proto.constructor.TYPE
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
getName: function () {
|
|
30
|
+
const proto = Object.getPrototypeOf(this)
|
|
31
|
+
return proto.constructor.NAME
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
getFeatures: function () {
|
|
35
|
+
return this._layers
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
getEditableOptions: () => [],
|
|
39
|
+
|
|
40
|
+
onEdit: () => {},
|
|
41
|
+
|
|
42
|
+
hasDataVisible: function () {
|
|
43
|
+
return !!Object.keys(this._layers).length
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// Called when data changed on the datalayer
|
|
47
|
+
dataChanged: () => {},
|
|
48
|
+
|
|
49
|
+
onMoveEnd: function () {
|
|
50
|
+
if (this.datalayer.hasDynamicData() && this.datalayer.showAtZoom()) {
|
|
51
|
+
this.datalayer.fetchRemoteData()
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
onZoomEnd() {
|
|
56
|
+
if (this.datalayer._forcedVisibility) return
|
|
57
|
+
if (!this.datalayer.showAtZoom() && this.datalayer.isVisible()) {
|
|
58
|
+
this.datalayer.hide()
|
|
59
|
+
}
|
|
60
|
+
if (this.datalayer.showAtZoom() && !this.datalayer.isVisible()) {
|
|
61
|
+
this.datalayer.show()
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const Default = FeatureGroup.extend({
|
|
67
|
+
statics: {
|
|
68
|
+
NAME: translate('Default'),
|
|
69
|
+
TYPE: 'Default',
|
|
70
|
+
},
|
|
71
|
+
includes: [LayerMixin],
|
|
72
|
+
|
|
73
|
+
initialize: function (datalayer) {
|
|
74
|
+
this.datalayer = datalayer
|
|
75
|
+
FeatureGroup.prototype.initialize.call(this)
|
|
76
|
+
LayerMixin.onInit.call(this, this.datalayer.map)
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
onAdd: function (map) {
|
|
80
|
+
LayerMixin.onAdd.call(this, map)
|
|
81
|
+
return FeatureGroup.prototype.onAdd.call(this, map)
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
onRemove: function (map) {
|
|
85
|
+
LayerMixin.onRemove.call(this, map)
|
|
86
|
+
return FeatureGroup.prototype.onRemove.call(this, map)
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
TileLayer.include({
|
|
91
|
+
toJSON() {
|
|
92
|
+
return {
|
|
93
|
+
minZoom: this.options.minZoom,
|
|
94
|
+
maxZoom: this.options.maxZoom,
|
|
95
|
+
attribution: this.options.attribution,
|
|
96
|
+
url_template: this._url,
|
|
97
|
+
name: this.options.name,
|
|
98
|
+
tms: this.options.tms,
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
getAttribution() {
|
|
103
|
+
return Utils.toHTML(this.options.attribution)
|
|
104
|
+
},
|
|
105
|
+
})
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { FeatureGroup, DomUtil } from '../../../../vendors/leaflet/leaflet-src.esm.js'
|
|
2
|
+
import { translate } from '../../i18n.js'
|
|
3
|
+
import { LayerMixin } from './base.js'
|
|
4
|
+
import * as Utils from '../../utils.js'
|
|
5
|
+
import { CircleMarker } from '../ui.js'
|
|
6
|
+
|
|
7
|
+
// Layer where each feature color is relative to the others,
|
|
8
|
+
// so we need all features before behing able to set one
|
|
9
|
+
// feature layer
|
|
10
|
+
const ClassifiedMixin = {
|
|
11
|
+
initialize: function (datalayer) {
|
|
12
|
+
this.datalayer = datalayer
|
|
13
|
+
this.colorSchemes = Object.keys(colorbrewer)
|
|
14
|
+
.filter((k) => k !== 'schemeGroups')
|
|
15
|
+
.sort()
|
|
16
|
+
const key = this.getType().toLowerCase()
|
|
17
|
+
if (!Utils.isObject(this.datalayer.options[key])) {
|
|
18
|
+
this.datalayer.options[key] = {}
|
|
19
|
+
}
|
|
20
|
+
this.ensureOptions(this.datalayer.options[key])
|
|
21
|
+
FeatureGroup.prototype.initialize.call(this, [], this.datalayer.options[key])
|
|
22
|
+
LayerMixin.onInit.call(this, this.datalayer.map)
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
ensureOptions: () => {},
|
|
26
|
+
|
|
27
|
+
dataChanged: function () {
|
|
28
|
+
this.redraw()
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
redraw: function () {
|
|
32
|
+
this.compute()
|
|
33
|
+
if (this._map) this.eachLayer(this._map.addLayer, this._map)
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
getStyleProperty: (feature) => {
|
|
37
|
+
return feature.staticOptions.mainColor
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
getOption: function (option, feature) {
|
|
41
|
+
if (!feature) return
|
|
42
|
+
if (option === this.getStyleProperty(feature)) {
|
|
43
|
+
const value = this._getOption(feature)
|
|
44
|
+
return value
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
addLayer: function (layer) {
|
|
49
|
+
// Do not add yet the layer to the map
|
|
50
|
+
// wait for datachanged event, so we can compute breaks only once
|
|
51
|
+
const id = this.getLayerId(layer)
|
|
52
|
+
this._layers[id] = layer
|
|
53
|
+
return this
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
onAdd: function (map) {
|
|
57
|
+
this.compute()
|
|
58
|
+
LayerMixin.onAdd.call(this, map)
|
|
59
|
+
return FeatureGroup.prototype.onAdd.call(this, map)
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
onRemove: function (map) {
|
|
63
|
+
LayerMixin.onRemove.call(this, map)
|
|
64
|
+
return FeatureGroup.prototype.onRemove.call(this, map)
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
getValues: function () {
|
|
68
|
+
const values = []
|
|
69
|
+
this.datalayer.eachFeature((feature) => {
|
|
70
|
+
const value = this._getValue(feature)
|
|
71
|
+
if (value !== undefined) values.push(value)
|
|
72
|
+
})
|
|
73
|
+
return values
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
renderLegend: function (container) {
|
|
77
|
+
const parent = DomUtil.create('ul', '', container)
|
|
78
|
+
const items = this.getLegendItems()
|
|
79
|
+
for (const [color, label] of items) {
|
|
80
|
+
const li = DomUtil.create('li', '', parent)
|
|
81
|
+
const colorEl = DomUtil.create('span', 'datalayer-color', li)
|
|
82
|
+
colorEl.style.backgroundColor = color
|
|
83
|
+
const labelEl = DomUtil.create('span', '', li)
|
|
84
|
+
labelEl.textContent = label
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
getColorSchemes: function (classes) {
|
|
89
|
+
return this.colorSchemes.filter((scheme) => Boolean(colorbrewer[scheme][classes]))
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const Choropleth = FeatureGroup.extend({
|
|
94
|
+
statics: {
|
|
95
|
+
NAME: translate('Choropleth'),
|
|
96
|
+
TYPE: 'Choropleth',
|
|
97
|
+
},
|
|
98
|
+
includes: [LayerMixin, ClassifiedMixin],
|
|
99
|
+
// Have defaults that better suit the choropleth mode.
|
|
100
|
+
defaults: {
|
|
101
|
+
color: 'white',
|
|
102
|
+
fillOpacity: 0.7,
|
|
103
|
+
weight: 2,
|
|
104
|
+
},
|
|
105
|
+
MODES: {
|
|
106
|
+
kmeans: translate('K-means'),
|
|
107
|
+
equidistant: translate('Equidistant'),
|
|
108
|
+
jenks: translate('Jenks-Fisher'),
|
|
109
|
+
quantiles: translate('Quantiles'),
|
|
110
|
+
manual: translate('Manual'),
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
_getValue: function (feature) {
|
|
114
|
+
const key = this.datalayer.options.choropleth.property || 'value'
|
|
115
|
+
const value = +feature.properties[key]
|
|
116
|
+
if (!Number.isNaN(value)) return value
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
compute: function () {
|
|
120
|
+
const values = this.getValues()
|
|
121
|
+
|
|
122
|
+
if (!values.length) {
|
|
123
|
+
this.options.breaks = []
|
|
124
|
+
this.options.colors = []
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
const mode = this.datalayer.options.choropleth.mode
|
|
128
|
+
let classes = +this.datalayer.options.choropleth.classes || 5
|
|
129
|
+
let breaks
|
|
130
|
+
classes = Math.min(classes, values.length)
|
|
131
|
+
if (mode === 'manual') {
|
|
132
|
+
const manualBreaks = this.datalayer.options.choropleth.breaks
|
|
133
|
+
if (manualBreaks) {
|
|
134
|
+
breaks = manualBreaks
|
|
135
|
+
.split(',')
|
|
136
|
+
.map((b) => +b)
|
|
137
|
+
.filter((b) => !Number.isNaN(b))
|
|
138
|
+
}
|
|
139
|
+
} else if (mode === 'equidistant') {
|
|
140
|
+
breaks = ss.equalIntervalBreaks(values, classes)
|
|
141
|
+
} else if (mode === 'jenks') {
|
|
142
|
+
breaks = ss.jenks(values, classes)
|
|
143
|
+
} else if (mode === 'quantiles') {
|
|
144
|
+
const quantiles = [...Array(classes)].map((e, i) => i / classes).concat(1)
|
|
145
|
+
breaks = ss.quantile(values, quantiles)
|
|
146
|
+
} else {
|
|
147
|
+
breaks = ss.ckmeans(values, classes).map((cluster) => cluster[0])
|
|
148
|
+
breaks.push(ss.max(values)) // Needed for computing the legend
|
|
149
|
+
}
|
|
150
|
+
this.options.breaks = breaks || []
|
|
151
|
+
this.datalayer.options.choropleth.breaks = this.options.breaks
|
|
152
|
+
.map((b) => +b.toFixed(2))
|
|
153
|
+
.join(',')
|
|
154
|
+
let colorScheme = this.datalayer.options.choropleth.brewer
|
|
155
|
+
if (!colorbrewer[colorScheme]) colorScheme = 'Blues'
|
|
156
|
+
this.options.colors = colorbrewer[colorScheme][this.options.breaks.length - 1] || []
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
_getOption: function (feature) {
|
|
160
|
+
if (!feature) return // FIXME should not happen
|
|
161
|
+
const featureValue = this._getValue(feature)
|
|
162
|
+
// Find the bucket/step/limit that this value is less than and give it that color
|
|
163
|
+
for (let i = 1; i < this.options.breaks.length; i++) {
|
|
164
|
+
if (featureValue <= this.options.breaks[i]) {
|
|
165
|
+
return this.options.colors[i - 1]
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
onEdit: function (field, builder) {
|
|
171
|
+
// Only compute the breaks if we're dealing with choropleth
|
|
172
|
+
if (!field.startsWith('options.choropleth')) return
|
|
173
|
+
// If user touches the breaks, then force manual mode
|
|
174
|
+
if (field === 'options.choropleth.breaks') {
|
|
175
|
+
this.datalayer.options.choropleth.mode = 'manual'
|
|
176
|
+
if (builder) builder.helpers['options.choropleth.mode'].fetch()
|
|
177
|
+
}
|
|
178
|
+
this.compute()
|
|
179
|
+
// If user changes the mode or the number of classes,
|
|
180
|
+
// then update the breaks input value
|
|
181
|
+
if (field === 'options.choropleth.mode' || field === 'options.choropleth.classes') {
|
|
182
|
+
if (builder) builder.helpers['options.choropleth.breaks'].fetch()
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
getEditableOptions: function () {
|
|
187
|
+
return [
|
|
188
|
+
[
|
|
189
|
+
'options.choropleth.property',
|
|
190
|
+
{
|
|
191
|
+
handler: 'Select',
|
|
192
|
+
selectOptions: this.datalayer._propertiesIndex,
|
|
193
|
+
label: translate('Choropleth property value'),
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
[
|
|
197
|
+
'options.choropleth.brewer',
|
|
198
|
+
{
|
|
199
|
+
handler: 'Select',
|
|
200
|
+
label: translate('Choropleth color palette'),
|
|
201
|
+
selectOptions: this.colorSchemes,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
[
|
|
205
|
+
'options.choropleth.classes',
|
|
206
|
+
{
|
|
207
|
+
handler: 'Range',
|
|
208
|
+
min: 3,
|
|
209
|
+
max: 9,
|
|
210
|
+
step: 1,
|
|
211
|
+
label: translate('Choropleth classes'),
|
|
212
|
+
helpText: translate('Number of desired classes (default 5)'),
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
[
|
|
216
|
+
'options.choropleth.breaks',
|
|
217
|
+
{
|
|
218
|
+
handler: 'BlurInput',
|
|
219
|
+
label: translate('Choropleth breakpoints'),
|
|
220
|
+
helpText: translate(
|
|
221
|
+
'Comma separated list of numbers, including min and max values.'
|
|
222
|
+
),
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
[
|
|
226
|
+
'options.choropleth.mode',
|
|
227
|
+
{
|
|
228
|
+
handler: 'MultiChoice',
|
|
229
|
+
default: 'kmeans',
|
|
230
|
+
choices: Object.entries(this.MODES),
|
|
231
|
+
label: translate('Choropleth mode'),
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
]
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
getLegendItems: function () {
|
|
238
|
+
return this.options.breaks.slice(0, -1).map((el, index) => {
|
|
239
|
+
const from = +this.options.breaks[index].toFixed(1)
|
|
240
|
+
const to = +this.options.breaks[index + 1].toFixed(1)
|
|
241
|
+
return [this.options.colors[index], `${from} - ${to}`]
|
|
242
|
+
})
|
|
243
|
+
},
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
export const Circles = FeatureGroup.extend({
|
|
247
|
+
statics: {
|
|
248
|
+
NAME: translate('Proportional circles'),
|
|
249
|
+
TYPE: 'Circles',
|
|
250
|
+
},
|
|
251
|
+
includes: [LayerMixin, ClassifiedMixin],
|
|
252
|
+
defaults: {
|
|
253
|
+
weight: 1,
|
|
254
|
+
UIClass: CircleMarker,
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
ensureOptions: function (options) {
|
|
258
|
+
if (!Utils.isObject(this.datalayer.options.circles.radius)) {
|
|
259
|
+
this.datalayer.options.circles.radius = {}
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
_getValue: function (feature) {
|
|
264
|
+
const key = this.datalayer.options.circles.property || 'value'
|
|
265
|
+
const value = +feature.properties[key]
|
|
266
|
+
if (!Number.isNaN(value)) return value
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
compute: function () {
|
|
270
|
+
const values = this.getValues()
|
|
271
|
+
this.options.minValue = Math.sqrt(Math.min(...values))
|
|
272
|
+
this.options.maxValue = Math.sqrt(Math.max(...values))
|
|
273
|
+
this.options.minPX = this.datalayer.options.circles.radius?.min || 2
|
|
274
|
+
this.options.maxPX = this.datalayer.options.circles.radius?.max || 50
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
onEdit: function (field, builder) {
|
|
278
|
+
this.compute()
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
_computeRadius: function (value) {
|
|
282
|
+
const valuesRange = this.options.maxValue - this.options.minValue
|
|
283
|
+
const pxRange = this.options.maxPX - this.options.minPX
|
|
284
|
+
const radius =
|
|
285
|
+
this.options.minPX +
|
|
286
|
+
((Math.sqrt(value) - this.options.minValue) / valuesRange) * pxRange
|
|
287
|
+
return radius || this.options.minPX
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
_getOption: function (feature) {
|
|
291
|
+
if (!feature) return // FIXME should not happen
|
|
292
|
+
return this._computeRadius(this._getValue(feature))
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
getEditableOptions: function () {
|
|
296
|
+
return [
|
|
297
|
+
[
|
|
298
|
+
'options.circles.property',
|
|
299
|
+
{
|
|
300
|
+
handler: 'Select',
|
|
301
|
+
selectOptions: this.datalayer._propertiesIndex,
|
|
302
|
+
label: translate('Property name to compute circles'),
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
[
|
|
306
|
+
'options.circles.radius.min',
|
|
307
|
+
{
|
|
308
|
+
handler: 'Range',
|
|
309
|
+
label: translate('Min circle radius'),
|
|
310
|
+
min: 2,
|
|
311
|
+
max: 10,
|
|
312
|
+
step: 1,
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
[
|
|
316
|
+
'options.circles.radius.max',
|
|
317
|
+
{
|
|
318
|
+
handler: 'Range',
|
|
319
|
+
label: translate('Max circle radius'),
|
|
320
|
+
min: 12,
|
|
321
|
+
max: 50,
|
|
322
|
+
step: 2,
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
]
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
getStyleProperty: (feature) => {
|
|
329
|
+
return 'radius'
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
renderLegend: function (container) {
|
|
333
|
+
const parent = DomUtil.create('ul', 'circles-layer-legend', container)
|
|
334
|
+
const color = this.datalayer.getOption('color')
|
|
335
|
+
const values = this.getValues()
|
|
336
|
+
if (!values.length) return
|
|
337
|
+
values.sort((a, b) => a - b)
|
|
338
|
+
const minValue = values[0]
|
|
339
|
+
const maxValue = values[values.length - 1]
|
|
340
|
+
const medianValue = values[Math.round(values.length / 2)]
|
|
341
|
+
const items = [
|
|
342
|
+
[this.options.minPX, minValue],
|
|
343
|
+
[this._computeRadius(medianValue), medianValue],
|
|
344
|
+
[this.options.maxPX, maxValue],
|
|
345
|
+
]
|
|
346
|
+
for (const [size, label] of items) {
|
|
347
|
+
const li = DomUtil.create('li', '', parent)
|
|
348
|
+
const circleEl = DomUtil.create('span', 'circle', li)
|
|
349
|
+
circleEl.style.backgroundColor = color
|
|
350
|
+
circleEl.style.height = `${size * 2}px`
|
|
351
|
+
circleEl.style.width = `${size * 2}px`
|
|
352
|
+
circleEl.style.opacity = this.datalayer.getOption('opacity')
|
|
353
|
+
const labelEl = DomUtil.create('span', 'label', li)
|
|
354
|
+
labelEl.textContent = label
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
export const Categorized = FeatureGroup.extend({
|
|
360
|
+
statics: {
|
|
361
|
+
NAME: translate('Categorized'),
|
|
362
|
+
TYPE: 'Categorized',
|
|
363
|
+
},
|
|
364
|
+
includes: [LayerMixin, ClassifiedMixin],
|
|
365
|
+
MODES: {
|
|
366
|
+
manual: translate('Manual'),
|
|
367
|
+
alpha: translate('Alphabetical'),
|
|
368
|
+
},
|
|
369
|
+
defaults: {
|
|
370
|
+
color: 'white',
|
|
371
|
+
// fillColor: 'red',
|
|
372
|
+
fillOpacity: 0.7,
|
|
373
|
+
weight: 2,
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
_getValue: function (feature) {
|
|
377
|
+
const key =
|
|
378
|
+
this.datalayer.options.categorized.property || this.datalayer._propertiesIndex[0]
|
|
379
|
+
return feature.properties[key]
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
_getOption: function (feature) {
|
|
383
|
+
if (!feature) return // FIXME should not happen
|
|
384
|
+
const featureValue = this._getValue(feature)
|
|
385
|
+
for (let i = 0; i < this.options.categories.length; i++) {
|
|
386
|
+
if (featureValue === this.options.categories[i]) {
|
|
387
|
+
return this.options.colors[i]
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
compute: function () {
|
|
393
|
+
const values = this.getValues()
|
|
394
|
+
|
|
395
|
+
if (!values.length) {
|
|
396
|
+
this.options.categories = []
|
|
397
|
+
this.options.colors = []
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
const mode = this.datalayer.options.categorized.mode
|
|
401
|
+
let categories = []
|
|
402
|
+
if (mode === 'manual') {
|
|
403
|
+
const manualCategories = this.datalayer.options.categorized.categories
|
|
404
|
+
if (manualCategories) {
|
|
405
|
+
categories = manualCategories.split(',')
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
categories = values
|
|
409
|
+
.filter((val, idx, arr) => arr.indexOf(val) === idx)
|
|
410
|
+
.sort(Utils.naturalSort)
|
|
411
|
+
}
|
|
412
|
+
this.options.categories = categories
|
|
413
|
+
this.datalayer.options.categorized.categories = this.options.categories.join(',')
|
|
414
|
+
const colorScheme = this.datalayer.options.categorized.brewer
|
|
415
|
+
this._classes = this.options.categories.length
|
|
416
|
+
if (colorbrewer[colorScheme]?.[this._classes]) {
|
|
417
|
+
this.options.colors = colorbrewer[colorScheme][this._classes]
|
|
418
|
+
} else {
|
|
419
|
+
this.options.colors = colorbrewer?.Accent[this._classes]
|
|
420
|
+
? colorbrewer?.Accent[this._classes]
|
|
421
|
+
: U.COLORS // Fixme: move COLORS to modules/
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
|
|
425
|
+
getEditableOptions: function () {
|
|
426
|
+
return [
|
|
427
|
+
[
|
|
428
|
+
'options.categorized.property',
|
|
429
|
+
{
|
|
430
|
+
handler: 'Select',
|
|
431
|
+
selectOptions: this.datalayer._propertiesIndex,
|
|
432
|
+
label: translate('Category property'),
|
|
433
|
+
},
|
|
434
|
+
],
|
|
435
|
+
[
|
|
436
|
+
'options.categorized.brewer',
|
|
437
|
+
{
|
|
438
|
+
handler: 'Select',
|
|
439
|
+
label: translate('Color palette'),
|
|
440
|
+
selectOptions: this.getColorSchemes(this._classes),
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
[
|
|
444
|
+
'options.categorized.categories',
|
|
445
|
+
{
|
|
446
|
+
handler: 'BlurInput',
|
|
447
|
+
label: translate('Categories'),
|
|
448
|
+
helpText: translate('Comma separated list of categories.'),
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
[
|
|
452
|
+
'options.categorized.mode',
|
|
453
|
+
{
|
|
454
|
+
handler: 'MultiChoice',
|
|
455
|
+
default: 'alpha',
|
|
456
|
+
choices: Object.entries(this.MODES),
|
|
457
|
+
label: translate('Categories mode'),
|
|
458
|
+
},
|
|
459
|
+
],
|
|
460
|
+
]
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
onEdit: function (field, builder) {
|
|
464
|
+
// Only compute the categories if we're dealing with categorized
|
|
465
|
+
if (!field.startsWith('options.categorized')) return
|
|
466
|
+
// If user touches the categories, then force manual mode
|
|
467
|
+
if (field === 'options.categorized.categories') {
|
|
468
|
+
this.datalayer.options.categorized.mode = 'manual'
|
|
469
|
+
if (builder) builder.helpers['options.categorized.mode'].fetch()
|
|
470
|
+
}
|
|
471
|
+
this.compute()
|
|
472
|
+
// If user changes the mode
|
|
473
|
+
// then update the categories input value
|
|
474
|
+
if (field === 'options.categorized.mode') {
|
|
475
|
+
if (builder) builder.helpers['options.categorized.categories'].fetch()
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
getLegendItems: function () {
|
|
480
|
+
return this.options.categories.map((limit, index) => {
|
|
481
|
+
return [this.options.colors[index], this.options.categories[index]]
|
|
482
|
+
})
|
|
483
|
+
},
|
|
484
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// WARNING must be loaded dynamically, or at least after leaflet.markercluster
|
|
2
|
+
// Uses global L.MarkerCluster and L.MarkerClusterGroup, not exposed as ESM
|
|
3
|
+
import { translate } from '../../i18n.js'
|
|
4
|
+
import { LayerMixin } from './base.js'
|
|
5
|
+
import * as Utils from '../../utils.js'
|
|
6
|
+
import { Evented } from '../../../../vendors/leaflet/leaflet-src.esm.js'
|
|
7
|
+
import { Cluster as ClusterIcon } from '../icon.js'
|
|
8
|
+
|
|
9
|
+
const MarkerCluster = L.MarkerCluster.extend({
|
|
10
|
+
// Custom class so we can call computeTextColor
|
|
11
|
+
// when element is already on the DOM.
|
|
12
|
+
|
|
13
|
+
_initIcon: function () {
|
|
14
|
+
L.MarkerCluster.prototype._initIcon.call(this)
|
|
15
|
+
const div = this._icon.querySelector('div')
|
|
16
|
+
// Compute text color only when icon is added to the DOM.
|
|
17
|
+
div.style.color = this._iconObj.computeTextColor(div)
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export const Cluster = L.MarkerClusterGroup.extend({
|
|
22
|
+
statics: {
|
|
23
|
+
NAME: translate('Clustered'),
|
|
24
|
+
TYPE: 'Cluster',
|
|
25
|
+
},
|
|
26
|
+
includes: [LayerMixin],
|
|
27
|
+
|
|
28
|
+
initialize: function (datalayer) {
|
|
29
|
+
this.datalayer = datalayer
|
|
30
|
+
if (!Utils.isObject(this.datalayer.options.cluster)) {
|
|
31
|
+
this.datalayer.options.cluster = {}
|
|
32
|
+
}
|
|
33
|
+
const options = {
|
|
34
|
+
polygonOptions: {
|
|
35
|
+
color: this.datalayer.getColor(),
|
|
36
|
+
},
|
|
37
|
+
iconCreateFunction: (cluster) => new ClusterIcon(datalayer, cluster),
|
|
38
|
+
}
|
|
39
|
+
if (this.datalayer.options.cluster?.radius) {
|
|
40
|
+
options.maxClusterRadius = this.datalayer.options.cluster.radius
|
|
41
|
+
}
|
|
42
|
+
L.MarkerClusterGroup.prototype.initialize.call(this, options)
|
|
43
|
+
LayerMixin.onInit.call(this, this.datalayer.map)
|
|
44
|
+
this._markerCluster = MarkerCluster
|
|
45
|
+
this._layers = []
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
onAdd: function (map) {
|
|
49
|
+
LayerMixin.onAdd.call(this, map)
|
|
50
|
+
return L.MarkerClusterGroup.prototype.onAdd.call(this, map)
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
onRemove: function (map) {
|
|
54
|
+
// In some situation, the onRemove is called before the layer is really
|
|
55
|
+
// added to the map: basically when combining a defaultView=data + max/minZoom
|
|
56
|
+
// and loading the map at a zoom outside of that zoom range.
|
|
57
|
+
// FIXME: move this upstream (_unbindEvents should accept a map parameter
|
|
58
|
+
// instead of relying on this._map)
|
|
59
|
+
this._map = map
|
|
60
|
+
LayerMixin.onRemove.call(this, map)
|
|
61
|
+
return L.MarkerClusterGroup.prototype.onRemove.call(this, map)
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
addLayer: function (layer) {
|
|
65
|
+
this._layers.push(layer)
|
|
66
|
+
return L.MarkerClusterGroup.prototype.addLayer.call(this, layer)
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
removeLayer: function (layer) {
|
|
70
|
+
this._layers.splice(this._layers.indexOf(layer), 1)
|
|
71
|
+
return L.MarkerClusterGroup.prototype.removeLayer.call(this, layer)
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
getEditableOptions: () => [
|
|
75
|
+
[
|
|
76
|
+
'options.cluster.radius',
|
|
77
|
+
{
|
|
78
|
+
handler: 'BlurIntInput',
|
|
79
|
+
placeholder: translate('Clustering radius'),
|
|
80
|
+
helpText: translate('Override clustering radius (default 80)'),
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
[
|
|
84
|
+
'options.cluster.textColor',
|
|
85
|
+
{
|
|
86
|
+
handler: 'TextColorPicker',
|
|
87
|
+
placeholder: translate('Auto'),
|
|
88
|
+
helpText: translate('Text color for the cluster label'),
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
],
|
|
92
|
+
|
|
93
|
+
onEdit: function (field, builder) {
|
|
94
|
+
if (field === 'options.cluster.radius') {
|
|
95
|
+
// No way to reset radius of an already instanciated MarkerClusterGroup...
|
|
96
|
+
this.datalayer.resetLayer(true)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
if (field === 'options.color') {
|
|
100
|
+
this.options.polygonOptions.color = this.datalayer.getColor()
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
})
|