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,994 @@
1
+ import {
2
+ DomUtil,
3
+ DomEvent,
4
+ stamp,
5
+ GeoJSON,
6
+ LineUtil,
7
+ } from '../../../vendors/leaflet/leaflet-src.esm.js'
8
+ import * as Utils from '../utils.js'
9
+ import { SCHEMA } from '../schema.js'
10
+ import { translate } from '../i18n.js'
11
+ import { uMapAlert as Alert } from '../../components/alerts/alert.js'
12
+ import {
13
+ LeafletMarker,
14
+ LeafletPolyline,
15
+ LeafletPolygon,
16
+ MaskPolygon,
17
+ } from '../rendering/ui.js'
18
+ import loadPopup from '../rendering/popup.js'
19
+
20
+ class Feature {
21
+ constructor(datalayer, geojson = {}, id = null) {
22
+ this.sync = datalayer.map.sync_engine.proxy(this)
23
+ this._marked_for_deletion = false
24
+ this._isDirty = false
25
+ this._ui = null
26
+
27
+ // DataLayer the feature belongs to
28
+ this.datalayer = datalayer
29
+ this.properties = { _umap_options: {}, ...(geojson.properties || {}) }
30
+ this.staticOptions = {}
31
+
32
+ if (geojson.coordinates) {
33
+ geojson = { geometry: geojson }
34
+ }
35
+ if (geojson.geometry) {
36
+ this.populate(geojson)
37
+ }
38
+
39
+ if (id) {
40
+ this.id = id
41
+ } else {
42
+ let geojson_id
43
+ if (geojson) {
44
+ geojson_id = geojson.id
45
+ }
46
+
47
+ // Each feature needs an unique identifier
48
+ if (Utils.checkId(geojson_id)) {
49
+ this.id = geojson_id
50
+ } else {
51
+ this.id = Utils.generateId()
52
+ }
53
+ }
54
+ }
55
+
56
+ set isDirty(status) {
57
+ this._isDirty = status
58
+ if (this.datalayer) {
59
+ this.datalayer.isDirty = status
60
+ }
61
+ }
62
+
63
+ get isDirty() {
64
+ return this._isDirty
65
+ }
66
+
67
+ get ui() {
68
+ if (!this._ui) this.makeUI()
69
+ return this._ui
70
+ }
71
+
72
+ get map() {
73
+ return this.datalayer?.map
74
+ }
75
+
76
+ get center() {
77
+ return this.ui.getCenter()
78
+ }
79
+
80
+ get bounds() {
81
+ return this.ui.getBounds()
82
+ }
83
+
84
+ get type() {
85
+ return this.geometry.type
86
+ }
87
+
88
+ get coordinates() {
89
+ return this.geometry.coordinates
90
+ }
91
+
92
+ get geometry() {
93
+ return this._geometry
94
+ }
95
+
96
+ set geometry(value) {
97
+ this._geometry = value
98
+ this.pushGeometry()
99
+ }
100
+
101
+ pushGeometry() {
102
+ this.ui.setLatLngs(this.toLatLngs())
103
+ }
104
+
105
+ pullGeometry(sync = true) {
106
+ this.fromLatLngs(this.ui.getLatLngs())
107
+ if (sync) {
108
+ this.sync.update('geometry', this.geometry)
109
+ }
110
+ }
111
+
112
+ fromLatLngs(latlngs) {
113
+ this._geometry = this.convertLatLngs(latlngs)
114
+ }
115
+
116
+ makeUI() {
117
+ const klass = this.getUIClass()
118
+ this._ui = new klass(this, this.toLatLngs())
119
+ }
120
+
121
+ getUIClass() {
122
+ return this.getOption('UIClass')
123
+ }
124
+
125
+ getClassName() {
126
+ return this.staticOptions.className
127
+ }
128
+
129
+ getPreviewColor() {
130
+ return this.getDynamicOption(this.staticOptions.mainColor)
131
+ }
132
+
133
+ getSyncMetadata() {
134
+ return {
135
+ subject: 'feature',
136
+ metadata: {
137
+ id: this.id,
138
+ layerId: this.datalayer?.umap_id || null,
139
+ featureType: this.getClassName(),
140
+ },
141
+ }
142
+ }
143
+
144
+ onCommit() {
145
+ // When the layer is a remote layer, we don't want to sync the creation of the
146
+ // points via the websocket, as the other peers will get them themselves.
147
+ if (this.datalayer?.isRemoteLayer()) return
148
+
149
+ // The "endEdit" event is triggered at the end of an edition,
150
+ // and will trigger the sync.
151
+ // In the case of a deletion (or a change of layer), we don't want this
152
+ // event triggered to cause a sync event, as it would reintroduce
153
+ // deleted features.
154
+ // The `._marked_for_deletion` private property is here to track this status.
155
+ if (this._marked_for_deletion === true) {
156
+ this._marked_for_deletion = false
157
+ return
158
+ }
159
+ this.sync.upsert(this.toGeoJSON())
160
+ }
161
+
162
+ isReadOnly() {
163
+ return this.datalayer?.isDataReadOnly()
164
+ }
165
+
166
+ getSlug() {
167
+ return this.properties[this.map.getOption('slugKey') || 'name'] || ''
168
+ }
169
+
170
+ getPermalink() {
171
+ const slug = this.getSlug()
172
+ if (slug)
173
+ return `${Utils.getBaseUrl()}?${Utils.buildQueryString({ feature: slug })}${
174
+ window.location.hash
175
+ }`
176
+ }
177
+
178
+ view({ latlng } = {}) {
179
+ const outlink = this.getOption('outlink')
180
+ const target = this.getOption('outlinkTarget')
181
+ if (outlink) {
182
+ switch (target) {
183
+ case 'self':
184
+ window.location = outlink
185
+ break
186
+ case 'parent':
187
+ window.top.location = outlink
188
+ break
189
+ default:
190
+ window.open(this.properties._umap_options.outlink)
191
+ }
192
+ return
193
+ }
194
+ // TODO deal with an event instead?
195
+ if (this.map.slideshow) {
196
+ this.map.slideshow.current = this
197
+ }
198
+ this.map.currentFeature = this
199
+ this.attachPopup()
200
+ this.ui.openPopup(latlng || this.center)
201
+ }
202
+
203
+ render(fields) {
204
+ const impactData = fields.some((field) => {
205
+ return field.startsWith('properties.')
206
+ })
207
+ if (impactData) {
208
+ if (this.map.currentFeature === this) {
209
+ this.view()
210
+ }
211
+ }
212
+ this.redraw()
213
+ }
214
+
215
+ edit(event) {
216
+ if (!this.map.editEnabled || this.isReadOnly()) return
217
+ const container = DomUtil.create('div', 'umap-feature-container')
218
+ DomUtil.createTitle(
219
+ container,
220
+ translate('Feature properties'),
221
+ `icon-${this.getClassName()}`
222
+ )
223
+
224
+ let builder = new U.FormBuilder(
225
+ this,
226
+ [['datalayer', { handler: 'DataLayerSwitcher' }]],
227
+ {
228
+ callback() {
229
+ this.edit(event)
230
+ }, // removeLayer step will close the edit panel, let's reopen it
231
+ }
232
+ )
233
+ container.appendChild(builder.build())
234
+
235
+ const properties = []
236
+ for (const property of this.datalayer._propertiesIndex) {
237
+ if (['name', 'description'].includes(property)) {
238
+ continue
239
+ }
240
+ properties.push([`properties.${property}`, { label: property }])
241
+ }
242
+ // We always want name and description for now (properties management to come)
243
+ properties.unshift('properties.description')
244
+ properties.unshift('properties.name')
245
+ builder = new U.FormBuilder(this, properties, {
246
+ id: 'umap-feature-properties',
247
+ })
248
+ container.appendChild(builder.build())
249
+ this.appendEditFieldsets(container)
250
+ const advancedActions = DomUtil.createFieldset(
251
+ container,
252
+ translate('Advanced actions')
253
+ )
254
+ this.getAdvancedEditActions(advancedActions)
255
+ const onLoad = this.map.editPanel.open({ content: container })
256
+ onLoad.then(() => {
257
+ builder.helpers['properties.name'].input.focus()
258
+ })
259
+ this.map.editedFeature = this
260
+ if (!this.isOnScreen()) this.zoomTo(event)
261
+ }
262
+
263
+ getAdvancedEditActions(container) {
264
+ const button = Utils.loadTemplate(`
265
+ <button class="button" type="button">
266
+ <i class="icon icon-24 icon-delete"></i>${translate('Delete')}
267
+ </button>`)
268
+ button.addEventListener('click', () => {
269
+ this.confirmDelete().then(() => this.map.editPanel.close())
270
+ })
271
+ container.appendChild(button)
272
+ }
273
+
274
+ appendEditFieldsets(container) {
275
+ const optionsFields = this.getShapeOptions()
276
+ let builder = new U.FormBuilder(this, optionsFields, {
277
+ id: 'umap-feature-shape-properties',
278
+ })
279
+ const shapeProperties = DomUtil.createFieldset(
280
+ container,
281
+ translate('Shape properties')
282
+ )
283
+ shapeProperties.appendChild(builder.build())
284
+
285
+ const advancedOptions = this.getAdvancedOptions()
286
+ builder = new U.FormBuilder(this, advancedOptions, {
287
+ id: 'umap-feature-advanced-properties',
288
+ })
289
+ const advancedProperties = DomUtil.createFieldset(
290
+ container,
291
+ translate('Advanced properties')
292
+ )
293
+ advancedProperties.appendChild(builder.build())
294
+
295
+ const interactionOptions = this.getInteractionOptions()
296
+ builder = new U.FormBuilder(this, interactionOptions)
297
+ const popupFieldset = DomUtil.createFieldset(
298
+ container,
299
+ translate('Interaction options')
300
+ )
301
+ popupFieldset.appendChild(builder.build())
302
+ }
303
+
304
+ getInteractionOptions() {
305
+ return [
306
+ 'properties._umap_options.popupShape',
307
+ 'properties._umap_options.popupTemplate',
308
+ 'properties._umap_options.showLabel',
309
+ 'properties._umap_options.labelDirection',
310
+ 'properties._umap_options.labelInteractive',
311
+ 'properties._umap_options.outlink',
312
+ 'properties._umap_options.outlinkTarget',
313
+ ]
314
+ }
315
+
316
+ endEdit() {}
317
+
318
+ getDisplayName(fallback) {
319
+ if (fallback === undefined) fallback = this.datalayer.getName()
320
+ const key = this.getOption('labelKey') || 'name'
321
+ // Variables mode.
322
+ if (U.Utils.hasVar(key))
323
+ return U.Utils.greedyTemplate(key, this.extendedProperties())
324
+ // Simple mode.
325
+ return this.properties[key] || this.properties.title || fallback
326
+ }
327
+
328
+ hasPopupFooter() {
329
+ if (this.datalayer.isRemoteLayer() && this.datalayer.options.remoteData.dynamic) {
330
+ return false
331
+ }
332
+ return this.map.getOption('displayPopupFooter')
333
+ }
334
+
335
+ getPopupClass() {
336
+ const old = this.getOption('popupTemplate') // Retrocompat.
337
+ return loadPopup(this.getOption('popupShape') || old)
338
+ }
339
+
340
+ attachPopup() {
341
+ const Class = this.getPopupClass()
342
+ this.ui.bindPopup(new Class(this))
343
+ }
344
+
345
+ async confirmDelete() {
346
+ const confirmed = await this.map.dialog.confirm(
347
+ translate('Are you sure you want to delete the feature?')
348
+ )
349
+ if (confirmed) {
350
+ this.del()
351
+ return true
352
+ }
353
+ return false
354
+ }
355
+
356
+ del(sync) {
357
+ this.isDirty = true
358
+ this.map.closePopup()
359
+ if (this.datalayer) {
360
+ this.datalayer.removeFeature(this, sync)
361
+ }
362
+ }
363
+
364
+ connectToDataLayer(datalayer) {
365
+ this.datalayer = datalayer
366
+ // FIXME should be in layer/ui
367
+ this.ui.options.renderer = this.datalayer.renderer
368
+ }
369
+
370
+ disconnectFromDataLayer(datalayer) {
371
+ if (this.datalayer === datalayer) {
372
+ this.datalayer = null
373
+ }
374
+ }
375
+
376
+ cleanProperty([key, value]) {
377
+ // dot in key will break the dot based property access
378
+ // while editing the feature
379
+ key = key.replace('.', '_')
380
+ return [key, value]
381
+ }
382
+
383
+ populate(geojson) {
384
+ this._geometry = geojson.geometry
385
+ this.properties = Object.fromEntries(
386
+ Object.entries(geojson.properties || {}).map(this.cleanProperty)
387
+ )
388
+ this.properties._umap_options = L.extend(
389
+ {},
390
+ this.properties._storage_options,
391
+ this.properties._umap_options
392
+ )
393
+ // Retrocompat
394
+ if (this.properties._umap_options.clickable === false) {
395
+ this.properties._umap_options.interactive = false
396
+ delete this.properties._umap_options.clickable
397
+ }
398
+ }
399
+
400
+ changeDataLayer(datalayer) {
401
+ if (this.datalayer) {
402
+ this.datalayer.isDirty = true
403
+ this.datalayer.removeFeature(this)
404
+ }
405
+
406
+ datalayer.addFeature(this)
407
+ this.sync.upsert(this.toGeoJSON())
408
+ datalayer.isDirty = true
409
+ this.redraw()
410
+ }
411
+
412
+ getOption(option, fallback) {
413
+ let value = fallback
414
+ if (typeof this.staticOptions[option] !== 'undefined') {
415
+ value = this.staticOptions[option]
416
+ } else if (U.Utils.usableOption(this.properties._umap_options, option)) {
417
+ value = this.properties._umap_options[option]
418
+ } else if (this.datalayer) {
419
+ value = this.datalayer.getOption(option, this)
420
+ } else {
421
+ value = this.map.getOption(option)
422
+ }
423
+ return value
424
+ }
425
+
426
+ getDynamicOption(option, fallback) {
427
+ let value = this.getOption(option, fallback)
428
+ // There is a variable inside.
429
+ if (U.Utils.hasVar(value)) {
430
+ value = U.Utils.greedyTemplate(value, this.properties, true)
431
+ if (U.Utils.hasVar(value)) value = this.map.getDefaultOption(option)
432
+ }
433
+ return value
434
+ }
435
+
436
+ zoomTo({ easing, latlng, callback } = {}) {
437
+ if (easing === undefined) easing = this.map.getOption('easing')
438
+ if (callback) this.map.once('moveend', callback.call(this))
439
+ if (easing) {
440
+ this.map.flyTo(this.center, this.getBestZoom())
441
+ } else {
442
+ latlng = latlng || this.center
443
+ this.map.setView(latlng, this.getBestZoom() || this.map.getZoom())
444
+ }
445
+ }
446
+
447
+ getBestZoom() {
448
+ return this.getOption('zoomTo')
449
+ }
450
+
451
+ getNext() {
452
+ return this.datalayer.getNextFeature(this)
453
+ }
454
+
455
+ getPrevious() {
456
+ return this.datalayer.getPreviousFeature(this)
457
+ }
458
+
459
+ cloneProperties() {
460
+ const properties = L.extend({}, this.properties)
461
+ properties._umap_options = L.extend({}, properties._umap_options)
462
+ if (Object.keys && Object.keys(properties._umap_options).length === 0) {
463
+ delete properties._umap_options // It can make a difference on big data sets
464
+ }
465
+ // Legacy
466
+ delete properties._storage_options
467
+ return properties
468
+ }
469
+
470
+ deleteProperty(property) {
471
+ delete this.properties[property]
472
+ this.isDirty = true
473
+ }
474
+
475
+ renameProperty(from, to) {
476
+ this.properties[to] = this.properties[from]
477
+ this.deleteProperty(from)
478
+ }
479
+
480
+ toGeoJSON() {
481
+ return Utils.CopyJSON({
482
+ type: 'Feature',
483
+ geometry: this.geometry,
484
+ properties: this.cloneProperties(),
485
+ id: this.id,
486
+ })
487
+ }
488
+
489
+ getInplaceToolbarActions() {
490
+ return [U.ToggleEditAction, U.DeleteFeatureAction]
491
+ }
492
+
493
+ getMap() {
494
+ return this.map
495
+ }
496
+
497
+ isFiltered() {
498
+ const filterKeys = this.datalayer.getFilterKeys()
499
+ const filter = this.map.browser.options.filter
500
+ if (filter && !this.matchFilter(filter, filterKeys)) return true
501
+ if (!this.matchFacets()) return true
502
+ return false
503
+ }
504
+
505
+ matchFilter(filter, keys) {
506
+ filter = filter.toLowerCase()
507
+ if (Utils.hasVar(keys)) {
508
+ return this.getDisplayName().toLowerCase().indexOf(filter) !== -1
509
+ }
510
+ keys = keys.split(',')
511
+ for (let i = 0, value; i < keys.length; i++) {
512
+ value = `${this.properties[keys[i]] || ''}`
513
+ if (value.toLowerCase().indexOf(filter) !== -1) return true
514
+ }
515
+ return false
516
+ }
517
+
518
+ matchFacets() {
519
+ const selected = this.map.facets.selected
520
+ for (const [name, { type, min, max, choices }] of Object.entries(selected)) {
521
+ let value = this.properties[name]
522
+ const parser = this.map.facets.getParser(type)
523
+ value = parser(value)
524
+ switch (type) {
525
+ case 'date':
526
+ case 'datetime':
527
+ case 'number':
528
+ if (!Number.isNaN(min) && !Number.isNaN(value) && min > value) return false
529
+ if (!Number.isNaN(max) && !Number.isNaN(value) && max < value) return false
530
+ break
531
+ default:
532
+ value = value || translate('<empty value>')
533
+ if (choices?.length && !choices.includes(value)) return false
534
+ break
535
+ }
536
+ }
537
+ return true
538
+ }
539
+
540
+ isMulti() {
541
+ return false
542
+ }
543
+
544
+ clone() {
545
+ const geojson = this.toGeoJSON()
546
+ delete geojson.id
547
+ delete geojson.properties.id
548
+ const feature = this.datalayer.makeFeature(geojson)
549
+ feature.isDirty = true
550
+ feature.edit()
551
+ return feature
552
+ }
553
+
554
+ extendedProperties() {
555
+ // Include context properties
556
+ const properties = this.map.getGeoContext()
557
+ const locale = L.getLocale()
558
+ if (locale) properties.locale = locale
559
+ if (L.lang) properties.lang = L.lang
560
+ properties.rank = this.getRank() + 1
561
+ properties.layer = this.datalayer.getName()
562
+ if (this.ui._map && this.hasGeom()) {
563
+ const center = this.center
564
+ properties.lat = center.lat
565
+ properties.lon = center.lng
566
+ properties.lng = center.lng
567
+ properties.alt = center?.alt
568
+ if (typeof this.ui.getMeasure !== 'undefined') {
569
+ properties.measure = this.ui.getMeasure()
570
+ }
571
+ }
572
+ return L.extend(properties, this.properties)
573
+ }
574
+
575
+ getRank() {
576
+ return this.datalayer._index.indexOf(L.stamp(this))
577
+ }
578
+
579
+ redraw() {
580
+ if (this.datalayer?.isVisible()) {
581
+ if (this.getUIClass() !== this.ui.getClass()) {
582
+ this.datalayer.hideFeature(this)
583
+ this.makeUI()
584
+ this.datalayer.showFeature(this)
585
+ } else {
586
+ this.ui._redraw()
587
+ }
588
+ }
589
+ }
590
+ }
591
+
592
+ export class Point extends Feature {
593
+ constructor(datalayer, geojson, id) {
594
+ super(datalayer, geojson, id)
595
+ this.staticOptions = {
596
+ mainColor: 'color',
597
+ className: 'marker',
598
+ }
599
+ }
600
+
601
+ toLatLngs() {
602
+ return GeoJSON.coordsToLatLng(this.coordinates)
603
+ }
604
+
605
+ convertLatLngs(latlng) {
606
+ return { coordinates: GeoJSON.latLngToCoords(latlng), type: 'Point' }
607
+ }
608
+
609
+ getUIClass() {
610
+ return super.getUIClass() || LeafletMarker
611
+ }
612
+
613
+ hasGeom() {
614
+ return Boolean(this.coordinates)
615
+ }
616
+
617
+ _getIconUrl(name = 'icon') {
618
+ return this.getOption(`${name}Url`)
619
+ }
620
+
621
+ getShapeOptions() {
622
+ return [
623
+ 'properties._umap_options.color',
624
+ 'properties._umap_options.iconClass',
625
+ 'properties._umap_options.iconUrl',
626
+ 'properties._umap_options.iconOpacity',
627
+ ]
628
+ }
629
+
630
+ getAdvancedOptions() {
631
+ return ['properties._umap_options.zoomTo']
632
+ }
633
+
634
+ appendEditFieldsets(container) {
635
+ super.appendEditFieldsets(container)
636
+ // FIXME edit feature geometry.coordinates instead
637
+ // (by learning FormBuilder to deal with array indexes ?)
638
+ const coordinatesOptions = [
639
+ ['ui._latlng.lat', { handler: 'FloatInput', label: translate('Latitude') }],
640
+ ['ui._latlng.lng', { handler: 'FloatInput', label: translate('Longitude') }],
641
+ ]
642
+ const builder = new U.FormBuilder(this, coordinatesOptions, {
643
+ callback: () => {
644
+ if (!this.ui._latlng.isValid()) {
645
+ Alert.error(translate('Invalid latitude or longitude'))
646
+ builder.restoreField('ui._latlng.lat')
647
+ builder.restoreField('ui._latlng.lng')
648
+ }
649
+ this.zoomTo({ easing: false })
650
+ },
651
+ })
652
+ const fieldset = DomUtil.createFieldset(container, translate('Coordinates'))
653
+ fieldset.appendChild(builder.build())
654
+ }
655
+
656
+ zoomTo(event) {
657
+ if (this.datalayer.isClustered() && !this._icon) {
658
+ // callback is mandatory for zoomToShowLayer
659
+ this.datalayer.layer.zoomToShowLayer(this, event.callback || (() => {}))
660
+ } else {
661
+ super.zoomTo(event)
662
+ }
663
+ }
664
+
665
+ isOnScreen(bounds) {
666
+ bounds = bounds || this.map.getBounds()
667
+ return bounds.contains(this.toLatLngs())
668
+ }
669
+ }
670
+
671
+ class Path extends Feature {
672
+ hasGeom() {
673
+ return !this.isEmpty()
674
+ }
675
+
676
+ connectToDataLayer(datalayer) {
677
+ super.connectToDataLayer(datalayer)
678
+ // We keep markers on their own layer on top of the paths.
679
+ this.ui.options.pane = this.datalayer.pane
680
+ }
681
+
682
+ edit(event) {
683
+ if (this.map.editEnabled) {
684
+ if (!this.ui.editEnabled()) this.ui.enableEdit()
685
+ super.edit(event)
686
+ }
687
+ }
688
+
689
+ _toggleEditing(event) {
690
+ if (this.map.editEnabled) {
691
+ if (this.ui.editEnabled()) {
692
+ this.endEdit()
693
+ this.map.editPanel.close()
694
+ } else {
695
+ this.edit(event)
696
+ }
697
+ }
698
+ // FIXME: disable when disabling global edit
699
+ L.DomEvent.stop(event)
700
+ }
701
+
702
+ getShapeOptions() {
703
+ return [
704
+ 'properties._umap_options.color',
705
+ 'properties._umap_options.opacity',
706
+ 'properties._umap_options.weight',
707
+ ]
708
+ }
709
+
710
+ getAdvancedOptions() {
711
+ return [
712
+ 'properties._umap_options.smoothFactor',
713
+ 'properties._umap_options.dashArray',
714
+ 'properties._umap_options.zoomTo',
715
+ ]
716
+ }
717
+
718
+ getBestZoom() {
719
+ return this.getOption('zoomTo') || this.map.getBoundsZoom(this.bounds, true)
720
+ }
721
+
722
+ endEdit() {
723
+ this.ui.disableEdit()
724
+ super.endEdit()
725
+ }
726
+
727
+ transferShape(at, to) {
728
+ const shape = this.ui.enableEdit().deleteShapeAt(at)
729
+ // FIXME: make Leaflet.Editable send an event instead
730
+ this.pullGeometry()
731
+ this.ui.disableEdit()
732
+ if (!shape) return
733
+ to.ui.enableEdit().appendShape(shape)
734
+ to.pullGeometry()
735
+ if (this.isEmpty()) this.del()
736
+ }
737
+
738
+ isolateShape(latlngs) {
739
+ const properties = this.cloneProperties()
740
+ const type = this instanceof LineString ? 'LineString' : 'Polygon'
741
+ const geometry = this.convertLatLngs(latlngs)
742
+ const other = this.datalayer.makeFeature({ type, geometry, properties })
743
+ other.edit()
744
+ return other
745
+ }
746
+
747
+ getInplaceToolbarActions(event) {
748
+ const items = super.getInplaceToolbarActions(event)
749
+ if (this.isMulti()) {
750
+ items.push(U.DeleteShapeAction)
751
+ items.push(U.ExtractShapeFromMultiAction)
752
+ }
753
+ return items
754
+ }
755
+
756
+ isOnScreen(bounds) {
757
+ bounds = bounds || this.map.getBounds()
758
+ return bounds.overlaps(this.bounds)
759
+ }
760
+
761
+ zoomTo({ easing, callback }) {
762
+ // Use bounds instead of centroid for paths.
763
+ easing = easing || this.map.getOption('easing')
764
+ if (easing) {
765
+ this.map.flyToBounds(this.bounds, this.getBestZoom())
766
+ } else {
767
+ this.map.fitBounds(this.bounds, this.getBestZoom() || this.map.getZoom())
768
+ }
769
+ if (callback) callback.call(this)
770
+ }
771
+ }
772
+
773
+ export class LineString extends Path {
774
+ constructor(datalayer, geojson, id) {
775
+ super(datalayer, geojson, id)
776
+ this.staticOptions = {
777
+ stroke: true,
778
+ fill: false,
779
+ mainColor: 'color',
780
+ className: 'polyline',
781
+ }
782
+ }
783
+
784
+ toLatLngs(geometry) {
785
+ return GeoJSON.coordsToLatLngs(this.coordinates, this.type === 'LineString' ? 0 : 1)
786
+ }
787
+
788
+ convertLatLngs(latlngs) {
789
+ let multi = !LineUtil.isFlat(latlngs)
790
+ let coordinates = GeoJSON.latLngsToCoords(latlngs, multi ? 1 : 0, false)
791
+ if (coordinates.length === 1 && typeof coordinates[0][0] !== 'number') {
792
+ coordinates = Utils.flattenCoordinates(coordinates)
793
+ multi = false
794
+ }
795
+ const type = multi ? 'MultiLineString' : 'LineString'
796
+ return { coordinates, type }
797
+ }
798
+
799
+ isEmpty() {
800
+ return !this.coordinates.length
801
+ }
802
+
803
+ getUIClass() {
804
+ return super.getUIClass() || LeafletPolyline
805
+ }
806
+
807
+ isSameClass(other) {
808
+ return other instanceof LineString
809
+ }
810
+
811
+ toPolygon() {
812
+ const geojson = this.toGeoJSON()
813
+ geojson.geometry.type = 'Polygon'
814
+ geojson.geometry.coordinates = [
815
+ Utils.flattenCoordinates(geojson.geometry.coordinates),
816
+ ]
817
+
818
+ delete geojson.id // delete the copied id, a new one will be generated.
819
+
820
+ const polygon = this.datalayer.makeFeature(geojson)
821
+ polygon.edit()
822
+ this.del()
823
+ }
824
+
825
+ getAdvancedEditActions(container) {
826
+ super.getAdvancedEditActions(container)
827
+ DomUtil.createButton(
828
+ 'button umap-to-polygon',
829
+ container,
830
+ translate('Transform to polygon'),
831
+ this.toPolygon,
832
+ this
833
+ )
834
+ }
835
+
836
+ _mergeShapes(from, to) {
837
+ const toLeft = to[0]
838
+ const toRight = to[to.length - 1]
839
+ const fromLeft = from[0]
840
+ const fromRight = from[from.length - 1]
841
+ const l2ldistance = toLeft.distanceTo(fromLeft)
842
+ const l2rdistance = toLeft.distanceTo(fromRight)
843
+ const r2ldistance = toRight.distanceTo(fromLeft)
844
+ const r2rdistance = toRight.distanceTo(fromRight)
845
+ let toMerge
846
+ if (l2rdistance < Math.min(l2ldistance, r2ldistance, r2rdistance)) {
847
+ toMerge = [from, to]
848
+ } else if (r2ldistance < Math.min(l2ldistance, l2rdistance, r2rdistance)) {
849
+ toMerge = [to, from]
850
+ } else if (r2rdistance < Math.min(l2ldistance, l2rdistance, r2ldistance)) {
851
+ from.reverse()
852
+ toMerge = [to, from]
853
+ } else {
854
+ from.reverse()
855
+ toMerge = [from, to]
856
+ }
857
+ const a = toMerge[0]
858
+ const b = toMerge[1]
859
+ const p1 = this.map.latLngToContainerPoint(a[a.length - 1])
860
+ const p2 = this.map.latLngToContainerPoint(b[0])
861
+ const tolerance = 5 // px on screen
862
+ if (Math.abs(p1.x - p2.x) <= tolerance && Math.abs(p1.y - p2.y) <= tolerance) {
863
+ a.pop()
864
+ }
865
+ return a.concat(b)
866
+ }
867
+
868
+ mergeShapes() {
869
+ if (!this.isMulti()) return
870
+ const latlngs = this.getLatLngs()
871
+ if (!latlngs.length) return
872
+ while (latlngs.length > 1) {
873
+ latlngs.splice(0, 2, this._mergeShapes(latlngs[1], latlngs[0]))
874
+ }
875
+ this.ui.setLatLngs(latlngs[0])
876
+ if (!this.editEnabled()) this.edit()
877
+ this.editor.reset()
878
+ this.isDirty = true
879
+ }
880
+
881
+ isMulti() {
882
+ return !LineUtil.isFlat(this.coordinates) && this.coordinates.length > 1
883
+ }
884
+ }
885
+
886
+ export class Polygon extends Path {
887
+ constructor(datalayer, geojson, id) {
888
+ super(datalayer, geojson, id)
889
+ this.staticOptions = {
890
+ mainColor: 'fillColor',
891
+ className: 'polygon',
892
+ }
893
+ }
894
+
895
+ toLatLngs() {
896
+ return GeoJSON.coordsToLatLngs(this.coordinates, this.type === 'Polygon' ? 1 : 2)
897
+ }
898
+
899
+ convertLatLngs(latlngs) {
900
+ const holes = !LineUtil.isFlat(latlngs)
901
+ let multi = holes && !LineUtil.isFlat(latlngs[0])
902
+ let coordinates = GeoJSON.latLngsToCoords(latlngs, multi ? 2 : holes ? 1 : 0, true)
903
+ if (Utils.polygonMustBeFlattened(coordinates)) {
904
+ coordinates = coordinates[0]
905
+ multi = false
906
+ }
907
+ const type = multi ? 'MultiPolygon' : 'Polygon'
908
+ return { coordinates, type }
909
+ }
910
+
911
+ isEmpty() {
912
+ return !this.coordinates.length || !this.coordinates[0].length
913
+ }
914
+
915
+ getUIClass() {
916
+ if (this.getOption('mask')) return MaskPolygon
917
+ return super.getUIClass() || LeafletPolygon
918
+ }
919
+
920
+ isSameClass(other) {
921
+ return other instanceof Polygon
922
+ }
923
+
924
+ getShapeOptions() {
925
+ const options = super.getShapeOptions()
926
+ options.push(
927
+ 'properties._umap_options.stroke',
928
+ 'properties._umap_options.fill',
929
+ 'properties._umap_options.fillColor',
930
+ 'properties._umap_options.fillOpacity'
931
+ )
932
+ return options
933
+ }
934
+
935
+ getPreviewColor() {
936
+ // If user set a fillColor, use it, otherwise default to color
937
+ // which is usually the only one set
938
+ const color = this.getDynamicOption(this.staticOptions.mainColor)
939
+ if (color && color !== SCHEMA.color.default) return color
940
+ return this.getDynamicOption('color')
941
+ }
942
+
943
+ getInteractionOptions() {
944
+ const options = super.getInteractionOptions()
945
+ options.push('properties._umap_options.interactive')
946
+ return options
947
+ }
948
+
949
+ toLineString() {
950
+ const geojson = this.toGeoJSON()
951
+ delete geojson.id
952
+ delete geojson.properties.id
953
+ geojson.geometry.type = 'LineString'
954
+ geojson.geometry.coordinates = Utils.flattenCoordinates(
955
+ geojson.geometry.coordinates
956
+ )
957
+ const polyline = this.datalayer.makeFeature(geojson)
958
+ polyline.edit()
959
+ this.del()
960
+ }
961
+
962
+ getAdvancedOptions() {
963
+ const actions = super.getAdvancedOptions()
964
+ actions.push('properties._umap_options.mask')
965
+ return actions
966
+ }
967
+
968
+ getAdvancedEditActions(container) {
969
+ super.getAdvancedEditActions(container)
970
+ const toLineString = DomUtil.createButton(
971
+ 'button umap-to-polyline',
972
+ container,
973
+ translate('Transform to lines'),
974
+ this.toLineString,
975
+ this
976
+ )
977
+ }
978
+
979
+ isMulti() {
980
+ // Change me when Leaflet#3279 is merged.
981
+ // FIXME use TurfJS
982
+ return (
983
+ !LineUtil.isFlat(this.coordinates) &&
984
+ !LineUtil.isFlat(this.coordinates[0]) &&
985
+ this.coordinates.length > 1
986
+ )
987
+ }
988
+
989
+ getInplaceToolbarActions(event) {
990
+ const items = super.getInplaceToolbarActions(event)
991
+ items.push(U.CreateHoleAction)
992
+ return items
993
+ }
994
+ }