umap-project 2.7.0b3__py3-none-any.whl → 2.7.2__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 (53) 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/caption.js +10 -4
  16. umap/static/umap/js/modules/data/layer.js +26 -14
  17. umap/static/umap/js/modules/formatter.js +5 -1
  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 +1 -0
  22. umap/static/umap/js/modules/rendering/ui.js +3 -1
  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/panel.js +12 -1
  29. umap/static/umap/js/modules/utils.js +24 -4
  30. umap/static/umap/js/umap.controls.js +42 -11
  31. umap/static/umap/js/umap.core.js +1 -1
  32. umap/static/umap/js/umap.js +11 -11
  33. umap/static/umap/locale/ca.js +8 -4
  34. umap/static/umap/locale/ca.json +8 -4
  35. umap/static/umap/locale/en.js +2 -1
  36. umap/static/umap/locale/en.json +2 -1
  37. umap/static/umap/locale/es.js +330 -319
  38. umap/static/umap/locale/es.json +330 -319
  39. umap/static/umap/locale/fr.js +2 -1
  40. umap/static/umap/locale/fr.json +2 -1
  41. umap/static/umap/map.css +27 -7
  42. umap/static/umap/unittests/hlc.js +2 -0
  43. umap/static/umap/unittests/utils.js +24 -0
  44. umap/static/umap/vars.css +2 -1
  45. umap/static/umap/vendors/csv2geojson/csv2geojson.js +13 -8
  46. umap/tests/integration/test_caption.py +3 -3
  47. umap/tests/integration/test_edit_map.py +1 -1
  48. umap/tests/integration/test_import.py +33 -0
  49. {umap_project-2.7.0b3.dist-info → umap_project-2.7.2.dist-info}/METADATA +9 -6
  50. {umap_project-2.7.0b3.dist-info → umap_project-2.7.2.dist-info}/RECORD +53 -50
  51. {umap_project-2.7.0b3.dist-info → umap_project-2.7.2.dist-info}/WHEEL +0 -0
  52. {umap_project-2.7.0b3.dist-info → umap_project-2.7.2.dist-info}/entry_points.txt +0 -0
  53. {umap_project-2.7.0b3.dist-info → umap_project-2.7.2.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>
@@ -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) {
@@ -81,6 +81,8 @@ export class Formatter {
81
81
  {
82
82
  delimiter: 'auto',
83
83
  includeLatLon: false,
84
+ sexagesimal: false,
85
+ parseLatLon: (raw) => Number.parseFloat(raw.toString().replace(',', '.')),
84
86
  },
85
87
  (err, result) => {
86
88
  // csv2geojson fallback to null geometries when it cannot determine
@@ -115,7 +117,9 @@ export class Formatter {
115
117
  }
116
118
 
117
119
  async fromGeoRSS(str) {
118
- const GeoRSSToGeoJSON = await import('../../vendors/georsstogeojson/GeoRSSToGeoJSON.js')
120
+ const GeoRSSToGeoJSON = await import(
121
+ '../../vendors/georsstogeojson/GeoRSSToGeoJSON.js'
122
+ )
119
123
  return GeoRSSToGeoJSON.parse(this.toDom(str))
120
124
  }
121
125
 
@@ -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,
@@ -75,6 +75,7 @@ const ClassifiedMixin = {
75
75
  },
76
76
 
77
77
  renderLegend: function (container) {
78
+ if (!this.datalayer.hasDataLoaded()) return
78
79
  const parent = DomUtil.create('ul', '', container)
79
80
  const items = this.getLegendItems()
80
81
  for (const [color, label] of items) {
@@ -32,7 +32,9 @@ 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
 
@@ -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
  }
@@ -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 {
@@ -35,7 +35,7 @@ export function checkId(string) {
35
35
  * @returns Array[string]
36
36
  */
37
37
  export function getImpactsFromSchema(fields, schema) {
38
- schema = schema || U.SCHEMA
38
+ const current_schema = schema || U.SCHEMA
39
39
  const impacted = fields
40
40
  .map((field) => {
41
41
  // remove the option prefix for fields
@@ -46,14 +46,30 @@ export function getImpactsFromSchema(fields, schema) {
46
46
  .reduce((acc, field) => {
47
47
  // retrieve the "impacts" field from the schema
48
48
  // and merge them together using sets
49
- const impacts = schema[field]?.impacts || []
50
- impacts.forEach((impact) => acc.add(impact))
49
+ const impacts = current_schema[field]?.impacts || []
50
+ for (const impact of impacts) {
51
+ acc.add(impact)
52
+ }
51
53
  return acc
52
54
  }, new Set())
53
55
 
54
56
  return Array.from(impacted)
55
57
  }
56
58
 
59
+ /**
60
+ * Check if a field exists in the schema.
61
+ *
62
+ * @param {string} field
63
+ * @param {object} schema
64
+ * @returns {boolean}
65
+ */
66
+ export function fieldInSchema(field, schema) {
67
+ const current_schema = schema || U.SCHEMA
68
+ if (typeof field !== 'string') return false
69
+ const field_name = field.replace('options.', '').split('.')[0]
70
+ return current_schema[field_name] !== undefined
71
+ }
72
+
57
73
  /**
58
74
  * Import DOM purify, and initialize it.
59
75
  *
@@ -407,6 +423,10 @@ export class WithTemplate {
407
423
  }
408
424
  }
409
425
 
410
- export function deepEqual(object1, object2){
426
+ export function deepEqual(object1, object2) {
411
427
  return JSON.stringify(object1) === JSON.stringify(object2)
412
428
  }
429
+
430
+ export function slugify(str) {
431
+ return (str || 'data').replace(/[^a-z0-9]/gi, '_').toLowerCase()
432
+ }
@@ -660,6 +660,33 @@ const ControlsMixin = {
660
660
  menu.openBelow(button, actions)
661
661
  })
662
662
  }
663
+
664
+ const connectedPeers = this.sync.getNumberOfConnectedPeers()
665
+ if (connectedPeers !== 0) {
666
+ const connectedPeersCount = L.DomUtil.createButton(
667
+ 'leaflet-control-connected-peers',
668
+ rightContainer,
669
+ ''
670
+ )
671
+ L.DomEvent.on(connectedPeersCount, 'mouseover', () => {
672
+ this.tooltip.open({
673
+ content: L._('{connectedPeers} peer(s) currently connected to this map', {
674
+ connectedPeers: connectedPeers,
675
+ }),
676
+ anchor: connectedPeersCount,
677
+ position: 'bottom',
678
+ delay: 500,
679
+ duration: 5000,
680
+ })
681
+ })
682
+
683
+ const updateConnectedPeersCount = () => {
684
+ connectedPeersCount.innerHTML =
685
+ '<span>' + this.sync.getNumberOfConnectedPeers() + '</span>'
686
+ }
687
+ updateConnectedPeersCount()
688
+ }
689
+
663
690
  this.help.getStartedLink(rightContainer)
664
691
  const controlEditCancel = L.DomUtil.createButton(
665
692
  'leaflet-control-edit-cancel',
@@ -1155,17 +1182,6 @@ U.Editable = L.Editable.extend({
1155
1182
  initialize: function (map, options) {
1156
1183
  L.Editable.prototype.initialize.call(this, map, options)
1157
1184
  this.on('editable:drawing:click editable:drawing:move', this.drawingTooltip)
1158
- this.on('editable:drawing:end', (event) => {
1159
- this.map.tooltip.close()
1160
- // Leaflet.Editable will delete the drawn shape if invalid
1161
- // (eg. line has only one drawn point)
1162
- // So let's check if the layer has no more shape
1163
- if (!event.layer.feature.hasGeom()) {
1164
- event.layer.feature.del()
1165
- } else {
1166
- event.layer.feature.edit()
1167
- }
1168
- })
1169
1185
  // Layer for items added by users
1170
1186
  this.on('editable:drawing:cancel', (event) => {
1171
1187
  if (event.layer instanceof U.LeafletMarker) event.layer.feature.del()
@@ -1295,4 +1311,19 @@ U.Editable = L.Editable.extend({
1295
1311
  L.DomEvent.stop(e)
1296
1312
  e.cancel()
1297
1313
  },
1314
+
1315
+ onEscape: function () {
1316
+ this.once('editable:drawing:end', (event) => {
1317
+ this.map.tooltip.close()
1318
+ // Leaflet.Editable will delete the drawn shape if invalid
1319
+ // (eg. line has only one drawn point)
1320
+ // So let's check if the layer has no more shape
1321
+ if (!event.layer.feature.hasGeom()) {
1322
+ event.layer.feature.del()
1323
+ } else {
1324
+ event.layer.feature.edit()
1325
+ }
1326
+ })
1327
+ this.stopDrawing()
1328
+ },
1298
1329
  })
@@ -143,7 +143,7 @@ L.DomUtil.createButtonIcon = (parent, className, title, callback, size = 16) =>
143
143
 
144
144
  L.DomUtil.createTitle = (parent, text, iconClassName, className = '', tag = 'h3') => {
145
145
  const title = L.DomUtil.create(tag, '', parent)
146
- if (className) L.DomUtil.createIcon(title, iconClassName)
146
+ if (iconClassName) L.DomUtil.createIcon(title, iconClassName)
147
147
  L.DomUtil.add('span', className, title, text)
148
148
  return title
149
149
  }