umap-project 2.7.0b2__py3-none-any.whl → 2.7.1__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 (71) hide show
  1. umap/__init__.py +1 -1
  2. umap/asgi.py +15 -0
  3. umap/locale/es/LC_MESSAGES/django.mo +0 -0
  4. umap/locale/es/LC_MESSAGES/django.po +134 -128
  5. umap/models.py +2 -1
  6. umap/settings/base.py +1 -1
  7. umap/static/umap/css/importers.css +4 -0
  8. umap/static/umap/img/16.svg +1 -184
  9. umap/static/umap/img/24-white.svg +1 -0
  10. umap/static/umap/img/24.svg +1 -0
  11. umap/static/umap/img/importers/cadastrefr.svg +23 -0
  12. umap/static/umap/img/source/16.svg +753 -200
  13. umap/static/umap/img/source/24-white.svg +3 -2
  14. umap/static/umap/img/source/24.svg +3 -2
  15. umap/static/umap/js/modules/browser.js +47 -0
  16. umap/static/umap/js/modules/caption.js +10 -4
  17. umap/static/umap/js/modules/data/layer.js +26 -14
  18. umap/static/umap/js/modules/importer.js +3 -0
  19. umap/static/umap/js/modules/importers/cadastrefr.js +62 -0
  20. umap/static/umap/js/modules/importers/communesfr.js +2 -2
  21. umap/static/umap/js/modules/rendering/layers/classified.js +2 -0
  22. umap/static/umap/js/modules/rendering/ui.js +30 -3
  23. umap/static/umap/js/modules/share.js +1 -3
  24. umap/static/umap/js/modules/slideshow.js +1 -1
  25. umap/static/umap/js/modules/sync/engine.js +14 -8
  26. umap/static/umap/js/modules/sync/hlc.js +3 -3
  27. umap/static/umap/js/modules/sync/updaters.js +14 -2
  28. umap/static/umap/js/modules/ui/contextmenu.js +5 -0
  29. umap/static/umap/js/modules/ui/panel.js +12 -1
  30. umap/static/umap/js/modules/utils.js +24 -4
  31. umap/static/umap/js/umap.controls.js +46 -21
  32. umap/static/umap/js/umap.core.js +1 -1
  33. umap/static/umap/js/umap.js +29 -20
  34. umap/static/umap/locale/ca.js +8 -4
  35. umap/static/umap/locale/ca.json +8 -4
  36. umap/static/umap/locale/en.js +5 -1
  37. umap/static/umap/locale/en.json +5 -1
  38. umap/static/umap/locale/es.js +330 -319
  39. umap/static/umap/locale/es.json +330 -319
  40. umap/static/umap/locale/fr.js +5 -1
  41. umap/static/umap/locale/fr.json +5 -1
  42. umap/static/umap/map.css +37 -7
  43. umap/static/umap/unittests/hlc.js +10 -3
  44. umap/static/umap/unittests/utils.js +24 -0
  45. umap/static/umap/vars.css +2 -1
  46. umap/static/umap/vendors/colorbrewer/colorbrewer.js +309 -317
  47. umap/static/umap/vendors/dompurify/purify.es.js +15 -16
  48. umap/static/umap/vendors/dompurify/purify.es.mjs.map +1 -1
  49. umap/static/umap/vendors/locatecontrol/L.Control.Locate.min.js +2 -2
  50. umap/static/umap/vendors/locatecontrol/L.Control.Locate.min.js.map +1 -1
  51. umap/static/umap/vendors/simple-statistics/simple-statistics.min.js +1 -1
  52. umap/static/umap/vendors/simple-statistics/simple-statistics.min.js.map +1 -1
  53. umap/templates/umap/js.html +0 -1
  54. umap/templates/umap/map_detail.html +2 -2
  55. umap/tests/fixtures/test_upload_data.csv +2 -2
  56. umap/tests/integration/test_browser.py +69 -7
  57. umap/tests/integration/test_caption.py +3 -3
  58. umap/tests/integration/test_datalayer.py +1 -5
  59. umap/tests/integration/test_edit_datalayer.py +1 -2
  60. umap/tests/integration/test_edit_map.py +1 -1
  61. umap/tests/integration/test_edit_marker.py +1 -1
  62. umap/tests/integration/test_facets_browser.py +3 -3
  63. umap/tests/integration/test_import.py +0 -4
  64. umap/tests/integration/test_map.py +0 -4
  65. umap/tests/integration/test_view_marker.py +63 -0
  66. umap/tests/test_map_views.py +19 -0
  67. {umap_project-2.7.0b2.dist-info → umap_project-2.7.1.dist-info}/METADATA +14 -10
  68. {umap_project-2.7.0b2.dist-info → umap_project-2.7.1.dist-info}/RECORD +71 -68
  69. {umap_project-2.7.0b2.dist-info → umap_project-2.7.1.dist-info}/WHEEL +0 -0
  70. {umap_project-2.7.0b2.dist-info → umap_project-2.7.1.dist-info}/entry_points.txt +0 -0
  71. {umap_project-2.7.0b2.dist-info → umap_project-2.7.1.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>
@@ -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) {
@@ -98,6 +101,7 @@ export default class Browser {
98
101
  datalayer.eachFeature((feature) => this.addFeature(feature, container))
99
102
 
100
103
  const total = datalayer.count()
104
+ if (!total) return
101
105
  const current = container.querySelectorAll('li').length
102
106
  const count = total === current ? total : `${current}/${total}`
103
107
  const counter = DomUtil.create('span', 'datalayer-counter', headline)
@@ -164,6 +168,7 @@ export default class Browser {
164
168
  })
165
169
  this.filtersTitle = container.querySelector('summary')
166
170
  this.toggleBadge()
171
+ this.addMainToolbox(container)
167
172
  this.dataContainer = DomUtil.create('div', '', container)
168
173
 
169
174
  let fields = [
@@ -215,6 +220,48 @@ export default class Browser {
215
220
  }
216
221
  }
217
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
+
218
265
  static backButton(map) {
219
266
  const button = DomUtil.createButtonIcon(
220
267
  DomUtil.create('li', '', undefined),
@@ -40,15 +40,21 @@ export default class Caption {
40
40
  )
41
41
  const creditsContainer = DomUtil.create('div', 'credits-container', container)
42
42
  this.addCredits(creditsContainer)
43
- 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
+ })
44
47
  }
