umap-project 3.1.2__py3-none-any.whl → 3.2.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 (151) hide show
  1. umap/__init__.py +1 -1
  2. umap/locale/en/LC_MESSAGES/django.po +21 -17
  3. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  4. umap/locale/fr/LC_MESSAGES/django.po +21 -17
  5. umap/management/commands/export_pictogram.py +29 -0
  6. umap/management/commands/migrate_to_S3.py +5 -1
  7. umap/management/commands/purge_old_versions.py +8 -6
  8. umap/settings/__init__.py +21 -0
  9. umap/settings/base.py +1 -0
  10. umap/static/umap/content.css +7 -2
  11. umap/static/umap/css/icon.css +77 -3
  12. umap/static/umap/css/panel.css +31 -1
  13. umap/static/umap/js/modules/browser.js +1 -1
  14. umap/static/umap/js/modules/data/features.js +14 -29
  15. umap/static/umap/js/modules/data/layer.js +248 -136
  16. umap/static/umap/js/modules/facets.js +2 -2
  17. umap/static/umap/js/modules/form/fields.js +56 -19
  18. umap/static/umap/js/modules/formatter.js +36 -8
  19. umap/static/umap/js/modules/importers/opendata.js +23 -6
  20. umap/static/umap/js/modules/managers.js +59 -0
  21. umap/static/umap/js/modules/rendering/icon.js +3 -5
  22. umap/static/umap/js/modules/rendering/layers/classified.js +8 -7
  23. umap/static/umap/js/modules/rendering/map.js +1 -1
  24. umap/static/umap/js/modules/rendering/ui.js +13 -0
  25. umap/static/umap/js/modules/rules.js +76 -23
  26. umap/static/umap/js/modules/schema.js +3 -0
  27. umap/static/umap/js/modules/slideshow.js +1 -1
  28. umap/static/umap/js/modules/sync/updaters.js +1 -6
  29. umap/static/umap/js/modules/tableeditor.js +13 -37
  30. umap/static/umap/js/modules/templates.js +7 -6
  31. umap/static/umap/js/modules/ui/panel.js +7 -0
  32. umap/static/umap/js/modules/umap.js +17 -6
  33. umap/static/umap/js/modules/utils.js +8 -7
  34. umap/static/umap/locale/am_ET.js +43 -6
  35. umap/static/umap/locale/am_ET.json +43 -6
  36. umap/static/umap/locale/ar.js +43 -6
  37. umap/static/umap/locale/ar.json +43 -6
  38. umap/static/umap/locale/ast.js +43 -6
  39. umap/static/umap/locale/ast.json +43 -6
  40. umap/static/umap/locale/bg.js +43 -6
  41. umap/static/umap/locale/bg.json +43 -6
  42. umap/static/umap/locale/br.js +30 -26
  43. umap/static/umap/locale/br.json +30 -26
  44. umap/static/umap/locale/ca.js +50 -13
  45. umap/static/umap/locale/ca.json +50 -13
  46. umap/static/umap/locale/cs_CZ.js +43 -6
  47. umap/static/umap/locale/cs_CZ.json +43 -6
  48. umap/static/umap/locale/da.js +10 -6
  49. umap/static/umap/locale/da.json +10 -6
  50. umap/static/umap/locale/de.js +10 -6
  51. umap/static/umap/locale/de.json +10 -6
  52. umap/static/umap/locale/el.js +20 -10
  53. umap/static/umap/locale/el.json +20 -10
  54. umap/static/umap/locale/en.js +10 -6
  55. umap/static/umap/locale/en.json +10 -6
  56. umap/static/umap/locale/en_US.json +43 -6
  57. umap/static/umap/locale/es.js +10 -6
  58. umap/static/umap/locale/es.json +10 -6
  59. umap/static/umap/locale/et.js +43 -6
  60. umap/static/umap/locale/et.json +43 -6
  61. umap/static/umap/locale/eu.js +43 -6
  62. umap/static/umap/locale/eu.json +43 -6
  63. umap/static/umap/locale/fa_IR.js +43 -6
  64. umap/static/umap/locale/fa_IR.json +43 -6
  65. umap/static/umap/locale/fi.js +43 -6
  66. umap/static/umap/locale/fi.json +43 -6
  67. umap/static/umap/locale/fr.js +10 -6
  68. umap/static/umap/locale/fr.json +10 -6
  69. umap/static/umap/locale/gl.js +43 -6
  70. umap/static/umap/locale/gl.json +43 -6
  71. umap/static/umap/locale/he.js +43 -6
  72. umap/static/umap/locale/he.json +43 -6
  73. umap/static/umap/locale/hr.js +43 -6
  74. umap/static/umap/locale/hr.json +43 -6
  75. umap/static/umap/locale/hu.js +34 -24
  76. umap/static/umap/locale/hu.json +34 -24
  77. umap/static/umap/locale/id.js +43 -6
  78. umap/static/umap/locale/id.json +43 -6
  79. umap/static/umap/locale/is.js +43 -6
  80. umap/static/umap/locale/is.json +43 -6
  81. umap/static/umap/locale/it.js +10 -6
  82. umap/static/umap/locale/it.json +10 -6
  83. umap/static/umap/locale/ja.js +43 -6
  84. umap/static/umap/locale/ja.json +43 -6
  85. umap/static/umap/locale/ko.js +43 -6
  86. umap/static/umap/locale/ko.json +43 -6
  87. umap/static/umap/locale/lt.js +43 -6
  88. umap/static/umap/locale/lt.json +43 -6
  89. umap/static/umap/locale/ms.js +43 -6
  90. umap/static/umap/locale/ms.json +43 -6
  91. umap/static/umap/locale/nl.js +10 -6
  92. umap/static/umap/locale/nl.json +10 -6
  93. umap/static/umap/locale/no.js +43 -6
  94. umap/static/umap/locale/no.json +43 -6
  95. umap/static/umap/locale/pl.js +43 -6
  96. umap/static/umap/locale/pl.json +43 -6
  97. umap/static/umap/locale/pl_PL.json +43 -6
  98. umap/static/umap/locale/pt.js +43 -6
  99. umap/static/umap/locale/pt.json +43 -6
  100. umap/static/umap/locale/pt_BR.js +53 -16
  101. umap/static/umap/locale/pt_BR.json +53 -16
  102. umap/static/umap/locale/pt_PT.js +43 -6
  103. umap/static/umap/locale/pt_PT.json +43 -6
  104. umap/static/umap/locale/ro.js +43 -6
  105. umap/static/umap/locale/ro.json +43 -6
  106. umap/static/umap/locale/ru.js +43 -6
  107. umap/static/umap/locale/ru.json +43 -6
  108. umap/static/umap/locale/sk_SK.js +43 -6
  109. umap/static/umap/locale/sk_SK.json +43 -6
  110. umap/static/umap/locale/sl.js +43 -6
  111. umap/static/umap/locale/sl.json +43 -6
  112. umap/static/umap/locale/sr.js +43 -6
  113. umap/static/umap/locale/sr.json +43 -6
  114. umap/static/umap/locale/sv.js +43 -6
  115. umap/static/umap/locale/sv.json +43 -6
  116. umap/static/umap/locale/th_TH.js +43 -6
  117. umap/static/umap/locale/th_TH.json +43 -6
  118. umap/static/umap/locale/tr.js +43 -6
  119. umap/static/umap/locale/tr.json +43 -6
  120. umap/static/umap/locale/uk_UA.js +43 -6
  121. umap/static/umap/locale/uk_UA.json +43 -6
  122. umap/static/umap/locale/vi.js +43 -6
  123. umap/static/umap/locale/vi.json +43 -6
  124. umap/static/umap/locale/vi_VN.json +43 -6
  125. umap/static/umap/locale/zh.js +43 -6
  126. umap/static/umap/locale/zh.json +43 -6
  127. umap/static/umap/locale/zh_CN.json +43 -6
  128. umap/static/umap/locale/zh_TW.Big5.json +43 -6
  129. umap/static/umap/locale/zh_TW.js +43 -6
  130. umap/static/umap/locale/zh_TW.json +43 -6
  131. umap/static/umap/map.css +239 -65
  132. umap/static/umap/vendors/betterknown/betterknown.mjs +287 -0
  133. umap/storage/fs.py +3 -2
  134. umap/templates/base.html +4 -1
  135. umap/tests/base.py +9 -1
  136. umap/tests/integration/test_basics.py +1 -1
  137. umap/tests/integration/test_conditional_rules.py +62 -20
  138. umap/tests/integration/test_edit_datalayer.py +1 -1
  139. umap/tests/integration/test_edit_marker.py +1 -1
  140. umap/tests/integration/test_export_map.py +10 -0
  141. umap/tests/integration/test_import.py +140 -0
  142. umap/tests/integration/test_optimistic_merge.py +72 -12
  143. umap/tests/integration/test_tableeditor.py +6 -3
  144. umap/utils.py +33 -0
  145. umap/views.py +16 -2
  146. umap_project-3.2.0.dist-info/METADATA +76 -0
  147. {umap_project-3.1.2.dist-info → umap_project-3.2.0.dist-info}/RECORD +150 -148
  148. umap_project-3.1.2.dist-info/METADATA +0 -68
  149. {umap_project-3.1.2.dist-info → umap_project-3.2.0.dist-info}/WHEEL +0 -0
  150. {umap_project-3.1.2.dist-info → umap_project-3.2.0.dist-info}/entry_points.txt +0 -0
  151. {umap_project-3.1.2.dist-info → umap_project-3.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -21,6 +21,8 @@ import TableEditor from '../tableeditor.js'
21
21
  import * as Utils from '../utils.js'
22
22
  import { LineString, Point, Polygon } from './features.js'
23
23
  import Rules from '../rules.js'
24
+ import Orderable from '../orderable.js'
25
+ import { FeatureManager } from '../managers.js'
24
26
 
25
27
  export const LAYER_TYPES = [
26
28
  DefaultLayer,
@@ -40,8 +42,7 @@ export class DataLayer {
40
42
  constructor(umap, leafletMap, data = {}) {
41
43
  this._umap = umap
42
44
  this.sync = umap.syncEngine.proxy(this)
43
- this._index = Array()
44
- this._features = {}
45
+ this.features = new FeatureManager()
45
46
  this._geojson = null
46
47
  this._propertiesIndex = []
47
48
 
@@ -89,6 +90,12 @@ export class DataLayer {
89
90
  if (!this.createdOnServer) {
90
91
  if (this.showAtLoad()) this.show()
91
92
  }
93
+ if (!this._needsFetch && !this._umap.fields.length) {
94
+ this.properties.fields = [
95
+ { key: U.DEFAULT_LABEL_KEY, type: 'String' },
96
+ { key: 'description', type: 'Text' },
97
+ ]
98
+ }
92
99
 
93
100
  // Only layers that are displayed on load must be hidden/shown
94
101
  // Automatically, others will be shown manually, and thus will
@@ -140,6 +147,23 @@ export class DataLayer {
140
147
  this.properties.rank = value
141
148
  }
142
149
 
150
+ get fields() {
151
+ if (!this.properties.fields) this.properties.fields = []
152
+ return this.properties.fields
153
+ }
154
+
155
+ set fields(fields) {
156
+ this.properties.fields = fields
157
+ }
158
+
159
+ get fieldKeys() {
160
+ return this.fields.map((field) => field.key)
161
+ }
162
+
163
+ get sortKey() {
164
+ return this.getProperty('sortKey') || U.DEFAULT_LABEL_KEY
165
+ }
166
+
143
167
  getSyncMetadata() {
144
168
  return {
145
169
  subject: 'datalayer',
@@ -249,19 +273,14 @@ export class DataLayer {
249
273
  const Class = LAYER_MAP[this.properties.type] || DefaultLayer
250
274
  this.layer = new Class(this)
251
275
  // Rendering layer changed, so let's force reset the feature rendering too.
252
- this.eachFeature((feature) => feature.makeUI())
253
- this.eachFeature(this.showFeature)
276
+ this.features.forEach((feature) => {
277
+ feature.makeUI()
278
+ this.showFeature(feature)
279
+ })
254
280
  if (visible) this.show()
255
281
  this.propagateRemote()
256
282
  }
257
283
 
258
- eachFeature(method, context) {
259
- for (const idx of this._index) {
260
- method.call(context || this, this._features[idx])
261
- }
262
- return this
263
- }
264
-
265
284
  async fetchData() {
266
285
  if (!this.createdOnServer) return
267
286
  if (this._loading) return
@@ -314,12 +333,6 @@ export class DataLayer {
314
333
  }
315
334
  }
316
335
 
317
- reindex() {
318
- const features = Object.values(this._features)
319
- this.sortFeatures(features)
320
- this._index = features.map((feature) => stamp(feature))
321
- }
322
-
323
336
  showAtZoom() {
324
337
  const from = Number.parseInt(this.properties.fromZoom, 10)
325
338
  const to = Number.parseInt(this.properties.toZoom, 10)
@@ -423,22 +436,20 @@ export class DataLayer {
423
436
  }
424
437
 
425
438
  addFeature(feature) {
426
- const id = stamp(feature)
427
439
  feature.connectToDataLayer(this)
428
- this._index.push(id)
429
- this._features[id] = feature
430
- this.indexProperties(feature)
440
+ this.features.add(feature)
431
441
  this._umap.featuresIndex[feature.getSlug()] = feature
442
+ // TODO: quid for remote data ?
443
+ this.inferFields(feature)
432
444
  this.showFeature(feature)
433
445
  this.dataChanged()
434
446
  }
435
447
 
436
448
  removeFeature(feature, sync) {
437
- const id = stamp(feature)
438
449
  // This feature was not yet added, may be after
439
450
  // hitting Escape while drawing a new line or
440
451
  // polygon, not yet valid (not enough points)
441
- if (!this._index.includes(id)) return
452
+ if (!this.features.has(feature.id)) return
442
453
  if (sync !== false) {
443
454
  const oldValue = feature.toGeoJSON()
444
455
  feature.sync.delete(oldValue)
@@ -446,57 +457,66 @@ export class DataLayer {
446
457
  this.hideFeature(feature)
447
458
  delete this._umap.featuresIndex[feature.getSlug()]
448
459
  feature.disconnectFromDataLayer(this)
449
- this._index.splice(this._index.indexOf(id), 1)
450
- delete this._features[id]
460
+ this.features.del(feature)
451
461
  if (this.isVisible()) this.dataChanged()
452
462
  }
453
463
 
454
- indexProperties(feature) {
455
- for (const i in feature.properties)
456
- if (typeof feature.properties[i] !== 'object') this.indexProperty(i)
457
- }
458
-
459
- indexProperty(name) {
460
- if (!name) return
461
- if (name.indexOf('_') === 0) return
462
- if (!this._propertiesIndex.includes(name)) {
463
- this._propertiesIndex.push(name)
464
- this._propertiesIndex.sort()
464
+ inferFields(feature) {
465
+ if (!this.properties.fields) this.properties.fields = []
466
+ const keys = this.fieldKeys
467
+ for (const key in feature.properties) {
468
+ if (typeof feature.properties[key] !== 'object') {
469
+ if (key.indexOf('_') === 0) continue
470
+ if (keys.includes(key)) continue
471
+ this.properties.fields.push({ key, type: 'String' })
472
+ }
465
473
  }
466
474
  }
467
475
 
468
- checkIndexForProperty(name) {
469
- for (const feature of Object.values(this._features)) {
470
- if (name in feature.properties) {
471
- this.indexProperty(name)
472
- return
473
- }
474
- }
475
- this.deindexProperty(name)
476
+ async confirmDeleteProperty(property) {
477
+ return this._umap.dialog
478
+ .confirm(
479
+ translate('Are you sure you want to delete this field on all the features?')
480
+ )
481
+ .then(() => {
482
+ this.deleteProperty(property)
483
+ })
476
484
  }
477
485
 
478
- deindexProperty(name) {
479
- const idx = this._propertiesIndex.indexOf(name)
480
- if (idx !== -1) this._propertiesIndex.splice(idx, 1)
486
+ async askForRenameProperty(property) {
487
+ return this._umap.dialog
488
+ .prompt(translate('Please enter the new name of this field'))
489
+ .then(({ prompt }) => {
490
+ if (!prompt || !this.validateName(prompt)) return
491
+ this.renameProperty(property, prompt)
492
+ })
481
493
  }
482
494
 
483
495
  renameProperty(oldName, newName) {
484
496
  this.sync.startBatch()
485
- this.eachFeature((feature) => {
497
+ const oldFields = Utils.CopyJSON(this.fields)
498
+ for (const field of this.fields) {
499
+ if (field.key === oldName) {
500
+ field.key = newName
501
+ break
502
+ }
503
+ }
504
+ this.sync.update('properties.fields', this.fields, oldFields)
505
+ this.features.forEach((feature) => {
486
506
  feature.renameProperty(oldName, newName)
487
507
  })
488
508
  this.sync.commitBatch()
489
- this.deindexProperty(oldName)
490
- this.indexProperty(newName)
491
509
  }
492
510
 
493
511
  deleteProperty(property) {
494
512
  this.sync.startBatch()
495
- this.eachFeature((feature) => {
513
+ const oldFields = Utils.CopyJSON(this.fields)
514
+ this.fields = this.fields.filter((field) => field.key !== property)
515
+ this.sync.update('properties.fields', this.fields, oldFields)
516
+ this.features.forEach((feature) => {
496
517
  feature.deleteProperty(property)
497
518
  })
498
519
  this.sync.commitBatch()
499
- this.deindexProperty(property)
500
520
  }
501
521
 
502
522
  addProperty() {
@@ -508,7 +528,7 @@ export class DataLayer {
508
528
  .prompt(translate('Please enter the name of the property'))
509
529
  .then(({ prompt }) => {
510
530
  if (!prompt || !this.validateName(prompt)) return
511
- this.indexProperty(prompt)
531
+ this.properties.fields.push({ key: prompt, type: 'String' })
512
532
  resolve()
513
533
  })
514
534
  return promise
@@ -519,19 +539,15 @@ export class DataLayer {
519
539
  Alert.error(translate('Name “{name}” should not contain a dot.', { name }))
520
540
  return false
521
541
  }
522
- if (this.allProperties().includes(name)) {
542
+ if (this.fieldKeys.includes(name)) {
523
543
  Alert.error(translate('This name already exists: “{name}”', { name }))
524
544
  return false
525
545
  }
526
546
  return true
527
547
  }
528
548
 
529
- allProperties() {
530
- return this._propertiesIndex
531
- }
532
-
533
549
  sortedValues(property) {
534
- return Object.values(this._features)
550
+ return Array.from(this.features.values())
535
551
  .map((feature) => feature.properties[property])
536
552
  .filter((val, idx, arr) => arr.indexOf(val) === idx)
537
553
  .sort(Utils.naturalSort)
@@ -549,11 +565,6 @@ export class DataLayer {
549
565
  }
550
566
  }
551
567
 
552
- sortFeatures(collection) {
553
- const sortKeys = this.getProperty('sortKey') || U.DEFAULT_LABEL_KEY
554
- return Utils.sortFeatures(collection, sortKeys, U.lang)
555
- }
556
-
557
568
  makeFeatures(geojson = {}, sync = true) {
558
569
  if (geojson.type === 'Feature' || geojson.coordinates) {
559
570
  geojson = [geojson]
@@ -563,7 +574,7 @@ export class DataLayer {
563
574
  : geojson.features || geojson.geometries
564
575
  if (!collection) return
565
576
  const features = []
566
- this.sortFeatures(collection)
577
+ Utils.sortFeatures(collection, this.sortKey, U.lang)
567
578
  for (const featureJson of collection) {
568
579
  if (featureJson.geometry?.type === 'GeometryCollection') {
569
580
  for (const geometry of featureJson.geometry.geometries) {
@@ -592,6 +603,14 @@ export class DataLayer {
592
603
  // FIXME: deal with MultiPoint
593
604
  feature = new Point(this._umap, this, geojson, id)
594
605
  break
606
+ case 'MultiPoint':
607
+ if (geometry.coordinates?.length === 1) {
608
+ geojson.geometry.coordinates = geojson.geometry.coordinates[0]
609
+ feature = new Point(this._umap, this, geojson, id)
610
+ } else if (this._umap.editEnabled) {
611
+ Alert.error(translate('Cannot process MultiPoint'))
612
+ }
613
+ break
595
614
  case 'MultiLineString':
596
615
  case 'LineString':
597
616
  feature = new LineString(this._umap, this, geojson, id)
@@ -602,11 +621,13 @@ export class DataLayer {
602
621
  break
603
622
  default:
604
623
  console.debug(geojson)
605
- Alert.error(
606
- translate('Skipping unknown geometry.type: {type}', {
607
- type: geometry.type || 'undefined',
608
- })
609
- )
624
+ if (this._umap.editEnabled) {
625
+ Alert.error(
626
+ translate('Skipping unknown geometry.type: {type}', {
627
+ type: geometry.type || 'undefined',
628
+ })
629
+ )
630
+ }
610
631
  }
611
632
  if (feature && !feature.isEmpty()) {
612
633
  this.addFeature(feature)
@@ -713,9 +734,7 @@ export class DataLayer {
713
734
  }
714
735
 
715
736
  clear(sync = true) {
716
- for (const feature of Object.values(this._features)) {
717
- feature.del(sync)
718
- }
737
+ this.features.forEach((feature) => feature.del(sync))
719
738
  this.dataChanged()
720
739
  }
721
740
 
@@ -731,14 +750,17 @@ export class DataLayer {
731
750
 
732
751
  redraw() {
733
752
  if (!this.isVisible()) return
734
- this.eachFeature((feature) => feature.redraw())
753
+ this.features.forEach((feature) => feature.redraw())
735
754
  }
736
755
 
737
- edit() {
738
- if (!this._umap.editEnabled) {
739
- return
756
+ reindex() {
757
+ this.features.sort(this.sortKey)
758
+ if (this.isBrowsable()) {
759
+ this.resetLayer(true)
740
760
  }
741
- const container = DomUtil.create('div', 'umap-layer-properties-container')
761
+ }
762
+
763
+ _editMetadata(container) {
742
764
  const metadataFields = [
743
765
  'properties.name',
744
766
  'properties.description',
@@ -768,29 +790,35 @@ export class DataLayer {
768
790
  ],
769
791
  ]
770
792
  DomUtil.createTitle(container, translate('Layer properties'), 'icon-layers')
771
- let builder = new MutatingForm(this, metadataFields)
793
+ const builder = new MutatingForm(this, metadataFields)
772
794
  builder.on('set', ({ detail }) => {
773
795
  this._umap.onDataLayersChanged()
774
796
  if (detail.helper.field === 'properties.type') {
775
- this.edit()
797
+ this.edit().then((panel) => panel.scrollTo('details#layer-properties'))
776
798
  }
777
799
  })
778
800
  container.appendChild(builder.build())
801
+ }
779
802
 
803
+ _editLayerProperties(container) {
780
804
  const layerFields = this.layer.getEditableProperties()
781
805
 
782
806
  if (layerFields.length) {
783
- builder = new MutatingForm(this, layerFields, {
784
- id: 'datalayer-layer-properties',
785
- })
786
- const layerProperties = DomUtil.createFieldset(
787
- container,
788
- `${this.layer.getName()}: ${translate('settings')}`
789
- )
790
- layerProperties.appendChild(builder.build())
807
+ const builder = new MutatingForm(this, layerFields)
808
+ const template = `
809
+ <details id="layer-properties">
810
+ <summary>${this.layer.getName()}: ${translate('settings')}</summary>
811
+ <fieldset data-ref=fieldset></fieldset>
812
+ </details>
813
+ `
814
+ const [details, { fieldset }] = Utils.loadTemplateWithRefs(template)
815
+ container.appendChild(details)
816
+ fieldset.appendChild(builder.build())
791
817
  }
818
+ }
792
819
 
793
- const shapeFields = [
820
+ _editShapeProperties(container) {
821
+ const fields = [
794
822
  'properties.color',
795
823
  'properties.iconClass',
796
824
  'properties.iconUrl',
@@ -803,7 +831,7 @@ export class DataLayer {
803
831
  'properties.fillOpacity',
804
832
  ]
805
833
 
806
- builder = new MutatingForm(this, shapeFields, {
834
+ const builder = new MutatingForm(this, fields, {
807
835
  id: 'datalayer-advanced-properties',
808
836
  })
809
837
  const shapeFieldset = DomUtil.createFieldset(
@@ -811,8 +839,10 @@ export class DataLayer {
811
839
  translate('Shape properties')
812
840
  )
813
841
  shapeFieldset.appendChild(builder.build())
842
+ }
814
843
 
815
- const advancedFields = [
844
+ _editAdvancedProperties(container) {
845
+ const fields = [
816
846
  'properties.smoothFactor',
817
847
  'properties.dashArray',
818
848
  'properties.zoomTo',
@@ -821,7 +851,7 @@ export class DataLayer {
821
851
  'properties.sortKey',
822
852
  ]
823
853
 
824
- builder = new MutatingForm(this, advancedFields, {
854
+ const builder = new MutatingForm(this, fields, {
825
855
  id: 'datalayer-advanced-properties',
826
856
  })
827
857
  builder.on('set', ({ detail }) => {
@@ -834,8 +864,10 @@ export class DataLayer {
834
864
  translate('Advanced properties')
835
865
  )
836
866
  advancedFieldset.appendChild(builder.build())
867
+ }
837
868
 
838
- const popupFields = [
869
+ _editInteractionProperties(container) {
870
+ const fields = [
839
871
  'properties.popupShape',
840
872
  'properties.popupTemplate',
841
873
  'properties.popupContentTemplate',
@@ -845,14 +877,16 @@ export class DataLayer {
845
877
  'properties.outlinkTarget',
846
878
  'properties.interactive',
847
879
  ]
848
- builder = new MutatingForm(this, popupFields)
880
+ const builder = new MutatingForm(this, fields)
849
881
  const popupFieldset = DomUtil.createFieldset(
850
882
  container,
851
883
  translate('Interaction options')
852
884
  )
853
885
  popupFieldset.appendChild(builder.build())
886
+ }
854
887
 
855
- const textPathFields = [
888
+ _editTextPathProperties(container) {
889
+ const fields = [
856
890
  'properties.textPath',
857
891
  'properties.textPathColor',
858
892
  'properties.textPathRepeat',
@@ -861,17 +895,77 @@ export class DataLayer {
861
895
  'properties.textPathOffset',
862
896
  'properties.textPathPosition',
863
897
  ]
864
- builder = new MutatingForm(this, textPathFields)
898
+ const builder = new MutatingForm(this, fields)
865
899
  const fieldset = DomUtil.createFieldset(container, translate('Line decoration'))
866
900
  fieldset.appendChild(builder.build())
901
+ }
867
902
 
903
+ _editFields(container) {
904
+ const template = `
905
+ <details id="fields">
906
+ <summary>${translate('Manage Fields')}</summary>
907
+ <fieldset>
908
+ <ul data-ref=ul></ul>
909
+ <button type="button" data-ref=add><i class="icon icon-16 icon-add"></i>${translate('Add a new field')}</button>
910
+ </fieldset>
911
+ </details>
912
+ `
913
+ const [fieldset, { ul, add }] = Utils.loadTemplateWithRefs(template)
914
+ add.addEventListener('click', () => {
915
+ this.addProperty().then(() => {
916
+ this.edit().then((panel) => {
917
+ panel.scrollTo('details#fields')
918
+ })
919
+ })
920
+ })
921
+ container.appendChild(fieldset)
922
+ for (const field of this.fields) {
923
+ const [row, { rename, del }] = Utils.loadTemplateWithRefs(
924
+ `<li class="orderable" data-key="${field.key}">
925
+ <button class="icon icon-16 icon-edit" title="${translate('Rename this field')}" data-ref=rename></button>
926
+ <button class="icon icon-16 icon-delete" title="${translate('Delete this field')}" data-ref=del></button>
927
+ <i class="icon icon-16 icon-drag" title="${translate('Drag to reorder')}"></i>
928
+ ${field.key}
929
+ </li>`
930
+ )
931
+ ul.appendChild(row)
932
+ rename.addEventListener('click', () => {
933
+ this.askForRenameProperty(field.key).then(() => {
934
+ this.edit().then((panel) => {
935
+ panel.scrollTo('details#fields')
936
+ })
937
+ })
938
+ })
939
+ del.addEventListener('click', () => {
940
+ this.confirmDeleteProperty(field.key).then(() => {
941
+ this.edit().then((panel) => {
942
+ panel.scrollTo('details#fields')
943
+ })
944
+ })
945
+ })
946
+ }
947
+ const onReorder = (src, dst, initialIndex, finalIndex) => {
948
+ const orderedKeys = Array.from(ul.querySelectorAll('li')).map(
949
+ (el) => el.dataset.key
950
+ )
951
+ const oldFields = Utils.CopyJSON(this.properties.fields)
952
+ this.properties.fields.sort(
953
+ (fieldA, fieldB) =>
954
+ orderedKeys.indexOf(fieldA.key) > orderedKeys.indexOf(fieldB.key)
955
+ )
956
+ this.sync.update('properties.fields', this.properties.fields, oldFields)
957
+ }
958
+ const orderable = new Orderable(ul, onReorder)
959
+ }
960
+
961
+ _editRemoteDataProperties(container) {
868
962
  // XXX I'm not sure **why** this is needed (as it's set during `this.initialize`)
869
963
  // but apparently it's needed.
870
964
  if (!Utils.isObject(this.properties.remoteData)) {
871
965
  this.properties.remoteData = {}
872
966
  }
873
967
 
874
- const remoteDataFields = [
968
+ const fields = [
875
969
  [
876
970
  'properties.remoteData.url',
877
971
  { handler: 'Url', label: translate('Url'), helpEntries: ['formatURL'] },
@@ -899,7 +993,7 @@ export class DataLayer {
899
993
  ],
900
994
  ]
901
995
  if (this._umap.properties.urls.ajax_proxy) {
902
- remoteDataFields.push([
996
+ fields.push([
903
997
  'properties.remoteData.proxy',
904
998
  {
905
999
  handler: 'Switch',
@@ -907,14 +1001,14 @@ export class DataLayer {
907
1001
  helpEntries: ['proxyRemoteData'],
908
1002
  },
909
1003
  ])
910
- remoteDataFields.push('properties.remoteData.ttl')
1004
+ fields.push('properties.remoteData.ttl')
911
1005
  }
912
1006
 
913
1007
  const remoteDataContainer = DomUtil.createFieldset(
914
1008
  container,
915
1009
  translate('Remote data')
916
1010
  )
917
- builder = new MutatingForm(this, remoteDataFields)
1011
+ const builder = new MutatingForm(this, fields)
918
1012
  remoteDataContainer.appendChild(builder.build())
919
1013
  DomUtil.createButton(
920
1014
  'button umap-verify',
@@ -923,12 +1017,9 @@ export class DataLayer {
923
1017
  () => this.fetchRemoteData(true),
924
1018
  this
925
1019
  )
926
- this.rules.edit(container)
927
-
928
- if (this._umap.properties.urls.datalayer_versions) {
929
- this.buildVersionsFieldset(container)
930
- }
1020
+ }
931
1021
 
1022
+ _buildAdvancedActions(container) {
932
1023
  const advancedActions = DomUtil.createFieldset(
933
1024
  container,
934
1025
  translate('Advanced actions')
@@ -963,6 +1054,31 @@ export class DataLayer {
963
1054
  }
964
1055
  clone.addEventListener('click', () => this.clone().edit())
965
1056
  if (this.createdOnServer) download.hidden = false
1057
+ }
1058
+
1059
+ edit() {
1060
+ if (!this._umap.editEnabled) {
1061
+ return
1062
+ }
1063
+ const container = DomUtil.create('div', 'umap-layer-properties-container')
1064
+ this._editMetadata(container)
1065
+ this._editLayerProperties(container)
1066
+ this._editShapeProperties(container)
1067
+ this._editAdvancedProperties(container)
1068
+ this._editInteractionProperties(container)
1069
+ this._editTextPathProperties(container)
1070
+ this._editRemoteDataProperties(container)
1071
+ if (!this.isRemoteLayer()) {
1072
+ this._editFields(container)
1073
+ }
1074
+ this.rules.edit(container)
1075
+
1076
+ if (this._umap.properties.urls.datalayer_versions) {
1077
+ this.buildVersionsFieldset(container)
1078
+ }
1079
+
1080
+ this._buildAdvancedActions(container)
1081
+
966
1082
  const backButton = DomUtil.createButtonIcon(
967
1083
  undefined,
968
1084
  'icon-back',
@@ -973,7 +1089,7 @@ export class DataLayer {
973
1089
  DomEvent.disableClickPropagation(backButton)
974
1090
  DomEvent.on(backButton, 'click', this._umap.editDatalayers, this._umap)
975
1091
 
976
- this._umap.editPanel.open({
1092
+ return this._umap.editPanel.open({
977
1093
  content: container,
978
1094
  highlight: 'layers',
979
1095
  actions: [backButton],
@@ -1062,7 +1178,7 @@ export class DataLayer {
1062
1178
 
1063
1179
  featuresToGeoJSON() {
1064
1180
  const features = []
1065
- this.eachFeature((feature) => features.push(feature.toGeoJSON()))
1181
+ this.features.forEach((feature) => features.push(feature.toGeoJSON()))
1066
1182
  return features
1067
1183
  }
1068
1184
 
@@ -1123,46 +1239,23 @@ export class DataLayer {
1123
1239
  }
1124
1240
 
1125
1241
  count() {
1126
- return this._index.length
1242
+ return this.features.count()
1127
1243
  }
1128
1244
 
1129
1245
  hasData() {
1130
- return !!this._index.length
1246
+ return !!this.count()
1131
1247
  }
1132
1248
 
1133
1249
  isVisible() {
1134
1250
  return Boolean(this.layer && this._leafletMap.hasLayer(this.layer))
1135
1251
  }
1136
1252
 
1137
- getFeatureByIndex(index) {
1138
- if (index === -1) index = this._index.length - 1
1139
- const id = this._index[index]
1140
- return this._features[id]
1141
- }
1142
-
1143
- // TODO Add an index
1144
- // For now, iterate on all the features.
1145
- getFeatureById(id) {
1146
- return Object.values(this._features).find((feature) => feature.id === id)
1147
- }
1148
-
1149
1253
  getNextFeature(feature) {
1150
- const id = this._index.indexOf(stamp(feature))
1151
- const nextId = this._index[id + 1]
1152
- return nextId
1153
- ? this._features[nextId]
1154
- : this.getNextBrowsable().getFeatureByIndex(0)
1254
+ return this.features.next(feature) || this.getNextBrowsable().features.first()
1155
1255
  }
1156
1256
 
1157
1257
  getPreviousFeature(feature) {
1158
- if (this._index <= 1) {
1159
- return null
1160
- }
1161
- const id = this._index.indexOf(stamp(feature))
1162
- const previousId = this._index[id - 1]
1163
- return previousId
1164
- ? this._features[previousId]
1165
- : this.getPreviousBrowsable().getFeatureByIndex(-1)
1258
+ return this.features.prev(feature) || this.getPreviousBrowsable().features.last()
1166
1259
  }
1167
1260
 
1168
1261
  getPreviousBrowsable() {
@@ -1322,8 +1415,27 @@ export class DataLayer {
1322
1415
  )) {
1323
1416
  container.innerHTML = ''
1324
1417
  if (this.layer.renderLegend) return this.layer.renderLegend(container)
1325
- const color = DomUtil.create('span', 'datalayer-color', container)
1326
- color.style.backgroundColor = this.getColor()
1418
+ const keys = new Set(this.fieldKeys)
1419
+ const rules = new Map()
1420
+ for (const rule of this.rules) {
1421
+ rules.set(rule.condition, rule)
1422
+ }
1423
+ for (const rule of this._umap.rules) {
1424
+ if (!rules.has(rule.condition) && keys.has(rule.key)) {
1425
+ rules.set(rule.condition, rule)
1426
+ }
1427
+ }
1428
+ if (rules.size) {
1429
+ const ul = Utils.loadTemplate('<ul class="rules-caption"></ul>')
1430
+ container.appendChild(ul)
1431
+ for (const [_, rule] of rules) {
1432
+ rule.renderLegend(ul)
1433
+ }
1434
+ } else {
1435
+ const color = Utils.loadTemplate('<span class="datalayer-color"></span>')
1436
+ color.style.backgroundColor = this.getColor()
1437
+ container.appendChild(color)
1438
+ }
1327
1439
  }
1328
1440
  }
1329
1441