umap-project 2.6.3__py3-none-any.whl → 2.7.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 (137) hide show
  1. umap/__init__.py +1 -1
  2. umap/admin.py +64 -1
  3. umap/asgi.py +15 -0
  4. umap/context_processors.py +1 -0
  5. umap/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  6. umap/locale/cs_CZ/LC_MESSAGES/django.po +96 -92
  7. umap/locale/de/LC_MESSAGES/django.mo +0 -0
  8. umap/locale/de/LC_MESSAGES/django.po +19 -18
  9. umap/locale/en/LC_MESSAGES/django.po +47 -43
  10. umap/locale/es/LC_MESSAGES/django.mo +0 -0
  11. umap/locale/es/LC_MESSAGES/django.po +134 -128
  12. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  13. umap/locale/fr/LC_MESSAGES/django.po +51 -47
  14. umap/locale/pt/LC_MESSAGES/django.mo +0 -0
  15. umap/locale/pt/LC_MESSAGES/django.po +64 -60
  16. umap/management/commands/clean_tilelayer.py +152 -0
  17. umap/management/commands/purge_purgatory.py +28 -0
  18. umap/models.py +27 -2
  19. umap/settings/base.py +3 -1
  20. umap/static/umap/base.css +4 -4
  21. umap/static/umap/css/contextmenu.css +6 -1
  22. umap/static/umap/css/icon.css +7 -2
  23. umap/static/umap/css/importers.css +4 -0
  24. umap/static/umap/img/16-white.svg +9 -2
  25. umap/static/umap/img/16.svg +1 -181
  26. umap/static/umap/img/24-white.svg +1 -0
  27. umap/static/umap/img/24.svg +1 -0
  28. umap/static/umap/img/importers/cadastrefr.svg +23 -0
  29. umap/static/umap/img/source/16-white.svg +10 -3
  30. umap/static/umap/img/source/16.svg +753 -197
  31. umap/static/umap/img/source/24-white.svg +3 -2
  32. umap/static/umap/img/source/24.svg +3 -2
  33. umap/static/umap/js/modules/autocomplete.js +7 -3
  34. umap/static/umap/js/modules/browser.js +54 -1
  35. umap/static/umap/js/modules/caption.js +16 -5
  36. umap/static/umap/js/modules/data/features.js +176 -2
  37. umap/static/umap/js/modules/data/layer.js +57 -40
  38. umap/static/umap/js/modules/formatter.js +3 -2
  39. umap/static/umap/js/modules/global.js +2 -0
  40. umap/static/umap/js/modules/importer.js +3 -0
  41. umap/static/umap/js/modules/importers/cadastrefr.js +62 -0
  42. umap/static/umap/js/modules/importers/communesfr.js +15 -3
  43. umap/static/umap/js/modules/permissions.js +123 -93
  44. umap/static/umap/js/modules/rendering/layers/classified.js +2 -0
  45. umap/static/umap/js/modules/rendering/ui.js +60 -213
  46. umap/static/umap/js/modules/share.js +1 -3
  47. umap/static/umap/js/modules/slideshow.js +1 -1
  48. umap/static/umap/js/modules/sync/engine.js +371 -14
  49. umap/static/umap/js/modules/sync/hlc.js +106 -0
  50. umap/static/umap/js/modules/sync/updaters.js +18 -6
  51. umap/static/umap/js/modules/sync/websocket.js +1 -1
  52. umap/static/umap/js/modules/tableeditor.js +1 -1
  53. umap/static/umap/js/modules/ui/base.js +2 -2
  54. umap/static/umap/js/modules/ui/contextmenu.js +51 -18
  55. umap/static/umap/js/modules/urls.js +5 -1
  56. umap/static/umap/js/modules/utils.js +28 -4
  57. umap/static/umap/js/umap.controls.js +73 -52
  58. umap/static/umap/js/umap.core.js +3 -3
  59. umap/static/umap/js/umap.forms.js +3 -1
  60. umap/static/umap/js/umap.js +115 -124
  61. umap/static/umap/locale/br.js +13 -4
  62. umap/static/umap/locale/br.json +13 -4
  63. umap/static/umap/locale/ca.js +28 -15
  64. umap/static/umap/locale/ca.json +28 -15
  65. umap/static/umap/locale/cs_CZ.js +87 -78
  66. umap/static/umap/locale/cs_CZ.json +87 -78
  67. umap/static/umap/locale/de.js +17 -8
  68. umap/static/umap/locale/de.json +17 -8
  69. umap/static/umap/locale/en.js +13 -2
  70. umap/static/umap/locale/en.json +13 -2
  71. umap/static/umap/locale/es.js +330 -319
  72. umap/static/umap/locale/es.json +330 -319
  73. umap/static/umap/locale/eu.js +10 -3
  74. umap/static/umap/locale/eu.json +10 -3
  75. umap/static/umap/locale/fa_IR.js +11 -4
  76. umap/static/umap/locale/fa_IR.json +11 -4
  77. umap/static/umap/locale/fr.js +15 -4
  78. umap/static/umap/locale/fr.json +15 -4
  79. umap/static/umap/locale/hu.js +10 -3
  80. umap/static/umap/locale/hu.json +10 -3
  81. umap/static/umap/locale/pt.js +17 -8
  82. umap/static/umap/locale/pt.json +17 -8
  83. umap/static/umap/locale/pt_PT.js +13 -4
  84. umap/static/umap/locale/pt_PT.json +13 -4
  85. umap/static/umap/locale/zh_TW.js +13 -4
  86. umap/static/umap/locale/zh_TW.json +13 -4
  87. umap/static/umap/map.css +44 -29
  88. umap/static/umap/unittests/hlc.js +165 -0
  89. umap/static/umap/unittests/sync.js +321 -15
  90. umap/static/umap/unittests/utils.js +47 -0
  91. umap/static/umap/vars.css +2 -1
  92. umap/static/umap/vendors/colorbrewer/colorbrewer.js +309 -317
  93. umap/static/umap/vendors/dompurify/purify.es.js +15 -16
  94. umap/static/umap/vendors/dompurify/purify.es.mjs.map +1 -1
  95. umap/static/umap/vendors/georsstogeojson/GeoRSSToGeoJSON.js +111 -80
  96. umap/static/umap/vendors/locatecontrol/L.Control.Locate.min.js +2 -2
  97. umap/static/umap/vendors/locatecontrol/L.Control.Locate.min.js.map +1 -1
  98. umap/static/umap/vendors/simple-statistics/simple-statistics.min.js +1 -1
  99. umap/static/umap/vendors/simple-statistics/simple-statistics.min.js.map +1 -1
  100. umap/templates/umap/css.html +0 -2
  101. umap/templates/umap/dashboard_menu.html +4 -2
  102. umap/templates/umap/js.html +0 -5
  103. umap/templates/umap/map_detail.html +2 -2
  104. umap/tests/fixtures/test_upload_data.csv +2 -2
  105. umap/tests/integration/test_anonymous_owned_map.py +1 -0
  106. umap/tests/integration/test_basics.py +1 -1
  107. umap/tests/integration/test_browser.py +69 -7
  108. umap/tests/integration/test_caption.py +3 -3
  109. umap/tests/integration/test_circles_layer.py +12 -0
  110. umap/tests/integration/test_datalayer.py +2 -1
  111. umap/tests/integration/test_draw_polygon.py +17 -9
  112. umap/tests/integration/test_draw_polyline.py +12 -8
  113. umap/tests/integration/test_edit_datalayer.py +5 -8
  114. umap/tests/integration/test_edit_map.py +2 -2
  115. umap/tests/integration/test_edit_marker.py +1 -1
  116. umap/tests/integration/test_facets_browser.py +3 -3
  117. umap/tests/integration/test_import.py +1 -0
  118. umap/tests/integration/test_map.py +1 -0
  119. umap/tests/integration/test_owned_map.py +1 -1
  120. umap/tests/integration/test_view_marker.py +63 -0
  121. umap/tests/integration/test_view_polygon.py +12 -12
  122. umap/tests/integration/test_websocket_sync.py +65 -3
  123. umap/tests/test_clean_tilelayer.py +83 -0
  124. umap/tests/test_datalayer.py +24 -0
  125. umap/tests/test_map_views.py +20 -0
  126. umap/tests/test_purge_purgatory.py +25 -0
  127. umap/tests/test_websocket_server.py +22 -0
  128. umap/urls.py +5 -1
  129. umap/views.py +6 -3
  130. umap/websocket_server.py +130 -27
  131. {umap_project-2.6.3.dist-info → umap_project-2.7.0.dist-info}/METADATA +18 -14
  132. {umap_project-2.6.3.dist-info → umap_project-2.7.0.dist-info}/RECORD +135 -127
  133. umap/static/umap/vendors/contextmenu/leaflet.contextmenu.min.css +0 -1
  134. umap/static/umap/vendors/contextmenu/leaflet.contextmenu.min.js +0 -7
  135. {umap_project-2.6.3.dist-info → umap_project-2.7.0.dist-info}/WHEEL +0 -0
  136. {umap_project-2.6.3.dist-info → umap_project-2.7.0.dist-info}/entry_points.txt +0 -0
  137. {umap_project-2.6.3.dist-info → umap_project-2.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,8 @@
1
1
  <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
2
  <!-- Created with Inkscape (http://www.inkscape.org/) -->
3
3
 
4
- <svg width="252" height="252" viewBox="0 0 66.674992 66.674992" version="1.1" id="svg2876" inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)" sodipodi:docname="24-white.svg" inkscape:export-filename="../24-white.svg" inkscape:export-xdpi="7.52" inkscape:export-ydpi="7.52" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
5
- <sodipodi:namedview id="namedview2878" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="px" showgrid="true" showguides="true" inkscape:zoom="2.6753614" inkscape:cx="107.64901" inkscape:cy="85.409023" inkscape:window-width="960" inkscape:window-height="1011" inkscape:window-x="20" inkscape:window-y="20" inkscape:window-maximized="0" inkscape:current-layer="layer1">
4
+ <svg width="252" height="252" viewBox="0 0 66.674992 66.674992" version="1.1" id="svg2876" inkscape:version="1.4 (e7c3feb100, 2024-10-09)" sodipodi:docname="24-white.svg" inkscape:export-filename="../24-white.svg" inkscape:export-xdpi="7.52" inkscape:export-ydpi="7.52" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
5
+ <sodipodi:namedview id="namedview2878" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="px" showgrid="true" showguides="true" inkscape:zoom="5.3566812" inkscape:cx="50.030978" inkscape:cy="170.06799" inkscape:window-width="1920" inkscape:window-height="1011" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="layer1">
6
6
  <inkscape:grid type="xygrid" id="grid2997" empspacing="6" originx="0" originy="0" spacingy="0.2645833" spacingx="0.2645833" units="px" visible="true" />
7
7
  <inkscape:grid id="grid1" units="px" originx="0" originy="0" spacingx="9.5249989" spacingy="9.5249989" empcolor="#3f3fff" empopacity="0.25098039" color="#3f3fff" opacity="0.1254902" empspacing="1" dotted="false" gridanglex="30" gridanglez="30" visible="true" />
8
8
  </sodipodi:namedview>
@@ -74,5 +74,6 @@
74
74
  <g id="info" transform="matrix(0.33072916,0,0,0.33072916,-11.906249,-256.10415)" style="stroke-width:0.8">
75
75
  <path id="path3762" style="fill:#f2f2f2;fill-opacity:1;stroke:#999999;stroke-width:0.2;stroke-dasharray:none;stroke-opacity:1" d="m 107.99999,838.36217 a 8,8 0 0 0 -7.999998,8 8,8 0 0 0 7.999998,8 8,8 0 0 0 8,-8 8,8 0 0 0 -8,-8 z m 0,2.5 a 1.5,1.5 0 0 1 1.5,1.5 1.5,1.5 0 0 1 -1.5,1.5 1.5,1.5 0 0 1 -1.5,-1.5 1.5,1.5 0 0 1 1.5,-1.5 z m -1,4.5 h 2 v 6 h -2 z" />
76
76
  </g>
77
+ <path d="m 5.6444435,59.43432 v 1.034991 h 1.0541546 z m 1.1759257,1.612262 h -1.1759257 c -0.3247228,0 -0.5879626,-0.258453 -0.5879626,-0.577271 v -1.154545 h -2.3518514 v 5.195451 h 4.1157397 z m -4.1157397,-2.309089 h 3.0615854 l 1.6421175,1.612259 v 4.160465 c 0,0.318822 -0.2632403,0.577275 -0.5879632,0.577275 h -4.1157397 c -0.3247231,0 -0.5879631,-0.258453 -0.5879631,-0.577275 v -5.195451 c 0,-0.31882 0.26324,-0.577273 0.5879631,-0.577273 z m 1.7638882,3.92135 v -1.323625 h 0.5879632 v 1.323625 l 0.3800861,-0.373177 0.415753,0.408196 -1.0898206,1.070004 -1.0898208,-1.070004 0.4157526,-0.408196 z" fill-rule="evenodd" id="downloadfile" style="fill:#f2f2f2;fill-opacity:1;stroke-width:0.06614583;stroke:#999999;stroke-opacity:0.50980395;stroke-dasharray:none" />
77
78
  </g>
78
79
  </svg>
@@ -1,8 +1,8 @@
1
1
  <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
2
  <!-- Created with Inkscape (http://www.inkscape.org/) -->
3
3
 
4
- <svg width="252" height="252" viewBox="0 0 66.674999 66.674999" version="1.1" id="svg6237" inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)" sodipodi:docname="24.svg" inkscape:export-filename="../24.svg" inkscape:export-xdpi="7.52" inkscape:export-ydpi="7.52" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
5
- <sodipodi:namedview id="namedview6239" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="px" showgrid="true" showguides="true" inkscape:zoom="3.1596124" inkscape:cx="117.10297" inkscape:cy="106.50041" inkscape:window-width="960" inkscape:window-height="1011" inkscape:window-x="20" inkscape:window-y="20" inkscape:window-maximized="0" inkscape:current-layer="layer1">
4
+ <svg width="252" height="252" viewBox="0 0 66.674999 66.674999" version="1.1" id="svg6237" inkscape:version="1.4 (e7c3feb100, 2024-10-09)" sodipodi:docname="24.svg" inkscape:export-filename="../24.svg" inkscape:export-xdpi="7.52" inkscape:export-ydpi="7.52" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
5
+ <sodipodi:namedview id="namedview6239" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="px" showgrid="true" showguides="true" inkscape:zoom="3.1596124" inkscape:cx="118.68544" inkscape:cy="166.47612" inkscape:window-width="1920" inkscape:window-height="1011" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="layer1">
6
6
  <inkscape:grid type="xygrid" id="grid6358" empspacing="6" originx="0" originy="0" spacingy="0.26458333" spacingx="0.26458333" units="px" visible="true" />
7
7
  <inkscape:grid id="grid1" units="px" originx="0" originy="0" spacingx="9.5249999" spacingy="9.5249999" empcolor="#3f3fff" empopacity="0.25098039" color="#ff0000" opacity="0.83529412" empspacing="0" dotted="false" gridanglex="30" gridanglez="30" visible="true" />
8
8
  </sodipodi:namedview>
@@ -98,5 +98,6 @@
98
98
  <g id="info" transform="matrix(0.33072916,0,0,0.33072916,-11.90625,-256.10415)" style="fill:#4d4d4d;fill-opacity:1;stroke-width:0.8">
99
99
  <path id="path3762" style="fill:#4d4d4d;fill-opacity:1;stroke:none;stroke-width:0.8" d="m 108,838.36217 a 8,8 0 0 0 -8,8 8,8 0 0 0 8,8 8,8 0 0 0 8,-8 8,8 0 0 0 -8,-8 z m 0,2.5 a 1.5,1.5 0 0 1 1.5,1.5 1.5,1.5 0 0 1 -1.5,1.5 1.5,1.5 0 0 1 -1.5,-1.5 1.5,1.5 0 0 1 1.5,-1.5 z m -1,4.5 h 2 v 6 h -2 z" />
100
100
  </g>
101
+ <path d="m 5.6444435,59.43432 v 1.034991 h 1.0541546 z m 1.1759257,1.612262 h -1.1759257 c -0.3247228,0 -0.5879626,-0.258453 -0.5879626,-0.577271 v -1.154545 h -2.3518514 v 5.195451 h 4.1157397 z m -4.1157397,-2.309089 h 3.0615854 l 1.6421175,1.612259 v 4.160465 c 0,0.318822 -0.2632403,0.577275 -0.5879632,0.577275 h -4.1157397 c -0.3247231,0 -0.5879631,-0.258453 -0.5879631,-0.577275 v -5.195451 c 0,-0.31882 0.26324,-0.577273 0.5879631,-0.577273 z m 1.7638882,3.92135 v -1.323625 h 0.5879632 v 1.323625 l 0.3800861,-0.373177 0.415753,0.408196 -1.0898206,1.070004 -1.0898208,-1.070004 0.4157526,-0.408196 z" fill-rule="evenodd" id="downloadfile" style="fill:#464646;fill-opacity:1;stroke-width:0.192424" />
101
102
  </g>
102
103
  </svg>
@@ -249,6 +249,10 @@ export class BaseAjax extends BaseAutocomplete {
249
249
  }
250
250
  }
