umap-project 3.1.2__py3-none-any.whl → 3.3.0__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 (199) hide show
  1. umap/__init__.py +1 -1
  2. umap/locale/en/LC_MESSAGES/django.mo +0 -0
  3. umap/locale/en/LC_MESSAGES/django.po +22 -18
  4. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  5. umap/locale/fr/LC_MESSAGES/django.po +21 -17
  6. umap/management/commands/export_pictogram.py +29 -0
  7. umap/management/commands/migrate_to_S3.py +5 -1
  8. umap/management/commands/purge_old_versions.py +8 -6
  9. umap/settings/__init__.py +21 -0
  10. umap/settings/base.py +3 -0
  11. umap/static/umap/content.css +7 -2
  12. umap/static/umap/css/contextmenu.css +58 -2
  13. umap/static/umap/css/form.css +175 -45
  14. umap/static/umap/css/icon.css +97 -3
  15. umap/static/umap/css/panel.css +31 -1
  16. umap/static/umap/img/16-white.svg +21 -40
  17. umap/static/umap/img/16.svg +1 -1
  18. umap/static/umap/img/24-white.svg +9 -9
  19. umap/static/umap/img/24.svg +23 -10
  20. umap/static/umap/img/source/16-white.svg +23 -41
  21. umap/static/umap/img/source/16.svg +1 -1
  22. umap/static/umap/img/source/24-white.svg +11 -11
  23. umap/static/umap/img/source/24.svg +25 -12
  24. umap/static/umap/js/modules/browser.js +1 -1
  25. umap/static/umap/js/modules/caption.js +8 -0
  26. umap/static/umap/js/modules/data/features.js +331 -202
  27. umap/static/umap/js/modules/data/layer.js +263 -152
  28. umap/static/umap/js/modules/facets.js +2 -2
  29. umap/static/umap/js/modules/form/builder.js +11 -7
  30. umap/static/umap/js/modules/form/fields.js +66 -26
  31. umap/static/umap/js/modules/formatter.js +78 -28
  32. umap/static/umap/js/modules/importer.js +6 -1
  33. umap/static/umap/js/modules/importers/opendata.js +138 -33
  34. umap/static/umap/js/modules/importers/openrouteservice.js +140 -0
  35. umap/static/umap/js/modules/managers.js +67 -0
  36. umap/static/umap/js/modules/printer.js +107 -0
  37. umap/static/umap/js/modules/rendering/controls.js +78 -2
  38. umap/static/umap/js/modules/rendering/icon.js +116 -87
  39. umap/static/umap/js/modules/rendering/layers/classified.js +8 -7
  40. umap/static/umap/js/modules/rendering/layers/cluster.js +199 -63
  41. umap/static/umap/js/modules/rendering/map.js +6 -2
  42. umap/static/umap/js/modules/rendering/template.js +71 -1
  43. umap/static/umap/js/modules/rendering/ui.js +111 -34
  44. umap/static/umap/js/modules/rules.js +76 -23
  45. umap/static/umap/js/modules/schema.js +27 -0
  46. umap/static/umap/js/modules/share.js +19 -12
  47. umap/static/umap/js/modules/slideshow.js +1 -1
  48. umap/static/umap/js/modules/sync/updaters.js +1 -6
  49. umap/static/umap/js/modules/tableeditor.js +13 -37
  50. umap/static/umap/js/modules/templates.js +7 -6
  51. umap/static/umap/js/modules/ui/bar.js +6 -1
  52. umap/static/umap/js/modules/ui/base.js +24 -9
  53. umap/static/umap/js/modules/ui/contextmenu.js +17 -7
  54. umap/static/umap/js/modules/ui/dialog.js +7 -4
  55. umap/static/umap/js/modules/ui/panel.js +7 -0
  56. umap/static/umap/js/modules/umap.js +84 -67
  57. umap/static/umap/js/modules/utils.js +8 -7
  58. umap/static/umap/js/umap.controls.js +22 -57
  59. umap/static/umap/locale/am_ET.js +81 -9
  60. umap/static/umap/locale/am_ET.json +81 -9
  61. umap/static/umap/locale/ar.js +81 -9
  62. umap/static/umap/locale/ar.json +81 -9
  63. umap/static/umap/locale/ast.js +81 -9
  64. umap/static/umap/locale/ast.json +81 -9
  65. umap/static/umap/locale/bg.js +81 -9
  66. umap/static/umap/locale/bg.json +81 -9
  67. umap/static/umap/locale/br.js +68 -29
  68. umap/static/umap/locale/br.json +68 -29
  69. umap/static/umap/locale/ca.js +88 -16
  70. umap/static/umap/locale/ca.json +88 -16
  71. umap/static/umap/locale/cs_CZ.js +81 -9
  72. umap/static/umap/locale/cs_CZ.json +81 -9
  73. umap/static/umap/locale/da.js +48 -9
  74. umap/static/umap/locale/da.json +48 -9
  75. umap/static/umap/locale/de.js +48 -9
  76. umap/static/umap/locale/de.json +48 -9
  77. umap/static/umap/locale/el.js +58 -13
  78. umap/static/umap/locale/el.json +58 -13
  79. umap/static/umap/locale/en.js +48 -9
  80. umap/static/umap/locale/en.json +48 -9
  81. umap/static/umap/locale/en_US.json +81 -9
  82. umap/static/umap/locale/es.js +48 -9
  83. umap/static/umap/locale/es.json +48 -9
  84. umap/static/umap/locale/et.js +81 -9
  85. umap/static/umap/locale/et.json +81 -9
  86. umap/static/umap/locale/eu.js +97 -25
  87. umap/static/umap/locale/eu.json +97 -25
  88. umap/static/umap/locale/fa_IR.js +81 -9
  89. umap/static/umap/locale/fa_IR.json +81 -9
  90. umap/static/umap/locale/fi.js +81 -9
  91. umap/static/umap/locale/fi.json +81 -9
  92. umap/static/umap/locale/fr.js +48 -9
  93. umap/static/umap/locale/fr.json +48 -9
  94. umap/static/umap/locale/gl.js +81 -9
  95. umap/static/umap/locale/gl.json +81 -9
  96. umap/static/umap/locale/he.js +81 -9
  97. umap/static/umap/locale/he.json +81 -9
  98. umap/static/umap/locale/hr.js +81 -9
  99. umap/static/umap/locale/hr.json +81 -9
  100. umap/static/umap/locale/hu.js +72 -27
  101. umap/static/umap/locale/hu.json +72 -27
  102. umap/static/umap/locale/id.js +81 -9
  103. umap/static/umap/locale/id.json +81 -9
  104. umap/static/umap/locale/is.js +81 -9
  105. umap/static/umap/locale/is.json +81 -9
  106. umap/static/umap/locale/it.js +48 -9
  107. umap/static/umap/locale/it.json +48 -9
  108. umap/static/umap/locale/ja.js +81 -9
  109. umap/static/umap/locale/ja.json +81 -9
  110. umap/static/umap/locale/ko.js +81 -9
  111. umap/static/umap/locale/ko.json +81 -9
  112. umap/static/umap/locale/lt.js +81 -9
  113. umap/static/umap/locale/lt.json +81 -9
  114. umap/static/umap/locale/ms.js +81 -9
  115. umap/static/umap/locale/ms.json +81 -9
  116. umap/static/umap/locale/nl.js +48 -9
  117. umap/static/umap/locale/nl.json +48 -9
  118. umap/static/umap/locale/no.js +81 -9
  119. umap/static/umap/locale/no.json +81 -9
  120. umap/static/umap/locale/pl.js +81 -9
  121. umap/static/umap/locale/pl.json +81 -9
  122. umap/static/umap/locale/pl_PL.json +81 -9
  123. umap/static/umap/locale/pt.js +81 -9
  124. umap/static/umap/locale/pt.json +81 -9
  125. umap/static/umap/locale/pt_BR.js +91 -19
  126. umap/static/umap/locale/pt_BR.json +91 -19
  127. umap/static/umap/locale/pt_PT.js +81 -9
  128. umap/static/umap/locale/pt_PT.json +81 -9
  129. umap/static/umap/locale/ro.js +81 -9
  130. umap/static/umap/locale/ro.json +81 -9
  131. umap/static/umap/locale/ru.js +81 -9
  132. umap/static/umap/locale/ru.json +81 -9
  133. umap/static/umap/locale/sk_SK.js +81 -9
  134. umap/static/umap/locale/sk_SK.json +81 -9
  135. umap/static/umap/locale/sl.js +81 -9
  136. umap/static/umap/locale/sl.json +81 -9
  137. umap/static/umap/locale/sr.js +81 -9
  138. umap/static/umap/locale/sr.json +81 -9
  139. umap/static/umap/locale/sv.js +81 -9
  140. umap/static/umap/locale/sv.json +81 -9
  141. umap/static/umap/locale/th_TH.js +81 -9
  142. umap/static/umap/locale/th_TH.json +81 -9
  143. umap/static/umap/locale/tr.js +81 -9
  144. umap/static/umap/locale/tr.json +81 -9
  145. umap/static/umap/locale/uk_UA.js +81 -9
  146. umap/static/umap/locale/uk_UA.json +81 -9
  147. umap/static/umap/locale/vi.js +81 -9
  148. umap/static/umap/locale/vi.json +81 -9
  149. umap/static/umap/locale/vi_VN.json +81 -9
  150. umap/static/umap/locale/zh.js +81 -9
  151. umap/static/umap/locale/zh.json +81 -9
  152. umap/static/umap/locale/zh_CN.json +81 -9
  153. umap/static/umap/locale/zh_TW.Big5.json +81 -9
  154. umap/static/umap/locale/zh_TW.js +98 -26
  155. umap/static/umap/locale/zh_TW.json +98 -26
  156. umap/static/umap/map.css +325 -102
  157. umap/static/umap/vars.css +1 -0
  158. umap/static/umap/vendors/betterknown/betterknown.mjs +287 -0
  159. umap/static/umap/vendors/editable/Leaflet.Editable.js +3 -1
  160. umap/static/umap/vendors/openrouteservice/ors-js-client.js +521 -0
  161. umap/static/umap/vendors/openrouteservice/ors-js-client.js.map +1 -0
  162. umap/static/umap/vendors/simple-elevation-chart/elevation.js +63 -0
  163. umap/static/umap/vendors/simple-elevation-chart/elevation.svg +8 -0
  164. umap/static/umap/vendors/snapdom/snapdom.min.mjs +3 -0
  165. umap/storage/fs.py +3 -2
  166. umap/storage/staticfiles.py +12 -0
  167. umap/templates/base.html +4 -1
  168. umap/templates/umap/css.html +0 -4
  169. umap/templates/umap/js.html +1 -3
  170. umap/tests/base.py +9 -1
  171. umap/tests/integration/test_basics.py +3 -1
  172. umap/tests/integration/test_conditional_rules.py +79 -37
  173. umap/tests/integration/test_datalayer.py +1 -1
  174. umap/tests/integration/test_draw_polygon.py +3 -5
  175. umap/tests/integration/test_draw_polyline.py +4 -6
  176. umap/tests/integration/test_draw_route.py +178 -0
  177. umap/tests/integration/test_edit_datalayer.py +1 -1
  178. umap/tests/integration/test_edit_map.py +1 -1
  179. umap/tests/integration/test_edit_marker.py +8 -8
  180. umap/tests/integration/test_edit_polygon.py +2 -2
  181. umap/tests/integration/test_export_map.py +84 -10
  182. umap/tests/integration/test_import.py +140 -0
  183. umap/tests/integration/test_map_preview.py +1 -1
  184. umap/tests/integration/test_optimistic_merge.py +72 -12
  185. umap/tests/integration/test_share.py +1 -1
  186. umap/tests/integration/test_tableeditor.py +10 -7
  187. umap/tests/integration/test_websocket_sync.py +4 -4
  188. umap/utils.py +37 -0
  189. umap/views.py +18 -2
  190. umap_project-3.3.0.dist-info/METADATA +76 -0
  191. {umap_project-3.1.2.dist-info → umap_project-3.3.0.dist-info}/RECORD +194 -188
  192. umap/static/umap/vendors/markercluster/MarkerCluster.Default.css +0 -60
  193. umap/static/umap/vendors/markercluster/MarkerCluster.css +0 -14
  194. umap/static/umap/vendors/markercluster/leaflet.markercluster.js +0 -2
  195. umap/static/umap/vendors/markercluster/leaflet.markercluster.js.map +0 -1
  196. umap_project-3.1.2.dist-info/METADATA +0 -68
  197. {umap_project-3.1.2.dist-info → umap_project-3.3.0.dist-info}/WHEEL +0 -0
  198. {umap_project-3.1.2.dist-info → umap_project-3.3.0.dist-info}/entry_points.txt +0 -0
  199. {umap_project-3.1.2.dist-info → umap_project-3.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,6 +3,7 @@ import {
3
3
  DomEvent,
4
4
  DomUtil,
5
5
  Icon,
6
+ Point,
6
7
  } from '../../../vendors/leaflet/leaflet-src.esm.js'
7
8
  import { SCHEMA } from '../schema.js'
8
9
  import * as Utils from '../utils.js'
@@ -11,6 +12,8 @@ export function getClass(name) {
11
12
  switch (name) {
12
13
  case 'Circle':
13
14
  return Circle
15
+ case 'LargeCircle':
16
+ return LargeCircle
14
17
  case 'Ball':
15
18
  return Ball
16
19
  case 'Drop':
@@ -25,13 +28,13 @@ export function getClass(name) {
25
28
  export const RECENT = []
26
29
 
27
30
  const BaseIcon = DivIcon.extend({
31
+ default_options: {
32
+ iconSize: null, // Made in css
33
+ iconUrl: SCHEMA.iconUrl.default,
34
+ feature: null,
35
+ },
28
36
  initialize: function (options) {
29
- const default_options = {
30
- iconSize: null, // Made in css
31
- iconUrl: SCHEMA.iconUrl.default,
32
- feature: null,
33
- }
34
- options = L.Util.extend({}, default_options, options)
37
+ options = { ...this.default_options, ...options }
35
38
  Icon.prototype.initialize.call(this, options)
36
39
  this.feature = this.options.feature
37
40
  if (this.feature?.isReadOnly()) {
@@ -66,6 +69,10 @@ const BaseIcon = DivIcon.extend({
66
69
  return color
67
70
  },
68
71
 
72
+ _getSize: function () {
73
+ return this.feature?.getOption('iconSize') || SCHEMA.iconSize.default
74
+ },
75
+
69
76
  _getOpacity: function () {
70
77
  if (this.feature) return this.feature.getOption('iconOpacity')
71
78
  return SCHEMA.iconOpacity.default
@@ -77,18 +84,33 @@ const BaseIcon = DivIcon.extend({
77
84
  if (this.feature.isActive()) this.options.className += ' umap-icon-active'
78
85
  DivIcon.prototype._setIconStyles.call(this, img, name)
79
86
  },
87
+
88
+ createIcon: function () {
89
+ const [root, elements] = Utils.loadTemplateWithRefs(this.getTemplate())
90
+ this.root = root
91
+ this.elements = elements
92
+ this.root.dataset.feature = this.feature?.id
93
+ if (this.elements.container) {
94
+ const src = this._getIconUrl('icon')
95
+ if (src) {
96
+ this.elements.icon = makeElement(src, this.elements.container)
97
+ }
98
+ }
99
+ this._setIconStyles(this.root, 'icon')
100
+ return this.root
101
+ },
80
102
  })
81
103
 
82
104
  const DefaultIcon = BaseIcon.extend({
83
105
  default_options: {
84
- iconAnchor: new L.Point(16, 40),
85
- popupAnchor: new L.Point(0, -40),
86
- tooltipAnchor: new L.Point(16, -24),
106
+ iconAnchor: new Point(16, 40),
107
+ popupAnchor: new Point(0, -40),
108
+ tooltipAnchor: new Point(16, -24),
87
109
  className: 'umap-div-icon',
88
110
  },
89
111
 
90
112
  initialize: function (options) {
91
- options = L.Util.extend({}, this.default_options, options)
113
+ options = { ...this.default_options, ...options }
92
114
  BaseIcon.prototype.initialize.call(this, options)
93
115
  },
94
116
 
@@ -108,92 +130,113 @@ const DefaultIcon = BaseIcon.extend({
108
130
  setContrast(this.elements.icon, this.elements.container, src, bgcolor)
109
131
  },
110
132
 
111
- createIcon: function () {
112
- this.elements = {}
113
- this.elements.main = DomUtil.create('div')
114
- this.elements.container = DomUtil.create(
115
- 'div',
116
- 'icon_container',
117
- this.elements.main
118
- )
119
- this.elements.main.dataset.feature = this.feature?.id
120
- this.elements.arrow = DomUtil.create('div', 'icon_arrow', this.elements.main)
121
- const src = this._getIconUrl('icon')
122
- if (src) {
123
- this.elements.icon = makeElement(src, this.elements.container)
124
- }
125
- this._setIconStyles(this.elements.main, 'icon')
126
- return this.elements.main
133
+ getTemplate: () => {
134
+ return `
135
+ <div>
136
+ <div class="icon-container" data-ref=container></div>
137
+ <div class="icon-arrow" data-ref=arrow></div>
138
+ </div>
139
+ `
127
140
  },
128
141
  })
129
142
 
130
143
  const Circle = BaseIcon.extend({
131
144
  initialize: function (options) {
132
145
  const default_options = {
133
- popupAnchor: new L.Point(0, -6),
134
- tooltipAnchor: new L.Point(6, 0),
146
+ iconSize: new Point(12, 12),
147
+ popupAnchor: new Point(0, -6),
148
+ tooltipAnchor: new Point(6, 0),
135
149
  className: 'umap-circle-icon',
136
150
  }
137
- options = L.Util.extend({}, default_options, options)
151
+ options = { ...default_options, ...(options || {}) }
138
152
  BaseIcon.prototype.initialize.call(this, options)
139
153
  },
140
154
 
141
155
  _setIconStyles: function (img, name) {
142
156
  BaseIcon.prototype._setIconStyles.call(this, img, name)
143
- this.elements.main.style.backgroundColor = this._getColor()
144
- this.elements.main.style.opacity = this._getOpacity()
157
+ this.root.style.backgroundColor = this._getColor()
158
+ this.root.style.opacity = this._getOpacity()
145
159
  },
146
160
 
147
- createIcon: function () {
148
- this.elements = {}
149
- this.elements.main = DomUtil.create('div')
150
- this.elements.main.innerHTML = '&nbsp;'
151
- this._setIconStyles(this.elements.main, 'icon')
152
- this.elements.main.dataset.feature = this.feature?.id
153
- return this.elements.main
161
+ getTemplate: () => {
162
+ return '<div>&nbsp;</div>'
163
+ },
164
+ })
165
+
166
+ const LargeCircle = BaseIcon.extend({
167
+ default_options: {
168
+ className: 'umap-large-circle-icon',
169
+ },
170
+ initialize: function (options) {
171
+ BaseIcon.prototype.initialize.call(this, options)
172
+ const size = this._getSize()
173
+ this.options.popupAnchor = new Point(0, (size / 2) * -1)
174
+ this.options.tooltipAnchor = new Point(size / 2, 0)
175
+ this.options.iconAnchor = new Point(size / 2, size / 2)
176
+ },
177
+
178
+ _setIconStyles: function (img, name) {
179
+ BaseIcon.prototype._setIconStyles.call(this, img, name)
180
+ this.root.style.opacity = this._getOpacity()
181
+ this.root.style.borderColor = this._getColor()
182
+ this.root.style.width = `${this._getSize()}px`
183
+ this.root.style.height = `${this._getSize()}px`
184
+ },
185
+
186
+ getTemplate: () => {
187
+ return '<div data-ref=container></div>'
154
188
  },
155
189
  })
156
190
 
157
191
  const Raw = DefaultIcon.extend({
158
192
  default_options: {
159
- iconSize: new L.Point(48, 48),
160
- popupAnchor: new L.Point(0, 0),
161
- tooltipAnchor: new L.Point(0, 0),
162
193
  className: 'umap-raw-icon',
163
194
  },
195
+ initialize: function (options) {
196
+ DefaultIcon.prototype.initialize.call(this, options)
197
+ const size = this._getSize()
198
+ this.options.popupAnchor = new Point(0, (size / 2) * -1)
199
+ this.options.tooltipAnchor = new Point(size / 2, 0)
200
+ this.options.iconAnchor = new Point(size / 2, size / 2)
201
+ },
202
+
203
+ _setIconStyles: function (img, name) {
204
+ BaseIcon.prototype._setIconStyles.call(this, img, name)
205
+ this.root.style.width = `${this._getSize()}px`
206
+ this.root.style.height = `${this._getSize()}px`
207
+ },
164
208
 
165
209
  _getColor: () => 'transparent',
210
+
211
+ getTemplate: () => {
212
+ return '<div data-ref=container></div>'
213
+ },
166
214
  })
167
215
 
168
216
  const Drop = DefaultIcon.extend({
169
217
  default_options: {
170
- iconAnchor: new L.Point(16, 42),
171
- popupAnchor: new L.Point(0, -42),
172
- tooltipAnchor: new L.Point(16, -24),
218
+ iconAnchor: new Point(16, 42),
219
+ popupAnchor: new Point(0, -42),
220
+ tooltipAnchor: new Point(16, -24),
173
221
  className: 'umap-drop-icon',
174
222
  },
175
223
  })
176
224
 
177
225
  const Ball = DefaultIcon.extend({
178
226
  default_options: {
179
- iconAnchor: new L.Point(8, 30),
180
- popupAnchor: new L.Point(0, -28),
181
- tooltipAnchor: new L.Point(8, -23),
227
+ iconAnchor: new Point(8, 30),
228
+ popupAnchor: new Point(0, -28),
229
+ tooltipAnchor: new Point(8, -23),
182
230
  className: 'umap-ball-icon',
183
231
  },
184
232
 
185
- createIcon: function () {
186
- this.elements = {}
187
- this.elements.main = DomUtil.create('div')
188
- this.elements.container = DomUtil.create(
189
- 'div',
190
- 'icon_container',
191
- this.elements.main
192
- )
193
- this.elements.main.dataset.feature = this.feature?.id
194
- this.elements.arrow = DomUtil.create('div', 'icon_arrow', this.elements.main)
195
- this._setIconStyles(this.elements.main, 'icon')
196
- return this.elements.main
233
+ getTemplate: () => {
234
+ return `
235
+ <div>
236
+ <div class="icon-container" data-ref=ball></div>
237
+ <div class="icon-arrow" data-ref=arrow></div>
238
+ </div>
239
+ `
197
240
  },
198
241
 
199
242
  _setIconStyles: function (img, name) {
@@ -207,38 +250,26 @@ const Ball = DefaultIcon.extend({
207
250
  } else {
208
251
  background = `radial-gradient(circle at 6px 38% , white -4px, ${color} 8px) repeat scroll 0 0 transparent`
209
252
  }
210
- this.elements.container.style.background = background
211
- this.elements.container.style.opacity = this._getOpacity()
253
+ this.elements.ball.style.background = background
254
+ this.elements.ball.style.opacity = this._getOpacity()
212
255
  },
213
256
  })
214
257
 
215
258
  export const Cluster = DivIcon.extend({
216
259
  options: {
217
260
  iconSize: [40, 40],
218
- },
219
-
220
- initialize: function (datalayer, cluster) {
221
- this.datalayer = datalayer
222
- this.cluster = cluster
261
+ className: 'umap-cluster-icon',
223
262
  },
224
263
 
225
264
  createIcon: function () {
226
- const container = DomUtil.create('div', 'leaflet-marker-icon marker-cluster')
227
- const div = DomUtil.create('div', '', container)
228
- const span = DomUtil.create('span', '', div)
229
- const backgroundColor = this.datalayer.getColor()
230
- span.textContent = this.cluster.getChildCount()
231
- div.style.backgroundColor = backgroundColor
232
- return container
233
- },
234
-
235
- computeTextColor: function (el) {
236
- let color
237
- const backgroundColor = this.datalayer.getColor()
238
- if (this.datalayer.properties.cluster?.textColor) {
239
- color = this.datalayer.properties.cluster.textColor
240
- }
241
- return color || DomUtil.TextColorFromBackgroundColor(el, backgroundColor)
265
+ const template = '<div><span data-ref=counter></span></div>'
266
+ const [root, { counter }] = Utils.loadTemplateWithRefs(template)
267
+ this.root = root
268
+ this.counter = counter
269
+ this.counter.textContent = this.options.getCounter()
270
+ this.root.style.backgroundColor = this.options.color
271
+ this._setIconStyles(this.root, 'icon')
272
+ return this.root
242
273
  },
243
274
  })
244
275
 
@@ -249,11 +280,9 @@ export function isImg(src) {
249
280
  export function makeElement(src, parent) {
250
281
  let icon
251
282
  if (isImg(src)) {
252
- icon = DomUtil.create('img')
253
- icon.src = src
283
+ icon = Utils.loadTemplate(`<img loading="lazy" src="${src}">`)
254
284
  } else {
255
- icon = DomUtil.create('span')
256
- icon.textContent = src
285
+ icon = Utils.loadTemplate(`<span>${src}</span>`)
257
286
  }
258
287
  parent.appendChild(icon)
259
288
  return icon
@@ -273,7 +302,7 @@ export function setContrast(icon, parent, src, bgcolor) {
273
302
  if (DomUtil.contrastedColor(parent, bgcolor)) {
274
303
  // Decide whether to switch svg to white or not, but do it
275
304
  // only for internal SVG, as invert could do weird things
276
- if (Utils.isPath(src) && src.endsWith('.svg') && src !== SCHEMA.iconUrl.default) {
305
+ if (src.endsWith('.svg') && src !== SCHEMA.iconUrl.default) {
277
306
  // Must be called after icon container is added to the DOM
278
307
  // An image
279
308
  icon.style.filter = 'invert(1)'
@@ -67,7 +67,7 @@ const ClassifiedMixin = {
67
67
 
68
68
  getValues: function () {
69
69
  const values = []
70
- this.datalayer.eachFeature((feature) => {
70
+ this.datalayer.features.forEach((feature) => {
71
71
  const value = this._getValue(feature)
72
72
  if (value !== undefined) values.push(value)
73
73
  })
@@ -198,7 +198,7 @@ export const Choropleth = FeatureGroup.extend({
198
198
  'properties.choropleth.property',
199
199
  {
200
200
  handler: 'Select',
201
- selectOptions: this.datalayer.allProperties(),
201
+ selectOptions: this.datalayer.fieldKeys,
202
202
  label: translate('Choropleth property value'),
203
203
  },
204
204
  ],
@@ -307,7 +307,7 @@ export const Circles = FeatureGroup.extend({
307
307
  'properties.circles.property',
308
308
  {
309
309
  handler: 'Select',
310
- selectOptions: this.datalayer.allProperties(),
310
+ selectOptions: this.datalayer.fieldKeys,
311
311
  label: translate('Property name to compute circles'),
312
312
  },
313
313
  ],
@@ -384,8 +384,7 @@ export const Categorized = FeatureGroup.extend({
384
384
 
385
385
  _getValue: function (feature) {
386
386
  const key =
387
- this.datalayer.properties.categorized.property ||
388
- this.datalayer.allProperties()[0]
387
+ this.datalayer.properties.categorized.property || this.datalayer.fieldKeys[0]
389
388
  return feature.properties[key]
390
389
  },
391
390
 
@@ -438,7 +437,7 @@ export const Categorized = FeatureGroup.extend({
438
437
  'properties.categorized.property',
439
438
  {
440
439
  handler: 'Select',
441
- selectOptions: this.datalayer.allProperties(),
440
+ selectOptions: this.datalayer.fieldKeys,
442
441
  label: translate('Category property'),
443
442
  },
444
443
  ],
@@ -447,7 +446,7 @@ export const Categorized = FeatureGroup.extend({
447
446
  {
448
447
  handler: 'Select',
449
448
  label: translate('Color palette'),
450
- selectOptions: this.getColorSchemes(this._classes),
449
+ getOptions: () => this.getColorSchemes(this._classes),
451
450
  },
452
451
  ],
453
452
  [
@@ -481,6 +480,8 @@ export const Categorized = FeatureGroup.extend({
481
480
  if (builder) builder.helpers['properties.categorized.mode'].fetch()
482
481
  }
483
482
  this.compute()
483
+ // Rebuild list of color palettes when aggregation property changes.
484
+ builder?.helpers['properties.categorized.brewer']?.fetch()
484
485
  // If user changes the mode
485
486
  // then update the categories input value
486
487
  if (field === 'properties.categorized.mode') {
@@ -1,24 +1,100 @@
1
- import { Evented } from '../../../../vendors/leaflet/leaflet-src.esm.js'
2
- // WARNING must be loaded dynamically, or at least after leaflet.markercluster
3
- // Uses global L.MarkerCluster and L.MarkerClusterGroup, not exposed as ESM
1
+ import {
2
+ FeatureGroup,
3
+ LayerGroup,
4
+ Point,
5
+ Marker,
6
+ Rectangle,
7
+ Polyline,
8
+ DomUtil,
9
+ latLngBounds,
10
+ } from '../../../../vendors/leaflet/leaflet-src.esm.js'
4
11
  import { translate } from '../../i18n.js'
5
12
  import * as Utils from '../../utils.js'
6
13
  import { Cluster as ClusterIcon } from '../icon.js'
7
14
  import { LayerMixin } from './base.js'
8
15
 
9
- const MarkerCluster = L.MarkerCluster.extend({
10
- // Custom class so we can call computeTextColor
11
- // when element is already on the DOM.
12
-
16
+ const MarkerCluster = Marker.extend({
13
17
  _initIcon: function () {
14
- L.MarkerCluster.prototype._initIcon.call(this)
15
- const div = this._icon.querySelector('div')
18
+ Marker.prototype._initIcon.call(this)
19
+ const counter = this._icon.querySelector('span')
16
20
  // Compute text color only when icon is added to the DOM.
17
- div.style.color = this._iconObj.computeTextColor(div)
21
+ const bgColor = this.options.icon.options.color
22
+ const textColor = this.options.icon.options.textColor
23
+ counter.style.color =
24
+ textColor || DomUtil.TextColorFromBackgroundColor(counter, bgColor)
25
+ },
26
+
27
+ computeCoverage() {
28
+ if (this._layers.length < 2) return
29
+ if (!this._coverage) {
30
+ const latlngs = this._layers.map((layer) => layer._latlng)
31
+ const bounds = latLngBounds(latlngs)
32
+ this._coverage = new Rectangle(latlngs, {
33
+ color: this.options.icon.options.color,
34
+ stroke: false,
35
+ })
36
+ this._latlng = bounds.getCenter()
37
+ }
38
+ },
39
+
40
+ async zoomToCoverage() {
41
+ let resolve = undefined
42
+ const promise = new Promise((r) => {
43
+ resolve = r
44
+ })
45
+ if (this._map && this._coverage) {
46
+ this._map.once('moveend', () => resolve())
47
+ this._map.fitBounds(this._coverage.getBounds())
48
+ }
49
+ return promise
50
+ },
51
+
52
+ _spiderfyLatLng: function (center, index) {
53
+ const step = 20
54
+ const maxRadius = 150
55
+ const zoom = this._map.getZoom()
56
+ const angle = (index * step * Math.PI) / 180
57
+ const progress = index / this._layers.length
58
+ const radius = maxRadius * (1 - progress) ** 0.4
59
+ const x = radius * Math.cos(angle)
60
+ const y = radius * Math.sin(angle)
61
+ const point = this._map.project([center.lat, center.lng], zoom)
62
+ const latlng = this._map.unproject(new Point(point.x + x, point.y + y), zoom)
63
+ return latlng
64
+ },
65
+
66
+ spiderfy() {
67
+ if (!this._map) return
68
+ const crs = this._map.options.crs
69
+ if (this._spider && this._map.hasLayer(this._spider)) this.unspiderfy()
70
+ this._spider = new LayerGroup()
71
+ let i = 1
72
+ const center = this.getLatLng()
73
+ for (const layer of this._layers) {
74
+ const latlng = this._spiderfyLatLng(center, i++)
75
+ layer._originalLatLng = layer._latlng
76
+ layer.setLatLng(latlng)
77
+ this._spider.addLayer(layer)
78
+ const line = new Polyline([center, latlng], { color: 'black', weight: 1 })
79
+ this._spider.addLayer(line)
80
+ }
81
+ this._map.addLayer(this._spider)
82
+ this._icon.hidden = true
83
+ this._map.once('click zoomstart', this.unspiderfy, this)
84
+ this.once('remove', this.unspiderfy, this)
85
+ },
86
+
87
+ unspiderfy() {
88
+ if (this._icon) this._icon.hidden = false
89
+ if (this._spider) this._spider.remove()
90
+ for (const layer of this._layers) {
91
+ if (layer._originalLatLng) layer.setLatLng(layer._originalLatLng)
92
+ delete layer._originalLatLng
93
+ }
18
94
  },
19
95
  })
20
96
 
21
- export const Cluster = L.MarkerClusterGroup.extend({
97
+ export const Cluster = FeatureGroup.extend({
22
98
  statics: {
23
99
  NAME: translate('Clustered'),
24
100
  TYPE: 'Cluster',
@@ -27,58 +103,132 @@ export const Cluster = L.MarkerClusterGroup.extend({
27
103
 
28
104
  initialize: function (datalayer) {
29
105
  this.datalayer = datalayer
106
+ this._bucket = []
30
107
  if (!Utils.isObject(this.datalayer.properties.cluster)) {
31
108
  this.datalayer.properties.cluster = {}
32
109
  }
33
- const options = {
34
- polygonOptions: {
35
- color: this.datalayer.getColor(),
36
- },
37
- iconCreateFunction: (cluster) => new ClusterIcon(datalayer, cluster),
110
+ FeatureGroup.prototype.initialize.call(this)
111
+ LayerMixin.onInit.call(this, this.datalayer._leafletMap)
112
+ },
113
+
114
+ dataChanged: function () {
115
+ this.redraw()
116
+ },
117
+
118
+ removeClusters() {
119
+ this.hideCoverage()
120
+ if (this._map) {
121
+ for (const cluster of this._clusters) {
122
+ const layer = cluster._layers.length === 1 ? cluster._layers[0] : cluster
123
+ this._map.removeLayer(layer)
124
+ }
38
125
  }
39
- if (this.datalayer.properties.cluster?.radius) {
40
- options.maxClusterRadius = this.datalayer.properties.cluster.radius
126
+ },
127
+
128
+ addClusters() {
129
+ if (this._map) {
130
+ for (const cluster of this._clusters) {
131
+ const layer = cluster._layers.length === 1 ? cluster._layers[0] : cluster
132
+ this._map.addLayer(layer)
133
+ }
41
134
  }
42
- L.MarkerClusterGroup.prototype.initialize.call(this, options)
43
- LayerMixin.onInit.call(this, this.datalayer._leafletMap)
44
- this._markerCluster = MarkerCluster
45
- this._layers = []
46
135
  },
47
136
 
48
- onAdd: function (map) {
49
- LayerMixin.onAdd.call(this, map)
50
- return L.MarkerClusterGroup.prototype.onAdd.call(this, map)
137
+ redraw: function () {
138
+ this.removeClusters()
139
+ this.compute()
140
+ this.addClusters()
51
141
  },
52
142
 
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)
143
+ compute() {
144
+ const radius = this.datalayer.properties.cluster?.radius || 80
145
+ this._clusters = []
146
+ const map = this.datalayer._umap._leafletMap
147
+ const CRS = map.options.crs
148
+ for (const layer of this._bucket) {
149
+ layer._xy = CRS.latLngToPoint(layer._latlng, map.getZoom())
150
+ let cluster = null
151
+ for (const candidate of this._clusters) {
152
+ if (candidate._xy.distanceTo(layer._xy) <= radius) {
153
+ cluster = candidate
154
+ break
155
+ }
156
+ }
157
+ if (!cluster) {
158
+ const icon = new ClusterIcon({
159
+ color: this.datalayer.getColor(),
160
+ textColor: this.datalayer.properties.cluster?.textColor,
161
+ getCounter: () => cluster._layers.length,
162
+ })
163
+ cluster = new MarkerCluster(layer._latlng, { icon })
164
+ cluster.addEventParent(this)
165
+ cluster._xy ??= layer._xy
166
+ cluster._layers = []
167
+ this._clusters.push(cluster)
168
+ }
169
+ cluster._layers.push(layer)
170
+ layer._cluster = cluster
171
+ }
172
+ for (const cluster of this._clusters) {
173
+ cluster.computeCoverage()
174
+ }
62
175
  },
63
176
 
64
177
  addLayer: function (layer) {
65
- this._layers.push(layer)
66
- try {
67
- return L.MarkerClusterGroup.prototype.addLayer.call(this, layer)
68
- } catch (error) {
69
- console.debug(error)
70
- // Certainly a race condition when loading a clustered layer
71
- // while zooming (this for example can happen at load, when the
72
- // initial zoom is changed by uMap).
73
- // FIXME: remove when this is merged:
74
- // https://github.com/Leaflet/Leaflet.markercluster/pull/1048/files
75
- return this
178
+ if (!layer.getLatLng) return FeatureGroup.prototype.addLayer.call(this, layer)
179
+ // Do not add yet the layer to the map
180
+ // wait for datachanged event, so we can compute breaks only once
181
+ this._bucket.push(layer)
182
+ return this
183
+ },
184
+
185
+ onAdd: function (leafletMap) {
186
+ this.on('click', this.onClick)
187
+ this.on('mouseover', this.onMouseOver)
188
+ this.on('mouseout', this.onMouseOut)
189
+ this.compute()
190
+ LayerMixin.onAdd.call(this, leafletMap)
191
+ leafletMap.on('zoomend', this.redraw, this)
192
+ this.addClusters()
193
+ return FeatureGroup.prototype.onAdd.call(this, leafletMap)
194
+ },
195
+
196
+ onRemove: function (leafletMap) {
197
+ leafletMap.off('zoomend', this.redraw, this)
198
+ this.off('click', this.onClick)
199
+ this.off('mouseover', this.onMouseOver)
200
+ this.off('mouseout', this.onMouseOut)
201
+ LayerMixin.onRemove.call(this, leafletMap)
202
+ this.removeClusters()
203
+ return FeatureGroup.prototype.onRemove.call(this, leafletMap)
204
+ },
205
+
206
+ showCoverage(cluster) {
207
+ if (cluster._coverage) {
208
+ this._shownCoverage = cluster._coverage
209
+ this._map.addLayer(this._shownCoverage)
76
210
  }
77
211
  },
78
212
 
79
- removeLayer: function (layer) {
80
- this._layers.splice(this._layers.indexOf(layer), 1)
81
- return L.MarkerClusterGroup.prototype.removeLayer.call(this, layer)
213
+ hideCoverage() {
214
+ if (this._shownCoverage) this._map.removeLayer(this._shownCoverage)
215
+ },
216
+
217
+ onMouseOver(event) {
218
+ event.layer?.computeCoverage?.()
219
+ this.showCoverage(event.layer)
220
+ },
221
+
222
+ onMouseOut(event) {
223
+ this.hideCoverage()
224
+ },
225
+
226
+ onClick(event) {
227
+ if (this._map.getZoom() === this._map.getMaxZoom()) {
228
+ event.layer.spiderfy?.()
229
+ } else {
230
+ event.layer.zoomToCoverage?.()
231
+ }
82
232
  },
83
233
 
84
234
  getEditableProperties: () => [
@@ -101,20 +251,6 @@ export const Cluster = L.MarkerClusterGroup.extend({
101
251
  ],
102
252
 
103
253
  onEdit: function (field, builder) {
104
- if (field === 'properties.cluster.radius') {
105
- // No way to reset radius of an already instanciated MarkerClusterGroup...
106
- this.datalayer.resetLayer(true)
107
- return
108
- }
109
- if (field === 'properties.color') {
110
- this.options.polygonOptions.color = this.datalayer.getColor()
111
- }
112
- },
113
-
114
- _moveChild: (layer, from, to) => {
115
- // Extend parent method, so to remove remove/addLayer,
116
- // to let our own dragend event listener be called
117
- // cf https://github.com/umap-project/umap/issues/2749
118
- layer._latlng = to
254
+ if (field === 'properties.cluster.radius') this.redraw()
119
255
  },
120
256
  })