umap-project 3.0.6__py3-none-any.whl → 3.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. umap/__init__.py +1 -1
  2. umap/forms.py +1 -1
  3. umap/locale/br/LC_MESSAGES/django.mo +0 -0
  4. umap/locale/br/LC_MESSAGES/django.po +219 -72
  5. umap/locale/ca/LC_MESSAGES/django.mo +0 -0
  6. umap/locale/ca/LC_MESSAGES/django.po +286 -95
  7. umap/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  8. umap/locale/cs_CZ/LC_MESSAGES/django.po +211 -65
  9. umap/locale/da/LC_MESSAGES/django.mo +0 -0
  10. umap/locale/da/LC_MESSAGES/django.po +394 -202
  11. umap/locale/de/LC_MESSAGES/django.mo +0 -0
  12. umap/locale/de/LC_MESSAGES/django.po +146 -75
  13. umap/locale/el/LC_MESSAGES/django.mo +0 -0
  14. umap/locale/el/LC_MESSAGES/django.po +125 -59
  15. umap/locale/en/LC_MESSAGES/django.po +124 -58
  16. umap/locale/es/LC_MESSAGES/django.mo +0 -0
  17. umap/locale/es/LC_MESSAGES/django.po +125 -59
  18. umap/locale/et/LC_MESSAGES/django.mo +0 -0
  19. umap/locale/et/LC_MESSAGES/django.po +210 -64
  20. umap/locale/eu/LC_MESSAGES/django.mo +0 -0
  21. umap/locale/eu/LC_MESSAGES/django.po +212 -65
  22. umap/locale/fa_IR/LC_MESSAGES/django.mo +0 -0
  23. umap/locale/fa_IR/LC_MESSAGES/django.po +286 -95
  24. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  25. umap/locale/fr/LC_MESSAGES/django.po +125 -59
  26. umap/locale/gl/LC_MESSAGES/django.mo +0 -0
  27. umap/locale/gl/LC_MESSAGES/django.po +212 -66
  28. umap/locale/hu/LC_MESSAGES/django.mo +0 -0
  29. umap/locale/hu/LC_MESSAGES/django.po +148 -78
  30. umap/locale/is/LC_MESSAGES/django.mo +0 -0
  31. umap/locale/is/LC_MESSAGES/django.po +130 -60
  32. umap/locale/it/LC_MESSAGES/django.mo +0 -0
  33. umap/locale/it/LC_MESSAGES/django.po +125 -59
  34. umap/locale/ms/LC_MESSAGES/django.mo +0 -0
  35. umap/locale/ms/LC_MESSAGES/django.po +289 -98
  36. umap/locale/nl/LC_MESSAGES/django.mo +0 -0
  37. umap/locale/nl/LC_MESSAGES/django.po +128 -61
  38. umap/locale/pl/LC_MESSAGES/django.mo +0 -0
  39. umap/locale/pl/LC_MESSAGES/django.po +287 -96
  40. umap/locale/pt/LC_MESSAGES/django.mo +0 -0
  41. umap/locale/pt/LC_MESSAGES/django.po +211 -65
  42. umap/locale/zh_TW/LC_MESSAGES/django.mo +0 -0
  43. umap/locale/zh_TW/LC_MESSAGES/django.po +212 -66
  44. umap/management/commands/migrate_to_S3.py +42 -20
  45. umap/management/commands/purge_old_versions.py +63 -0
  46. umap/management/commands/switch_user.py +52 -0
  47. umap/managers.py +29 -2
  48. umap/middleware.py +1 -1
  49. umap/migrations/0028_map_is_template.py +21 -0
  50. umap/models.py +14 -4
  51. umap/settings/base.py +22 -0
  52. umap/static/umap/base.css +4 -2
  53. umap/static/umap/content.css +1 -1
  54. umap/static/umap/css/dialog.css +5 -2
  55. umap/static/umap/css/form.css +19 -12
  56. umap/static/umap/css/icon.css +6 -0
  57. umap/static/umap/css/importers.css +4 -0
  58. umap/static/umap/css/panel.css +2 -0
  59. umap/static/umap/img/16-white.svg +5 -1
  60. umap/static/umap/img/16.svg +1 -1
  61. umap/static/umap/img/24-white.svg +3 -2
  62. umap/static/umap/img/24.svg +3 -4
  63. umap/static/umap/img/importers/opendata.svg +1 -0
  64. umap/static/umap/img/providers/bitbucket.png +0 -0
  65. umap/static/umap/img/source/16-white.svg +8 -4
  66. umap/static/umap/img/source/16.svg +1 -1
  67. umap/static/umap/img/source/24-white.svg +5 -4
  68. umap/static/umap/img/source/24.svg +5 -6
  69. umap/static/umap/js/components/modal.js +27 -0
  70. umap/static/umap/js/modules/caption.js +4 -4
  71. umap/static/umap/js/modules/data/features.js +40 -4
  72. umap/static/umap/js/modules/data/layer.js +208 -138
  73. umap/static/umap/js/modules/form/builder.js +6 -14
  74. umap/static/umap/js/modules/form/fields.js +2 -2
  75. umap/static/umap/js/modules/help.js +11 -3
  76. umap/static/umap/js/modules/importer.js +7 -4
  77. umap/static/umap/js/modules/importers/opendata.js +142 -0
  78. umap/static/umap/js/modules/permissions.js +3 -3
  79. umap/static/umap/js/modules/rendering/controls.js +34 -2
  80. umap/static/umap/js/modules/rendering/icon.js +2 -2
  81. umap/static/umap/js/modules/rendering/layers/base.js +1 -1
  82. umap/static/umap/js/modules/rendering/layers/classified.js +55 -49
  83. umap/static/umap/js/modules/rendering/layers/cluster.js +16 -9
  84. umap/static/umap/js/modules/rendering/layers/heat.js +13 -11
  85. umap/static/umap/js/modules/rendering/map.js +5 -0
  86. umap/static/umap/js/modules/rendering/ui.js +23 -0
  87. umap/static/umap/js/modules/rules.js +24 -23
  88. umap/static/umap/js/modules/schema.js +60 -4
  89. umap/static/umap/js/modules/sync/updaters.js +7 -3
  90. umap/static/umap/js/modules/tableeditor.js +7 -30
  91. umap/static/umap/js/modules/templates.js +122 -0
  92. umap/static/umap/js/modules/ui/bar.js +13 -3
  93. umap/static/umap/js/modules/ui/panel.js +1 -1
  94. umap/static/umap/js/modules/umap.js +28 -13
  95. umap/static/umap/js/umap.controls.js +11 -4
  96. umap/static/umap/locale/br.js +51 -18
  97. umap/static/umap/locale/br.json +51 -18
  98. umap/static/umap/locale/da.js +343 -310
  99. umap/static/umap/locale/da.json +343 -310
  100. umap/static/umap/locale/de.js +40 -7
  101. umap/static/umap/locale/de.json +40 -7
  102. umap/static/umap/locale/el.js +31 -4
  103. umap/static/umap/locale/el.json +31 -4
  104. umap/static/umap/locale/en.js +33 -1
  105. umap/static/umap/locale/en.json +33 -1
  106. umap/static/umap/locale/es.js +37 -4
  107. umap/static/umap/locale/es.json +37 -4
  108. umap/static/umap/locale/fr.js +34 -1
  109. umap/static/umap/locale/fr.json +34 -1
  110. umap/static/umap/locale/hu.js +44 -17
  111. umap/static/umap/locale/hu.json +44 -17
  112. umap/static/umap/locale/it.js +74 -41
  113. umap/static/umap/locale/it.json +74 -41
  114. umap/static/umap/locale/nl.js +42 -9
  115. umap/static/umap/locale/nl.json +42 -9
  116. umap/static/umap/map.css +3 -23
  117. umap/static/umap/vendors/textpath/leaflet.textpath.js +184 -0
  118. umap/storage/fs.py +19 -9
  119. umap/templates/umap/dashboard_menu.html +5 -0
  120. umap/templates/umap/design_system.html +9 -0
  121. umap/templates/umap/js.html +3 -0
  122. umap/templates/umap/map_list.html +2 -2
  123. umap/templates/umap/map_table.html +18 -18
  124. umap/templates/umap/user_dashboard.html +9 -58
  125. umap/templates/umap/user_map_table.html +36 -0
  126. umap/templates/umap/user_templates.html +19 -0
  127. umap/templatetags/umap_tags.py +5 -0
  128. umap/tests/integration/test_conditional_rules.py +57 -0
  129. umap/tests/integration/test_datalayer.py +16 -9
  130. umap/tests/integration/test_edit_marker.py +11 -0
  131. umap/tests/integration/test_tableeditor.py +42 -7
  132. umap/tests/integration/test_templates.py +44 -0
  133. umap/tests/test_dashboard.py +19 -0
  134. umap/tests/test_purge_old_versions.py +91 -0
  135. umap/tests/test_switch_user.py +31 -0
  136. umap/tests/test_views.py +67 -0
  137. umap/urls.py +7 -1
  138. umap/views.py +64 -18
  139. {umap_project-3.0.6.dist-info → umap_project-3.1.1.dist-info}/METADATA +13 -13
  140. {umap_project-3.0.6.dist-info → umap_project-3.1.1.dist-info}/RECORD +143 -130
  141. {umap_project-3.0.6.dist-info → umap_project-3.1.1.dist-info}/WHEEL +0 -0
  142. {umap_project-3.0.6.dist-info → umap_project-3.1.1.dist-info}/entry_points.txt +0 -0
  143. {umap_project-3.0.6.dist-info → umap_project-3.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -27,8 +27,8 @@ export const Cluster = L.MarkerClusterGroup.extend({
27
27
 
28
28
  initialize: function (datalayer) {
29
29
  this.datalayer = datalayer
30
- if (!Utils.isObject(this.datalayer.options.cluster)) {
31
- this.datalayer.options.cluster = {}
30
+ if (!Utils.isObject(this.datalayer.properties.cluster)) {
31
+ this.datalayer.properties.cluster = {}
32
32
  }
33
33
  const options = {
34
34
  polygonOptions: {
@@ -36,8 +36,8 @@ export const Cluster = L.MarkerClusterGroup.extend({
36
36
  },
37
37
  iconCreateFunction: (cluster) => new ClusterIcon(datalayer, cluster),
38
38
  }
39
- if (this.datalayer.options.cluster?.radius) {
40
- options.maxClusterRadius = this.datalayer.options.cluster.radius
39
+ if (this.datalayer.properties.cluster?.radius) {
40
+ options.maxClusterRadius = this.datalayer.properties.cluster.radius
41
41
  }
42
42
  L.MarkerClusterGroup.prototype.initialize.call(this, options)
43
43
  LayerMixin.onInit.call(this, this.datalayer._leafletMap)
@@ -81,9 +81,9 @@ export const Cluster = L.MarkerClusterGroup.extend({
81
81
  return L.MarkerClusterGroup.prototype.removeLayer.call(this, layer)
82
82
  },
83
83
 
84
- getEditableOptions: () => [
84
+ getEditableProperties: () => [
85
85
  [
86
- 'options.cluster.radius',
86
+ 'properties.cluster.radius',
87
87
  {
88
88
  handler: 'BlurIntInput',
89
89
  placeholder: translate('Clustering radius'),
@@ -91,7 +91,7 @@ export const Cluster = L.MarkerClusterGroup.extend({
91
91
  },
92
92
  ],
93
93
  [
94
- 'options.cluster.textColor',
94
+ 'properties.cluster.textColor',
95
95
  {
96
96
  handler: 'TextColorPicker',
97
97
  placeholder: translate('Auto'),
@@ -101,13 +101,20 @@ export const Cluster = L.MarkerClusterGroup.extend({
101
101
  ],
102
102
 
103
103
  onEdit: function (field, builder) {
104
- if (field === 'options.cluster.radius') {
104
+ if (field === 'properties.cluster.radius') {
105
105
  // No way to reset radius of an already instanciated MarkerClusterGroup...
106
106
  this.datalayer.resetLayer(true)
107
107
  return
108
108
  }
109
- if (field === 'options.color') {
109
+ if (field === 'properties.color') {
110
110
  this.options.polygonOptions.color = this.datalayer.getColor()
111
111
  }
112
112
  },
113
+
114
+ _moveChild: (layer, from, to) => {
115
+ // Extend parent method, so to remove remove/addLayer,
116
+ // to let our own dragend event listener be called
117
+ // cf https://github.com/umap-project/umap/issues/2749
118
+ layer._latlng = to
119
+ },
113
120
  })
@@ -20,10 +20,10 @@ export const Heat = L.HeatLayer.extend({
20
20
 
21
21
  initialize: function (datalayer) {
22
22
  this.datalayer = datalayer
23
- L.HeatLayer.prototype.initialize.call(this, [], this.datalayer.options.heat)
23
+ L.HeatLayer.prototype.initialize.call(this, [], this.datalayer.properties.heat)
24
24
  LayerMixin.onInit.call(this, this.datalayer._leafletMap)
25
- if (!Utils.isObject(this.datalayer.options.heat)) {
26
- this.datalayer.options.heat = {}
25
+ if (!Utils.isObject(this.datalayer.properties.heat)) {
26
+ this.datalayer.properties.heat = {}
27
27
  }
28
28
  },
29
29
 
@@ -31,9 +31,11 @@ export const Heat = L.HeatLayer.extend({
31
31
  if (layer instanceof Marker) {
32
32
  let latlng = layer.getLatLng()
33
33
  let alt
34
- if (this.datalayer.options.heat?.intensityProperty) {
34
+ if (this.datalayer.properties.heat?.intensityProperty) {
35
35
  alt = Number.parseFloat(
36
- layer.feature.properties[this.datalayer.options.heat.intensityProperty || 0]
36
+ layer.feature.properties[
37
+ this.datalayer.properties.heat.intensityProperty || 0
38
+ ]
37
39
  )
38
40
  latlng = new LatLng(latlng.lat, latlng.lng, alt)
39
41
  }
@@ -66,9 +68,9 @@ export const Heat = L.HeatLayer.extend({
66
68
  return latLngBounds(this._latlngs)
67
69
  },
68
70
 
69
- getEditableOptions: () => [
71
+ getEditableProperties: () => [
70
72
  [
71
- 'options.heat.radius',
73
+ 'properties.heat.radius',
72
74
  {
73
75
  handler: 'Range',
74
76
  min: 10,
@@ -79,7 +81,7 @@ export const Heat = L.HeatLayer.extend({
79
81
  },
80
82
  ],
81
83
  [
82
- 'options.heat.intensityProperty',
84
+ 'properties.heat.intensityProperty',
83
85
  {
84
86
  handler: 'BlurInput',
85
87
  placeholder: translate('Heatmap intensity property'),
@@ -89,12 +91,12 @@ export const Heat = L.HeatLayer.extend({
89
91
  ],
90
92
 
91
93
  onEdit: function (field, builder) {
92
- if (field === 'options.heat.intensityProperty') {
94
+ if (field === 'properties.heat.intensityProperty') {
93
95
  this.datalayer.resetLayer(true) // We need to repopulate the latlngs
94
96
  return
95
97
  }
96
- if (field === 'options.heat.radius') {
97
- this.options.radius = this.datalayer.options.heat.radius
98
+ if (field === 'properties.heat.radius') {
99
+ this.options.radius = this.datalayer.properties.heat.radius
98
100
  }
99
101
  this._updateOptions()
100
102
  },
@@ -21,6 +21,7 @@ import {
21
21
  MoreControl,
22
22
  PermanentCreditsControl,
23
23
  TileLayerChooser,
24
+ LoadTemplateControl,
24
25
  } from './controls.js'
25
26
  import * as Utils from '../utils.js'
26
27
  import * as Icon from './icon.js'
@@ -50,6 +51,10 @@ const ControlsMixin = {
50
51
  initControls: function () {
51
52
  this._controls = {}
52
53
 
54
+ if (this._umap.properties.is_template && !this.options.noControl) {
55
+ new LoadTemplateControl(this).addTo(this)
56
+ }
57
+
53
58
  if (this._umap.hasEditMode() && !this.options.noControl) {
54
59
  new EditControl(this).addTo(this)
55
60
  }
@@ -372,6 +372,29 @@ export const LeafletPolyline = Polyline.extend({
372
372
  parentClass: Polyline,
373
373
  includes: [FeatureMixin, PathMixin],
374
374
 
375
+ setStyle: function (options) {
376
+ PathMixin.setStyle.call(this, options)
377
+ this.setText(null) // Reset.
378
+ const textPath = this.feature.getDynamicOption('textPath')
379
+ if (textPath) {
380
+ const color =
381
+ this.feature.getOption('textPathColor') ||
382
+ this.feature.getDynamicOption('color')
383
+ const textPathOptions = {
384
+ repeat: this.feature.getOption('textPathRepeat'),
385
+ offset: this.feature.getOption('textPathOffset') || undefined,
386
+ position: this.feature.getOption('textPathPosition'),
387
+ attributes: {
388
+ fill: color,
389
+ opacity: this.feature.getDynamicOption('opacity'),
390
+ rotate: this.feature.getOption('textPathRotate'),
391
+ 'font-size': this.feature.getOption('textPathSize'),
392
+ },
393
+ }
394
+ this.setText(textPath, textPathOptions)
395
+ }
396
+ },
397
+
375
398
  getClass: () => LeafletPolyline,
376
399
 
377
400
  getMeasure: function (shape) {
@@ -17,7 +17,7 @@ class Rule {
17
17
  this.parse()
18
18
  }
19
19
 
20
- constructor(umap, condition = '', options = {}) {
20
+ constructor(umap, parent, condition = '', options = {}) {
21
21
  // TODO make this public properties when browser coverage is ok
22
22
  // cf https://caniuse.com/?search=public%20class%20field
23
23
  this._condition = null
@@ -29,6 +29,7 @@ class Rule {
29
29
  ['!=', this.not_equal],
30
30
  ['=', this.equal],
31
31
  ]
32
+ this.parent = parent
32
33
  this._umap = umap
33
34
  this.active = true
34
35
  this.options = options
@@ -36,7 +37,7 @@ class Rule {
36
37
  }
37
38
 
38
39
  render(fields) {
39
- this._umap.render(fields)
40
+ this.parent.render(fields)
40
41
  }
41
42
 
42
43
  equal(other) {
@@ -123,7 +124,7 @@ class Rule {
123
124
  const container = document.createElement('div')
124
125
  container.appendChild(builder.build())
125
126
  const autocomplete = new AutocompleteDatalist(builder.helpers.condition.input)
126
- const properties = this._umap.allProperties()
127
+ const properties = this.parent.allProperties()
127
128
  autocomplete.suggestions = properties
128
129
  autocomplete.input.addEventListener('input', (event) => {
129
130
  const value = event.target.value
@@ -131,9 +132,9 @@ class Rule {
131
132
  autocomplete.suggestions = [`${value}=`, `${value}!=`, `${value}>`, `${value}<`]
132
133
  } else if (value.endsWith('=')) {
133
134
  const key = value.split('!')[0].split('=')[0]
134
- autocomplete.suggestions = this._umap
135
+ autocomplete.suggestions = this.parent
135
136
  .sortedValues(key)
136
- .map((str) => `${value}${str || ''}`)
137
+ .map((str) => `${value}${str ?? ''}`)
137
138
  }
138
139
  })
139
140
  const backButton = Utils.loadTemplate(`
@@ -141,8 +142,8 @@ class Rule {
141
142
  <i class="icon icon-16 icon-back" title="${translate('Back to list')}"></i>
142
143
  </button>`)
143
144
  backButton.addEventListener('click', () =>
144
- this._umap.edit().then(() => {
145
- this._umap.editPanel.container.querySelector('details#rules').open = true
145
+ this.parent.edit().then((panel) => {
146
+ panel.container.querySelector('details#rules').open = true
146
147
  })
147
148
  )
148
149
 
@@ -175,40 +176,41 @@ class Rule {
175
176
  toggle.addEventListener('click', () => {
176
177
  this.active = !this.active
177
178
  li.classList.toggle('off', !this.active)
178
- this._umap.render(['rules'])
179
+ this.parent.render(['rules'])
179
180
  })
180
181
  }
181
182
 
182
183
  _delete() {
183
- this._umap.rules.rules = this._umap.rules.rules.filter((rule) => rule !== this)
184
- this._umap.rules.commit()
184
+ this.parent.rules.rules = this.parent.rules.rules.filter((rule) => rule !== this)
185
+ this.parent.rules.commit()
185
186
  }
186
187
 
187
188
  setter(key, value) {
188
- const oldRules = Utils.CopyJSON(this._umap.properties.rules || {})
189
+ const oldRules = Utils.CopyJSON(this.parent.properties.rules || {})
189
190
  Utils.setObjectValue(this, key, value)
190
- this._umap.rules.commit()
191
- this._umap.sync.update('properties.rules', this._umap.properties.rules, oldRules)
191
+ this.parent.rules.commit()
192
+ this.parent.sync.update('properties.rules', this.parent.properties.rules, oldRules)
192
193
  }
193
194
  }
194
195
 
195
196
  export default class Rules {
196
- constructor(umap) {
197
+ constructor(umap, parent) {
197
198
  this._umap = umap
199
+ this.parent = parent
198
200
  this.load()
199
201
  }
200
202
 
201
203
  load() {
202
204
  this.rules = []
203
- if (!this._umap.properties.rules?.length) return
204
- for (const { condition, options } of this._umap.properties.rules) {
205
+ if (!this.parent.properties.rules?.length) return
206
+ for (const { condition, options } of this.parent.properties.rules) {
205
207
  if (!condition) continue
206
- this.rules.push(new Rule(this._umap, condition, options))
208
+ this.rules.push(new Rule(this._umap, this.parent, condition, options))
207
209
  }
208
210
  }
209
211
 
210
212
  onReorder(src, dst, initialIndex, finalIndex) {
211
- const oldRules = Utils.CopyJSON(this._umap.properties.rules || {})
213
+ const oldRules = Utils.CopyJSON(this.parent.properties.rules || {})
212
214
  const moved = this.rules.find((rule) => stamp(rule) === +src.dataset.id)
213
215
  const reference = this.rules.find((rule) => stamp(rule) === +dst.dataset.id)
214
216
  const movedIdx = this.rules.indexOf(moved)
@@ -222,9 +224,9 @@ export default class Rules {
222
224
  else if (finalIndex > initialIndex) newIdx = referenceIdx
223
225
  else newIdx = referenceIdx + 1
224
226
  this.rules.splice(newIdx, 0, moved)
225
- this._umap.render(['rules'])
227
+ this.parent.render(['rules'])
226
228
  this.commit()
227
- this._umap.sync.update('properties.rules', this._umap.properties.rules, oldRules)
229
+ this.parent.sync.update('properties.rules', this.parent.properties.rules, oldRules)
228
230
  }
229
231
 
230
232
  edit(container) {
@@ -249,13 +251,13 @@ export default class Rules {
249
251
  }
250
252
 
251
253
  addRule() {
252
- const rule = new Rule(this._umap)
254
+ const rule = new Rule(this._umap, this.parent)
253
255
  this.rules.push(rule)
254
256
  rule.edit(map)
255
257
  }
256
258
 
257
259
  commit() {
258
- this._umap.properties.rules = this.rules.map((rule) => {
260
+ this.parent.properties.rules = this.rules.map((rule) => {
259
261
  return {
260
262
  condition: rule.condition,
261
263
  options: rule.options,
@@ -267,7 +269,6 @@ export default class Rules {
267
269
  for (const rule of this.rules) {
268
270
  if (rule.match(feature.properties)) {
269
271
  if (Utils.usableOption(rule.options, option)) return rule.options[option]
270
- break
271
272
  }
272
273
  }
273
274
  }
@@ -51,7 +51,6 @@ export const SCHEMA = {
51
51
  color: {
52
52
  type: String,
53
53
  impacts: ['data'],
54
- handler: 'ColorPicker',
55
54
  label: translate('color'),
56
55
  helpEntries: ['colorValue'],
57
56
  inheritable: true,
@@ -84,7 +83,6 @@ export const SCHEMA = {
84
83
  type: Boolean,
85
84
  impacts: ['ui'],
86
85
  nullable: true,
87
- handler: 'DataLayersControl',
88
86
  label: translate('Display the open browser control'),
89
87
  default: true,
90
88
  },
@@ -160,7 +158,6 @@ export const SCHEMA = {
160
158
  fillColor: {
161
159
  type: String,
162
160
  impacts: ['data'],
163
- handler: 'ColorPicker',
164
161
  label: translate('fill color'),
165
162
  helpEntries: ['fillColor'],
166
163
  inheritable: true,
@@ -237,7 +234,6 @@ export const SCHEMA = {
237
234
  iconUrl: {
238
235
  type: String,
239
236
  impacts: ['data'],
240
- handler: 'IconUrl',
241
237
  label: translate('Icon symbol'),
242
238
  inheritable: true,
243
239
  },
@@ -253,6 +249,12 @@ export const SCHEMA = {
253
249
  inheritable: true,
254
250
  default: true,
255
251
  },
252
+ is_template: {
253
+ type: Boolean,
254
+ impacts: ['ui'],
255
+ label: translate('This map is a template'),
256
+ default: false,
257
+ },
256
258
  labelDirection: {
257
259
  type: String,
258
260
  impacts: ['data'],
@@ -539,6 +541,60 @@ export const SCHEMA = {
539
541
  team: {
540
542
  type: Object,
541
543
  },
544
+ textPath: {
545
+ type: String,
546
+ impacts: ['data'],
547
+ label: translate('Add text along path'),
548
+ },
549
+ textPathColor: {
550
+ type: String,
551
+ impacts: ['data'],
552
+ label: translate('Text color'),
553
+ },
554
+ textPathOffset: {
555
+ type: Number,
556
+ label: translate('Text offset'),
557
+ impacts: ['data'],
558
+ default: 1,
559
+ min: -20,
560
+ max: 20,
561
+ step: 1,
562
+ },
563
+ textPathPosition: {
564
+ type: String,
565
+ impacts: ['data'],
566
+ label: translate('Text position'),
567
+ default: 'center',
568
+ choices: [
569
+ ['start', translate('start')],
570
+ ['center', translate('center')],
571
+ ['end', translate('end')],
572
+ ],
573
+ },
574
+ textPathRepeat: {
575
+ type: Boolean,
576
+ label: translate('Text repeat'),
577
+ impacts: ['data'],
578
+ default: true,
579
+ },
580
+ textPathRotate: {
581
+ type: Number,
582
+ label: translate('Text rotate'),
583
+ impacts: ['data'],
584
+ default: 0,
585
+ min: 0,
586
+ max: 360,
587
+ step: 1,
588
+ },
589
+ textPathSize: {
590
+ type: Number,
591
+ label: translate('Text size'),
592
+ impacts: ['data'],
593
+ default: 20,
594
+ min: 10,
595
+ max: 30,
596
+ step: 1,
597
+ },
542
598
  tilelayer: {
543
599
  type: Object,
544
600
  impacts: ['background'],
@@ -52,8 +52,8 @@ export class DataLayerUpdater extends BaseUpdater {
52
52
 
53
53
  update({ key, metadata, value }) {
54
54
  const datalayer = this.getDataLayerFromID(metadata.id)
55
- if (key === 'options') {
56
- datalayer.setOptions(value)
55
+ if (key === 'properties') {
56
+ datalayer.setProperties(value)
57
57
  } else if (Utils.fieldInSchema(key)) {
58
58
  Utils.setObjectValue(datalayer, key, value)
59
59
  } else {
@@ -109,7 +109,11 @@ export class FeatureUpdater extends BaseUpdater {
109
109
  feature.geometry = value
110
110
  } else {
111
111
  Utils.setObjectValue(feature, key, value)
112
- feature.datalayer.indexProperties(feature)
112
+ if (key.startsWith('properties')) {
113
+ feature.datalayer.indexProperties(feature)
114
+ const name = key.replace('properties.', '')
115
+ feature.datalayer.checkIndexForProperty(name)
116
+ }
113
117
  }
114
118
 
115
119
  feature.render([key])
@@ -96,7 +96,7 @@ export default class TableEditor extends WithTemplate {
96
96
  if (inBbox && !feature.isOnScreen(bounds)) return
97
97
  const tds = this.properties.map(
98
98
  (prop) =>
99
- `<td tabindex="0" data-property="${prop}">${feature.properties[prop] || ''}</td>`
99
+ `<td tabindex="0" data-property="${prop}">${feature.properties[prop] ?? ''}</td>`
100
100
  )
101
101
  html += `<tr data-feature="${feature.id}"><th><input type="checkbox" /></th>${tds.join('')}</tr>`
102
102
  })
@@ -110,28 +110,12 @@ export default class TableEditor extends WithTemplate {
110
110
  }
111
111
  }
112
112
 
113
- validateName(name) {
114
- if (name.includes('.')) {
115
- U.Alert.error(translate('Name “{name}” should not contain a dot.', { name }))
116
- return false
117
- }
118
- if (this.properties.includes(name)) {
119
- U.Alert.error(translate('This name already exists: “{name}”', { name }))
120
- return false
121
- }
122
- return true
123
- }
124
-
125
113
  renameProperty(property) {
126
114
  this._umap.dialog
127
115
  .prompt(translate('Please enter the new name of this property'))
128
116
  .then(({ prompt }) => {
129
- if (!prompt || !this.validateName(prompt)) return
130
- this.datalayer.eachFeature((feature) => {
131
- feature.renameProperty(property, prompt)
132
- })
133
- this.datalayer.deindexProperty(property)
134
- this.datalayer.indexProperty(prompt)
117
+ if (!prompt || !this.datalayer.validateName(prompt)) return
118
+ this.datalayer.renameProperty(property, prompt)
135
119
  this.open()
136
120
  })
137
121
  }
@@ -142,23 +126,16 @@ export default class TableEditor extends WithTemplate {
142
126
  translate('Are you sure you want to delete this property on all the features?')
143
127
  )
144
128
  .then(() => {
145
- this.datalayer.eachFeature((feature) => {
146
- feature.deleteProperty(property)
147
- })
148
- this.datalayer.deindexProperty(property)
129
+ this.datalayer.deleteProperty(property)
149
130
  this.resetProperties()
150
131
  this.open()
151
132
  })
152
133
  }
153
134
 
154
135
  addProperty() {
155
- this._umap.dialog
156
- .prompt(translate('Please enter the name of the property'))
157
- .then(({ prompt }) => {
158
- if (!prompt || !this.validateName(prompt)) return
159
- this.datalayer.indexProperty(prompt)
160
- this.open()
161
- })
136
+ this.datalayer.addProperty().then(() => {
137
+ this.open()
138
+ })
162
139
  }
163
140
 
164
141
  open() {
@@ -0,0 +1,122 @@
1
+ import { uMapAlert as Alert } from '../components/alerts/alert.js'
2
+ import { translate } from './i18n.js'
3
+ import * as Utils from './utils.js'
4
+
5
+ const TEMPLATE = `
6
+ <div>
7
+ <form data-ref="form">
8
+ <h3><i class="icon icon-24 icon-template"></i>${translate('Load map template')}</h3>
9
+ <p>${translate('Loading a template will apply predefined styles and settings to your map')}.</p>
10
+ <div class="formbox">
11
+ <div class="flat-tabs" data-ref="tabs">
12
+ <button type="button" class="flat" data-value="mine" data-ref="mine">${translate('My templates')}</button>
13
+ <button type="button" class="flat" data-value="staff">${translate('From staff')}</button>
14
+ <button type="button" class="flat" data-value="community">${translate('From community')}</button>
15
+ </div>
16
+ <div data-ref="body" class="body"></div>
17
+ <div class="button-bar half">
18
+ <button type="button" class="primary" data-ref="confirm" disabled>${translate('Load template')}</button>
19
+ <button type="button" data-ref="confirmData" disabled>${translate('Load template with data')}</button>
20
+ </div>
21
+ </div>
22
+ </form>
23
+ </div>
24
+ `
25
+
26
+ export default class TemplateImporter {
27
+ constructor(umap) {
28
+ this.umap = umap
29
+ }
30
+
31
+ async open() {
32
+ const [root, { tabs, form, body, mine, confirm, confirmData }] =
33
+ Utils.loadTemplateWithRefs(TEMPLATE)
34
+ const uri = this.umap.urls.get('template_list')
35
+ const userIsAuth = Boolean(this.umap.properties.user?.id)
36
+ const defaultTab = userIsAuth ? 'mine' : 'staff'
37
+ mine.hidden = !userIsAuth
38
+
39
+ const loadTemplates = async (source) => {
40
+ const [data, response, error] = await this.umap.server.get(
41
+ `${uri}?source=${source}`
42
+ )
43
+ if (!error) {
44
+ body.innerHTML = ''
45
+ if (!data.templates.length) {
46
+ let message
47
+ switch (source) {
48
+ case 'mine':
49
+ message = translate(
50
+ 'You have no registered template yet. You can add one by creating a new map and flagging it as "template".'
51
+ )
52
+ break
53
+ case 'staff':
54
+ message = translate(
55
+ 'There is no recommended template yet. Recommended templates are the ones starred by uMap administrators.'
56
+ )
57
+ break
58
+ case 'community':
59
+ message = translate('There is no public template yet.')
60
+ break
61
+ }
62
+ body.textContent = message
63
+ }
64
+ for (const template of data.templates) {
65
+ const item = Utils.loadTemplate(
66
+ `<dl>
67
+ <dt>
68
+ <label>
69
+ <input type="radio" value="${template.id}" name="template" />${template.name}
70
+ <a href="${template.url}" target="_blank"><nobr>${translate('Explore')}<i class="icon icon-16 icon-external-link"></i></nobr></a>
71
+ </label>
72
+ </dt>
73
+ <dd class="text">${Utils.toHTML(template.description)}</dd>
74
+ </dl>`
75
+ )
76
+ body.appendChild(item)
77
+ }
78
+ tabs.querySelectorAll('button').forEach((el) => el.classList.remove('on'))
79
+ tabs.querySelector(`[data-value="${source}"]`).classList.add('on')
80
+ } else {
81
+ console.error(response)
82
+ }
83
+ }
84
+ loadTemplates(defaultTab)
85
+ tabs
86
+ .querySelectorAll('button')
87
+ .forEach((el) =>
88
+ el.addEventListener('click', () => loadTemplates(el.dataset.value))
89
+ )
90
+ form.addEventListener('change', () => {
91
+ if (form.template.value) {
92
+ confirm.disabled = false
93
+ confirmData.disabled = false
94
+ }
95
+ })
96
+ const onConfirm = (includeData) => {
97
+ const templateId = form.template.value
98
+ if (!templateId) {
99
+ Alert.error(translate('You must select a template.'))
100
+ return false
101
+ }
102
+ let url = this.umap.urls.get('map_download', {
103
+ map_id: templateId,
104
+ })
105
+ if (!includeData) {
106
+ url = `${url}?include_data=0`
107
+ }
108
+ this.umap.importer.build()
109
+ this.umap.importer.url = url
110
+ this.umap.importer.format = 'umap'
111
+ this.umap.importer.submit()
112
+ this.umap.editPanel.close()
113
+ }
114
+ confirm.addEventListener('click', () => onConfirm(false))
115
+ confirmData.addEventListener('click', () => onConfirm(true))
116
+
117
+ this.umap.editPanel.open({
118
+ content: root,
119
+ highlight: 'templates',
120
+ })
121
+ }
122
+ }