251
251
 
252
+ buildUrl(value) {
253
+ return Util.template(this.url, { q: encodeURIComponent(value) })
254
+ }
255
+
252
256
  async search() {
253
257
  let val = this.input.value
254
258
  if (val.length < this.options.minChar) {
@@ -258,7 +262,7 @@ export class BaseAjax extends BaseAutocomplete {
258
262
  if (val === this.cache) return
259
263
  this.cache = val
260
264
  val = val.toLowerCase()
261
- const url = Util.template(this.url, { q: encodeURIComponent(val) })
265
+ const url = this.buildUrl(val)
262
266
  this.handleResults(await this._search(url))
263
267
  }
264
268
 
@@ -309,7 +313,7 @@ export const SingleMixin = (Base) =>
309
313
  DomEvent.on(close, 'click', () => {
310
314
  this.selectedContainer.innerHTML = ''
311
315
  this.input.style.display = 'block'
312
- this.options.on_unselect(result)
316
+ this.options.on_unselect?.(result)
313
317
  })
314
318
  this.hide()
315
319
  }
@@ -338,7 +342,7 @@ export const MultipleMixin = (Base) =>
338
342
  })
339
343
  DomEvent.on(close, 'click', () => {
340
344
  this.selectedContainer.removeChild(result_el)
341
- this.options.on_unselect(result)
345
+ this.options.on_unselect?.(result)
342
346
  })
