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
@@ -1,4 +1,4 @@
1
- import { DomEvent, DomUtil, stamp } from '../../vendors/leaflet/leaflet-src.esm.js'
1
+ import { DomEvent, DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
2
2
  import { translate } from './i18n.js'
3
3
  import * as Utils from './utils.js'
4
4
 
@@ -25,7 +25,7 @@ export default class Facets {
25
25
  }
26
26
 
27
27
  this._umap.datalayers.browsable().map((datalayer) => {
28
- datalayer.eachFeature((feature) => {
28
+ datalayer.features.forEach((feature) => {
29
29
  for (const name of names) {
30
30
  let value = feature.properties[name]
31
31
  const type = defined.get(name).type
@@ -131,12 +131,13 @@ class BaseElement {
131
131
 
132
132
  Fields.Textarea = class extends BaseElement {
133
133
  getTemplate() {
134
- return `<textarea placeholder="${this.properties.placeholder || ''}" data-ref=textarea></textarea>`
134
+ return `<textarea placeholder="${this.properties.placeholder || ''}" name="${this.name}" data-ref=textarea></textarea>`
135
135
  }
136
136
 
137
137
  build() {
138
138
  super.build()
139
139
  this.textarea = this.elements.textarea
140
+ this.input = this.textarea
140
141
  this.fetch()
141
142
  this.textarea.addEventListener(
142
143
  'input',
@@ -363,6 +364,9 @@ Fields.Select = class extends BaseElement {
363
364
  }
364
365
 
365
366
  getOptions() {
367
+ if (this.properties.getOptions) {
368
+ return this.properties.getOptions()
369
+ }
366
370
  return this.properties.selectOptions
367
371
  }
368
372
 
@@ -421,7 +425,7 @@ Fields.IntSelect = class extends Fields.Select {
421
425
 
422
426
  Fields.EditableText = class extends BaseElement {
423
427
  getTemplate() {
424
- return `<span contentEditable class="${this.properties.className || ''}" data-ref=input></span>`
428
+ return `<span class="${this.properties.className || ''}" data-ref=input></span>`
425
429
  }
426
430
 
427
431
  buildTemplate() {
@@ -435,6 +439,24 @@ Fields.EditableText = class extends BaseElement {
435
439
  this.fetch()
436
440
  this.input.addEventListener('input', () => this.sync())
437
441
  this.input.addEventListener('keypress', (event) => this.onKeyPress(event))
442
+ this.input.addEventListener('dblclick', () => {
443
+ if (this.input.contentEditable !== true) {
444
+ this.input.contentEditable = true
445
+ this.input.focus()
446
+ }
447
+ })
448
+ this.input.addEventListener('blur', () => {
449
+ this.input.contentEditable = false
450
+ })
451
+ this.input.addEventListener('mouseover', () => {
452
+ this.builder._umap.tooltip.open({
453
+ content: translate('Double click to edit the name'),
454
+ anchor: this.input,
455
+ position: 'bottom',
456
+ delay: 500,
457
+ duration: 5000,
458
+ })
459
+ })
438
460
  }
439
461
 
440
462
  value() {
@@ -654,7 +676,7 @@ Fields.PropertyInput = class extends Fields.BlurInput {
654
676
  super.build()
655
677
  const autocomplete = new AutocompleteDatalist(this.input)
656
678
  // Will be used on Umap and DataLayer
657
- const properties = this.builder.obj.allProperties()
679
+ const properties = this.builder.obj.fieldKeys
658
680
  autocomplete.suggestions = properties
659
681
  }
660
682
  }
@@ -691,10 +713,10 @@ Fields.IconUrl = class extends Fields.BlurInput {
691
713
 
692
714
  async onDefine() {
693
715
  this.footer.innerHTML = ''
694
- const [{ pictogram_list }, response, error] = await this.builder._umap.server.get(
716
+ const [{ data }, response, error] = await this.builder._umap.server.get(
695
717
  this.builder._umap.properties.urls.pictogram_list_json
696
718
  )
697
- if (!error) this.pictogram_list = pictogram_list
719
+ if (!error) this.pictogramCollections = data
698
720
  this.buildTabs()
699
721
  const value = this.value()
700
722
  if (Icon.RECENT.length) this.showRecentTab()
@@ -807,36 +829,51 @@ Fields.IconUrl = class extends Fields.BlurInput {
807
829
  this.updatePreview()
808
830
  }
809
831
 
810
- addCategory(items, name) {
832
+ addCategory(items, name, parent, attribution = null) {
811
833
  const hidden = name ? '' : ' hidden'
812
- const [parent, { grid }] = Utils.loadTemplateWithRefs(`
834
+ const [container, { grid }] = Utils.loadTemplateWithRefs(`
813
835
  <div class="umap-pictogram-category">
814
836
  <h6${hidden}>${name}</h6>
815
837
  <div class="umap-pictogram-grid" data-ref=grid></div>
816
838
  </div>
817
839
  `)
818
840
  let hasIcons = false
819
- for (const item of items) {
841
+ const sorted = items.sort((a, b) => Utils.naturalSort(a.name, b.name, U.lang))
842
+ for (const item of sorted) {
843
+ item.attribution ??= attribution
820
844
  hasIcons = this.addIconPreview(item, grid) || hasIcons
821
845
  }
822
- if (hasIcons) this.grid.appendChild(parent)
846
+ if (hasIcons) parent.appendChild(container)
823
847
  }
824
848
 
825
849
  buildSymbolsList() {
826
850
  this.grid.innerHTML = ''
827
851
  const categories = {}
828
852
  let category
829
- for (const props of this.pictogram_list) {
830
- category = props.category || translate('Generic')
831
- categories[category] = categories[category] || []
832
- categories[category].push(props)
833
- }
834
- const sorted = Object.entries(categories).sort(([a], [b]) =>
835
- Utils.naturalSort(a, b, U.lang)
853
+ const collectionsNames = Object.keys(this.pictogramCollections)
854
+ const [container, { select, icons }] = Utils.loadTemplateWithRefs(
855
+ '<div><select data-ref="select"></select><div data-ref="icons"></div></div>'
836
856
  )
837
- for (const [name, items] of sorted) {
838
- this.addCategory(items, name)
857
+ for (const name of collectionsNames) {
858
+ const option = Utils.loadTemplate(`<option value="${name}">${name}</option>`)
859
+ select.appendChild(option)
839
860
  }
861
+ this.grid.appendChild(container)
862
+ select.hidden = collectionsNames.length === 1
863
+ const loadCollection = (name) => {
864
+ icons.innerHTML = ''
865
+ const collection = this.pictogramCollections[name || collectionsNames[0]]
866
+ const sorted = Object.entries(collection.categories).sort(([a], [b]) =>
867
+ Utils.naturalSort(a, b, U.lang)
868
+ )
869
+ for (const [name, items] of sorted) {
870
+ this.addCategory(items, name, icons, collection.attribution)
871
+ }
872
+ }
873
+ loadCollection()
874
+ select.addEventListener('change', (event) => {
875
+ loadCollection(event.target.value)
876
+ })
840
877
  }
841
878
 
842
879
  buildRecentList() {
@@ -844,7 +881,7 @@ Fields.IconUrl = class extends Fields.BlurInput {
844
881
  const items = U.Icon.RECENT.map((src) => ({
845
882
  src,
846
883
  }))
847
- this.addCategory(items)
884
+ this.addCategory(items, null, this.grid)
848
885
  }
849
886
 
850
887
  isDefault() {
@@ -2,6 +2,19 @@ import { uMapAlert as Alert } from '../components/alerts/alert.js'
2
2
  /* Uses globals for: csv2geojson, osmtogeojson (not available as ESM) */
3
3
  import { translate } from './i18n.js'
4
4
 
5
+ const parseTextGeom = async (geom) => {
6
+ try {
7
+ return JSON.parse(geom)
8
+ } catch (e) {
9
+ try {
10
+ const betterknown = await import('../../vendors/betterknown/betterknown.mjs')
11
+ return betterknown.wktToGeoJSON(geom)
12
+ } catch {
13
+ return null
14
+ }
15
+ }
16
+ }
17
+
5
18
  export const EXPORT_FORMATS = {
6
19
  geojson: {
7
20
  formatter: async (umap) => JSON.stringify(umap.toGeoJSON(), null, 2),
@@ -81,15 +94,30 @@ export class Formatter {
81
94
  sexagesimal: false,
82
95
  parseLatLon: (raw) => Number.parseFloat(raw.toString().replace(',', '.')),
83
96
  },
84
- (err, result) => {
85
- // csv2geojson fallback to null geometries when it cannot determine
86
- // lat or lon columns. This is valid geojson, but unwanted from a user
87
- // point of view.
97
+ async (err, result) => {
88
98
  if (result?.features.length) {
89
- if (result.features[0].geometry === null) {
90
- err = {
91
- type: 'Error',
92
- message: translate('Cannot determine latitude and longitude columns.'),
99
+ const first = result.features[0]
100
+ if (first.geometry === null) {
101
+ const geomFields = ['geom', 'geometry', 'wkt', 'geojson']
102
+ for (const field of geomFields) {
103
+ if (first.properties[field]) {
104
+ for (const feature of result.features) {
105
+ feature.geometry = await parseTextGeom(feature.properties[field])
106
+ delete feature.properties[field]
107
+ }
108
+ break
109
+ }
110
+ }
111
+ if (first.geometry === null) {
112
+ // csv2geojson fallback to null geometries when it cannot determine
113
+ // lat or lon columns. This is valid geojson, but unwanted from a user
114
+ // point of view.
115
+ err = {
116
+ type: 'Error',
117
+ message: translate(
118
+ 'No geo column found: must be either `lat(itude)` and `lon(gitude)` or `geom(etry)`.'
119
+ ),
120
+ }
93
121
  }
94
122
  }
95
123
  }
@@ -70,15 +70,33 @@ export class Importer {
70
70
  this.portals = options.choices || PORTALS
71
71
  }
72
72
 
73
+ async fetchDatasets(baseUrl) {
74
+ let results = []
75
+ let total = null
76
+ const hardLimit = 500
77
+ while (total === null || results.length < total) {
78
+ const offset = results.length
79
+ const response = await this.umap.request.get(
80
+ `${baseUrl}/api/explore/v2.1/catalog/datasets?where=features%20in%20%28%22geo%22%29&limit=100&offset=${offset}&order_by=title asc`
81
+ )
82
+ if (!response.ok) break
83
+ const data = await response.json()
84
+ if (total === null) {
85
+ total = data.total_count
86
+ }
87
+ results = results.concat(data.results)
88
+ if (total === null || results.length > hardLimit) break
89
+ }
90
+ return results
91
+ }
92
+
73
93
  async open(importer) {
74
94
  let fields_map = {}
75
95
  const [container, { portals, datasets, geofield }] =
76
96
  Utils.loadTemplateWithRefs(TEMPLATE)
77
97
  portals.addEventListener('change', async (event) => {
78
- const response = await this.umap.request.get(
79
- `${event.target.value}/api/explore/v2.1/catalog/datasets?where=features%20in%20%28%22geo%22%29&limit=-1&offset=0&timezone=UTC`
80
- )
81
- if (response.ok) {
98
+ const results = await this.fetchDatasets(event.target.value)
99
+ if (results) {
82
100
  fields_map = {}
83
101
  Array.from(datasets.children).forEach((option) => {
84
102
  if (!option.disabled) {
@@ -87,8 +105,7 @@ export class Importer {
87
105
  option.selected = true
88
106
  }
89
107
  })
90
- const data = await response.json()
91
- for (const result of data.results) {
108
+ for (const result of results) {
92
109
  const fields = result.fields.filter((field) => field.type === 'geo_point_2d')
93
110
  if (!fields.length) {
94
111
  console.debug('No geofield found for', result)
@@ -1,3 +1,5 @@
1
+ import * as Utils from './utils.js'
2
+
1
3
  export class DataLayerManager extends Object {
2
4
  add(datalayer) {
3
5
  this[datalayer.id] = datalayer
@@ -43,4 +45,61 @@ export class DataLayerManager extends Object {
43
45
  if (!next.canBrowse()) return this.next(next)
44
46
  return next
45
47
  }
48
+ first() {
49
+ return this.active()[0]
50
+ }
51
+ last() {
52
+ const layers = this.active()
53
+ return layers[layers.length - 1]
54
+ }
55
+ }
56
+
57
+ export class FeatureManager extends Map {
58
+ add(feature) {
59
+ if (this.has(feature.id)) {
60
+ console.error('Duplicate id', feature, this.get(feature.id))
61
+ feature.id = Utils.generateId()
62
+ }
63
+ this.set(feature.id, feature)
64
+ }
65
+
66
+ del(feature) {
67
+ this.delete(feature.id)
68
+ }
69
+
70
+ count() {
71
+ return this.size
72
+ }
73
+
74
+ sort(by) {
75
+ const features = Array.from(this.values())
76
+ Utils.sortFeatures(features, by, U.lang)
77
+ this.clear()
78
+ for (const feature of features) {
79
+ this.set(feature.id, feature)
80
+ }
81
+ }
82
+
83
+ getIndex(feature) {
84
+ const entries = Array.from(this)
85
+ return entries.findIndex(([id]) => id === feature.id)
86
+ }
87
+
88
+ first() {
89
+ return this.values().next().value
90
+ }
91
+
92
+ last() {
93
+ return Array.from(this.values())[this.size - 1]
94
+ }
95
+
96
+ next(feature) {
97
+ const index = this.getIndex(feature)
98
+ return Array.from(this.values())[index + 1]
99
+ }
100
+
101
+ prev(feature) {
102
+ const index = this.getIndex(feature)
103
+ return Array.from(this.values())[index - 1]
104
+ }
46
105
  }
@@ -249,11 +249,9 @@ export function isImg(src) {
249
249
  export function makeElement(src, parent) {
250
250
  let icon
251
251
  if (isImg(src)) {
252
- icon = DomUtil.create('img')
253
- icon.src = src
252
+ icon = Utils.loadTemplate(`<img loading="lazy" src="${src}">`)
254
253
  } else {
255
- icon = DomUtil.create('span')
256
- icon.textContent = src
254
+ icon = Utils.loadTemplate(`<span>${src}</span>`)
257
255
  }
258
256
  parent.appendChild(icon)
259
257
  return icon
@@ -273,7 +271,7 @@ export function setContrast(icon, parent, src, bgcolor) {
273
271
  if (DomUtil.contrastedColor(parent, bgcolor)) {
274
272
  // Decide whether to switch svg to white or not, but do it
275
273
  // only for internal SVG, as invert could do weird things
276
- if (Utils.isPath(src) && src.endsWith('.svg') && src !== SCHEMA.iconUrl.default) {
274
+ if (src.endsWith('.svg') && src !== SCHEMA.iconUrl.default) {
277
275
  // Must be called after icon container is added to the DOM
278
276
  // An image
279
277
  icon.style.filter = 'invert(1)'
@@ -67,7 +67,7 @@ const ClassifiedMixin = {
67
67
 
68
68
  getValues: function () {
69
69
  const values = []
70
- this.datalayer.eachFeature((feature) => {
70
+ this.datalayer.features.forEach((feature) => {
71
71
  const value = this._getValue(feature)
72
72
  if (value !== undefined) values.push(value)
73
73
  })
@@ -198,7 +198,7 @@ export const Choropleth = FeatureGroup.extend({
198
198
  'properties.choropleth.property',
199
199
  {
200
200
  handler: 'Select',
201
- selectOptions: this.datalayer.allProperties(),
201
+ selectOptions: this.datalayer.fieldKeys,
202
202
  label: translate('Choropleth property value'),
203
203
  },
204
204
  ],
@@ -307,7 +307,7 @@ export const Circles = FeatureGroup.extend({
307
307
  'properties.circles.property',
308
308
  {
309
309
  handler: 'Select',
310
- selectOptions: this.datalayer.allProperties(),
310
+ selectOptions: this.datalayer.fieldKeys,
311
311
  label: translate('Property name to compute circles'),
312
312
  },
313
313
  ],
@@ -384,8 +384,7 @@ export const Categorized = FeatureGroup.extend({
384
384
 
385
385
  _getValue: function (feature) {
386
386
  const key =
387
- this.datalayer.properties.categorized.property ||
388
- this.datalayer.allProperties()[0]
387
+ this.datalayer.properties.categorized.property || this.datalayer.fieldKeys[0]
389
388
  return feature.properties[key]
390
389
  },
391
390
 
@@ -438,7 +437,7 @@ export const Categorized = FeatureGroup.extend({
438
437
  'properties.categorized.property',
439
438
  {
440
439
  handler: 'Select',
441
- selectOptions: this.datalayer.allProperties(),
440
+ selectOptions: this.datalayer.fieldKeys,
442
441
  label: translate('Category property'),
443
442
  },
444
443
  ],
@@ -447,7 +446,7 @@ export const Categorized = FeatureGroup.extend({
447
446
  {
448
447
  handler: 'Select',
449
448
  label: translate('Color palette'),
450
- selectOptions: this.getColorSchemes(this._classes),
449
+ getOptions: () => this.getColorSchemes(this._classes),
451
450
  },
452
451
  ],
453
452
  [
@@ -481,6 +480,8 @@ export const Categorized = FeatureGroup.extend({
481
480
  if (builder) builder.helpers['properties.categorized.mode'].fetch()
482
481
  }
483
482
  this.compute()
483
+ // Rebuild list of color palettes when aggregation property changes.
484
+ builder?.helpers['properties.categorized.brewer']?.fetch()
484
485
  // If user changes the mode
485
486
  // then update the categories input value
486
487
  if (field === 'properties.categorized.mode') {
@@ -335,7 +335,7 @@ export const LeafletMap = BaseMap.extend({
335
335
  const datalayer = this._umap.datalayers.visible()[0]
336
336
  let feature
337
337
  if (datalayer) {
338
- const feature = datalayer.getFeatureByIndex(-1)
338
+ const feature = datalayer.features.last()
339
339
  if (feature) {
340
340
  feature.zoomTo({ callback: this.options.noControl ? null : feature.view })
341
341
  return
@@ -238,6 +238,14 @@ export const LeafletMarker = Marker.extend({
238
238
  this._redraw()
239
239
  this._resetZIndex()
240
240
  },
241
+
242
+ _resetZIndex() {
243
+ // Override Leaflet default behaviour, which set the zIndex
244
+ // according to feature's y coordinate, and group features
245
+ // zIndex by their datalayer order
246
+ this._zIndex = this.feature.datalayer.getDOMOrder()
247
+ this._updateZIndex(0)
248
+ },
241
249
  })
242
250
 
243
251
  const PathMixin = {
@@ -296,6 +304,11 @@ const PathMixin = {
296
304
  if (this._tooltip) this._tooltip.setLatLng(this.getCenter())
297
305
  },
298
306
 
307
+ beforeAdd: function (map) {
308
+ this.options.renderer = this.feature.datalayer.renderer
309
+ this.parentClass.prototype.beforeAdd.call(this, map)
310
+ },
311
+
299
312
  onAdd: function (map) {
300
313
  this._container = null
301
314
  FeatureMixin.onAdd.call(this, map)
@@ -4,6 +4,8 @@ import { MutatingForm } from './form/builder.js'
4
4
  import { translate } from './i18n.js'
5
5
  import Orderable from './orderable.js'
6
6
  import * as Utils from './utils.js'
7
+ import * as Icon from './rendering/icon.js'
8
+ import { SCHEMA } from './schema.js'
7
9
 
8
10
  const EMPTY_VALUES = ['', undefined, null]
9
11
 
@@ -12,12 +14,16 @@ class Rule {
12
14
  return this._condition
13
15
  }
14
16
 
17
+ get label() {
18
+ return this.name || this.condition
19
+ }
20
+
15
21
  set condition(value) {
16
22
  this._condition = value
17
23
  this.parse()
18
24
  }
19
25
 
20
- constructor(umap, parent, condition = '', options = {}) {
26
+ constructor(umap, parent, condition = '', name = '', properties = {}) {
21
27
  // TODO make this public properties when browser coverage is ok
22
28
  // cf https://caniuse.com/?search=public%20class%20field
23
29
  this._condition = null
@@ -32,8 +38,9 @@ class Rule {
32
38
  this.parent = parent
33
39
  this._umap = umap
34
40
  this.active = true
35
- this.options = options
41
+ this.properties = properties
36
42
  this.condition = condition
43
+ this.name = name
37
44
  }
38
45
 
39
46
  render(fields) {
@@ -95,7 +102,7 @@ class Rule {
95
102
  }
96
103
 
97
104
  getOption(option) {
98
- return this.options[option]
105
+ return this.properties[option]
99
106
  }
100
107
 
101
108
  edit() {
@@ -108,23 +115,24 @@ class Rule {
108
115
  placeholder: translate('key=value or key!=value'),
109
116
  },
110
117
  ],
111
- 'options.color',
112
- 'options.iconClass',
113
- 'options.iconUrl',
114
- 'options.iconOpacity',
115
- 'options.opacity',
116
- 'options.weight',
117
- 'options.fill',
118
- 'options.fillColor',
119
- 'options.fillOpacity',
120
- 'options.smoothFactor',
121
- 'options.dashArray',
118
+ 'name',
119
+ 'properties.color',
120
+ 'properties.iconClass',
121
+ 'properties.iconUrl',
122
+ 'properties.iconOpacity',
123
+ 'properties.opacity',
124
+ 'properties.weight',
125
+ 'properties.fill',
126
+ 'properties.fillColor',
127
+ 'properties.fillOpacity',
128
+ 'properties.smoothFactor',
129
+ 'properties.dashArray',
122
130
  ]
123
131
  const builder = new MutatingForm(this, options)
124
132
  const container = document.createElement('div')
125
133
  container.appendChild(builder.build())
126
134
  const autocomplete = new AutocompleteDatalist(builder.helpers.condition.input)
127
- const properties = this.parent.allProperties()
135
+ const properties = this.parent.fieldKeys
128
136
  autocomplete.suggestions = properties
129
137
  autocomplete.input.addEventListener('input', (event) => {
130
138
  const value = event.target.value
@@ -143,7 +151,7 @@ class Rule {
143
151
  </button>`)
144
152
  backButton.addEventListener('click', () =>
145
153
  this.parent.edit().then((panel) => {
146
- panel.container.querySelector('details#rules').open = true
154
+ panel.scrollTo('details#rules')
147
155
  })
148
156
  )
149
157
 
@@ -160,7 +168,7 @@ class Rule {
160
168
  <button class="icon icon-16 icon-eye" title="${translate('Toggle rule')}" data-ref=toggle></button>
161
169
  <button class="icon icon-16 icon-edit show-on-edit" title="${translate('Edit')}" data-ref=edit></button>
162
170
  <button class="icon icon-16 icon-delete show-on-edit" title="${translate('Delete rule')}" data-ref=remove></button>
163
- <span>${this.condition || translate('empty rule')}</span>
171
+ <span>${this.label || translate('empty rule')}</span>
164
172
  <i class="icon icon-16 icon-drag" title="${translate('Drag to reorder')}"></i>
165
173
  </li>
166
174
  `
@@ -171,7 +179,7 @@ class Rule {
171
179
  remove.addEventListener('click', () => {
172
180
  if (!confirm(translate('Are you sure you want to delete this rule?'))) return
173
181
  this._delete()
174
- this._umap.editPanel.close()
182
+ this.parent.edit().then((panel) => panel.scrollTo('details#rules'))
175
183
  })
176
184
  toggle.addEventListener('click', () => {
177
185
  this.active = !this.active
@@ -181,8 +189,11 @@ class Rule {
181
189
  }
182
190
 
183
191
  _delete() {
192
+ // TODO refactor this call to update
193
+ const oldRules = Utils.CopyJSON(this.parent.properties.rules || {})
184
194
  this.parent.rules.rules = this.parent.rules.rules.filter((rule) => rule !== this)
185
195
  this.parent.rules.commit()
196
+ this.parent.sync.update('properties.rules', this.parent.properties.rules, oldRules)
186
197
  }
187
198
 
188
199
  setter(key, value) {
@@ -191,6 +202,20 @@ class Rule {
191
202
  this.parent.rules.commit()
192
203
  this.parent.sync.update('properties.rules', this.parent.properties.rules, oldRules)
193
204
  }
205
+
206
+ renderLegend(ul) {
207
+ const [li, { colorBox }] = Utils.loadTemplateWithRefs(
208
+ `<li><span class="color-box" data-ref=colorBox></span>${this.label}</li>`
209
+ )
210
+ const bgcolor = this.properties.color || this.parent.getColor()
211
+ const symbol = this.properties.iconUrl
212
+ colorBox.style.backgroundColor = bgcolor
213
+ if (symbol && symbol !== SCHEMA.iconUrl.default) {
214
+ const icon = Icon.makeElement(symbol, colorBox)
215
+ Icon.setContrast(icon, colorBox, symbol, bgcolor)
216
+ }
217
+ ul.appendChild(li)
218
+ }
194
219
  }
195
220
 
196
221
  export default class Rules {
@@ -203,9 +228,17 @@ export default class Rules {
203
228
  load() {
204
229
  this.rules = []
205
230
  if (!this.parent.properties.rules?.length) return
206
- for (const { condition, options } of this.parent.properties.rules) {
231
+ for (const { condition, name, properties, options } of this.parent.properties
232
+ .rules) {
207
233
  if (!condition) continue
208
- this.rules.push(new Rule(this._umap, this.parent, condition, options))
234
+ const rule = new Rule(
235
+ this._umap,
236
+ this.parent,
237
+ condition,
238
+ name,
239
+ properties || options
240
+ )
241
+ this.rules.push(rule)
209
242
  }
210
243
  }
211
244
 
@@ -250,6 +283,19 @@ export default class Rules {
250
283
  container.appendChild(body)
251
284
  }
252
285
 
286
+ count() {
287
+ return this.rules.length
288
+ }
289
+
290
+ renderLegend(container, keys = new Set()) {
291
+ const ul = Utils.loadTemplate('<ul class="rules-caption"></ul>')
292
+ container.appendChild(ul)
293
+ for (const rule of this.rules) {
294
+ if (keys.size && !keys.has(rule.key)) continue
295
+ rule.renderLegend(ul)
296
+ }
297
+ }
298
+
253
299
  addRule() {
254
300
  const rule = new Rule(this._umap, this.parent)
255
301
  this.rules.push(rule)
@@ -259,17 +305,24 @@ export default class Rules {
259
305
  commit() {
260
306
  this.parent.properties.rules = this.rules.map((rule) => {
261
307
  return {
308
+ name: rule.name,
262
309
  condition: rule.condition,
263
- options: rule.options,
310
+ properties: rule.properties,
264
311
  }
265
312
  })
266
313
  }
267
314
 
268
- getOption(option, feature) {
315
+ getOption(name, feature) {
269
316
  for (const rule of this.rules) {
270
317
  if (rule.match(feature.properties)) {
271
- if (Utils.usableOption(rule.options, option)) return rule.options[option]
318
+ if (Utils.usableOption(rule.properties, name)) return rule.properties[name]
272
319
  }
273
320
  }
274
321
  }
322
+
323
+ *[Symbol.iterator]() {
324
+ for (const rule of this.rules) {
325
+ yield rule
326
+ }
327
+ }
275
328
  }
@@ -147,6 +147,9 @@ export const SCHEMA = {
147
147
  placeholder: translate('Example: key1,key2|Label 2,key3|Label 3|checkbox'),
148
148
  label: translate('Filters keys'),
149
149
  },
150
+ fields: {
151
+ type: Object,
152
+ },
150
153
  fill: {
151
154
  type: Boolean,
152
155
  impacts: ['data'],