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.

Files changed (193) hide show
  1. umap/__init__.py +1 -1
  2. umap/admin.py +6 -1
  3. umap/context_processors.py +2 -1
  4. umap/decorators.py +13 -2
  5. umap/forms.py +26 -2
  6. umap/locale/br/LC_MESSAGES/django.mo +0 -0
  7. umap/locale/br/LC_MESSAGES/django.po +252 -146
  8. umap/locale/ca/LC_MESSAGES/django.mo +0 -0
  9. umap/locale/ca/LC_MESSAGES/django.po +274 -162
  10. umap/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  11. umap/locale/cs_CZ/LC_MESSAGES/django.po +261 -150
  12. umap/locale/de/LC_MESSAGES/django.mo +0 -0
  13. umap/locale/de/LC_MESSAGES/django.po +299 -187
  14. umap/locale/el/LC_MESSAGES/django.mo +0 -0
  15. umap/locale/el/LC_MESSAGES/django.po +215 -159
  16. umap/locale/en/LC_MESSAGES/django.po +211 -155
  17. umap/locale/es/LC_MESSAGES/django.mo +0 -0
  18. umap/locale/es/LC_MESSAGES/django.po +255 -144
  19. umap/locale/eu/LC_MESSAGES/django.mo +0 -0
  20. umap/locale/eu/LC_MESSAGES/django.po +254 -198
  21. umap/locale/fa_IR/LC_MESSAGES/django.mo +0 -0
  22. umap/locale/fa_IR/LC_MESSAGES/django.po +346 -234
  23. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  24. umap/locale/fr/LC_MESSAGES/django.po +216 -160
  25. umap/locale/hu/LC_MESSAGES/django.mo +0 -0
  26. umap/locale/hu/LC_MESSAGES/django.po +215 -159
  27. umap/locale/it/LC_MESSAGES/django.mo +0 -0
  28. umap/locale/it/LC_MESSAGES/django.po +252 -146
  29. umap/locale/ms/LC_MESSAGES/django.mo +0 -0
  30. umap/locale/ms/LC_MESSAGES/django.po +252 -146
  31. umap/locale/pl/LC_MESSAGES/django.mo +0 -0
  32. umap/locale/pl/LC_MESSAGES/django.po +254 -148
  33. umap/locale/pt/LC_MESSAGES/django.mo +0 -0
  34. umap/locale/pt/LC_MESSAGES/django.po +215 -159
  35. umap/locale/sv/LC_MESSAGES/django.mo +0 -0
  36. umap/locale/sv/LC_MESSAGES/django.po +254 -143
  37. umap/locale/th_TH/LC_MESSAGES/django.mo +0 -0
  38. umap/locale/th_TH/LC_MESSAGES/django.po +125 -70
  39. umap/locale/zh_TW/LC_MESSAGES/django.mo +0 -0
  40. umap/locale/zh_TW/LC_MESSAGES/django.po +256 -145
  41. umap/migrations/0022_add_team.py +94 -0
  42. umap/models.py +45 -10
  43. umap/settings/__init__.py +2 -0
  44. umap/settings/base.py +3 -2
  45. umap/static/umap/base.css +32 -41
  46. umap/static/umap/content.css +19 -25
  47. umap/static/umap/css/icon.css +63 -37
  48. umap/static/umap/css/importers.css +1 -1
  49. umap/static/umap/css/slideshow.css +7 -5
  50. umap/static/umap/css/tableeditor.css +4 -3
  51. umap/static/umap/img/16-white.svg +1 -4
  52. umap/static/umap/img/16.svg +2 -6
  53. umap/static/umap/img/24-white.svg +4 -4
  54. umap/static/umap/img/24.svg +6 -6
  55. umap/static/umap/img/source/16-white.svg +2 -5
  56. umap/static/umap/img/source/16.svg +3 -7
  57. umap/static/umap/img/source/24-white.svg +7 -14
  58. umap/static/umap/img/source/24.svg +10 -17
  59. umap/static/umap/js/components/alerts/alert.css +20 -8
  60. umap/static/umap/js/modules/autocomplete.js +3 -3
  61. umap/static/umap/js/modules/browser.js +4 -3
  62. umap/static/umap/js/modules/caption.js +9 -11
  63. umap/static/umap/js/modules/data/features.js +994 -0
  64. umap/static/umap/js/modules/data/layer.js +1210 -0
  65. umap/static/umap/js/modules/formatter.js +12 -3
  66. umap/static/umap/js/modules/global.js +21 -5
  67. umap/static/umap/js/modules/permissions.js +280 -0
  68. umap/static/umap/js/{umap.icon.js → modules/rendering/icon.js} +77 -56
  69. umap/static/umap/js/modules/rendering/layers/base.js +105 -0
  70. umap/static/umap/js/modules/rendering/layers/classified.js +484 -0
  71. umap/static/umap/js/modules/rendering/layers/cluster.js +103 -0
  72. umap/static/umap/js/modules/rendering/layers/heat.js +182 -0
  73. umap/static/umap/js/modules/rendering/popup.js +99 -0
  74. umap/static/umap/js/modules/rendering/template.js +217 -0
  75. umap/static/umap/js/modules/rendering/ui.js +573 -0
  76. umap/static/umap/js/modules/schema.js +24 -0
  77. umap/static/umap/js/modules/share.js +66 -45
  78. umap/static/umap/js/modules/sync/updaters.js +9 -10
  79. umap/static/umap/js/modules/tableeditor.js +7 -7
  80. umap/static/umap/js/modules/ui/dialog.js +8 -4
  81. umap/static/umap/js/modules/utils.js +22 -13
  82. umap/static/umap/js/umap.controls.js +79 -146
  83. umap/static/umap/js/umap.core.js +9 -9
  84. umap/static/umap/js/umap.forms.js +32 -12
  85. umap/static/umap/js/umap.js +65 -63
  86. umap/static/umap/locale/br.js +35 -35
  87. umap/static/umap/locale/br.json +35 -35
  88. umap/static/umap/locale/ca.js +50 -50
  89. umap/static/umap/locale/ca.json +50 -50
  90. umap/static/umap/locale/de.js +136 -136
  91. umap/static/umap/locale/de.json +136 -136
  92. umap/static/umap/locale/el.js +47 -47
  93. umap/static/umap/locale/el.json +47 -47
  94. umap/static/umap/locale/en.js +7 -1
  95. umap/static/umap/locale/en.json +7 -1
  96. umap/static/umap/locale/fa_IR.js +44 -44
  97. umap/static/umap/locale/fa_IR.json +44 -44
  98. umap/static/umap/locale/fr.js +8 -2
  99. umap/static/umap/locale/fr.json +8 -2
  100. umap/static/umap/locale/pt.js +17 -17
  101. umap/static/umap/locale/pt.json +17 -17
  102. umap/static/umap/locale/pt_PT.js +207 -207
  103. umap/static/umap/locale/pt_PT.json +207 -207
  104. umap/static/umap/locale/th_TH.js +25 -25
  105. umap/static/umap/locale/th_TH.json +25 -25
  106. umap/static/umap/map.css +107 -104
  107. umap/static/umap/nav.css +19 -10
  108. umap/static/umap/unittests/utils.js +230 -107
  109. umap/static/umap/vendors/csv2geojson/csv2geojson.js +62 -40
  110. umap/static/umap/vendors/markercluster/MarkerCluster.Default.css +1 -1
  111. umap/storage.py +1 -0
  112. umap/templates/404.html +5 -1
  113. umap/templates/500.html +3 -1
  114. umap/templates/auth/user_detail.html +8 -2
  115. umap/templates/auth/user_form.html +19 -10
  116. umap/templates/auth/user_stars.html +8 -2
  117. umap/templates/base.html +1 -0
  118. umap/templates/registration/login.html +18 -3
  119. umap/templates/umap/about.html +1 -0
  120. umap/templates/umap/about_summary.html +22 -7
  121. umap/templates/umap/components/alerts/alert.html +42 -21
  122. umap/templates/umap/content.html +2 -0
  123. umap/templates/umap/content_footer.html +6 -2
  124. umap/templates/umap/css.html +1 -0
  125. umap/templates/umap/dashboard_menu.html +15 -0
  126. umap/templates/umap/home.html +14 -4
  127. umap/templates/umap/js.html +4 -9
  128. umap/templates/umap/login_popup_end.html +10 -4
  129. umap/templates/umap/map_detail.html +8 -2
  130. umap/templates/umap/map_fragment.html +3 -1
  131. umap/templates/umap/map_init.html +2 -1
  132. umap/templates/umap/map_list.html +4 -3
  133. umap/templates/umap/map_table.html +36 -12
  134. umap/templates/umap/messages.html +0 -1
  135. umap/templates/umap/navigation.html +2 -1
  136. umap/templates/umap/password_change.html +5 -1
  137. umap/templates/umap/password_change_done.html +8 -2
  138. umap/templates/umap/search.html +8 -2
  139. umap/templates/umap/search_bar.html +1 -0
  140. umap/templates/umap/team_confirm_delete.html +19 -0
  141. umap/templates/umap/team_detail.html +27 -0
  142. umap/templates/umap/team_form.html +60 -0
  143. umap/templates/umap/user_dashboard.html +7 -9
  144. umap/templates/umap/user_teams.html +51 -0
  145. umap/tests/base.py +8 -1
  146. umap/tests/conftest.py +6 -0
  147. umap/tests/fixtures/test_circles_layer.geojson +219 -0
  148. umap/tests/fixtures/test_upload_georss.xml +20 -0
  149. umap/tests/integration/conftest.py +18 -4
  150. umap/tests/integration/helpers.py +12 -0
  151. umap/tests/integration/test_anonymous_owned_map.py +23 -0
  152. umap/tests/integration/test_basics.py +29 -0
  153. umap/tests/integration/test_caption.py +20 -0
  154. umap/tests/integration/test_circles_layer.py +69 -0
  155. umap/tests/integration/test_draw_polygon.py +110 -13
  156. umap/tests/integration/test_draw_polyline.py +8 -18
  157. umap/tests/integration/test_edit_datalayer.py +1 -1
  158. umap/tests/integration/test_import.py +64 -5
  159. umap/tests/integration/test_owned_map.py +21 -13
  160. umap/tests/integration/test_team.py +47 -0
  161. umap/tests/integration/test_tilelayer.py +19 -2
  162. umap/tests/integration/test_view_marker.py +28 -1
  163. umap/tests/integration/test_websocket_sync.py +5 -5
  164. umap/tests/test_datalayer.py +32 -7
  165. umap/tests/test_datalayer_views.py +1 -1
  166. umap/tests/test_map.py +30 -4
  167. umap/tests/test_map_views.py +2 -2
  168. umap/tests/test_statics.py +40 -0
  169. umap/tests/test_team_views.py +131 -0
  170. umap/tests/test_views.py +15 -1
  171. umap/urls.py +23 -13
  172. umap/views.py +116 -10
  173. {umap_project-2.5.1.dist-info → umap_project-2.6.0b0.dist-info}/METADATA +9 -9
  174. {umap_project-2.5.1.dist-info → umap_project-2.6.0b0.dist-info}/RECORD +177 -170
  175. umap/static/umap/js/umap.datalayer.permissions.js +0 -70
  176. umap/static/umap/js/umap.features.js +0 -1290
  177. umap/static/umap/js/umap.layer.js +0 -1837
  178. umap/static/umap/js/umap.permissions.js +0 -208
  179. umap/static/umap/js/umap.popup.js +0 -341
  180. umap/static/umap/test/TableEditor.js +0 -104
  181. umap/static/umap/vendors/leaflet/leaflet-src.js +0 -14512
  182. umap/static/umap/vendors/leaflet/leaflet-src.js.map +0 -1
  183. umap/static/umap/vendors/leaflet/leaflet.js +0 -6
  184. umap/static/umap/vendors/leaflet/leaflet.js.map +0 -1
  185. umap/static/umap/vendors/markercluster/WhereAreTheJavascriptFiles.txt +0 -5
  186. umap/static/umap/vendors/markercluster/leaflet.markercluster-src.js +0 -2718
  187. umap/static/umap/vendors/markercluster/leaflet.markercluster-src.js.map +0 -1
  188. umap/static/umap/vendors/toolbar/leaflet.toolbar-src.css +0 -117
  189. umap/static/umap/vendors/toolbar/leaflet.toolbar-src.js +0 -365
  190. umap/tests/integration/test_statics.py +0 -47
  191. {umap_project-2.5.1.dist-info → umap_project-2.6.0b0.dist-info}/WHEEL +0 -0
  192. {umap_project-2.5.1.dist-info → umap_project-2.6.0b0.dist-info}/entry_points.txt +0 -0
  193. {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
+ })