umap-project 2.3.1__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 (204) 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/management/commands/run_websocket_server.py +23 -0
  6. umap/models.py +6 -1
  7. umap/settings/base.py +11 -3
  8. umap/static/umap/base.css +64 -184
  9. umap/static/umap/content.css +3 -2
  10. umap/static/umap/css/dialog.css +18 -0
  11. umap/static/umap/css/icon.css +8 -0
  12. umap/static/umap/css/importers.css +51 -0
  13. umap/static/umap/css/panel.css +18 -57
  14. umap/static/umap/css/tooltip.css +59 -0
  15. umap/static/umap/css/window.css +35 -0
  16. umap/static/umap/img/16-white.svg +1 -3
  17. umap/static/umap/img/alert-icon-error.svg +8 -0
  18. umap/static/umap/img/alert-icon-info.svg +4 -0
  19. umap/static/umap/img/alert-icon-success.svg +3 -0
  20. umap/static/umap/img/icon-external-link.svg +3 -0
  21. umap/static/umap/img/importers/communesfr.svg +5 -0
  22. umap/static/umap/img/importers/datasets.svg +13 -0
  23. umap/static/umap/img/importers/geodatamine.svg +10 -0
  24. umap/static/umap/img/importers/overpass.svg +7 -0
  25. umap/static/umap/img/importers/random.svg +18 -0
  26. umap/static/umap/img/importers/random1.svg +4 -0
  27. umap/static/umap/img/importers/random2.svg +4 -0
  28. umap/static/umap/img/source/16-white.svg +2 -4
  29. umap/static/umap/js/components/alerts/alert.css +160 -0
  30. umap/static/umap/js/components/alerts/alert.js +169 -0
  31. umap/static/umap/js/components/base.js +54 -0
  32. umap/static/umap/js/modules/autocomplete.js +347 -0
  33. umap/static/umap/js/modules/browser.js +6 -6
  34. umap/static/umap/js/modules/caption.js +5 -4
  35. umap/static/umap/js/modules/global.js +36 -12
  36. umap/static/umap/js/modules/help.js +255 -0
  37. umap/static/umap/js/modules/importer.js +308 -0
  38. umap/static/umap/js/modules/importers/communesfr.js +44 -0
  39. umap/static/umap/js/modules/importers/datasets.js +42 -0
  40. umap/static/umap/js/modules/importers/geodatamine.js +95 -0
  41. umap/static/umap/js/modules/importers/overpass.js +84 -0
  42. umap/static/umap/js/modules/request.js +12 -14
  43. umap/static/umap/js/modules/rules.js +241 -0
  44. umap/static/umap/js/modules/schema.js +63 -14
  45. umap/static/umap/js/modules/sync/engine.js +93 -0
  46. umap/static/umap/js/modules/sync/updaters.js +109 -0
  47. umap/static/umap/js/modules/sync/websocket.js +25 -0
  48. umap/static/umap/js/modules/ui/dialog.js +52 -0
  49. umap/static/umap/js/modules/{panel.js → ui/panel.js} +25 -14
  50. umap/static/umap/js/modules/ui/tooltip.js +116 -0
  51. umap/static/umap/js/modules/utils.js +25 -18
  52. umap/static/umap/js/umap.controls.js +13 -14
  53. umap/static/umap/js/umap.core.js +1 -324
  54. umap/static/umap/js/umap.features.js +77 -29
  55. umap/static/umap/js/umap.forms.js +9 -13
  56. umap/static/umap/js/umap.js +254 -215
  57. umap/static/umap/js/umap.layer.js +152 -74
  58. umap/static/umap/js/umap.permissions.js +5 -9
  59. umap/static/umap/js/umap.popup.js +1 -1
  60. umap/static/umap/js/umap.tableeditor.js +8 -8
  61. umap/static/umap/locale/am_ET.js +51 -16
  62. umap/static/umap/locale/am_ET.json +51 -16
  63. umap/static/umap/locale/ar.js +51 -16
  64. umap/static/umap/locale/ar.json +51 -16
  65. umap/static/umap/locale/ast.js +51 -16
  66. umap/static/umap/locale/ast.json +51 -16
  67. umap/static/umap/locale/bg.js +51 -16
  68. umap/static/umap/locale/bg.json +51 -16
  69. umap/static/umap/locale/br.js +55 -20
  70. umap/static/umap/locale/br.json +55 -20
  71. umap/static/umap/locale/ca.js +51 -16
  72. umap/static/umap/locale/ca.json +51 -16
  73. umap/static/umap/locale/cs_CZ.js +93 -58
  74. umap/static/umap/locale/cs_CZ.json +93 -58
  75. umap/static/umap/locale/da.js +51 -16
  76. umap/static/umap/locale/da.json +51 -16
  77. umap/static/umap/locale/de.js +56 -21
  78. umap/static/umap/locale/de.json +56 -21
  79. umap/static/umap/locale/el.js +51 -16
  80. umap/static/umap/locale/el.json +51 -16
  81. umap/static/umap/locale/en.js +52 -16
  82. umap/static/umap/locale/en.json +52 -16
  83. umap/static/umap/locale/en_US.json +51 -16
  84. umap/static/umap/locale/es.js +51 -16
  85. umap/static/umap/locale/es.json +51 -16
  86. umap/static/umap/locale/et.js +51 -16
  87. umap/static/umap/locale/et.json +51 -16
  88. umap/static/umap/locale/eu.js +51 -16
  89. umap/static/umap/locale/eu.json +51 -16
  90. umap/static/umap/locale/fa_IR.js +51 -16
  91. umap/static/umap/locale/fa_IR.json +51 -16
  92. umap/static/umap/locale/fi.js +51 -16
  93. umap/static/umap/locale/fi.json +51 -16
  94. umap/static/umap/locale/fr.js +61 -25
  95. umap/static/umap/locale/fr.json +61 -25
  96. umap/static/umap/locale/gl.js +51 -16
  97. umap/static/umap/locale/gl.json +51 -16
  98. umap/static/umap/locale/he.js +51 -16
  99. umap/static/umap/locale/he.json +51 -16
  100. umap/static/umap/locale/hr.js +51 -16
  101. umap/static/umap/locale/hr.json +51 -16
  102. umap/static/umap/locale/hu.js +51 -16
  103. umap/static/umap/locale/hu.json +51 -16
  104. umap/static/umap/locale/id.js +51 -16
  105. umap/static/umap/locale/id.json +51 -16
  106. umap/static/umap/locale/is.js +51 -16
  107. umap/static/umap/locale/is.json +51 -16
  108. umap/static/umap/locale/it.js +51 -16
  109. umap/static/umap/locale/it.json +51 -16
  110. umap/static/umap/locale/ja.js +51 -16
  111. umap/static/umap/locale/ja.json +51 -16
  112. umap/static/umap/locale/ko.js +51 -16
  113. umap/static/umap/locale/ko.json +51 -16
  114. umap/static/umap/locale/lt.js +51 -16
  115. umap/static/umap/locale/lt.json +51 -16
  116. umap/static/umap/locale/ms.js +51 -16
  117. umap/static/umap/locale/ms.json +51 -16
  118. umap/static/umap/locale/nl.js +51 -16
  119. umap/static/umap/locale/nl.json +51 -16
  120. umap/static/umap/locale/no.js +51 -16
  121. umap/static/umap/locale/no.json +51 -16
  122. umap/static/umap/locale/pl.js +93 -58
  123. umap/static/umap/locale/pl.json +93 -58
  124. umap/static/umap/locale/pl_PL.json +51 -16
  125. umap/static/umap/locale/pt.js +215 -180
  126. umap/static/umap/locale/pt.json +215 -180
  127. umap/static/umap/locale/pt_BR.js +51 -16
  128. umap/static/umap/locale/pt_BR.json +51 -16
  129. umap/static/umap/locale/pt_PT.js +51 -16
  130. umap/static/umap/locale/pt_PT.json +51 -16
  131. umap/static/umap/locale/ro.js +51 -16
  132. umap/static/umap/locale/ro.json +51 -16
  133. umap/static/umap/locale/ru.js +51 -16
  134. umap/static/umap/locale/ru.json +51 -16
  135. umap/static/umap/locale/si.js +51 -16
  136. umap/static/umap/locale/si.json +51 -16
  137. umap/static/umap/locale/sk_SK.js +51 -16
  138. umap/static/umap/locale/sk_SK.json +51 -16
  139. umap/static/umap/locale/sl.js +51 -16
  140. umap/static/umap/locale/sl.json +51 -16
  141. umap/static/umap/locale/sr.js +51 -16
  142. umap/static/umap/locale/sr.json +51 -16
  143. umap/static/umap/locale/sv.js +51 -16
  144. umap/static/umap/locale/sv.json +51 -16
  145. umap/static/umap/locale/th_TH.js +51 -16
  146. umap/static/umap/locale/th_TH.json +51 -16
  147. umap/static/umap/locale/tr.js +51 -16
  148. umap/static/umap/locale/tr.json +51 -16
  149. umap/static/umap/locale/uk_UA.js +51 -16
  150. umap/static/umap/locale/uk_UA.json +51 -16
  151. umap/static/umap/locale/vi.js +51 -16
  152. umap/static/umap/locale/vi.json +51 -16
  153. umap/static/umap/locale/vi_VN.json +51 -16
  154. umap/static/umap/locale/zh.js +51 -16
  155. umap/static/umap/locale/zh.json +51 -16
  156. umap/static/umap/locale/zh_CN.json +51 -16
  157. umap/static/umap/locale/zh_TW.Big5.json +51 -16
  158. umap/static/umap/locale/zh_TW.js +51 -16
  159. umap/static/umap/locale/zh_TW.json +51 -16
  160. umap/static/umap/map.css +40 -53
  161. umap/static/umap/unittests/sync.js +105 -0
  162. umap/static/umap/unittests/utils.js +78 -36
  163. umap/static/umap/vars.css +19 -1
  164. umap/static/umap/vendors/formbuilder/Leaflet.FormBuilder.js +2 -2
  165. umap/templates/umap/components/alerts/alert.html +89 -0
  166. umap/templates/umap/content.html +4 -3
  167. umap/templates/umap/css.html +4 -0
  168. umap/templates/umap/home.html +3 -0
  169. umap/templates/umap/js.html +0 -3
  170. umap/templates/umap/map_init.html +2 -8
  171. umap/templates/umap/messages.html +9 -11
  172. umap/templates/umap/search.html +3 -0
  173. umap/tests/base.py +2 -0
  174. umap/tests/integration/conftest.py +30 -0
  175. umap/tests/integration/test_anonymous_owned_map.py +8 -13
  176. umap/tests/integration/test_browser.py +77 -4
  177. umap/tests/integration/test_conditional_rules.py +201 -0
  178. umap/tests/integration/test_dashboard.py +1 -1
  179. umap/tests/integration/test_datalayer.py +2 -3
  180. umap/tests/integration/test_edit_datalayer.py +4 -4
  181. umap/tests/integration/test_edit_map.py +1 -1
  182. umap/tests/integration/test_facets_browser.py +3 -3
  183. umap/tests/integration/test_import.py +185 -49
  184. umap/tests/integration/test_map.py +31 -2
  185. umap/tests/integration/{test_collaborative_editing.py → test_optimistic_merge.py} +7 -7
  186. umap/tests/integration/test_owned_map.py +1 -1
  187. umap/tests/integration/test_picto.py +2 -2
  188. umap/tests/integration/test_statics.py +1 -1
  189. umap/tests/integration/test_view_marker.py +2 -2
  190. umap/tests/integration/test_websocket_sync.py +283 -0
  191. umap/tests/settings.py +5 -0
  192. umap/tests/test_datalayer_views.py +0 -1
  193. umap/tests/test_views.py +53 -0
  194. umap/urls.py +5 -0
  195. umap/views.py +40 -11
  196. umap/websocket_server.py +92 -0
  197. {umap_project-2.3.1.dist-info → umap_project-2.4.0.dist-info}/METADATA +10 -8
  198. {umap_project-2.3.1.dist-info → umap_project-2.4.0.dist-info}/RECORD +201 -167
  199. umap/static/umap/js/umap.autocomplete.js +0 -341
  200. umap/static/umap/js/umap.importer.js +0 -187
  201. umap/static/umap/js/umap.ui.js +0 -190
  202. {umap_project-2.3.1.dist-info → umap_project-2.4.0.dist-info}/WHEEL +0 -0
  203. {umap_project-2.3.1.dist-info → umap_project-2.4.0.dist-info}/entry_points.txt +0 -0
  204. {umap_project-2.3.1.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,42 +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.panel.setDefaultMode('expanded')
218
- this.openBrowser('data')
219
- } else if (this.options.onLoadPanel === 'datalayers') {
220
- this.panel.setDefaultMode('condensed')
221
- this.openBrowser('layers')
222
- } else if (this.options.onLoadPanel === 'datafilters') {
223
- this.panel.setDefaultMode('expanded')
224
- this.openBrowser('filters')
225
- } else if (this.options.onLoadPanel === 'caption') {
226
- this.panel.setDefaultMode('condensed')
227
- this.openCaption()
228
- }
229
- if (L.Util.queryString('edit')) {
230
- if (this.hasEditMode()) this.enableEdit()
231
- // Sometimes users share the ?edit link by mistake, let's remove
232
- // this search parameter from URL to prevent this
233
- const url = new URL(window.location)
234
- url.searchParams.delete('edit')
235
- history.pushState({}, '', url)
236
- }
237
- if (L.Util.queryString('download')) {
238
- const download_url = this.urls.get('map_download', {
239
- map_id: this.options.umap_id,
240
- })
241
- window.location = download_url
242
- }
243
- })
210
+ this.onceDataLoaded(this.setViewFromQueryString)
244
211
 