343
347
  this.hide()
344
348
  }
@@ -1,6 +1,9 @@
1
1
  import { DomEvent, DomUtil, stamp } from '../../vendors/leaflet/leaflet-src.esm.js'
2
2
  import { translate } from './i18n.js'
3
3
  import * as Icon from './rendering/icon.js'
4
+ import * as Utils from './utils.js'
5
+ import { EXPORT_FORMATS } from './formatter.js'
6
+ import ContextMenu from './ui/contextmenu.js'
4
7
 
5
8
  export default class Browser {
6
9
  constructor(map) {
@@ -32,7 +35,11 @@ export default class Browser {
32
35
  'show-on-edit icon-delete',
33
36
  translate('Delete this feature')
34
37
  )
35
- const colorBox = DomUtil.create('i', 'icon icon-16 feature-color', row)
38
+ const colorBox = DomUtil.create(
39
+ 'i',
40
+ `icon icon-16 icon-${feature.getClassName()} feature-color`,
41
+ row
42
+ )
36
43
  const title = DomUtil.create('span', 'feature-title', row)
37
44
  const symbol = feature._getIconUrl
38
45
  ? Icon.formatUrl(feature._getIconUrl(), feature)
@@ -43,6 +50,8 @@ export default class Browser {
43
50
  if (symbol && symbol !== U.SCHEMA.iconUrl.default) {
44
51
  const icon = Icon.makeElement(symbol, colorBox)
45
52
  Icon.setContrast(icon, colorBox, symbol, bgcolor)
53
+ } else if (DomUtil.contrastedColor(colorBox, bgcolor)) {
54
+ colorBox.classList.add('icon-white')
46
55
  }
47
56
  const viewFeature = (e) => {
48
57
  feature.zoomTo({ ...e, callback: () => feature.view() })
@@ -92,6 +101,7 @@ export default class Browser {
92
101
  datalayer.eachFeature((feature) => this.addFeature(feature, container))
93
102
 
94
103
  const total = datalayer.count()
104
+ if (!total) return
95
105
  const current = container.querySelectorAll('li').length
96
106
  const count = total === current ? total : `${current}/${total}`
97
107
  const counter = DomUtil.create('span', 'datalayer-counter', headline)
@@ -158,6 +168,7 @@ export default class Browser {
158
168
  })
159
169
  this.filtersTitle = container.querySelector('summary')
160
170
  this.toggleBadge()
171
+ this.addMainToolbox(container)
161
172
  this.dataContainer = DomUtil.create('div', '', container)
162
173
 
163
174
  let fields = [
@@ -209,6 +220,48 @@ export default class Browser {
209
220
  }
210
221
  }
211
222
 
223
+ addMainToolbox(container) {
224
+ const [toolbox, { toggle, fitBounds, download }] = Utils.loadTemplateWithRefs(`
225
+ <div class="main-toolbox">
226
+ <i class="icon icon-16 icon-eye" title="${translate('show/hide all layers')}" data-ref="toggle"></i>
227
+ <i class="icon icon-16 icon-zoom" title="${translate('zoom to data extent')}" data-ref="fitBounds"></i>
228
+ <i class="icon icon-16 icon-download" title="${translate('download visible data')}" data-ref="download"></i>
229
+ </div>
230
+ `)
231
+ container.appendChild(toolbox)
232
+ toggle.addEventListener('click', () => this.toggleLayers())
233
+ fitBounds.addEventListener('click', () => this.map.fitDataBounds())
234
+ download.addEventListener('click', () => this.downloadVisible(download))
235
+ }
236
+
237
+ downloadVisible(element) {
238
+ const menu = new ContextMenu({ fixed: true })
239
+ const items = []
240
+ for (const format of Object.keys(EXPORT_FORMATS)) {
241
+ items.push({
242
+ label: format,
243
+ action: () => this.map.share.download(format),
244
+ })
245
+ }
246
+ menu.openBelow(element, items)
247
+ }
248
+
249
+ toggleLayers() {
250
+ // If at least one layer is shown, hide it
251
+ // otherwise show all
252
+ let allHidden = true
253
+ this.map.eachBrowsableDataLayer((datalayer) => {
254
+ if (datalayer.isVisible()) allHidden = false
255
+ })
256
+ this.map.eachBrowsableDataLayer((datalayer) => {
257
+ if (allHidden) {
258
+ datalayer.show()
259
+ } else {
260
+ if (datalayer.isVisible()) datalayer.hide()
261
+ }
262
+ })
263
+ }
264
+
212
265
  static backButton(map) {
213
266
  const button = DomUtil.createButtonIcon(
214
267
  DomUtil.create('li', '', undefined),
@@ -19,7 +19,12 @@ export default class Caption {
19
19
  open() {
20
20
  const container = DomUtil.create('div', 'umap-caption')
21
21
  const hgroup = DomUtil.element({ tagName: 'hgroup', parent: container })
22
- DomUtil.createTitle(hgroup, this.map.options.name, 'icon-caption icon-block')
22
+ DomUtil.createTitle(
23
+ hgroup,
24
+ this.map.getDisplayName(),
25
+ 'icon-caption icon-block',
26
+ 'map-name'
27
+ )
23
28
  this.map.addAuthorLink('h4', hgroup)
24
29
  if (this.map.options.description) {
25
30
  const description = DomUtil.element({
@@ -35,15 +40,21 @@ export default class Caption {
35
40
  )
36
41
  const creditsContainer = DomUtil.create('div', 'credits-container', container)
37
42
  this.addCredits(creditsContainer)
38
- this.map.panel.open({ content: container })
43
+ this.map.panel.open({ content: container }).then(() => {
44
+ // Create the legend when the panel is actually on the DOM
45
+ this.map.eachDataLayerReverse((datalayer) => datalayer.renderLegend())
46
+ })
39
47
  }
40
48
 
41
49
  addDataLayer(datalayer, container) {
42
50
  if (!datalayer.options.inCaption) return
43
- const p = DomUtil.create('p', 'datalayer-legend', container)
44
- const legend = DomUtil.create('span', '', p)
51
+ const p = DomUtil.create(
52
+ 'p',
53
+ `caption-item ${datalayer.cssId}`,
54
+ container
55
+ )
56
+ const legend = DomUtil.create('span', 'datalayer-legend', p)
45
57
  const headline = DomUtil.create('strong', '', p)
46
- datalayer.renderLegend(legend)
47
58
  if (datalayer.options.description) {
48
59
  DomUtil.element({
49
60
  tagName: 'span',
@@ -103,11 +103,11 @@ class Feature {
103
103
  }
104
104
 
105
105
  pushGeometry() {
106
- this.ui.setLatLngs(this.toLatLngs())
106
+ this._setLatLngs(this.toLatLngs())
107
107
  }
108
108
 
109
109
  pullGeometry(sync = true) {
110
- this.fromLatLngs(this.ui.getLatLngs())
110
+ this.fromLatLngs(this._getLatLngs())
111
111
  if (sync) {
112
112
  this.sync.update('geometry', this.geometry)
113
113
  }
@@ -596,6 +596,55 @@ class Feature {
596
596
  }
597
597
  }
598
598
  }
599
+
600
+ getContextMenuItems(event) {
601
+ const permalink = this.getPermalink()
602
+ let items = []
603
+ if (permalink) {
604
+ items.push({
605
+ label: translate('Permalink'),
606
+ action: () => {
607
+ window.open(permalink)
608
+ },
609
+ })
610
+ }
611
+ items.push({
612
+ label: translate('Copy as GeoJSON'),
613
+ action: () => {
614
+ L.Util.copyToClipboard(JSON.stringify(this.toGeoJSON()))
615
+ this.map.tooltip.open({ content: L._('✅ Copied!') })
616
+ },
617
+ })
618
+ if (this.map.editEnabled && !this.isReadOnly()) {
619
+ items = items.concat(this.getContextMenuEditItems(event))
620
+ }
621
+ return items
622
+ }
623
+
624
+ getContextMenuEditItems() {
625
+ let items = ['-']
626
+ if (this.map.editedFeature !== this) {
627
+ items.push({
628
+ label: `${translate('Edit this feature')} (⇧+Click)`,
629
+ action: () => this.edit(),
630
+ })
631
+ }
632
+ items = items.concat(
633
+ {
634
+ label: this.map.help.displayLabel('EDIT_FEATURE_LAYER'),
635
+ action: () => this.datalayer.edit(),
636
+ },
637
+ {
638
+ label: translate('Delete this feature'),
639
+ action: () => this.confirmDelete(),
640
+ },
641
+ {
642
+ label: translate('Clone this feature'),
643
+ action: () => this.clone(),
644
+ }
645
+ )
646
+ return items
647
+ }
599
648
  }
600
649
 
601
650
  export class Point extends Feature {
@@ -607,6 +656,14 @@ export class Point extends Feature {
607
656
  }
608
657
  }
609
658
 
659
+ _getLatLngs() {
660
+ return this.ui.getLatLng()
661
+ }
662
+
663
+ _setLatLngs(latlng) {
664
+ this.ui.setLatLng(latlng)
665
+ }
666
+
610
667
  toLatLngs() {
611
668
  return GeoJSON.coordsToLatLng(this.coordinates)
612
669
  }
@@ -655,6 +712,7 @@ export class Point extends Feature {
655
712
  builder.restoreField('ui._latlng.lat')
656
713
  builder.restoreField('ui._latlng.lng')
657
714
  }
715
+ this.pullGeometry()
658
716
  this.zoomTo({ easing: false })
659
717
  },
660
718
  })
@@ -677,6 +735,14 @@ class Path extends Feature {
677
735
  return !this.isEmpty()
678
736
  }
679
737
 
738
+ _getLatLngs() {
739
+ return this.ui.getLatLngs()
740
+ }
741
+
742
+ _setLatLngs(latlngs) {
743
+ this.ui.setLatLngs(latlngs)
744
+ }
745
+
680
746
  connectToDataLayer(datalayer) {
681
747
  super.connectToDataLayer(datalayer)
682
748
  // We keep markers on their own layer on top of the paths.
@@ -767,6 +833,62 @@ class Path extends Feature {
767
833
  }
768
834
  if (callback) callback.call(this)
769
835
  }
836
+
837
+ getContextMenuItems(event) {
838
+ const items = super.getContextMenuItems(event)
839
+ items.push({
840
+ label: translate('Display measure'),
841
+ action: () => Alert.info(this.ui.getMeasure()),
842
+ })
843
+ if (this.map.editEnabled && !this.isReadOnly() && this.isMulti()) {
844
+ items.push(...this.getContextMenuMultiItems(event))
845
+ }
846
+ return items
847
+ }
848
+
849
+ getContextMenuMultiItems(event) {
850
+ const items = [
851
+ '-',
852
+ {
853
+ label: translate('Remove shape from the multi'),
854
+ action: () => {
855
+ this.ui.enableEdit().deleteShapeAt(event.latlng)
856
+ },
857
+ },
858
+ ]
859
+ const shape = this.ui.shapeAt(event.latlng)
860
+ if (this.ui._latlngs.indexOf(shape) > 0) {
861
+ items.push({
862
+ label: translate('Make main shape'),
863
+ action: () => {
864
+ this.ui.enableEdit().deleteShape(shape)
865
+ this.ui.editor.prependShape(shape)
866
+ },
867
+ })
868
+ }
869
+ return items
870
+ }
871
+
872
+ getContextMenuEditItems(event) {
873
+ const items = super.getContextMenuEditItems(event)
874
+ if (this.map?.editedFeature !== this && this.isSameClass(this.map.editedFeature)) {
875
+ items.push({
876
+ label: translate('Transfer shape to edited feature'),
877
+ action: () => {
878
+ this.transferShape(event.latlng, this.map.editedFeature)
879
+ },
880
+ })
881
+ }
882
+ if (this.isMulti()) {
883
+ items.push({
884
+ label: translate('Extract shape to separate feature'),
885
+ action: () => {
886
+ this.ui.isolateShape(event.latlng)
887
+ },
888
+ })
889
+ }
890
+ return items
891
+ }
770
892
  }
771
893
 
772
894
  export class LineString extends Path {
@@ -881,6 +1003,41 @@ export class LineString extends Path {
881
1003
  isMulti() {
882
1004
  return !LineUtil.isFlat(this.coordinates) && this.coordinates.length > 1
883
1005
  }
1006
+
1007
+ getContextMenuEditItems(event) {
1008
+ const items = super.getContextMenuEditItems(event)
1009
+ const vertexClicked = event.vertex
1010
+ if (!this.isMulti()) {
1011
+ items.push({
1012
+ label: translate('Transform to polygon'),
1013
+ action: () => this.toPolygon(),
1014
+ })
1015
+ }
1016
+ if (vertexClicked) {
1017
+ const index = event.vertex.getIndex()
1018
+ if (index !== 0 && index !== event.vertex.getLastIndex()) {
1019
+ items.push({
1020
+ label: translate('Split line'),
1021
+ action: () => event.vertex.split(),
1022
+ })
1023
+ } else if (index === 0 || index === event.vertex.getLastIndex()) {
1024
+ items.push({
1025
+ label: this.map.help.displayLabel('CONTINUE_LINE'),
1026
+ action: () => event.vertex.continue(),
1027
+ })
1028
+ }
1029
+ }
1030
+ return items
1031
+ }
1032
+
1033
+ getContextMenuMultiItems(event) {
1034
+ const items = super.getContextMenuMultiItems(event)
1035
+ items.push({
1036
+ label: translate('Merge lines'),
1037
+ action: () => this.mergeShapes(),
1038
+ })
1039
+ return items
1040
+ }
884
1041
  }
885
1042
 
886
1043
  export class Polygon extends Path {
@@ -991,4 +1148,21 @@ export class Polygon extends Path {
991
1148
  items.push(U.CreateHoleAction)
992
1149
  return items
993
1150
  }
1151
+
1152
+ getContextMenuEditItems(event) {
1153
+ const items = super.getContextMenuEditItems(event)
1154
+ const shape = this.ui.shapeAt(event.latlng)
1155
+ // No multi and no holes.
1156
+ if (shape && !this.isMulti() && (LineUtil.isFlat(shape) || shape.length === 1)) {
1157
+ items.push({
1158
+ label: translate('Transform to lines'),
1159
+ action: () => this.toLineString(),
1160
+ })
1161
+ }
1162
+ items.push({
1163
+ label: translate('Start a hole here'),
1164
+ action: () => this.ui.startHole(event),
1165
+ })
1166
+ return items
1167
+ }
994
1168
  }
@@ -92,12 +92,12 @@ export class DataLayer {
92
92
  set isDirty(status) {
93
93
  this._isDirty = status
94
94
  if (status) {
95
- this.map.addDirtyDatalayer(this)
95
+ this.map.isDirty = true
96
96
  // A layer can be made dirty by indirect action (like dragging layers)
97
97
  // we need to have it loaded before saving it.
98
98
  if (!this.isLoaded()) this.fetchData()
99
99
  } else {
100
- this.map.removeDirtyDatalayer(this)
100
+ this.map.checkDirty()
101
101
  this.isDeleted = false
102
102
  }
103
103
  }
@@ -115,6 +115,10 @@ export class DataLayer {
115
115
  return this._isDeleted
116
116
  }
117
117
 
118
+ get cssId() {
119
+ return `datalayer-${stamp(this)}`
120
+ }
121
+
118
122
  getSyncMetadata() {
119
123
  return {
120
124
  subject: 'datalayer',
@@ -235,6 +239,7 @@ export class DataLayer {
235
239
  }
236
240
 
237
241
  dataChanged() {
242
+ if (!this.hasDataLoaded()) return
238
243
  this.map.onDataLayersChanged()
239
244
  this.layer.dataChanged()
240
245
  }
@@ -242,10 +247,15 @@ export class DataLayer {
242
247
  fromGeoJSON(geojson, sync = true) {
243
248
  this.addData(geojson, sync)
244
249
  this._geojson = geojson
245
- this._dataloaded = true
250
+ this.onDataLoaded()
246
251
  this.dataChanged()
247
252
  }
248
253
 
254
+ onDataLoaded() {
255
+ this._dataloaded = true
256
+ this.renderLegend()
257
+ }
258
+
249
259
  async fromUmapGeoJSON(geojson) {
250
260
  if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat
251
261
  if (geojson._umap_options) this.setOptions(geojson._umap_options)
@@ -339,11 +349,11 @@ export class DataLayer {
339
349
  const id = stamp(this)
340
350
  if (!this.map.datalayers[id]) {
341
351
  this.map.datalayers[id] = this
342
- if (!this.map.datalayers_index.includes(this)) {
343
- this.map.datalayers_index.push(this)
344
- }
345
- this.map.onDataLayersChanged()
346
352
  }
353
+ if (!this.map.datalayers_index.includes(this)) {
354
+ this.map.datalayers_index.push(this)
355
+ }
356
+ this.map.onDataLayersChanged()
347
357
  }
348
358
 
349
359
  _dataUrl() {
@@ -384,7 +394,7 @@ export class DataLayer {
384
394
  this.indexProperties(feature)
385
395
  this.map.features_index[feature.getSlug()] = feature
386
396
  this.showFeature(feature)
387
- if (this.hasDataLoaded()) this.dataChanged()
397
+ this.dataChanged()
388
398
  }
389
399
 
390
400
  removeFeature(feature, sync) {
@@ -395,7 +405,7 @@ export class DataLayer {
395
405
  feature.disconnectFromDataLayer(this)
396
406
  this._index.splice(this._index.indexOf(id), 1)
397
407
  delete this._features[id]
398
- if (this.hasDataLoaded() && this.isVisible()) this.dataChanged()
408
+ if (this.isVisible()) this.dataChanged()
399
409
  }
400
410
 
401
411
  indexProperties(feature) {
@@ -559,7 +569,6 @@ export class DataLayer {
559
569
 
560
570
  erase() {
561
571
  this.hide()
562
- delete this.map.datalayers[stamp(this)]
563
572
  this.map.datalayers_index.splice(this.getRank(), 1)
564
573
  this.parentPane.removeChild(this.pane)
565
574
  this.map.onDataLayersChanged()
@@ -716,7 +725,7 @@ export class DataLayer {
716
725
  const remoteDataFields = [
717
726
  [
718
727
  'options.remoteData.url',
719
- { handler: 'Url', label: translate('Url'), helpEntries: 'formatURL' },
728
+ { handler: 'Url', label: translate('Url'), helpEntries: ['formatURL'] },
720
729
  ],
721
730
  [
722
731
  'options.remoteData.format',
@@ -729,7 +738,7 @@ export class DataLayer {
729
738
  {
730
739
  handler: 'Switch',
731
740
  label: translate('Dynamic'),
732
- helpEntries: 'dynamicRemoteData',
741
+ helpEntries: ['dynamicRemoteData'],
733
742
  },
734
743
  ],
735
744
  [
@@ -746,7 +755,7 @@ export class DataLayer {
746
755
  {
747
756
  handler: 'Switch',
748
757
  label: translate('Proxy request'),
749
- helpEntries: 'proxyRemoteData',
758
+ helpEntries: ['proxyRemoteData'],
750
759
  },
751
760
  ])
752
761
  remoteDataFields.push('options.remoteData.ttl')
@@ -803,13 +812,12 @@ export class DataLayer {
803
812
  this
804
813
  )
805
814
  if (this.umap_id) {
806
- const download = DomUtil.createLink(
807
- 'button umap-download',
808
- advancedButtons,
809
- translate('Download'),
810
- this._dataUrl(),
811
- '_blank'
812
- )
815
+ const filename = `${Utils.slugify(this.options.name)}.geojson`
816
+ const download = Utils.loadTemplate(`
817
+ <a class="button" href="${this._dataUrl()}" download="${filename}">
818
+ <i class="icon icon-24 icon-download"></i>${translate('Download')}
819
+ </a>`)
820
+ advancedButtons.appendChild(download)
813
821
  }
814
822
  const backButton = DomUtil.createButtonIcon(
815
823
  undefined,
@@ -874,18 +882,21 @@ export class DataLayer {
874
882
 
875
883
  async restore(version) {
876
884
  if (!this.map.editEnabled) return
877
- if (!confirm(translate('Are you sure you want to restore this version?'))) return
878
- const [geojson, response, error] = await this.map.server.get(
879
- this.getVersionUrl(version)
880
- )
881
- if (!error) {
882
- if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat.
883
- if (geojson._umap_options) this.setOptions(geojson._umap_options)
884
- this.empty()
885
- if (this.isRemoteLayer()) this.fetchRemoteData()
886
- else this.addData(geojson)
887
- this.isDirty = true
888
- }
885
+ this.map.dialog
886
+ .confirm(translate('Are you sure you want to restore this version?'))
887
+ .then(async () => {
888
+ const [geojson, response, error] = await this.map.server.get(
889
+ this.getVersionUrl(version)
890
+ )
891
+ if (!error) {
892
+ if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat.
893
+ if (geojson._umap_options) this.setOptions(geojson._umap_options)
894
+ this.empty()
895
+ if (this.isRemoteLayer()) this.fetchRemoteData()
896
+ else this.addData(geojson)
897
+ this.isDirty = true
898
+ }
899
+ })
889
900
  }
890
901
 
891
902
  featuresToGeoJSON() {
@@ -1026,7 +1037,7 @@ export class DataLayer {
1026
1037
  }
1027
1038
 
1028
1039
  async save() {
1029
- if (this.isDeleted) return this.saveDelete()
1040
+ if (this.isDeleted) return await this.saveDelete()
1030
1041
  if (!this.isLoaded()) {
1031
1042
  return
1032
1043
  }
@@ -1090,8 +1101,8 @@ export class DataLayer {
1090
1101
  if (this.umap_id) {
1091
1102
  await this.map.server.post(this.getDeleteUrl())
1092
1103
  }
1104
+ delete this.map.datalayers[stamp(this)]
1093
1105
  this.isDirty = false
1094
- this.map.continueSaving()
1095
1106
  }
1096
1107
 
1097
1108
  getMap() {
@@ -1118,10 +1129,13 @@ export class DataLayer {
1118
1129
  return 'displayName'
1119
1130
  }
1120
1131
 
1121
- renderLegend(container) {
1122
- if (this.layer.renderLegend) return this.layer.renderLegend(container)
1123
- const color = DomUtil.create('span', 'datalayer-color', container)
1124
- color.style.backgroundColor = this.getColor()
1132
+ renderLegend() {
1133
+ for (const container of document.querySelectorAll(`.${this.cssId} .datalayer-legend`)) {
1134
+ container.innerHTML = ''
1135
+ if (this.layer.renderLegend) return this.layer.renderLegend(container)
1136
+ const color = DomUtil.create('span', 'datalayer-color', container)
1137
+ color.style.backgroundColor = this.getColor()
1138
+ }
1125
1139
  }
1126
1140
 
1127
1141
  renderToolbox(container) {
@@ -1160,8 +1174,11 @@ export class DataLayer {
1160
1174
  'click',
1161
1175
  function () {
1162
1176
  if (!this.isVisible()) return
1163
- if (!confirm(translate('Are you sure you want to delete this layer?'))) return
1164
- this._delete()
1177
+ this.map.dialog
1178
+ .confirm(translate('Are you sure you want to delete this layer?'))
1179
+ .then(() => {
1180
+ this._delete()
1181
+ })
1165
1182
  },
1166
1183
  this
1167
1184
  )