umap-project 2.3.0__py3-none-any.whl → 2.4.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 (211) hide show
  1. umap/__init__.py +1 -1
  2. umap/locale/en/LC_MESSAGES/django.po +81 -31
  3. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  4. umap/locale/fr/LC_MESSAGES/django.po +117 -66
  5. umap/locale/pl/LC_MESSAGES/django.mo +0 -0
  6. umap/locale/pl/LC_MESSAGES/django.po +83 -78
  7. umap/locale/pt/LC_MESSAGES/django.mo +0 -0
  8. umap/locale/pt/LC_MESSAGES/django.po +129 -123
  9. umap/management/commands/run_websocket_server.py +23 -0
  10. umap/models.py +6 -1
  11. umap/settings/base.py +11 -3
  12. umap/static/umap/base.css +68 -186
  13. umap/static/umap/content.css +3 -2
  14. umap/static/umap/css/dialog.css +18 -0
  15. umap/static/umap/css/icon.css +8 -0
  16. umap/static/umap/css/importers.css +51 -0
  17. umap/static/umap/css/panel.css +18 -57
  18. umap/static/umap/css/tooltip.css +59 -0
  19. umap/static/umap/css/window.css +35 -0
  20. umap/static/umap/img/16-white.svg +1 -3
  21. umap/static/umap/img/alert-icon-error.svg +8 -0
  22. umap/static/umap/img/alert-icon-info.svg +4 -0
  23. umap/static/umap/img/alert-icon-success.svg +3 -0
  24. umap/static/umap/img/icon-external-link.svg +3 -0
  25. umap/static/umap/img/importers/communesfr.svg +5 -0
  26. umap/static/umap/img/importers/datasets.svg +13 -0
  27. umap/static/umap/img/importers/geodatamine.svg +10 -0
  28. umap/static/umap/img/importers/overpass.svg +7 -0
  29. umap/static/umap/img/importers/random.svg +18 -0
  30. umap/static/umap/img/importers/random1.svg +4 -0
  31. umap/static/umap/img/importers/random2.svg +4 -0
  32. umap/static/umap/img/source/16-white.svg +2 -4
  33. umap/static/umap/js/components/alerts/alert.css +160 -0
  34. umap/static/umap/js/components/alerts/alert.js +169 -0
  35. umap/static/umap/js/components/base.js +54 -0
  36. umap/static/umap/js/modules/autocomplete.js +347 -0
  37. umap/static/umap/js/modules/browser.js +14 -21
  38. umap/static/umap/js/modules/caption.js +119 -0
  39. umap/static/umap/js/modules/global.js +37 -11
  40. umap/static/umap/js/modules/help.js +255 -0
  41. umap/static/umap/js/modules/importer.js +308 -0
  42. umap/static/umap/js/modules/importers/communesfr.js +44 -0
  43. umap/static/umap/js/modules/importers/datasets.js +42 -0
  44. umap/static/umap/js/modules/importers/geodatamine.js +95 -0
  45. umap/static/umap/js/modules/importers/overpass.js +84 -0
  46. umap/static/umap/js/modules/request.js +12 -14
  47. umap/static/umap/js/modules/rules.js +241 -0
  48. umap/static/umap/js/modules/schema.js +63 -14
  49. umap/static/umap/js/modules/sync/engine.js +93 -0
  50. umap/static/umap/js/modules/sync/updaters.js +109 -0
  51. umap/static/umap/js/modules/sync/websocket.js +25 -0
  52. umap/static/umap/js/modules/ui/dialog.js +52 -0
  53. umap/static/umap/js/modules/{panel.js → ui/panel.js} +37 -20
  54. umap/static/umap/js/modules/ui/tooltip.js +116 -0
  55. umap/static/umap/js/modules/utils.js +25 -18
  56. umap/static/umap/js/umap.controls.js +37 -112
  57. umap/static/umap/js/umap.core.js +1 -327
  58. umap/static/umap/js/umap.features.js +77 -29
  59. umap/static/umap/js/umap.forms.js +17 -19
  60. umap/static/umap/js/umap.js +265 -228
  61. umap/static/umap/js/umap.layer.js +154 -76
  62. umap/static/umap/js/umap.permissions.js +5 -9
  63. umap/static/umap/js/umap.popup.js +2 -1
  64. umap/static/umap/js/umap.tableeditor.js +8 -8
  65. umap/static/umap/locale/am_ET.js +51 -16
  66. umap/static/umap/locale/am_ET.json +51 -16
  67. umap/static/umap/locale/ar.js +51 -16
  68. umap/static/umap/locale/ar.json +51 -16
  69. umap/static/umap/locale/ast.js +51 -16
  70. umap/static/umap/locale/ast.json +51 -16
  71. umap/static/umap/locale/bg.js +51 -16
  72. umap/static/umap/locale/bg.json +51 -16
  73. umap/static/umap/locale/br.js +55 -20
  74. umap/static/umap/locale/br.json +55 -20
  75. umap/static/umap/locale/ca.js +51 -16
  76. umap/static/umap/locale/ca.json +51 -16
  77. umap/static/umap/locale/cs_CZ.js +93 -58
  78. umap/static/umap/locale/cs_CZ.json +93 -58
  79. umap/static/umap/locale/da.js +51 -16
  80. umap/static/umap/locale/da.json +51 -16
  81. umap/static/umap/locale/de.js +56 -21
  82. umap/static/umap/locale/de.json +56 -21
  83. umap/static/umap/locale/el.js +51 -16
  84. umap/static/umap/locale/el.json +51 -16
  85. umap/static/umap/locale/en.js +52 -16
  86. umap/static/umap/locale/en.json +52 -16
  87. umap/static/umap/locale/en_US.json +51 -16
  88. umap/static/umap/locale/es.js +51 -16
  89. umap/static/umap/locale/es.json +51 -16
  90. umap/static/umap/locale/et.js +51 -16
  91. umap/static/umap/locale/et.json +51 -16
  92. umap/static/umap/locale/eu.js +51 -16
  93. umap/static/umap/locale/eu.json +51 -16
  94. umap/static/umap/locale/fa_IR.js +51 -16
  95. umap/static/umap/locale/fa_IR.json +51 -16
  96. umap/static/umap/locale/fi.js +51 -16
  97. umap/static/umap/locale/fi.json +51 -16
  98. umap/static/umap/locale/fr.js +61 -25
  99. umap/static/umap/locale/fr.json +61 -25
  100. umap/static/umap/locale/gl.js +51 -16
  101. umap/static/umap/locale/gl.json +51 -16
  102. umap/static/umap/locale/he.js +51 -16
  103. umap/static/umap/locale/he.json +51 -16
  104. umap/static/umap/locale/hr.js +51 -16
  105. umap/static/umap/locale/hr.json +51 -16
  106. umap/static/umap/locale/hu.js +51 -16
  107. umap/static/umap/locale/hu.json +51 -16
  108. umap/static/umap/locale/id.js +51 -16
  109. umap/static/umap/locale/id.json +51 -16
  110. umap/static/umap/locale/is.js +51 -16
  111. umap/static/umap/locale/is.json +51 -16
  112. umap/static/umap/locale/it.js +51 -16
  113. umap/static/umap/locale/it.json +51 -16
  114. umap/static/umap/locale/ja.js +51 -16
  115. umap/static/umap/locale/ja.json +51 -16
  116. umap/static/umap/locale/ko.js +51 -16
  117. umap/static/umap/locale/ko.json +51 -16
  118. umap/static/umap/locale/lt.js +51 -16
  119. umap/static/umap/locale/lt.json +51 -16
  120. umap/static/umap/locale/ms.js +51 -16
  121. umap/static/umap/locale/ms.json +51 -16
  122. umap/static/umap/locale/nl.js +51 -16
  123. umap/static/umap/locale/nl.json +51 -16
  124. umap/static/umap/locale/no.js +51 -16
  125. umap/static/umap/locale/no.json +51 -16
  126. umap/static/umap/locale/pl.js +93 -58
  127. umap/static/umap/locale/pl.json +93 -58
  128. umap/static/umap/locale/pl_PL.json +51 -16
  129. umap/static/umap/locale/pt.js +215 -180
  130. umap/static/umap/locale/pt.json +215 -180
  131. umap/static/umap/locale/pt_BR.js +51 -16
  132. umap/static/umap/locale/pt_BR.json +51 -16
  133. umap/static/umap/locale/pt_PT.js +51 -16
  134. umap/static/umap/locale/pt_PT.json +51 -16
  135. umap/static/umap/locale/ro.js +51 -16
  136. umap/static/umap/locale/ro.json +51 -16
  137. umap/static/umap/locale/ru.js +51 -16
  138. umap/static/umap/locale/ru.json +51 -16
  139. umap/static/umap/locale/si.js +51 -16
  140. umap/static/umap/locale/si.json +51 -16
  141. umap/static/umap/locale/sk_SK.js +51 -16
  142. umap/static/umap/locale/sk_SK.json +51 -16
  143. umap/static/umap/locale/sl.js +51 -16
  144. umap/static/umap/locale/sl.json +51 -16
  145. umap/static/umap/locale/sr.js +51 -16
  146. umap/static/umap/locale/sr.json +51 -16
  147. umap/static/umap/locale/sv.js +51 -16
  148. umap/static/umap/locale/sv.json +51 -16
  149. umap/static/umap/locale/th_TH.js +51 -16
  150. umap/static/umap/locale/th_TH.json +51 -16
  151. umap/static/umap/locale/tr.js +51 -16
  152. umap/static/umap/locale/tr.json +51 -16
  153. umap/static/umap/locale/uk_UA.js +51 -16
  154. umap/static/umap/locale/uk_UA.json +51 -16
  155. umap/static/umap/locale/vi.js +51 -16
  156. umap/static/umap/locale/vi.json +51 -16
  157. umap/static/umap/locale/vi_VN.json +51 -16
  158. umap/static/umap/locale/zh.js +51 -16
  159. umap/static/umap/locale/zh.json +51 -16
  160. umap/static/umap/locale/zh_CN.json +51 -16
  161. umap/static/umap/locale/zh_TW.Big5.json +51 -16
  162. umap/static/umap/locale/zh_TW.js +51 -16
  163. umap/static/umap/locale/zh_TW.json +51 -16
  164. umap/static/umap/map.css +40 -53
  165. umap/static/umap/unittests/sync.js +105 -0
  166. umap/static/umap/unittests/utils.js +78 -36
  167. umap/static/umap/vars.css +19 -1
  168. umap/static/umap/vendors/dompurify/purify.es.js +50 -15
  169. umap/static/umap/vendors/dompurify/purify.es.mjs.map +1 -1
  170. umap/static/umap/vendors/formbuilder/Leaflet.FormBuilder.js +2 -2
  171. umap/templates/umap/components/alerts/alert.html +89 -0
  172. umap/templates/umap/content.html +4 -3
  173. umap/templates/umap/css.html +4 -0
  174. umap/templates/umap/home.html +3 -0
  175. umap/templates/umap/js.html +0 -3
  176. umap/templates/umap/map_init.html +2 -8
  177. umap/templates/umap/messages.html +9 -11
  178. umap/templates/umap/search.html +3 -0
  179. umap/tests/base.py +3 -0
  180. umap/tests/integration/conftest.py +30 -0
  181. umap/tests/integration/test_anonymous_owned_map.py +8 -13
  182. umap/tests/integration/test_browser.py +81 -6
  183. umap/tests/integration/test_caption.py +27 -0
  184. umap/tests/integration/test_conditional_rules.py +201 -0
  185. umap/tests/integration/test_dashboard.py +1 -1
  186. umap/tests/integration/test_datalayer.py +2 -3
  187. umap/tests/integration/test_edit_datalayer.py +32 -3
  188. umap/tests/integration/test_edit_map.py +1 -1
  189. umap/tests/integration/test_facets_browser.py +7 -4
  190. umap/tests/integration/test_import.py +185 -49
  191. umap/tests/integration/test_map.py +31 -17
  192. umap/tests/integration/{test_collaborative_editing.py → test_optimistic_merge.py} +7 -7
  193. umap/tests/integration/test_owned_map.py +1 -1
  194. umap/tests/integration/test_picto.py +2 -2
  195. umap/tests/integration/test_statics.py +1 -1
  196. umap/tests/integration/test_view_marker.py +19 -2
  197. umap/tests/integration/test_websocket_sync.py +283 -0
  198. umap/tests/settings.py +5 -0
  199. umap/tests/test_datalayer_views.py +0 -1
  200. umap/tests/test_views.py +53 -0
  201. umap/urls.py +5 -0
  202. umap/views.py +40 -11
  203. umap/websocket_server.py +92 -0
  204. {umap_project-2.3.0.dist-info → umap_project-2.4.0.dist-info}/METADATA +13 -11
  205. {umap_project-2.3.0.dist-info → umap_project-2.4.0.dist-info}/RECORD +208 -172
  206. umap/static/umap/js/umap.autocomplete.js +0 -341
  207. umap/static/umap/js/umap.importer.js +0 -187
  208. umap/static/umap/js/umap.ui.js +0 -190
  209. {umap_project-2.3.0.dist-info → umap_project-2.4.0.dist-info}/WHEEL +0 -0
  210. {umap_project-2.3.0.dist-info → umap_project-2.4.0.dist-info}/entry_points.txt +0 -0
  211. {umap_project-2.3.0.dist-info → umap_project-2.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,5 @@
1
1
  L.Map.mergeOptions({
2
- overlay: null,
2
+ overlay: {},
3
3
  datalayers: [],
4
4
  hash: true,
5
5
  maxZoomLimit: 24,
@@ -20,9 +20,6 @@ L.Map.mergeOptions({
20
20
  enablePolygonDraw: true,
21
21
  enablePolylineDraw: true,
22
22
  limitBounds: {},
23
- importPresets: [
24
- // {url: 'http://localhost:8019/en/datalayer/1502/', label: 'Simplified World Countries', format: 'geojson'}
25
- ],
26
23
  slideshow: {},
27
24
  clickable: true,
28
25
  permissions: {},
@@ -33,6 +30,8 @@ U.Map = L.Map.extend({
33
30
  includes: [ControlsMixin],
34
31
 
35
32
  initialize: function (el, geojson) {
33
+ this.sync_engine = new U.SyncEngine(this)
34
+ this.sync = this.sync_engine.proxy(this)
36
35
  // Locale name (pt_PT, en_US…)
37
36
  // To be used for Django localization
38
37
  if (geojson.properties.locale) L.setLocale(geojson.properties.locale)
@@ -57,15 +56,16 @@ U.Map = L.Map.extend({
57
56
  this.urls = new U.URLs(this.options.urls)
58
57
 
59
58
  this.panel = new U.Panel(this)
59
+ this.tooltip = new U.Tooltip(this._controlContainer)
60
+ this.dialog = new U.Dialog(this._controlContainer)
60
61
  if (this.hasEditMode()) {
61
62
  this.editPanel = new U.EditPanel(this)
62
63
  this.fullPanel = new U.FullPanel(this)
63
64
  }
64
- this.ui = new U.UI(this._container)
65
- this.ui.on('dataloading', (e) => this.fire('dataloading', e))
66
- this.ui.on('dataload', (e) => this.fire('dataload', e))
67
- this.server = new U.ServerRequest(this.ui)
68
- this.request = new U.Request(this.ui)
65
+ L.DomEvent.on(document.body, 'dataloading', (e) => this.fire('dataloading', e))
66
+ L.DomEvent.on(document.body, 'dataload', (e) => this.fire('dataload', e))
67
+ this.server = new U.ServerRequest()
68
+ this.request = new U.Request()
69
69
 
70
70
  this.initLoader()
71
71
  this.name = this.options.name
@@ -205,40 +205,9 @@ U.Map = L.Map.extend({
205
205
  this.editTools = new U.Editable(this)
206
206
  this.renderEditToolbar()
207
207
  }
208
+
208
209
  this.initShortcuts()
209
- this.onceDataLoaded(function () {
210
- const slug = L.Util.queryString('feature')
211
- if (slug && this.features_index[slug]) this.features_index[slug].view()
212
- if (this.options.noControl) return
213
- this.initCaptionBar()
214
- if (L.Util.queryString('share')) {
215
- this.share.open()
216
- } else if (this.options.onLoadPanel === 'databrowser') {
217
- this.openBrowser('data')
218
- } else if (this.options.onLoadPanel === 'datalayers') {
219
- this.openBrowser('layers')
220
- } else if (this.options.onLoadPanel === 'datafilters') {
221
- this.panel.mode = 'expanded'
222
- this.openBrowser('filters')
223
- } else if (this.options.onLoadPanel === 'caption') {
224
- this.panel.mode = 'condensed'
225
- this.displayCaption()
226
- }
227
- if (L.Util.queryString('edit')) {
228
- if (this.hasEditMode()) this.enableEdit()
229
- // Sometimes users share the ?edit link by mistake, let's remove
230
- // this search parameter from URL to prevent this
231
- const url = new URL(window.location)
232
- url.searchParams.delete('edit')
233
- history.pushState({}, '', url)
234
- }
235
- if (L.Util.queryString('download')) {
236
- const download_url = this.urls.get('map_download', {
237
- map_id: this.options.umap_id,
238
- })
239
- window.location = download_url
240
- }
241
- })
210
+ this.onceDataLoaded(this.setViewFromQueryString)
242
211
 
243
212
  window.onbeforeunload = () => (this.editEnabled && this.isDirty) || null
244
213
  this.backup()
@@ -246,6 +215,25 @@ U.Map = L.Map.extend({
246
215
  this.on('click contextmenu.show', this.closeInplaceToolbar)
247
216
  },
248
217
 
218
+ initSyncEngine: async function () {
219
+ if (this.options.websocketEnabled == false) return
220
+ if (this.options.syncEnabled != true) {
221
+ this.sync.stop()
222
+ } else {
223
+ const ws_token_uri = this.urls.get('map_websocket_auth_token', {
224
+ map_id: this.options.umap_id,
225
+ })
226
+ await this.sync.authenticate(ws_token_uri, this.options.websocketURI, this.server)
227
+ }
228
+ },
229
+
230
+ getSyncMetadata: function () {
231
+ return {
232
+ engine: this.sync,
233
+ subject: 'map',
234
+ }
235
+ },
236
+
249
237
  render: function (fields) {
250
238
  let impacts = U.Utils.getImpactsFromSchema(fields)
251
239
 
@@ -269,6 +257,8 @@ U.Map = L.Map.extend({
269
257
  case 'bounds':
270
258
  this.handleLimitBounds()
271
259
  break
260
+ case 'sync':
261
+ this.initSyncEngine()
272
262
  }
273
263
  }
274
264
  },
@@ -311,6 +301,44 @@ U.Map = L.Map.extend({
311
301
  }
312
302
  },
313
303
 
304
+ setViewFromQueryString: function () {
305
+ if (this.options.noControl) return
306
+ this.initCaptionBar()
307
+ if (L.Util.queryString('share')) {
308
+ this.share.open()
309
+ } else if (this.options.onLoadPanel === 'databrowser') {
310
+ this.panel.setDefaultMode('expanded')
311
+ this.openBrowser('data')
312
+ } else if (this.options.onLoadPanel === 'datalayers') {
313
+ this.panel.setDefaultMode('condensed')
314
+ this.openBrowser('layers')
315
+ } else if (this.options.onLoadPanel === 'datafilters') {
316
+ this.panel.setDefaultMode('expanded')
317
+ this.openBrowser('filters')
318
+ } else if (this.options.onLoadPanel === 'caption') {
319
+ this.panel.setDefaultMode('condensed')
320
+ this.openCaption()
321
+ }
322
+ // Comes after default panels, so if it opens in a panel it will
323
+ // take precedence.
324
+ const slug = L.Util.queryString('feature')
325
+ if (slug && this.features_index[slug]) this.features_index[slug].view()
326
+ if (L.Util.queryString('edit')) {
327
+ if (this.hasEditMode()) this.enableEdit()
328
+ // Sometimes users share the ?edit link by mistake, let's remove
329
+ // this search parameter from URL to prevent this
330
+ const url = new URL(window.location)
331
+ url.searchParams.delete('edit')
332
+ history.pushState({}, '', url)
333
+ }
334
+ if (L.Util.queryString('download')) {
335
+ const download_url = this.urls.get('map_download', {
336
+ map_id: this.options.umap_id,
337
+ })
338
+ window.location = download_url
339
+ }
340
+ },
341
+
314
342
  // Merge the given schema with the default one
315
343
  // Missing keys inside the schema are merged with the default ones.
316
344
  overrideSchema: function (schema) {
@@ -357,7 +385,7 @@ U.Map = L.Map.extend({
357
385
  icon: 'umap-fake-class',
358
386
  iconLoading: 'umap-fake-class',
359
387
  flyTo: this.options.easing,
360
- onLocationError: (err) => this.ui.alert({ content: err.message }),
388
+ onLocationError: (err) => U.Alert.error(err.message),
361
389
  })
362
390
  this._controls.fullscreen = new L.Control.Fullscreen({
363
391
  title: { false: L._('View Fullscreen'), true: L._('Exit Fullscreen') },
@@ -382,24 +410,21 @@ U.Map = L.Map.extend({
382
410
  else this.scrollWheelZoom.disable()
383
411
  this.browser = new U.Browser(this)
384
412
  this.facets = new U.Facets(this)
413
+ this.caption = new U.Caption(this)
385
414
  this.importer = new U.Importer(this)
386
415
  this.drop = new U.DropControl(this)
387
416
  this.share = new U.Share(this)
417
+ this.rules = new U.Rules(this)
388
418
  this._controls.tilelayers = new U.TileLayerControl(this)
389
419
  },
390
420
 
391
421
  renderControls: function () {
392
- L.DomUtil.classIf(
393
- document.body,
394
- 'umap-caption-bar-enabled',
395
- this.options.captionBar ||
396
- (this.options.slideshow && this.options.slideshow.active)
397
- )
398
- L.DomUtil.classIf(
399
- document.body,
400
- 'umap-slideshow-enabled',
422
+ const hasSlideshow = Boolean(
401
423
  this.options.slideshow && this.options.slideshow.active
402
424
  )
425
+ const barEnabled = this.options.captionBar || hasSlideshow
426
+ document.body.classList.toggle('umap-caption-bar-enabled', barEnabled)
427
+ document.body.classList.toggle('umap-slideshow-enabled', hasSlideshow)
403
428
  for (const control of Object.values(this._controls)) {
404
429
  this.removeControl(control)
405
430
  }
@@ -474,6 +499,7 @@ U.Map = L.Map.extend({
474
499
 
475
500
  onDataLayersChanged: function () {
476
501
  if (this.browser) this.browser.update()
502
+ this.caption.refresh()
477
503
  },
478
504
 
479
505
  ensurePanesOrder: function () {
@@ -517,73 +543,80 @@ U.Map = L.Map.extend({
517
543
 
518
544
  initShortcuts: function () {
519
545
  const globalShortcuts = function (e) {
520
- const key = e.keyCode,
521
- modifierKey = e.ctrlKey || e.metaKey
522
-
523
- /* Generic shortcuts */
524
- if (key === U.Keys.F && modifierKey) {
525
- L.DomEvent.stop(e)
526
- this.search()
527
- } else if (e.keyCode === U.Keys.ESC) {
528
- if (this.help.visible()) {
529
- this.help.hide()
530
- } else {
531
- this.panel.close()
546
+ if (e.key === 'Escape') {
547
+ if (this.dialog.visible) {
548
+ this.dialog.close()
549
+ } else if (this.importer.dialog.visible) {
550
+ this.importer.dialog.close()
551
+ } else if (this.editEnabled && this.editTools.drawing()) {
552
+ this.editTools.stopDrawing()
553
+ } else if (this.measureTools.enabled()) {
554
+ this.measureTools.stopDrawing()
555
+ } else if (this.editPanel?.isOpen()) {
532
556
  this.editPanel?.close()
557
+ } else if (this.fullPanel?.isOpen()) {
533
558
  this.fullPanel?.close()
559
+ } else if (this.panel.isOpen()) {
560
+ this.panel.close()
534
561
  }
535
562
  }
536
563
 
537
- if (!this.hasEditMode()) return
564
+ // From now on, only ctrl/meta shortcut
565
+ if (!(e.ctrlKey || e.metaKey) || e.shiftKey) return
538
566
 
539
- /* Edit mode only shortcuts */
540
- if (key === U.Keys.E && modifierKey && !this.editEnabled) {
541
- L.DomEvent.stop(e)
542
- this.enableEdit()
543
- } else if (key === U.Keys.E && modifierKey && this.editEnabled && !this.isDirty) {
567
+ if (e.key === 'f') {
544
568
  L.DomEvent.stop(e)
545
- this.disableEdit()
569
+ this.search()
546
570
  }
547
- if (key === U.Keys.S && modifierKey) {
548
- L.DomEvent.stop(e)
549
- if (this.isDirty) {
550
- this.save()
571
+
572
+ /* Edit mode only shortcuts */
573
+ if (!this.hasEditMode()) return
574
+
575
+ // Edit mode Off
576
+ if (!this.editEnabled) {
577
+ switch (e.key) {
578
+ case 'e':
579
+ L.DomEvent.stop(e)
580
+ this.enableEdit()
581
+ break
551
582
  }
583
+ return
552
584
  }
553
- if (key === U.Keys.Z && modifierKey && this.isDirty) {
554
- L.DomEvent.stop(e)
555
- this.askForReset()
556
- }
557
- if (key === U.Keys.M && modifierKey && this.editEnabled) {
558
- L.DomEvent.stop(e)
559
- this.editTools.startMarker()
560
- }
561
- if (key === U.Keys.P && modifierKey && this.editEnabled) {
562
- L.DomEvent.stop(e)
563
- this.editTools.startPolygon()
564
- }
565
- if (key === U.Keys.L && modifierKey && this.editEnabled) {
566
- L.DomEvent.stop(e)
567
- this.editTools.startPolyline()
568
- }
569
- if (key === U.Keys.I && modifierKey && this.editEnabled) {
570
- L.DomEvent.stop(e)
571
- this.importer.open()
572
- }
573
- if (key === U.Keys.O && modifierKey && this.editEnabled) {
574
- L.DomEvent.stop(e)
575
- this.importer.openFiles()
576
- }
577
- if (key === U.Keys.H && modifierKey && this.editEnabled) {
578
- L.DomEvent.stop(e)
579
- this.help.show('edit')
580
- }
581
- if (e.keyCode === U.Keys.ESC) {
582
- if (this.editEnabled && this.editTools.drawing()) {
583
- this.editTools.stopDrawing()
584
- }
585
- if (this.measureTools.enabled()) this.measureTools.stopDrawing()
585
+
586
+ // Edit mode on
587
+ let used = true
588
+ switch (e.key) {
589
+ case 'e':
590
+ if (!this.isDirty) this.disableEdit()
591
+ break
592
+ case 's':
593
+ if (this.isDirty) this.save()
594
+ break
595
+ case 'z':
596
+ if (this.isDirty) this.askForReset()
597
+ break
598
+ case 'm':
599
+ this.editTools.startMarker()
600
+ break
601
+ case 'p':
602
+ this.editTools.startPolygon()
603
+ break
604
+ case 'l':
605
+ this.editTools.startPolyline()
606
+ break
607
+ case 'i':
608
+ this.importer.open()
609
+ break
610
+ case 'o':
611
+ this.importer.openFiles()
612
+ break
613
+ case 'h':
614
+ this.help.show('edit')
615
+ break
616
+ default:
617
+ used = false
586
618
  }
619
+ if (used) L.DomEvent.stop(e)
587
620
  }
588
621
  L.DomEvent.addListener(document, 'keydown', globalShortcuts, this)
589
622
  },
@@ -644,10 +677,7 @@ U.Map = L.Map.extend({
644
677
  } catch (e) {
645
678
  console.error(e)
646
679
  this.removeLayer(tilelayer)
647
- this.ui.alert({
648
- content: `${L._('Error in the tilelayer URL')}: ${tilelayer._url}`,
649
- level: 'error',
650
- })
680
+ U.Alert.error(`${L._('Error in the tilelayer URL')}: ${tilelayer._url}`)
651
681
  // Users can put tilelayer URLs by hand, and if they add wrong {variable},
652
682
  // Leaflet throw an error, and then the map is no more editable
653
683
  }
@@ -679,10 +709,7 @@ U.Map = L.Map.extend({
679
709
  } catch (e) {
680
710
  this.removeLayer(overlay)
681
711
  console.error(e)
682
- this.ui.alert({
683
- content: `${L._('Error in the overlay URL')}: ${overlay._url}`,
684
- level: 'error',
685
- })
712
+ U.Alert.error(`${L._('Error in the overlay URL')}: ${overlay._url}`)
686
713
  }
687
714
  },
688
715
 
@@ -773,11 +800,14 @@ U.Map = L.Map.extend({
773
800
  return L.Map.prototype.setMaxBounds.call(this, bounds)
774
801
  },
775
802
 
776
- createDataLayer: function (datalayer) {
777
- datalayer = datalayer || {
778
- name: `${L._('Layer')} ${this.datalayers_index.length + 1}`,
803
+ createDataLayer: function (options = {}, sync) {
804
+ options.name = options.name || `${L._('Layer')} ${this.datalayers_index.length + 1}`
805
+ const datalayer = new U.DataLayer(this, options, sync)
806
+
807
+ if (sync !== false) {
808
+ datalayer.sync.upsert(datalayer.options)
779
809
  }
780
- return new U.DataLayer(this, datalayer)
810
+ return datalayer
781
811
  },
782
812
 
783
813
  newDataLayer: function () {
@@ -789,24 +819,25 @@ U.Map = L.Map.extend({
789
819
  return U.SCHEMA[option] && U.SCHEMA[option].default
790
820
  },
791
821
 
792
- getOption: function (option) {
822
+ getOption: function (option, feature) {
823
+ if (feature) {
824
+ const value = this.rules.getOption(option, feature)
825
+ if (value !== undefined) return value
826
+ }
793
827
  if (U.Utils.usableOption(this.options, option)) return this.options[option]
794
828
  return this.getDefaultOption(option)
795
829
  },
796
830
 
797
- updateExtent: function () {
831
+ setCenterAndZoom: function () {
832
+ this._setCenterAndZoom()
833
+ U.Alert.success(L._('The zoom and center have been modified.'))
834
+ },
835
+
836
+ _setCenterAndZoom: function () {
798
837
  this.options.center = this.getCenter()
799
838
  this.options.zoom = this.getZoom()
800
839
  this.isDirty = true
801
840
  this._default_extent = false
802
- if (this.options.umap_id) {
803
- // We do not want an extra message during the map creation
804
- // to avoid the double notification/alert.
805
- this.ui.alert({
806
- content: L._('The zoom and center have been modified.'),
807
- level: 'info',
808
- })
809
- }
810
841
  },
811
842
 
812
843
  updateTileLayers: function () {
@@ -845,12 +876,11 @@ U.Map = L.Map.extend({
845
876
  processFileToImport: function (file, layer, type) {
846
877
  type = type || U.Utils.detectFileType(file)
847
878
  if (!type) {
848
- this.ui.alert({
849
- content: L._('Unable to detect format of file {filename}', {
879
+ U.Alert.error(
880
+ L._('Unable to detect format of file {filename}', {
850
881
  filename: file.name,
851
- }),
852
- level: 'error',
853
- })
882
+ })
883
+ )
854
884
  return
855
885
  }
856
886
  if (type === 'umap') {
@@ -861,6 +891,13 @@ U.Map = L.Map.extend({
861
891
  }
862
892
  },
863
893
 
894
+ importFromUrl: async function (uri) {
895
+ const response = await this.request.get(uri)
896
+ if (response && response.ok) {
897
+ this.importRaw(await response.text())
898
+ }
899
+ },
900
+
864
901
  importRaw: function (rawData) {
865
902
  const importedData = JSON.parse(rawData)
866
903
 
@@ -876,7 +913,11 @@ U.Map = L.Map.extend({
876
913
  if (importedData.geometry) this.options.center = this.latLng(importedData.geometry)
877
914
  const self = this
878
915
  importedData.layers.forEach((geojson) => {
879
- delete geojson._umap_options['id'] // Never trust an id at this stage
916
+ if (!geojson._umap_options && geojson._storage) {
917
+ geojson._umap_options = geojson._storage
918
+ delete geojson._storage
919
+ }
920
+ delete geojson._umap_options?.id // Never trust an id at this stage
880
921
  const dataLayer = self.createDataLayer(geojson._umap_options)
881
922
  dataLayer.fromUmapGeoJSON(geojson)
882
923
  })
@@ -902,18 +943,17 @@ U.Map = L.Map.extend({
902
943
  self.importRaw(rawData)
903
944
  } catch (e) {
904
945
  console.error('Error importing data', e)
905
- self.ui.alert({
906
- content: L._('Invalid umap data in {filename}', { filename: file.name }),
907
- level: 'error',
908
- })
946
+ U.Alert.error(L._('Invalid umap data in {filename}', { filename: file.name }))
909
947
  }
910
948
  }
911
949
  },
912
950
 
913
951
  openBrowser: function (mode) {
914
- this.onceDatalayersLoaded(function () {
915
- this.browser.open(mode)
916
- })
952
+ this.onceDatalayersLoaded(() => this.browser.open(mode))
953
+ },
954
+
955
+ openCaption: function () {
956
+ this.onceDatalayersLoaded(() => this.caption.open())
917
957
  },
918
958
 
919
959
  eachDataLayer: function (method, context) {
@@ -965,7 +1005,7 @@ U.Map = L.Map.extend({
965
1005
  },
966
1006
 
967
1007
  checkDirty: function () {
968
- L.DomUtil.classIf(this._container, 'umap-is-dirty', this.isDirty)
1008
+ this._container.classList.toggle('umap-is-dirty', this.isDirty)
969
1009
  },
970
1010
 
971
1011
  addDirtyDatalayer: function (datalayer) {
@@ -998,6 +1038,7 @@ U.Map = L.Map.extend({
998
1038
  },
999
1039
 
1000
1040
  saveSelf: async function () {
1041
+ this.rules.commit()
1001
1042
  const geojson = {
1002
1043
  type: 'Feature',
1003
1044
  geometry: this.geometry(),
@@ -1008,66 +1049,56 @@ U.Map = L.Map.extend({
1008
1049
  formData.append('center', JSON.stringify(this.geometry()))
1009
1050
  formData.append('settings', JSON.stringify(geojson))
1010
1051
  const uri = this.urls.get('map_save', { map_id: this.options.umap_id })
1011
- const [data, response, error] = await this.server.post(uri, {}, formData)
1052
+ const [data, _, error] = await this.server.post(uri, {}, formData)
1012
1053
  // FIXME: login_required response will not be an error, so it will not
1013
1054
  // stop code while it should
1014
- if (!error) {
1015
- let duration = 3000,
1016
- alert = { content: L._('Map has been saved!'), level: 'info' }
1017
- if (!this.options.umap_id) {
1018
- alert.content = L._('Congratulations, your map has been created!')
1019
- this.options.umap_id = data.id
1020
- this.permissions.setOptions(data.permissions)
1021
- this.permissions.commit()
1022
- if (data.permissions && data.permissions.anonymous_edit_url) {
1023
- alert.duration = Infinity
1024
- alert.content =
1025
- L._(
1026
- 'Your map has been created! As you are not logged in, here is your secret link to edit the map, please keep it safe:'
1027
- ) + `<br>${data.permissions.anonymous_edit_url}`
1028
-
1029
- alert.actions = [
1030
- {
1031
- label: L._('Copy link'),
1032
- callback: () => {
1033
- L.Util.copyToClipboard(data.permissions.anonymous_edit_url)
1034
- this.ui.alert({
1035
- content: L._('Secret edit link copied to clipboard!'),
1036
- level: 'info',
1037
- })
1038
- },
1039
- callbackContext: this,
1040
- },
1041
- ]
1042
- if (this.options.urls.map_send_edit_link) {
1043
- alert.actions.push({
1044
- label: L._('Send me the link'),
1045
- input: L._('Email'),
1046
- callback: this.sendEditLink,
1047
- callbackContext: this,
1048
- })
1049
- }
1050
- }
1051
- } else if (!this.permissions.isDirty) {
1055
+ if (error) {
1056
+ return
1057
+ }
1058
+
1059
+ if (!this.options.umap_id) {
1060
+ this.options.umap_id = data.id
1061
+ this.permissions.setOptions(data.permissions)
1062
+ this.permissions.commit()
1063
+ if (data?.permissions?.anonymous_edit_url) {
1064
+ this.once('saved', () => {
1065
+ U.AlertCreation.info(
1066
+ L._('Your map has been created with an anonymous account!'),
1067
+ Number.Infinity,
1068
+ data.permissions.anonymous_edit_url,
1069
+ this.options.urls.map_send_edit_link
1070
+ ? this.sendEditLinkEmail.bind(this)
1071
+ : null
1072
+ )
1073
+ })
1074
+ } else {
1075
+ this.once('saved', () => {
1076
+ U.Alert.success(L._('Congratulations, your map has been created!'))
1077
+ })
1078
+ }
1079
+ } else {
1080
+ if (!this.permissions.isDirty) {
1052
1081
  // Do not override local changes to permissions,
1053
1082
  // but update in case some other editors changed them in the meantime.
1054
1083
  this.permissions.setOptions(data.permissions)
1055
1084
  this.permissions.commit()
1056
1085
  }
1057
- // Update URL in case the name has changed.
1058
- if (history && history.pushState)
1059
- history.pushState({}, this.options.name, data.url)
1060
- else window.location = data.url
1061
- alert.content = data.info || alert.content
1062
- this.once('saved', () => this.ui.alert(alert))
1063
- this.editPanel.close()
1064
- this.permissions.save()
1086
+ this.once('saved', () => {
1087
+ U.Alert.success(data.info || L._('Map has been saved!'))
1088
+ })
1065
1089
  }
1090
+ // Update URL in case the name has changed.
1091
+ if (history?.pushState) {
1092
+ history.pushState({}, this.options.name, data.url)
1093
+ } else {
1094
+ window.location = data.url
1095
+ }
1096
+ this.permissions.save()
1066
1097
  },
1067
1098
 
1068
1099
  save: function () {
1069
1100
  if (!this.isDirty) return
1070
- if (this._default_extent) this.updateExtent()
1101
+ if (this._default_extent) this._setCenterAndZoom()
1071
1102
  this.backup()
1072
1103
  this.once('saved', () => {
1073
1104
  this.isDirty = false
@@ -1080,33 +1111,20 @@ U.Map = L.Map.extend({
1080
1111
  }
1081
1112
  },
1082
1113
 
1083
- sendEditLink: async function () {
1084
- const input = this.ui._alert.querySelector('input')
1085
- const email = input.value
1086
-
1087
- const formData = new FormData()
1088
- formData.append('email', email)
1089
-
1090
- const url = this.urls.get('map_send_edit_link', { map_id: this.options.umap_id })
1091
- await this.server.post(url, {}, formData)
1092
- },
1093
-
1094
1114
  star: async function () {
1095
- if (!this.options.umap_id)
1096
- return this.ui.alert({
1097
- content: L._('Please save the map first'),
1098
- level: 'error',
1099
- })
1115
+ if (!this.options.umap_id) {
1116
+ return U.Alert.error(L._('Please save the map first'))
1117
+ }
1100
1118
  const url = this.urls.get('map_star', { map_id: this.options.umap_id })
1101
1119
  const [data, response, error] = await this.server.post(url)
1102
- if (!error) {
1103
- this.options.starred = data.starred
1104
- let msg = data.starred
1105
- ? L._('Map has been starred')
1106
- : L._('Map has been unstarred')
1107
- this.ui.alert({ content: msg, level: 'info' })
1108
- this.renderControls()
1120
+ if (error) {
1121
+ return
1109
1122
  }
1123
+ this.options.starred = data.starred
1124
+ U.Alert.success(
1125
+ data.starred ? L._('Map has been starred') : L._('Map has been unstarred')
1126
+ )
1127
+ this.renderControls()
1110
1128
  },
1111
1129
 
1112
1130
  geometry: function () {
@@ -1410,6 +1428,8 @@ U.Map = L.Map.extend({
1410
1428
  this.options.limitBounds.north = L.Util.formatNum(bounds.getNorth())
1411
1429
  this.options.limitBounds.east = L.Util.formatNum(bounds.getEast())
1412
1430
  boundsBuilder.fetchAll()
1431
+
1432
+ this.sync.update(this, 'options.limitBounds', this.options.limitBounds)
1413
1433
  this.isDirty = true
1414
1434
  this.handleLimitBounds()
1415
1435
  },
@@ -1465,6 +1485,12 @@ U.Map = L.Map.extend({
1465
1485
  slideshow.appendChild(slideshowBuilder.build())
1466
1486
  },
1467
1487
 
1488
+ _editSync: function (container) {
1489
+ const sync = L.DomUtil.createFieldset(container, L._('Real-time collaboration'))
1490
+ const builder = new U.FormBuilder(this, ['options.syncEnabled'])
1491
+ sync.appendChild(builder.build())
1492
+ },
1493
+
1468
1494
  _advancedActions: function (container) {
1469
1495
  const advancedActions = L.DomUtil.createFieldset(container, L._('Advanced actions'))
1470
1496
  const advancedButtons = L.DomUtil.create('div', 'button-bar half', advancedActions)
@@ -1510,10 +1536,10 @@ U.Map = L.Map.extend({
1510
1536
  editCaption: function () {
1511
1537
  if (!this.editEnabled) return
1512
1538
  if (this.options.editMode !== 'advanced') return
1513
- const container = L.DomUtil.create('div', 'umap-edit-container'),
1514
- metadataFields = ['options.name', 'options.description'],
1515
- title = L.DomUtil.create('h3', '', container)
1516
- title.textContent = L._('Edit map details')
1539
+ const container = L.DomUtil.create('div', 'umap-edit-container')
1540
+ const metadataFields = ['options.name', 'options.description']
1541
+
1542
+ L.DomUtil.createTitle(container, L._('Edit map details'), 'icon-caption')
1517
1543
  const builder = new U.FormBuilder(this, metadataFields, {
1518
1544
  className: 'map-metadata',
1519
1545
  })
@@ -1542,10 +1568,14 @@ U.Map = L.Map.extend({
1542
1568
  this._editShapeProperties(container)
1543
1569
  this._editDefaultProperties(container)
1544
1570
  this._editInteractionsProperties(container)
1571
+ this.rules.edit(container)
1545
1572
  this._editTilelayer(container)
1546
1573
  this._editOverlay(container)
1547
1574
  this._editBounds(container)
1548
1575
  this._editSlideshow(container)
1576
+ if (this.options.websocketEnabled) {
1577
+ this._editSync(container)
1578
+ }
1549
1579
  this._advancedActions(container)
1550
1580
 
1551
1581
  this.editPanel.open({ content: container, className: 'dark' })
@@ -1556,6 +1586,7 @@ U.Map = L.Map.extend({
1556
1586
  this.editEnabled = true
1557
1587
  this.drop.enable()
1558
1588
  this.fire('edit:enabled')
1589
+ this.initSyncEngine()
1559
1590
  },
1560
1591
 
1561
1592
  disableEdit: function () {
@@ -1567,6 +1598,7 @@ U.Map = L.Map.extend({
1567
1598
  this.fire('edit:disabled')
1568
1599
  this.editPanel.close()
1569
1600
  this.fullPanel.close()
1601
+ this.sync.stop()
1570
1602
  },
1571
1603
 
1572
1604
  hasEditMode: function () {
@@ -1590,8 +1622,8 @@ U.Map = L.Map.extend({
1590
1622
  L.DomUtil.createButton(
1591
1623
  'umap-about-link flat',
1592
1624
  container,
1593
- L._('About'),
1594
- this.displayCaption,
1625
+ L._('Open caption'),
1626
+ this.openCaption,
1595
1627
  this
1596
1628
  )
1597
1629
  L.DomUtil.createButton(
@@ -1741,7 +1773,7 @@ U.Map = L.Map.extend({
1741
1773
  items.push(
1742
1774
  '-',
1743
1775
  {
1744
- text: L._('See layers'),
1776
+ text: L._('Open browser'),
1745
1777
  callback: () => this.openBrowser('layers'),
1746
1778
  },
1747
1779
  {
@@ -1757,8 +1789,8 @@ U.Map = L.Map.extend({
1757
1789
  }
1758
1790
  items.push(
1759
1791
  {
1760
- text: L._('About'),
1761
- callback: this.displayCaption,
1792
+ text: L._('Open caption'),
1793
+ callback: this.openCaption,
1762
1794
  },
1763
1795
  {
1764
1796
  text: this.help.displayLabel('SEARCH'),
@@ -1844,10 +1876,6 @@ U.Map = L.Map.extend({
1844
1876
  if (this._controls.search) this._controls.search.open()
1845
1877
  },
1846
1878
 
1847
- getFilterKeys: function () {
1848
- return (this.options.filterKey || this.options.sortKey || 'name').split(',')
1849
- },
1850
-
1851
1879
  getLayersBounds: function () {
1852
1880
  const bounds = new L.latLngBounds()
1853
1881
  this.eachBrowsableDataLayer((d) => {
@@ -1855,4 +1883,13 @@ U.Map = L.Map.extend({
1855
1883
  })
1856
1884
  return bounds
1857
1885
  },
1886
+
1887
+ sendEditLinkEmail: async function (formData) {
1888
+ const sendLink =
1889
+ this.options.urls.map_send_edit_link &&
1890
+ this.urls.get('map_send_edit_link', {
1891
+ map_id: this.options.umap_id,
1892
+ })
1893
+ await this.server.post(sendLink, {}, formData)
1894
+ },
1858
1895
  })