245
212
  window.onbeforeunload = () => (this.editEnabled && this.isDirty) || null
246
213
  this.backup()
@@ -248,6 +215,25 @@ U.Map = L.Map.extend({
248
215
  this.on('click contextmenu.show', this.closeInplaceToolbar)
249
216
  },
250
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
+
251
237
  render: function (fields) {
252
238
  let impacts = U.Utils.getImpactsFromSchema(fields)
253
239
 
@@ -271,6 +257,8 @@ U.Map = L.Map.extend({
271
257
  case 'bounds':
272
258
  this.handleLimitBounds()
273
259
  break
260
+ case 'sync':
261
+ this.initSyncEngine()
274
262
  }
275
263
  }
276
264
  },
@@ -313,6 +301,44 @@ U.Map = L.Map.extend({
313
301
  }
314
302
  },
315
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
+
316
342
  // Merge the given schema with the default one
317
343
  // Missing keys inside the schema are merged with the default ones.
318
344
  overrideSchema: function (schema) {
@@ -359,7 +385,7 @@ U.Map = L.Map.extend({
359
385
  icon: 'umap-fake-class',
360
386
  iconLoading: 'umap-fake-class',
361
387
  flyTo: this.options.easing,
362
- onLocationError: (err) => this.ui.alert({ content: err.message }),
388
+ onLocationError: (err) => U.Alert.error(err.message),
363
389
  })
364
390
  this._controls.fullscreen = new L.Control.Fullscreen({
365
391
  title: { false: L._('View Fullscreen'), true: L._('Exit Fullscreen') },
@@ -388,11 +414,14 @@ U.Map = L.Map.extend({
388
414
  this.importer = new U.Importer(this)
389
415
  this.drop = new U.DropControl(this)
390
416
  this.share = new U.Share(this)
417
+ this.rules = new U.Rules(this)
391
418
  this._controls.tilelayers = new U.TileLayerControl(this)
392
419
  },
393
420
 
394
421
  renderControls: function () {
395
- const hasSlideshow = Boolean(this.options.slideshow && this.options.slideshow.active)
422
+ const hasSlideshow = Boolean(
423
+ this.options.slideshow && this.options.slideshow.active
424
+ )
396
425
  const barEnabled = this.options.captionBar || hasSlideshow
397
426
  document.body.classList.toggle('umap-caption-bar-enabled', barEnabled)
398
427
  document.body.classList.toggle('umap-slideshow-enabled', hasSlideshow)
@@ -514,73 +543,80 @@ U.Map = L.Map.extend({
514
543
 
515
544
  initShortcuts: function () {
516
545
  const globalShortcuts = function (e) {
517
- const key = e.keyCode
518
- const hasModifier = (e.ctrlKey || e.metaKey) && !e.shiftKey
519
-
520
- /* Generic shortcuts */
521
- if (key === U.Keys.F && hasModifier) {
522
- L.DomEvent.stop(e)
523
- this.search()
524
- } else if (e.keyCode === U.Keys.ESC) {
525
- if (this.help.visible()) {
526
- this.help.hide()
527
- } else {
528
- 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()) {
529
556
  this.editPanel?.close()
557
+ } else if (this.fullPanel?.isOpen()) {
530
558
  this.fullPanel?.close()
559
+ } else if (this.panel.isOpen()) {
560
+ this.panel.close()
531
561
  }
532
562
  }
533
563
 
534
- if (!this.hasEditMode()) return
564
+ // From now on, only ctrl/meta shortcut
565
+ if (!(e.ctrlKey || e.metaKey) || e.shiftKey) return
535
566
 
536
- /* Edit mode only shortcuts */
537
- if (key === U.Keys.E && hasModifier && !this.editEnabled) {
567
+ if (e.key === 'f') {
538
568
  L.DomEvent.stop(e)
539
- this.enableEdit()
540
- } else if (key === U.Keys.E && hasModifier && this.editEnabled && !this.isDirty) {
541
- L.DomEvent.stop(e)
542
- this.disableEdit()
569
+ this.search()
543
570
  }
544
- if (key === U.Keys.S && hasModifier) {
545
- L.DomEvent.stop(e)
546
- if (this.isDirty) {
547
- 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
548
582
  }
583
+ return
549
584
  }
550
- if (key === U.Keys.Z && hasModifier && this.isDirty) {
551
- L.DomEvent.stop(e)
552
- this.askForReset()
553
- }
554
- if (key === U.Keys.M && hasModifier && this.editEnabled) {
555
- L.DomEvent.stop(e)
556
- this.editTools.startMarker()
557
- }
558
- if (key === U.Keys.P && hasModifier && this.editEnabled) {
559
- L.DomEvent.stop(e)
560
- this.editTools.startPolygon()
561
- }
562
- if (key === U.Keys.L && hasModifier && this.editEnabled) {
563
- L.DomEvent.stop(e)
564
- this.editTools.startPolyline()
565
- }
566
- if (key === U.Keys.I && hasModifier && this.editEnabled) {
567
- L.DomEvent.stop(e)
568
- this.importer.open()
569
- }
570
- if (key === U.Keys.O && hasModifier && this.editEnabled) {
571
- L.DomEvent.stop(e)
572
- this.importer.openFiles()
573
- }
574
- if (key === U.Keys.H && hasModifier && this.editEnabled) {
575
- L.DomEvent.stop(e)
576
- this.help.show('edit')
577
- }
578
- if (e.keyCode === U.Keys.ESC) {
579
- if (this.editEnabled && this.editTools.drawing()) {
580
- this.editTools.stopDrawing()
581
- }
582
- 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
583
618
  }
619
+ if (used) L.DomEvent.stop(e)
584
620
  }
585
621
  L.DomEvent.addListener(document, 'keydown', globalShortcuts, this)
586
622
  },
@@ -641,10 +677,7 @@ U.Map = L.Map.extend({
641
677
  } catch (e) {
642
678
  console.error(e)
643
679
  this.removeLayer(tilelayer)
644
- this.ui.alert({
645
- content: `${L._('Error in the tilelayer URL')}: ${tilelayer._url}`,
646
- level: 'error',
647
- })
680
+ U.Alert.error(`${L._('Error in the tilelayer URL')}: ${tilelayer._url}`)
648
681
  // Users can put tilelayer URLs by hand, and if they add wrong {variable},
649
682
  // Leaflet throw an error, and then the map is no more editable
650
683
  }
@@ -676,10 +709,7 @@ U.Map = L.Map.extend({
676
709
  } catch (e) {
677
710
  this.removeLayer(overlay)
678
711
  console.error(e)
679
- this.ui.alert({
680
- content: `${L._('Error in the overlay URL')}: ${overlay._url}`,
681
- level: 'error',
682
- })
712
+ U.Alert.error(`${L._('Error in the overlay URL')}: ${overlay._url}`)
683
713
  }
684
714
  },
685
715
 
@@ -770,11 +800,14 @@ U.Map = L.Map.extend({
770
800
  return L.Map.prototype.setMaxBounds.call(this, bounds)
771
801
  },
772
802
 
773
- createDataLayer: function (datalayer) {
774
- datalayer = datalayer || {
775
- 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)
776
809
  }
777
- return new U.DataLayer(this, datalayer)
810
+ return datalayer
778
811
  },
779
812
 
780
813
  newDataLayer: function () {
@@ -786,24 +819,25 @@ U.Map = L.Map.extend({
786
819
  return U.SCHEMA[option] && U.SCHEMA[option].default
787
820
  },
788
821
 
789
- 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
+ }
790
827
  if (U.Utils.usableOption(this.options, option)) return this.options[option]
791
828
  return this.getDefaultOption(option)
792
829
  },
793
830
 
794
- 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 () {
795
837
  this.options.center = this.getCenter()
796
838
  this.options.zoom = this.getZoom()
797
839
  this.isDirty = true
798
840
  this._default_extent = false
799
- if (this.options.umap_id) {
800
- // We do not want an extra message during the map creation
801
- // to avoid the double notification/alert.
802
- this.ui.alert({
803
- content: L._('The zoom and center have been modified.'),
804
- level: 'info',
805
- })
806
- }
807
841
  },
808
842
 
809
843
  updateTileLayers: function () {
@@ -842,12 +876,11 @@ U.Map = L.Map.extend({
842
876
  processFileToImport: function (file, layer, type) {
843
877
  type = type || U.Utils.detectFileType(file)
844
878
  if (!type) {
845
- this.ui.alert({
846
- content: L._('Unable to detect format of file {filename}', {
879
+ U.Alert.error(
880
+ L._('Unable to detect format of file {filename}', {
847
881
  filename: file.name,
848
- }),
849
- level: 'error',
850
- })
882
+ })
883
+ )
851
884
  return
852
885
  }
853
886
  if (type === 'umap') {
@@ -858,6 +891,13 @@ U.Map = L.Map.extend({
858
891
  }
859
892
  },
860
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
+
861
901
  importRaw: function (rawData) {
862
902
  const importedData = JSON.parse(rawData)
863
903
 
@@ -873,7 +913,11 @@ U.Map = L.Map.extend({
873
913
  if (importedData.geometry) this.options.center = this.latLng(importedData.geometry)
874
914
  const self = this
875
915
  importedData.layers.forEach((geojson) => {
876
- 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
877
921
  const dataLayer = self.createDataLayer(geojson._umap_options)
878
922
  dataLayer.fromUmapGeoJSON(geojson)
879
923
  })
@@ -899,10 +943,7 @@ U.Map = L.Map.extend({
899
943
  self.importRaw(rawData)
900
944
  } catch (e) {
901
945
  console.error('Error importing data', e)
902
- self.ui.alert({
903
- content: L._('Invalid umap data in {filename}', { filename: file.name }),
904
- level: 'error',
905
- })
946
+ U.Alert.error(L._('Invalid umap data in {filename}', { filename: file.name }))
906
947
  }
907
948
  }
908
949
  },
@@ -997,6 +1038,7 @@ U.Map = L.Map.extend({
997
1038
  },
998
1039
 
999
1040
  saveSelf: async function () {
1041
+ this.rules.commit()
1000
1042
  const geojson = {
1001
1043
  type: 'Feature',
1002
1044
  geometry: this.geometry(),
@@ -1007,65 +1049,56 @@ U.Map = L.Map.extend({
1007
1049
  formData.append('center', JSON.stringify(this.geometry()))
1008
1050
  formData.append('settings', JSON.stringify(geojson))
1009
1051
  const uri = this.urls.get('map_save', { map_id: this.options.umap_id })
1010
- const [data, response, error] = await this.server.post(uri, {}, formData)
1052
+ const [data, _, error] = await this.server.post(uri, {}, formData)
1011
1053
  // FIXME: login_required response will not be an error, so it will not
1012
1054
  // stop code while it should
1013
- if (!error) {
1014
- let duration = 3000,
1015
- alert = { content: L._('Map has been saved!'), level: 'info' }
1016
- if (!this.options.umap_id) {
1017
- alert.content = L._('Congratulations, your map has been created!')
1018
- this.options.umap_id = data.id
1019
- this.permissions.setOptions(data.permissions)
1020
- this.permissions.commit()
1021
- if (data.permissions && data.permissions.anonymous_edit_url) {
1022
- alert.duration = Infinity
1023
- alert.content =
1024
- L._(
1025
- 'Your map has been created! As you are not logged in, here is your secret link to edit the map, please keep it safe:'
1026
- ) + `<br>${data.permissions.anonymous_edit_url}`
1027
-
1028
- alert.actions = [
1029
- {
1030
- label: L._('Copy link'),
1031
- callback: () => {
1032
- L.Util.copyToClipboard(data.permissions.anonymous_edit_url)
1033
- this.ui.alert({
1034
- content: L._('Secret edit link copied to clipboard!'),
1035
- level: 'info',
1036
- })
1037
- },
1038
- callbackContext: this,
1039
- },
1040
- ]
1041
- if (this.options.urls.map_send_edit_link) {
1042
- alert.actions.push({
1043
- label: L._('Send me the link'),
1044
- input: L._('Email'),
1045
- callback: this.sendEditLink,
1046
- callbackContext: this,
1047
- })
1048
- }
1049
- }
1050
- } 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) {
1051
1081
  // Do not override local changes to permissions,
1052
1082
  // but update in case some other editors changed them in the meantime.
1053
1083
  this.permissions.setOptions(data.permissions)
1054
1084
  this.permissions.commit()
1055
1085
  }
1056
- // Update URL in case the name has changed.
1057
- if (history && history.pushState)
1058
- history.pushState({}, this.options.name, data.url)
1059
- else window.location = data.url
1060
- alert.content = data.info || alert.content
1061
- this.once('saved', () => this.ui.alert(alert))
1062
- this.permissions.save()
1086
+ this.once('saved', () => {
1087
+ U.Alert.success(data.info || L._('Map has been saved!'))
1088
+ })
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
1063
1095
  }
1096
+ this.permissions.save()
1064
1097
  },
1065
1098
 
1066
1099
  save: function () {
1067
1100
  if (!this.isDirty) return
1068
- if (this._default_extent) this.updateExtent()
1101
+ if (this._default_extent) this._setCenterAndZoom()
1069
1102
  this.backup()
1070
1103
  this.once('saved', () => {
1071
1104
  this.isDirty = false
@@ -1078,33 +1111,20 @@ U.Map = L.Map.extend({
1078
1111
  }
1079
1112
  },
1080
1113
 
1081
- sendEditLink: async function () {
1082
- const input = this.ui._alert.querySelector('input')
1083
- const email = input.value
1084
-
1085
- const formData = new FormData()
1086
- formData.append('email', email)
1087
-
1088
- const url = this.urls.get('map_send_edit_link', { map_id: this.options.umap_id })
1089
- await this.server.post(url, {}, formData)
1090
- },
1091
-
1092
1114
  star: async function () {
1093
- if (!this.options.umap_id)
1094
- return this.ui.alert({
1095
- content: L._('Please save the map first'),
1096
- level: 'error',
1097
- })
1115
+ if (!this.options.umap_id) {
1116
+ return U.Alert.error(L._('Please save the map first'))
1117
+ }
1098
1118
  const url = this.urls.get('map_star', { map_id: this.options.umap_id })
1099
1119
  const [data, response, error] = await this.server.post(url)
1100
- if (!error) {
1101
- this.options.starred = data.starred
1102
- let msg = data.starred
1103
- ? L._('Map has been starred')
1104
- : L._('Map has been unstarred')
1105
- this.ui.alert({ content: msg, level: 'info' })
1106
- this.renderControls()
1120
+ if (error) {
1121
+ return
1107
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()
1108
1128
  },
1109
1129
 
1110
1130
  geometry: function () {
@@ -1408,6 +1428,8 @@ U.Map = L.Map.extend({
1408
1428
  this.options.limitBounds.north = L.Util.formatNum(bounds.getNorth())
1409
1429
  this.options.limitBounds.east = L.Util.formatNum(bounds.getEast())
1410
1430
  boundsBuilder.fetchAll()
1431
+
1432
+ this.sync.update(this, 'options.limitBounds', this.options.limitBounds)
1411
1433
  this.isDirty = true
1412
1434
  this.handleLimitBounds()
1413
1435
  },
@@ -1463,6 +1485,12 @@ U.Map = L.Map.extend({
1463
1485
  slideshow.appendChild(slideshowBuilder.build())
1464
1486
  },
1465
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
+
1466
1494
  _advancedActions: function (container) {
1467
1495
  const advancedActions = L.DomUtil.createFieldset(container, L._('Advanced actions'))
1468
1496
  const advancedButtons = L.DomUtil.create('div', 'button-bar half', advancedActions)
@@ -1508,10 +1536,10 @@ U.Map = L.Map.extend({
1508
1536
  editCaption: function () {
1509
1537
  if (!this.editEnabled) return
1510
1538
  if (this.options.editMode !== 'advanced') return
1511
- const container = L.DomUtil.create('div', 'umap-edit-container'),
1512
- metadataFields = ['options.name', 'options.description'],
1513
- title = L.DomUtil.create('h3', '', container)
1514
- 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')
1515
1543
  const builder = new U.FormBuilder(this, metadataFields, {
1516
1544
  className: 'map-metadata',
1517
1545
  })
@@ -1540,10 +1568,14 @@ U.Map = L.Map.extend({
1540
1568
  this._editShapeProperties(container)
1541
1569
  this._editDefaultProperties(container)
1542
1570
  this._editInteractionsProperties(container)
1571
+ this.rules.edit(container)
1543
1572
  this._editTilelayer(container)
1544
1573
  this._editOverlay(container)
1545
1574
  this._editBounds(container)
1546
1575
  this._editSlideshow(container)
1576
+ if (this.options.websocketEnabled) {
1577
+ this._editSync(container)
1578
+ }
1547
1579
  this._advancedActions(container)
1548
1580
 
1549
1581
  this.editPanel.open({ content: container, className: 'dark' })
@@ -1554,6 +1586,7 @@ U.Map = L.Map.extend({
1554
1586
  this.editEnabled = true
1555
1587
  this.drop.enable()
1556
1588
  this.fire('edit:enabled')
1589
+ this.initSyncEngine()
1557
1590
  },
1558
1591
 
1559
1592
  disableEdit: function () {
@@ -1565,6 +1598,7 @@ U.Map = L.Map.extend({
1565
1598
  this.fire('edit:disabled')
1566
1599
  this.editPanel.close()
1567
1600
  this.fullPanel.close()
1601
+ this.sync.stop()
1568
1602
  },
1569
1603
 
1570
1604
  hasEditMode: function () {
@@ -1588,7 +1622,7 @@ U.Map = L.Map.extend({
1588
1622
  L.DomUtil.createButton(
1589
1623
  'umap-about-link flat',
1590
1624
  container,
1591
- L._('About'),
1625
+ L._('Open caption'),
1592
1626
  this.openCaption,
1593
1627
  this
1594
1628
  )
@@ -1739,7 +1773,7 @@ U.Map = L.Map.extend({
1739
1773
  items.push(
1740
1774
  '-',
1741
1775
  {
1742
- text: L._('See layers'),
1776
+ text: L._('Open browser'),
1743
1777
  callback: () => this.openBrowser('layers'),
1744
1778
  },
1745
1779
  {
@@ -1755,7 +1789,7 @@ U.Map = L.Map.extend({
1755
1789
  }
1756
1790
  items.push(
1757
1791
  {
1758
- text: L._('About'),
1792
+ text: L._('Open caption'),
1759
1793
  callback: this.openCaption,
1760
1794
  },
1761
1795
  {
@@ -1842,10 +1876,6 @@ U.Map = L.Map.extend({
1842
1876
  if (this._controls.search) this._controls.search.open()
1843
1877
  },
1844
1878
 
1845
- getFilterKeys: function () {
1846
- return (this.options.filterKey || this.options.sortKey || 'name').split(',')
1847
- },
1848
-
1849
1879
  getLayersBounds: function () {
1850
1880
  const bounds = new L.latLngBounds()
1851
1881
  this.eachBrowsableDataLayer((d) => {
@@ -1853,4 +1883,13 @@ U.Map = L.Map.extend({
1853
1883
  })
1854
1884
  return bounds
1855
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
+ },
1856
1895
  })