45
48
 
46
49
  addDataLayer(datalayer, container) {
47
50
  if (!datalayer.options.inCaption) return
48
- const p = DomUtil.create('p', 'datalayer-legend', container)
49
- 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)
50
57
  const headline = DomUtil.create('strong', '', p)
51
- datalayer.renderLegend(legend)
52
58
  if (datalayer.options.description) {
53
59
  DomUtil.element({
54
60
  tagName: 'span',
@@ -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)
@@ -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) {
@@ -802,13 +812,12 @@ export class DataLayer {
802
812
  this
803
813
  )
804
814
  if (this.umap_id) {
805
- const download = DomUtil.createLink(
806
- 'button umap-download',
807
- advancedButtons,
808
- translate('Download'),
809
- this._dataUrl(),
810
- '_blank'
811
- )
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)
812
821
  }
813
822
  const backButton = DomUtil.createButtonIcon(
814
823
  undefined,
@@ -1120,10 +1129,13 @@ export class DataLayer {
1120
1129
  return 'displayName'
1121
1130
  }
1122
1131
 
1123
- renderLegend(container) {
1124
- if (this.layer.renderLegend) return this.layer.renderLegend(container)
1125
- const color = DomUtil.create('span', 'datalayer-color', container)
1126
- 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
+ }
1127
1139
  }
1128
1140
 
1129
1141
  renderToolbox(container) {
@@ -69,6 +69,9 @@ export default class Importer {
69
69
  case 'communesfr':
70
70
  import('./importers/communesfr.js').then(register)
71
71
  break
72
+ case 'cadastrefr':
73
+ import('./importers/cadastrefr.js').then(register)
74
+ break
72
75
  case 'overpass':
73
76
  import('./importers/overpass.js').then(register)
74
77
  break
@@ -0,0 +1,62 @@
1
+ import { DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js'
2
+ import { BaseAjax, SingleMixin } from '../autocomplete.js'
3
+ import * as Util from '../utils.js'
4
+ import { AutocompleteCommunes } from './communesfr.js'
5
+
6
+ const TEMPLATE = `
7
+ <h3>Cadastre</h3>
8
+ <p>Importer les données cadastrales d’une commune française.</p>
9
+ <select name="theme">
10
+ <option value="batiments">Bâtiments</option>
11
+ <option value="communes">Communes</option>
12
+ <option value="feuilles">Feuilles</option>
13
+ <option value="lieux_dits">Lieux dits</option>
14
+ <option value="parcelles" selected>Parcelles</option>
15
+ <option value="prefixes_sections">Préfixes sections</option>
16
+ <option value="sections">Sections</option>
17
+ <option value="subdivisions_fiscales">Subdivisions fiscales</option>
18
+ </select>
19
+ <label id="boundary">
20
+ </label>
21
+ `
22
+
23
+ export class Importer {
24
+ constructor(map, options) {
25
+ this.name = options.name || 'Cadastre'
26
+ this.id = 'cadastrefr'
27
+ }
28
+
29
+ async open(importer) {
30
+ let boundary = null
31
+ let boundaryName = null
32
+ const container = DomUtil.create('div')
33
+ container.innerHTML = TEMPLATE
34
+ const select = container.querySelector('select')
35
+ const options = {
36
+ placeholder: 'Nom ou code INSEE…',
37
+ url: 'https://geo.api.gouv.fr/communes?nom={q}&limit=5',
38
+ on_select: (choice) => {
39
+ boundary = choice.item.value
40
+ boundaryName = choice.item.label
41
+ },
42
+ }
43
+ this.autocomplete = new AutocompleteCommunes(container, options)
44
+
45
+ const confirm = (form) => {
46
+ if (!boundary || !form.theme) {
47
+ Alert.error(translate('Please choose a theme and a boundary first.'))
48
+ return
49
+ }
50
+ importer.url = `https://cadastre.data.gouv.fr/bundler/cadastre-etalab/communes/${boundary}/geojson/${form.theme}`
51
+ importer.format = 'geojson'
52
+ importer.layerName = `${boundaryName} — ${select.options[select.selectedIndex].textContent}`
53
+ }
54
+
55
+ importer.dialog
56
+ .open({
57
+ template: container,
58
+ className: `${this.id} importer dark`,
59
+ })
60
+ .then(confirm)
61
+ }
62
+ }
@@ -2,7 +2,7 @@ import { DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js'
2
2
  import { BaseAjax, SingleMixin } from '../autocomplete.js'
3
3
  import * as Util from '../utils.js'
4
4
 
5
- class Autocomplete extends SingleMixin(BaseAjax) {
5
+ export class AutocompleteCommunes extends SingleMixin(BaseAjax) {
6
6
  createResult(item) {
7
7
  return super.createResult({
8
8
  value: item.code,
@@ -46,7 +46,7 @@ export class Importer {
46
46
  importer.dialog.close()
47
47
  },
48
48
  }
49
- this.autocomplete = new Autocomplete(container, options)
49
+ this.autocomplete = new AutocompleteCommunes(container, options)
50
50
 
51
51
  importer.dialog.open({
52
52
  template: container,
@@ -3,6 +3,7 @@ import { translate } from '../../i18n.js'
3
3
  import { LayerMixin } from './base.js'
4
4
  import * as Utils from '../../utils.js'
5
5
  import { CircleMarker } from '../ui.js'
6
+ import colorbrewer from '../../../../vendors/colorbrewer/colorbrewer.js'
6
7
 
7
8
  // Layer where each feature color is relative to the others,
8
9
  // so we need all features before behing able to set one
@@ -74,6 +75,7 @@ const ClassifiedMixin = {
74
75
  },
75
76
 
76
77
  renderLegend: function (container) {
78
+ if (!this.datalayer.hasDataLoaded()) return
77
79
  const parent = DomUtil.create('ul', '', container)
78
80
  const items = this.getLegendItems()
79
81
  for (const [color, label] of items) {
@@ -32,10 +32,18 @@ const FeatureMixin = {
32
32
  if (map.editedFeature === this.feature) {
33
33
  this.feature._marked_for_deletion = true
34
34
  this.feature.endEdit()
35
- map.editPanel.close()
35
+ if (map.editedFeature === this.feature) {
36
+ map.editPanel.close()
37
+ }
36
38
  }
37
39
  },
38
40
 
41
+ _removeIcon: function () {
42
+ // It may not be in the DOM, and Leaflet does not deal with this
43
+ // situation
44
+ if (this._icon) Marker.prototype._removeIcon.call(this)
45
+ },
46
+
39
47
  addInteractions: function () {
40
48
  this.on('contextmenu editable:vertex:contextmenu', this.onContextMenu)
41
49
  this.on('click', this.onClick)
@@ -86,8 +94,9 @@ const FeatureMixin = {
86
94
 
87
95
  onContextMenu: function (event) {
88
96
  DomEvent.stop(event)
89
- const items = this._map.getContextMenuItems(event)
90
- items.push('-', ...this.feature.getContextMenuItems(event))
97
+ const items = this.feature
98
+ .getContextMenuItems(event)
99
+ .concat(this._map.getContextMenuItems(event))
91
100
  this._map.contextmenu.open(event.originalEvent, items)
92
101
  },
93
102
 
@@ -160,6 +169,12 @@ export const LeafletMarker = Marker.extend({
160
169
  return this.setLatLng(latlng)
161
170
  },
162
171
 
172
+ getEvents: function () {
173
+ const events = Marker.prototype.getEvents.call(this)
174
+ events.moveend = this.onMoveEnd
175
+ return events
176
+ },
177
+
163
178
  addInteractions() {
164
179
  PointMixin.addInteractions.call(this)
165
180
  this._popupHandlersAdded = true // prevent Leaflet from binding event on bindPopup
@@ -167,7 +182,19 @@ export const LeafletMarker = Marker.extend({
167
182
  this.on('popupclose', this.resetHighlight)
168
183
  },
169
184
 
185
+ onMoveEnd: function () {
186
+ this._initIcon()
187
+ this.update()
188
+ },
189
+
170
190
  _initIcon: function () {
191
+ if (!this._map.getBounds().contains(this.getCenter())) {
192
+ if (this._icon) this._removeIcon()
193
+ if (this._tooltip && this.isTooltipOpen()) {
194
+ this.unbindTooltip()
195
+ }
196
+ return
197
+ }
171
198
  this.options.icon = this.getIcon()
172
199
  Marker.prototype._initIcon.call(this)
173
200
  // Allow to run code when icon is actually part of the DOM
@@ -142,9 +142,7 @@ export default class Share {
142
142
  async format(mode) {
143
143
  const type = EXPORT_FORMATS[mode]
144
144
  const content = await type.formatter(this.map)
145
- let name = this.map.options.name || 'data'
146
- name = name.replace(/[^a-z0-9]/gi, '_').toLowerCase()
147
- const filename = name + type.ext
145
+ const filename = Utils.slugify(this.map.options.name) + type.ext
148
146
  return { content, filetype: type.filetype, filename }
149
147
  }
150
148
 
@@ -1,5 +1,5 @@
1
- import { WithTemplate } from './utils.js'
2
1
  import { translate } from './i18n.js'
2
+ import { WithTemplate } from './utils.js'
3
3
 
4
4
  const TOOLBOX_TEMPLATE = `
5
5
  <ul class="umap-slideshow-toolbox dark">
@@ -1,7 +1,7 @@
1
+ import * as Utils from '../utils.js'
2
+ import { HybridLogicalClock } from './hlc.js'
1
3
  import { DataLayerUpdater, FeatureUpdater, MapUpdater } from './updaters.js'
2
4
  import { WebSocketTransport } from './websocket.js'
3
- import { HybridLogicalClock } from './hlc.js'
4
- import * as Utils from '../utils.js'
5
5
 
6
6
  /**
7
7
  * The syncEngine exposes an API to sync messages between peers over the network.
@@ -81,7 +81,7 @@ export class SyncEngine {
81
81
  }
82
82
 
83
83
  _send(inputMessage) {
84
- let message = this._operations.addLocal(inputMessage)
84
+ const message = this._operations.addLocal(inputMessage)
85
85
 
86
86
  if (this.offline) return
87
87
  if (this.transport) {
@@ -101,6 +101,11 @@ export class SyncEngine {
101
101
  updater.applyMessage(operation)
102
102
  }
103
103
 
104
+ getNumberOfConnectedPeers() {
105
+ if (this.peers) return this.peers.length
106
+ return 0
107
+ }
108
+
104
109
  /**
105
110
  * This is called by the transport layer on new messages,
106
111
  * and dispatches the different "on*" methods.
@@ -146,10 +151,10 @@ export class SyncEngine {
146
151
  onJoinResponse({ uuid, peers }) {
147
152
  debug('received join response', { uuid, peers })
148
153
  this.uuid = uuid
149
- this.peers = peers
154
+ this.onListPeersResponse({ peers })
150
155
 
151
156
  // Get one peer at random
152
- let randomPeer = this._getRandomPeer()
157
+ const randomPeer = this._getRandomPeer()
153
158
 
154
159
  if (randomPeer) {
155
160
  // Retrieve the operations which happened before join.
@@ -168,6 +173,7 @@ export class SyncEngine {
168
173
  onListPeersResponse({ peers }) {
169
174
  debug('received peerinfo', { peers })
170
175
  this.peers = peers
176
+ this.updaters.map.update({ key: 'numberOfConnectedPeers' })
171
177
  }
172
178
 
173
179
  /**
@@ -248,7 +254,7 @@ export class SyncEngine {
248
254
  * @returns {string|bool} the selected peer uuid, or False if none was found.
249
255
  */
250
256
  _getRandomPeer() {
251
- let otherPeers = this.peers.filter((p) => p !== this.uuid)
257
+ const otherPeers = this.peers.filter((p) => p !== this.uuid)
252
258
  if (otherPeers.length > 0) {
253
259
  const random = Math.floor(Math.random() * otherPeers.length)
254
260
  return otherPeers[random]
@@ -302,7 +308,7 @@ export class Operations {
302
308
  * @returns {*} clock-aware message
303
309
  */
304
310
  addLocal(inputMessage) {
305
- let message = { ...inputMessage, hlc: this._hlc.tick() }
311
+ const message = { ...inputMessage, hlc: this._hlc.tick() }
306
312
  this._operations.push(message)
307
313
  return message
308
314
  }
@@ -342,7 +348,7 @@ export class Operations {
342
348
  */
343
349
  storeRemoteOperations(remoteOperations) {
344
350
  // get the highest date from the passed operations
345
- let greatestHLC = remoteOperations
351
+ const greatestHLC = remoteOperations
346
352
  .map((op) => op.hlc)
347
353
  .reduce((max, current) => (current > max ? current : max))
348
354
 
@@ -30,12 +30,12 @@ export class HybridLogicalClock {
30
30
  * @returns object
31
31
  */
32
32
  parse(raw) {
33
- let tokens = raw.split(':')
33
+ const tokens = raw.split(':')
34
34
 
35
35
  if (tokens.length !== 3) {
36
36
  throw new SyntaxError(`Unable to parse ${raw}`)
37
37
  }
38
- let [walltime, rawNN, id] = tokens
38
+ const [walltime, rawNN, id] = tokens
39
39
 
40
40
  let nn = Number.parseInt(rawNN)
41
41
  if (Number.isNaN(nn)) {
@@ -92,7 +92,7 @@ export class HybridLogicalClock {
92
92
  if (now > local.walltime && now > remote.walltime) {
93
93
  nextValue = { ...local, walltime: now }
94
94
  } else if (local.walltime == remote.walltime) {
95
- let nn = Math.max(local.nn, remote.nn) + 1
95
+ const nn = Math.max(local.nn, remote.nn) + 1
96
96
  nextValue = { ...local, nn: nn }
97
97
  } else if (remote.walltime > local.walltime) {
98
98
  nextValue = { ...remote, id: local.id, nn: remote.nn + 1 }
@@ -1,3 +1,5 @@
1
+ import { fieldInSchema } from '../utils.js'
2
+
1
3
  /**
2
4
  * Updaters are classes able to convert messages
3
5
  * received from other peers (or from the server) to changes on the map.
@@ -42,7 +44,10 @@ class BaseUpdater {
42
44
 
43
45
  export class MapUpdater extends BaseUpdater {
44
46
  update({ key, value }) {
45
- this.updateObjectValue(this.map, key, value)
47
+ if (fieldInSchema(key)) {
48
+ this.updateObjectValue(this.map, key, value)
49
+ }
50
+
46
51
  this.map.render([key])
47
52
  }
48
53
  }
@@ -56,7 +61,14 @@ export class DataLayerUpdater extends BaseUpdater {
56
61
 
57
62
  update({ key, metadata, value }) {
58
63
  const datalayer = this.getDataLayerFromID(metadata.id)
59
- this.updateObjectValue(datalayer, key, value)
64
+ if (fieldInSchema(key)) {
65
+ this.updateObjectValue(datalayer, key, value)
66
+ } else {
67
+ console.debug(
68
+ 'Not applying update for datalayer because key is not in the schema',
69
+ key
70
+ )
71
+ }
60
72
  datalayer.render([key])
61
73
  }
62
74
  }
@@ -21,6 +21,11 @@ export default class ContextMenu extends Positioned {
21
21
  this.openAt([left, top], items)
22
22
  }
23
23
 
24
+ openBelow(element, items) {
25
+ const coords = this.getPosition(element)
26
+ this.openAt([coords.left, coords.bottom], items)
27
+ }
28
+
24
29
  openAt([left, top], items) {
25
30
  this.container.innerHTML = ''
26
31
  for (const item of items) {
@@ -25,6 +25,9 @@ export class Panel {
25
25
  }
26
26
 
27
27
  open({ content, className, actions = [] } = {}) {
28
+ if (this.isOpen()) {
29
+ this.onClose()
30
+ }
28
31
  this.container.className = `with-transition panel window ${this.className} ${
29
32
  this.mode || ''
30
33
  }`
@@ -71,10 +74,13 @@ export class Panel {
71
74
 
72
75
  close() {
73
76
  document.body.classList.remove(`panel-${this.className.split(' ')[0]}-on`)
77
+ this.onClose()
78
+ }
79
+
80
+ onClose() {
74
81
  if (DomUtil.hasClass(this.container, 'on')) {
75
82
  DomUtil.removeClass(this.container, 'on')
76
83
  this.map.invalidateSize({ pan: false })
77
- this.map.editedFeature = null
78
84
  }
79
85
  }
80
86
  }
@@ -84,6 +90,11 @@ export class EditPanel extends Panel {
84
90
  super(map)
85
91
  this.className = 'right dark'
86
92
  }
93
+
94
+ onClose() {
95
+ super.onClose()
96
+ this.map.editedFeature = null
97
+ }
87
98
  }
88
99
 
89
100
  export class FullPanel extends Panel {