umap-project 3.3.2__py3-none-any.whl → 3.4.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.
Files changed (242) hide show
  1. umap/__init__.py +1 -1
  2. umap/context_processors.py +4 -1
  3. umap/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  4. umap/locale/cs_CZ/LC_MESSAGES/django.po +43 -33
  5. umap/locale/da/LC_MESSAGES/django.mo +0 -0
  6. umap/locale/da/LC_MESSAGES/django.po +43 -33
  7. umap/locale/de/LC_MESSAGES/django.mo +0 -0
  8. umap/locale/de/LC_MESSAGES/django.po +64 -53
  9. umap/locale/el/LC_MESSAGES/django.mo +0 -0
  10. umap/locale/el/LC_MESSAGES/django.po +35 -29
  11. umap/locale/en/LC_MESSAGES/django.po +47 -41
  12. umap/locale/es/LC_MESSAGES/django.mo +0 -0
  13. umap/locale/es/LC_MESSAGES/django.po +43 -33
  14. umap/locale/et/LC_MESSAGES/django.mo +0 -0
  15. umap/locale/et/LC_MESSAGES/django.po +58 -54
  16. umap/locale/eu/LC_MESSAGES/django.mo +0 -0
  17. umap/locale/eu/LC_MESSAGES/django.po +43 -33
  18. umap/locale/fa_IR/LC_MESSAGES/django.mo +0 -0
  19. umap/locale/fa_IR/LC_MESSAGES/django.po +43 -33
  20. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  21. umap/locale/fr/LC_MESSAGES/django.po +36 -30
  22. umap/locale/gl/LC_MESSAGES/django.mo +0 -0
  23. umap/locale/gl/LC_MESSAGES/django.po +43 -33
  24. umap/locale/hu/LC_MESSAGES/django.mo +0 -0
  25. umap/locale/hu/LC_MESSAGES/django.po +35 -29
  26. umap/locale/is/LC_MESSAGES/django.mo +0 -0
  27. umap/locale/is/LC_MESSAGES/django.po +43 -33
  28. umap/locale/it/LC_MESSAGES/django.mo +0 -0
  29. umap/locale/it/LC_MESSAGES/django.po +43 -33
  30. umap/locale/nl/LC_MESSAGES/django.mo +0 -0
  31. umap/locale/nl/LC_MESSAGES/django.po +35 -29
  32. umap/locale/pl/LC_MESSAGES/django.mo +0 -0
  33. umap/locale/pl/LC_MESSAGES/django.po +114 -103
  34. umap/locale/pt/LC_MESSAGES/django.mo +0 -0
  35. umap/locale/pt/LC_MESSAGES/django.po +43 -33
  36. umap/locale/th_TH/LC_MESSAGES/django.mo +0 -0
  37. umap/locale/th_TH/LC_MESSAGES/django.po +310 -109
  38. umap/locale/zh_TW/LC_MESSAGES/django.mo +0 -0
  39. umap/locale/zh_TW/LC_MESSAGES/django.po +80 -70
  40. umap/management/commands/switch_user.py +2 -2
  41. umap/migrations/0018_datalayer_uuid.py +1 -1
  42. umap/models.py +7 -3
  43. umap/settings/local.py.sample +1 -1
  44. umap/static/umap/base.css +89 -32
  45. umap/static/umap/content.css +129 -33
  46. umap/static/umap/css/bar.css +82 -20
  47. umap/static/umap/css/browser.css +163 -0
  48. umap/static/umap/css/contextmenu.css +15 -0
  49. umap/static/umap/css/dialog.css +36 -16
  50. umap/static/umap/css/form.css +123 -33
  51. umap/static/umap/css/icon.css +46 -3
  52. umap/static/umap/css/panel.css +7 -3
  53. umap/static/umap/css/popup.css +34 -8
  54. umap/static/umap/css/tooltip.css +8 -4
  55. umap/static/umap/img/16-white.svg +26 -8
  56. umap/static/umap/img/16.svg +1 -1
  57. umap/static/umap/img/source/16-white.svg +36 -18
  58. umap/static/umap/img/source/16.svg +1 -1
  59. umap/static/umap/js/components/alerts/alert.css +69 -31
  60. umap/static/umap/js/components/alerts/alert.js +20 -2
  61. umap/static/umap/js/components/base.js +1 -1
  62. umap/static/umap/js/modules/browser.js +69 -61
  63. umap/static/umap/js/modules/caption.js +10 -7
  64. umap/static/umap/js/modules/data/features.js +89 -63
  65. umap/static/umap/js/modules/data/fields.js +446 -0
  66. umap/static/umap/js/modules/data/layer.js +116 -196
  67. umap/static/umap/js/modules/domutils.js +109 -0
  68. umap/static/umap/js/modules/filters.js +780 -0
  69. umap/static/umap/js/modules/form/builder.js +8 -5
  70. umap/static/umap/js/modules/form/fields.js +111 -221
  71. umap/static/umap/js/modules/formatter.js +24 -1
  72. umap/static/umap/js/modules/help.js +4 -3
  73. umap/static/umap/js/modules/i18n.js +1 -1
  74. umap/static/umap/js/modules/importer.js +1 -1
  75. umap/static/umap/js/modules/importers/opendata.js +15 -0
  76. umap/static/umap/js/modules/importers/openrouteservice.js +6 -1
  77. umap/static/umap/js/modules/managers.js +2 -1
  78. umap/static/umap/js/modules/permissions.js +39 -31
  79. umap/static/umap/js/modules/rendering/controls.js +11 -9
  80. umap/static/umap/js/modules/rendering/icon.js +3 -8
  81. umap/static/umap/js/modules/rendering/layers/base.js +3 -3
  82. umap/static/umap/js/modules/rendering/layers/classified.js +18 -11
  83. umap/static/umap/js/modules/rendering/layers/cluster.js +23 -11
  84. umap/static/umap/js/modules/rendering/layers/heat.js +27 -21
  85. umap/static/umap/js/modules/rendering/map.js +1 -0
  86. umap/static/umap/js/modules/rendering/template.js +50 -23
  87. umap/static/umap/js/modules/rendering/ui.js +33 -25
  88. umap/static/umap/js/modules/rules.js +38 -44
  89. umap/static/umap/js/modules/schema.js +3 -6
  90. umap/static/umap/js/modules/share.js +5 -4
  91. umap/static/umap/js/modules/tableeditor.js +50 -38
  92. umap/static/umap/js/modules/templates.js +2 -3
  93. umap/static/umap/js/modules/ui/bar.js +55 -23
  94. umap/static/umap/js/modules/ui/dialog.js +38 -27
  95. umap/static/umap/js/modules/ui/panel.js +23 -8
  96. umap/static/umap/js/modules/ui/tooltip.js +6 -5
  97. umap/static/umap/js/modules/umap.js +158 -51
  98. umap/static/umap/js/modules/utils.js +24 -2
  99. umap/static/umap/js/umap.core.js +1 -110
  100. umap/static/umap/locale/am_ET.js +52 -17
  101. umap/static/umap/locale/am_ET.json +52 -17
  102. umap/static/umap/locale/ar.js +52 -17
  103. umap/static/umap/locale/ar.json +52 -17
  104. umap/static/umap/locale/ast.js +52 -17
  105. umap/static/umap/locale/ast.json +52 -17
  106. umap/static/umap/locale/bg.js +52 -17
  107. umap/static/umap/locale/bg.json +52 -17
  108. umap/static/umap/locale/br.js +48 -22
  109. umap/static/umap/locale/br.json +48 -22
  110. umap/static/umap/locale/ca.js +52 -17
  111. umap/static/umap/locale/ca.json +52 -17
  112. umap/static/umap/locale/cs_CZ.js +52 -17
  113. umap/static/umap/locale/cs_CZ.json +52 -17
  114. umap/static/umap/locale/da.js +54 -17
  115. umap/static/umap/locale/da.json +54 -17
  116. umap/static/umap/locale/de.js +102 -67
  117. umap/static/umap/locale/de.json +102 -67
  118. umap/static/umap/locale/el.js +52 -17
  119. umap/static/umap/locale/el.json +52 -17
  120. umap/static/umap/locale/en.js +54 -16
  121. umap/static/umap/locale/en.json +54 -16
  122. umap/static/umap/locale/en_US.json +52 -17
  123. umap/static/umap/locale/es.js +54 -17
  124. umap/static/umap/locale/es.json +54 -17
  125. umap/static/umap/locale/et.js +91 -56
  126. umap/static/umap/locale/et.json +91 -56
  127. umap/static/umap/locale/eu.js +84 -49
  128. umap/static/umap/locale/eu.json +84 -49
  129. umap/static/umap/locale/fa_IR.js +52 -17
  130. umap/static/umap/locale/fa_IR.json +52 -17
  131. umap/static/umap/locale/fi.js +52 -17
  132. umap/static/umap/locale/fi.json +52 -17
  133. umap/static/umap/locale/fr.js +54 -17
  134. umap/static/umap/locale/fr.json +54 -17
  135. umap/static/umap/locale/gl.js +52 -17
  136. umap/static/umap/locale/gl.json +52 -17
  137. umap/static/umap/locale/he.js +52 -17
  138. umap/static/umap/locale/he.json +52 -17
  139. umap/static/umap/locale/hr.js +52 -17
  140. umap/static/umap/locale/hr.json +52 -17
  141. umap/static/umap/locale/hu.js +59 -24
  142. umap/static/umap/locale/hu.json +59 -24
  143. umap/static/umap/locale/id.js +52 -17
  144. umap/static/umap/locale/id.json +52 -17
  145. umap/static/umap/locale/is.js +52 -17
  146. umap/static/umap/locale/is.json +52 -17
  147. umap/static/umap/locale/it.js +52 -17
  148. umap/static/umap/locale/it.json +52 -17
  149. umap/static/umap/locale/ja.js +52 -17
  150. umap/static/umap/locale/ja.json +52 -17
  151. umap/static/umap/locale/ko.js +52 -17
  152. umap/static/umap/locale/ko.json +52 -17
  153. umap/static/umap/locale/lt.js +52 -17
  154. umap/static/umap/locale/lt.json +52 -17
  155. umap/static/umap/locale/ms.js +52 -17
  156. umap/static/umap/locale/ms.json +52 -17
  157. umap/static/umap/locale/nl.js +52 -17
  158. umap/static/umap/locale/nl.json +52 -17
  159. umap/static/umap/locale/no.js +52 -17
  160. umap/static/umap/locale/no.json +52 -17
  161. umap/static/umap/locale/pl.js +53 -17
  162. umap/static/umap/locale/pl.json +53 -17
  163. umap/static/umap/locale/pl_PL.json +52 -17
  164. umap/static/umap/locale/pt.js +52 -17
  165. umap/static/umap/locale/pt.json +52 -17
  166. umap/static/umap/locale/pt_BR.js +52 -17
  167. umap/static/umap/locale/pt_BR.json +52 -17
  168. umap/static/umap/locale/pt_PT.js +52 -17
  169. umap/static/umap/locale/pt_PT.json +52 -17
  170. umap/static/umap/locale/ro.js +52 -17
  171. umap/static/umap/locale/ro.json +52 -17
  172. umap/static/umap/locale/ru.js +52 -17
  173. umap/static/umap/locale/ru.json +52 -17
  174. umap/static/umap/locale/si.js +1 -1
  175. umap/static/umap/locale/si.json +1 -1
  176. umap/static/umap/locale/sk_SK.js +52 -17
  177. umap/static/umap/locale/sk_SK.json +52 -17
  178. umap/static/umap/locale/sl.js +52 -17
  179. umap/static/umap/locale/sl.json +52 -17
  180. umap/static/umap/locale/sr.js +52 -17
  181. umap/static/umap/locale/sr.json +52 -17
  182. umap/static/umap/locale/sv.js +52 -17
  183. umap/static/umap/locale/sv.json +52 -17
  184. umap/static/umap/locale/th_TH.js +52 -17
  185. umap/static/umap/locale/th_TH.json +52 -17
  186. umap/static/umap/locale/tr.js +52 -17
  187. umap/static/umap/locale/tr.json +52 -17
  188. umap/static/umap/locale/uk_UA.js +52 -17
  189. umap/static/umap/locale/uk_UA.json +52 -17
  190. umap/static/umap/locale/vi.js +52 -17
  191. umap/static/umap/locale/vi.json +52 -17
  192. umap/static/umap/locale/vi_VN.json +52 -17
  193. umap/static/umap/locale/zh.js +52 -17
  194. umap/static/umap/locale/zh.json +52 -17
  195. umap/static/umap/locale/zh_CN.json +52 -17
  196. umap/static/umap/locale/zh_TW.Big5.json +52 -17
  197. umap/static/umap/locale/zh_TW.js +53 -17
  198. umap/static/umap/locale/zh_TW.json +53 -17
  199. umap/static/umap/map.css +63 -226
  200. umap/static/umap/unittests/utils.js +18 -0
  201. umap/static/umap/vars.css +23 -5
  202. umap/templates/umap/components/alerts/alert.html +32 -29
  203. umap/templates/umap/css.html +2 -1
  204. umap/templates/umap/login_popup_end.html +18 -9
  205. umap/templates/umap/user_map_table.html +7 -2
  206. umap/tests/integration/conftest.py +10 -6
  207. umap/tests/integration/test_anonymous_owned_map.py +90 -37
  208. umap/tests/integration/test_basics.py +25 -1
  209. umap/tests/integration/test_browser.py +37 -0
  210. umap/tests/integration/test_cluster.py +110 -0
  211. umap/tests/integration/test_conditional_rules.py +107 -52
  212. umap/tests/integration/test_datalayer.py +9 -16
  213. umap/tests/integration/test_draw_polygon.py +6 -0
  214. umap/tests/integration/test_draw_polyline.py +11 -0
  215. umap/tests/integration/test_edit_marker.py +12 -1
  216. umap/tests/integration/test_export_map.py +19 -0
  217. umap/tests/integration/test_fields.py +541 -0
  218. umap/tests/integration/test_filters.py +616 -0
  219. umap/tests/integration/test_iframe.py +1 -1
  220. umap/tests/integration/test_import.py +38 -42
  221. umap/tests/integration/test_map_preview.py +1 -1
  222. umap/tests/integration/test_picto.py +1 -1
  223. umap/tests/integration/test_popup.py +31 -0
  224. umap/tests/integration/test_remote_data.py +60 -4
  225. umap/tests/integration/test_save.py +1 -1
  226. umap/tests/integration/test_share.py +4 -4
  227. umap/tests/integration/test_tableeditor.py +31 -7
  228. umap/tests/integration/test_websocket_sync.py +71 -20
  229. umap/tests/test_dashboard.py +11 -1
  230. umap/tests/test_statics.py +2 -2
  231. umap/tests/test_utils.py +19 -2
  232. umap/tests/test_views.py +1 -1
  233. umap/urls.py +1 -0
  234. umap/utils.py +8 -1
  235. umap/views.py +5 -0
  236. {umap_project-3.3.2.dist-info → umap_project-3.4.0.dist-info}/METADATA +15 -15
  237. {umap_project-3.3.2.dist-info → umap_project-3.4.0.dist-info}/RECORD +240 -236
  238. umap/static/umap/js/modules/facets.js +0 -164
  239. umap/tests/integration/test_facets_browser.py +0 -279
  240. {umap_project-3.3.2.dist-info → umap_project-3.4.0.dist-info}/WHEEL +0 -0
  241. {umap_project-3.3.2.dist-info → umap_project-3.4.0.dist-info}/entry_points.txt +0 -0
  242. {umap_project-3.3.2.dist-info → umap_project-3.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -9,7 +9,7 @@ import {
9
9
  uMapAlert as Alert,
10
10
  uMapAlertConflict as AlertConflict,
11
11
  } from '../../components/alerts/alert.js'
12
- import { MutatingForm } from '../form/builder.js'
12
+ import { MutatingForm, Form } from '../form/builder.js'
13
13
  import { translate } from '../i18n.js'
14
14
  import { DataLayerPermissions } from '../permissions.js'
15
15
  import { Default as DefaultLayer } from '../rendering/layers/base.js'
@@ -21,8 +21,9 @@ 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
24
  import { FeatureManager } from '../managers.js'
25
+ import { Filters } from '../filters.js'
26
+ import { Fields, getDefaultFields } from './fields.js'
26
27
 
27
28
  export const LAYER_TYPES = [
28
29
  DefaultLayer,
@@ -83,18 +84,18 @@ export class DataLayer {
83
84
  }
84
85
  this.connectToMap()
85
86
  this.permissions = new DataLayerPermissions(this._umap, this)
86
- this.rules = new Rules(umap, this)
87
87
 
88
88
  this._needsFetch = this.createdOnServer || this.isRemoteLayer()
89
+ if (!this._needsFetch && !this._umap.fields.size) {
90
+ this.properties.fields = getDefaultFields()
91
+ }
92
+ this.fields = new Fields(this, this._umap.dialog)
93
+ this.filters = new Filters(this, this._umap)
94
+ this.rules = new Rules(umap, this)
95
+
89
96
  if (!this.createdOnServer) {
90
97
  if (this.showAtLoad()) this.show()
91
98
  }
92
- if (!this._needsFetch && !this._umap.fields.length) {
93
- this.properties.fields = [
94
- { key: U.DEFAULT_LABEL_KEY, type: 'String' },
95
- { key: 'description', type: 'Text' },
96
- ]
97
- }
98
99
 
99
100
  // Only layers that are displayed on load must be hidden/shown
100
101
  // Automatically, others will be shown manually, and thus will
@@ -129,7 +130,7 @@ export class DataLayer {
129
130
  }
130
131
 
131
132
  get cssId() {
132
- return `datalayer-${stamp(this)}`
133
+ return `datalayer-${this.id}`
133
134
  }
134
135
 
135
136
  get rank() {
@@ -146,19 +147,6 @@ export class DataLayer {
146
147
  this.properties.rank = value
147
148
  }
148
149
 
149
- get fields() {
150
- if (!this.properties.fields) this.properties.fields = []
151
- return this.properties.fields
152
- }
153
-
154
- set fields(fields) {
155
- this.properties.fields = fields
156
- }
157
-
158
- get fieldKeys() {
159
- return this.fields.map((field) => field.key)
160
- }
161
-
162
150
  get sortKey() {
163
151
  return this.getProperty('sortKey') || U.DEFAULT_LABEL_KEY
164
152
  }
@@ -174,6 +162,18 @@ export class DataLayer {
174
162
  // Propagate will remove the fields it has already
175
163
  // processed
176
164
  fields = this.propagate(fields)
165
+ if (fields.includes('properties.fields')) {
166
+ this.fields?.pull()
167
+ if (this._umap.browser.isOpen()) {
168
+ this._umap.browser.buildFilters()
169
+ }
170
+ }
171
+ if (fields.includes('properties.filters')) {
172
+ this.filters.load()
173
+ if (this._umap.browser.isOpen()) {
174
+ this._umap.browser.buildFilters()
175
+ }
176
+ }
177
177
 
178
178
  const impacts = Utils.getImpactsFromSchema(fields)
179
179
 
@@ -202,7 +202,7 @@ export class DataLayer {
202
202
  }
203
203
 
204
204
  // This method does a targeted update of the UI,
205
- // it whould be merged with `render`` method and the
205
+ // it would be merged with `render`` method and the
206
206
  // SCHEMA at some point
207
207
  propagate(fields = []) {
208
208
  const impacts = {
@@ -225,17 +225,28 @@ export class DataLayer {
225
225
  }
226
226
 
227
227
  showAtLoad() {
228
- return this.autoLoaded() && this.showAtZoom()
228
+ return this.autoVisibility && this.showAtZoom()
229
+ }
230
+
231
+ get autoVisibility() {
232
+ if (this._autoVisibility === undefined) {
233
+ if (this._umap.datalayersFromQueryString) {
234
+ const datalayerIds = this._umap.datalayersFromQueryString
235
+ this._autoVisibility = datalayerIds.includes(this.id.toString())
236
+ if (this.properties.old_id) {
237
+ this._autoVisibility =
238
+ this._autoVisibility ||
239
+ datalayerIds.includes(this.properties.old_id.toString())
240
+ }
241
+ } else {
242
+ this._autoVisibility = this.properties.displayOnLoad
243
+ }
244
+ }
245
+ return this._autoVisibility
229
246
  }
230
247
 
231
- autoLoaded() {
232
- if (!this._umap.datalayersFromQueryString) return this.properties.displayOnLoad
233
- const datalayerIds = this._umap.datalayersFromQueryString
234
- let loadMe = datalayerIds.includes(this.id.toString())
235
- if (this.properties.old_id) {
236
- loadMe = loadMe || datalayerIds.includes(this.properties.old_id.toString())
237
- }
238
- return loadMe
248
+ set autoVisibility(value) {
249
+ this._autoVisibility = value
239
250
  }
240
251
 
241
252
  insertBefore(other) {
@@ -304,10 +315,9 @@ export class DataLayer {
304
315
 
305
316
  fromGeoJSON(geojson, sync = true) {
306
317
  if (!geojson) return []
307
- const features = this.addData(geojson, sync)
308
318
  this._needsFetch = false
319
+ const features = this.addData(geojson, sync)
309
320
  this.onDataLoaded()
310
- this.dataChanged()
311
321
  return features
312
322
  }
313
323
 
@@ -431,14 +441,28 @@ export class DataLayer {
431
441
  this.layer.removeLayer(feature.ui)
432
442
  }
433
443
 
434
- addFeature(feature) {
444
+ addFeature(feature, sync = false) {
435
445
  feature.connectToDataLayer(this)
436
446
  this.features.add(feature)
437
447
  this._umap.featuresIndex[feature.getSlug()] = feature
438
448
  // TODO: quid for remote data ?
439
449
  this.inferFields(feature)
440
- this.showFeature(feature)
450
+ try {
451
+ this.showFeature(feature)
452
+ } catch (error) {
453
+ console.error(error)
454
+ if (this._umap.editEnabled) {
455
+ Alert.error(translate('Skipping invalid geometry'))
456
+ }
457
+ console.error('Invalid geometry', feature)
458
+ this.removeFeature(feature, false)
459
+ return
460
+ }
441
461
  this.dataChanged()
462
+ if (sync) {
463
+ feature.sync.upsert(feature.toGeoJSON(), null)
464
+ }
465
+ return feature
442
466
  }
443
467
 
444
468
  removeFeature(feature, sync) {
@@ -450,7 +474,9 @@ export class DataLayer {
450
474
  const oldValue = feature.toGeoJSON()
451
475
  feature.sync.delete(oldValue)
452
476
  }
453
- this.hideFeature(feature)
477
+ try {
478
+ this.hideFeature(feature)
479
+ } catch {}
454
480
  delete this._umap.featuresIndex[feature.getSlug()]
455
481
  feature.disconnectFromDataLayer(this)
456
482
  this.features.del(feature)
@@ -458,96 +484,49 @@ export class DataLayer {
458
484
  }
459
485
 
460
486
  inferFields(feature) {
461
- if (!this.properties.fields) this.properties.fields = []
462
- const keys = this.fieldKeys
463
487
  for (const key in feature.properties) {
464
- if (typeof feature.properties[key] !== 'object') {
488
+ if (key && typeof feature.properties[key] !== 'object') {
465
489
  if (key.indexOf('_') === 0) continue
466
- if (keys.includes(key)) continue
467
- this.properties.fields.push({ key, type: 'String' })
490
+ if (this.fields.has(key)) continue
491
+ if (this._umap.fields.has(key)) continue
492
+ let type = 'String'
493
+ if (key === 'description') type = 'Text'
494
+ this.fields.add({ key, type })
468
495
  }
469
496
  }
497
+ this.fields.push()
470
498
  }
471
499
 
472
- async confirmDeleteProperty(property) {
473
- return this._umap.dialog
474
- .confirm(
475
- translate('Are you sure you want to delete this field on all the features?')
476
- )
477
- .then(() => {
478
- this.deleteProperty(property)
479
- })
480
- }
481
-
482
- async askForRenameProperty(property) {
483
- return this._umap.dialog
484
- .prompt(translate('Please enter the new name of this field'))
485
- .then(({ prompt }) => {
486
- if (!prompt || !this.validateName(prompt)) return
487
- this.renameProperty(property, prompt)
488
- })
500
+ renameField(oldName, newName) {
501
+ this.renameFeaturesField(oldName, newName)
489
502
  }
490
503
 
491
- renameProperty(oldName, newName) {
492
- this.sync.startBatch()
493
- const oldFields = Utils.CopyJSON(this.fields)
494
- for (const field of this.fields) {
495
- if (field.key === oldName) {
496
- field.key = newName
497
- break
498
- }
499
- }
500
- this.sync.update('properties.fields', this.fields, oldFields)
504
+ renameFeaturesField(oldName, newName) {
501
505
  this.features.forEach((feature) => {
502
- feature.renameProperty(oldName, newName)
506
+ feature.renameField(oldName, newName)
503
507
  })
504
- this.sync.commitBatch()
505
508
  }
506
509
 
507
- deleteProperty(property) {
508
- this.sync.startBatch()
509
- const oldFields = Utils.CopyJSON(this.fields)
510
- this.fields = this.fields.filter((field) => field.key !== property)
511
- this.sync.update('properties.fields', this.fields, oldFields)
512
- this.features.forEach((feature) => {
513
- feature.deleteProperty(property)
514
- })
515
- this.sync.commitBatch()
510
+ deleteField(name) {
511
+ this.deleteFeaturesField(name)
516
512
  }
517
513
 
518
- addProperty() {
519
- let resolve = undefined
520
- const promise = new Promise((r) => {
521
- resolve = r
522
- })
523
- this._umap.dialog
524
- .prompt(translate('Please enter the name of the property'))
525
- .then(({ prompt }) => {
526
- if (!prompt || !this.validateName(prompt)) return
527
- this.properties.fields.push({ key: prompt, type: 'String' })
528
- resolve()
514
+ deleteFeaturesField(name) {
515
+ if (!this._umap.fields.has(name) && !this.fields.has(name)) {
516
+ this.features.forEach((feature) => {
517
+ feature.deleteField(name)
529
518
  })
530
- return promise
519
+ }
531
520
  }
532
521
 
533
- validateName(name) {
534
- if (name.includes('.')) {
535
- Alert.error(translate('Name “{name}” should not contain a dot.', { name }))
536
- return false
537
- }
538
- if (this.fieldKeys.includes(name)) {
539
- Alert.error(translate('This name already exists: “{name}”', { name }))
540
- return false
541
- }
542
- return true
522
+ eachFeature(callback) {
523
+ this.features.forEach((feature) => callback(feature))
543
524
  }
544
525
 
545
- sortedValues(property) {
546
- return this.features
547
- .all()
548
- .map((feature) => feature.properties[property])
549
- .filter((val, idx, arr) => arr.indexOf(val) === idx)
550
- .sort(Utils.naturalSort)
526
+ sortedValues(key) {
527
+ const field = this.fields.get(key) || this._umap.fields.get(key)
528
+ if (!field) return []
529
+ return field.values(this.features.all()).sort(Utils.naturalSort)
551
530
  }
552
531
 
553
532
  addData(geojson, sync) {
@@ -601,16 +580,18 @@ export class DataLayer {
601
580
 
602
581
  switch (geometry.type) {
603
582
  case 'Point':
604
- // FIXME: deal with MultiPoint
605
- feature = new Point(this._umap, this, geojson, id)
606
- break
607
583
  case 'MultiPoint':
608
- if (geometry.coordinates?.length === 1) {
584
+ if (
585
+ geometry.coordinates?.length === 1 &&
586
+ Array.isArray(geometry.coordinates?.[0])
587
+ ) {
609
588
  geojson.geometry.coordinates = geojson.geometry.coordinates[0]
610
- feature = new Point(this._umap, this, geojson, id)
611
- } else if (this._umap.editEnabled) {
589
+ geojson.geometry.type = 'Point'
590
+ } else if (geometry.type === 'MultiPoint' && this._umap.editEnabled) {
612
591
  Alert.error(translate('Cannot process MultiPoint'))
592
+ break
613
593
  }
594
+ feature = new Point(this._umap, this, geojson, id)
614
595
  break
615
596
  case 'MultiLineString':
616
597
  case 'LineString':
@@ -631,9 +612,7 @@ export class DataLayer {
631
612
  }
632
613
  }
633
614
  if (feature && !feature.isEmpty()) {
634
- this.addFeature(feature)
635
- if (sync) feature.sync.upsert(feature.toGeoJSON(), null)
636
- return feature
615
+ return this.addFeature(feature, sync)
637
616
  }
638
617
  }
639
618
 
@@ -735,7 +714,9 @@ export class DataLayer {
735
714
  }
736
715
 
737
716
  clear(sync = true) {
717
+ this._batch = true
738
718
  this.features.forEach((feature) => feature.del(sync))
719
+ this._batch = false
739
720
  this.dataChanged()
740
721
  }
741
722
 
@@ -808,7 +789,7 @@ export class DataLayer {
808
789
  const builder = new MutatingForm(this, layerFields)
809
790
  const template = `
810
791
  <details id="layer-properties">
811
- <summary>${this.layer.getName()}: ${translate('settings')}</summary>
792
+ <summary><h4>${this.layer.getName()}: ${translate('settings')}</h4></summary>
812
793
  <fieldset data-ref=fieldset></fieldset>
813
794
  </details>
814
795
  `
@@ -902,64 +883,6 @@ export class DataLayer {
902
883
  fieldset.appendChild(builder.build())
903
884
  }
904
885
 
905
- _editFields(container) {
906
- const template = `
907
- <details id="fields">
908
- <summary>${translate('Manage Fields')}</summary>
909
- <fieldset>
910
- <ul data-ref=ul></ul>
911
- <button type="button" data-ref=add><i class="icon icon-16 icon-add"></i>${translate('Add a new field')}</button>
912
- </fieldset>
913
- </details>
914
- `
915
- const [fieldset, { ul, add }] = Utils.loadTemplateWithRefs(template)
916
- add.addEventListener('click', () => {
917
- this.addProperty().then(() => {
918
- this.edit().then((panel) => {
919
- panel.scrollTo('details#fields')
920
- })
921
- })
922
- })
923
- container.appendChild(fieldset)
924
- for (const field of this.fields) {
925
- const [row, { rename, del }] = Utils.loadTemplateWithRefs(
926
- `<li class="orderable" data-key="${field.key}">
927
- <button class="icon icon-16 icon-edit" title="${translate('Rename this field')}" data-ref=rename></button>
928
- <button class="icon icon-16 icon-delete" title="${translate('Delete this field')}" data-ref=del></button>
929
- <i class="icon icon-16 icon-drag" title="${translate('Drag to reorder')}"></i>
930
- ${field.key}
931
- </li>`
932
- )
933
- ul.appendChild(row)
934
- rename.addEventListener('click', () => {
935
- this.askForRenameProperty(field.key).then(() => {
936
- this.edit().then((panel) => {
937
- panel.scrollTo('details#fields')
938
- })
939
- })
940
- })
941
- del.addEventListener('click', () => {
942
- this.confirmDeleteProperty(field.key).then(() => {
943
- this.edit().then((panel) => {
944
- panel.scrollTo('details#fields')
945
- })
946
- })
947
- })
948
- }
949
- const onReorder = (src, dst, initialIndex, finalIndex) => {
950
- const orderedKeys = Array.from(ul.querySelectorAll('li')).map(
951
- (el) => el.dataset.key
952
- )
953
- const oldFields = Utils.CopyJSON(this.properties.fields)
954
- this.properties.fields.sort(
955
- (fieldA, fieldB) =>
956
- orderedKeys.indexOf(fieldA.key) > orderedKeys.indexOf(fieldB.key)
957
- )
958
- this.sync.update('properties.fields', this.properties.fields, oldFields)
959
- }
960
- const orderable = new Orderable(ul, onReorder)
961
- }
962
-
963
886
  _editRemoteDataProperties(container) {
964
887
  // XXX I'm not sure **why** this is needed (as it's set during `this.initialize`)
965
888
  // but apparently it's needed.
@@ -1070,9 +993,7 @@ export class DataLayer {
1070
993
  this._editInteractionProperties(container)
1071
994
  this._editTextPathProperties(container)
1072
995
  this._editRemoteDataProperties(container)
1073
- if (!this.isRemoteLayer()) {
1074
- this._editFields(container)
1075
- }
996
+ this.fields.edit(container)
1076
997
  this.rules.edit(container)
1077
998
 
1078
999
  if (this._umap.properties.urls.datalayer_versions) {
@@ -1197,7 +1118,7 @@ export class DataLayer {
1197
1118
  // From now on, do not try to how/hide
1198
1119
  // automatically this layer, as user
1199
1120
  // has taken control on this.
1200
- this._forcedVisibility = true
1121
+ this.autoVisibility = false
1201
1122
  let display = force
1202
1123
  if (force === undefined) {
1203
1124
  if (!this.isVisible()) display = true
@@ -1268,7 +1189,8 @@ export class DataLayer {
1268
1189
  }
1269
1190
 
1270
1191
  umapGeoJSON() {
1271
- const geojson = this._umap.formatter.toFeatureCollection(this.features.all())
1192
+ const features = this.isRemoteLayer() ? [] : this.features.all()
1193
+ const geojson = this._umap.formatter.toFeatureCollection(features)
1272
1194
  geojson._umap_options = this.properties
1273
1195
  return geojson
1274
1196
  }
@@ -1305,12 +1227,12 @@ export class DataLayer {
1305
1227
  async save() {
1306
1228
  if (this.isDeleted) return await this.saveDelete()
1307
1229
  if (!this.isRemoteLayer() && !this.isLoaded()) return
1308
- const geojson = this.umapGeoJSON()
1309
1230
  const formData = new FormData()
1310
1231
  formData.append('name', this.properties.name)
1311
1232
  formData.append('display_on_load', !!this.properties.displayOnLoad)
1312
1233
  formData.append('rank', this.rank)
1313
1234
  formData.append('settings', this.prepareProperties())
1235
+ const geojson = this.umapGeoJSON()
1314
1236
  // Filename support is shaky, don't do it for now.
1315
1237
  const blob = new Blob([JSON.stringify(geojson)], { type: 'application/json' })
1316
1238
  formData.append('geojson', blob)
@@ -1412,13 +1334,15 @@ export class DataLayer {
1412
1334
  )) {
1413
1335
  container.innerHTML = ''
1414
1336
  if (this.layer.renderLegend) return this.layer.renderLegend(container)
1415
- const keys = new Set(this.fieldKeys)
1416
1337
  const rules = new Map()
1417
1338
  for (const rule of this.rules) {
1418
1339
  rules.set(rule.condition, rule)
1419
1340
  }
1420
1341
  for (const rule of this._umap.rules) {
1421
- if (!rules.has(rule.condition) && keys.has(rule.key)) {
1342
+ if (
1343
+ !rules.has(rule.condition) &&
1344
+ (this.fields.has(rule.field.key) || this._umap.fields.has(rule.field.key))
1345
+ ) {
1422
1346
  rules.set(rule.condition, rule)
1423
1347
  }
1424
1348
  }
@@ -1442,6 +1366,11 @@ export class DataLayer {
1442
1366
  'icon-eye',
1443
1367
  translate('Show/hide layer')
1444
1368
  )
1369
+ const table = DomUtil.createButtonIcon(
1370
+ container,
1371
+ 'icon-table show-on-edit',
1372
+ translate('Edit properties in a table')
1373
+ )
1445
1374
  const zoomTo = DomUtil.createButtonIcon(
1446
1375
  container,
1447
1376
  'icon-zoom',
@@ -1452,11 +1381,6 @@ export class DataLayer {
1452
1381
  'icon-edit show-on-edit',
1453
1382
  translate('Edit')
1454
1383
  )
1455
- const table = DomUtil.createButtonIcon(
1456
- container,
1457
- 'icon-table show-on-edit',
1458
- translate('Edit properties in a table')
1459
- )
1460
1384
  const remove = DomUtil.createButtonIcon(
1461
1385
  container,
1462
1386
  'icon-delete show-on-edit',
@@ -1474,16 +1398,12 @@ export class DataLayer {
1474
1398
  }
1475
1399
  DomEvent.on(toggle, 'click', () => this.toggle())
1476
1400
  DomEvent.on(zoomTo, 'click', this.zoomTo, this)
1477
- container.classList.add(this.getHidableClass())
1401
+ container.classList.add(this.cssId)
1478
1402
  container.classList.toggle('off', !this.isVisible())
1479
1403
  }
1480
1404
 
1481
1405
  getHidableElements() {
1482
- return document.querySelectorAll(`.${this.getHidableClass()}`)
1483
- }
1484
-
1485
- getHidableClass() {
1486
- return `show_with_datalayer_${stamp(this)}`
1406
+ return document.querySelectorAll(`.${this.cssId}`)
1487
1407
  }
1488
1408
 
1489
1409
  propagateDelete() {
@@ -0,0 +1,109 @@
1
+ // Utils that needs the DOM
2
+ import * as Utils from './utils.js'
3
+ import { translate } from './i18n.js'
4
+ import Tooltip from './ui/tooltip.js'
5
+
6
+ export const copyToClipboard = (textToCopy) => {
7
+ const tooltip = new Tooltip()
8
+ // https://stackoverflow.com/a/65996386
9
+ // Navigator clipboard api needs a secure context (https)
10
+ if (navigator.clipboard && window.isSecureContext) {
11
+ navigator.clipboard.writeText(textToCopy)
12
+ } else {
13
+ // Use the 'out of viewport hidden text area' trick
14
+ const textArea = document.createElement('textarea')
15
+ textArea.value = textToCopy
16
+
17
+ // Move textarea out of the viewport so it's not visible
18
+ textArea.style.position = 'absolute'
19
+ textArea.style.left = '-999999px'
20
+
21
+ document.body.prepend(textArea)
22
+ textArea.select()
23
+
24
+ try {
25
+ document.execCommand('copy')
26
+ } catch (error) {
27
+ console.error(error)
28
+ } finally {
29
+ textArea.remove()
30
+ }
31
+ }
32
+ tooltip.open({ content: translate('✅ Copied!'), duration: 5000 })
33
+ }
34
+
35
+ export const copiableInput = (parent, label, value) => {
36
+ const [container, { input, button }] = Utils.loadTemplateWithRefs(`
37
+ <div class="copiable-input">
38
+ <label>${label}<input type="text" readOnly value="${value}" data-ref=input /></label>
39
+ <button type="button" class="icon icon-24 icon-copy" title="${translate('copy')}" data-ref=button></button>
40
+ </div>
41
+ `)
42
+ button.addEventListener('click', () => copyToClipboard(input.value))
43
+ parent.appendChild(container)
44
+ return input
45
+ }
46
+
47
+ // From https://gist.github.com/Accudio/b9cb16e0e3df858cef0d31e38f1fe46f
48
+ // convert colour in range 0-255 to the modifier used within luminance calculation
49
+ const colourMod = (colour) => {
50
+ const sRGB = colour / 255
51
+ let mod = ((sRGB + 0.055) / 1.055) ** 2.4
52
+ if (sRGB < 0.03928) mod = sRGB / 12.92
53
+ return mod
54
+ }
55
+ const RGBRegex = /rgb *\( *([0-9]{1,3}) *, *([0-9]{1,3}) *, *([0-9]{1,3}) *\)/
56
+
57
+ export const textColorFromBackgroundColor = (el, bgcolor) => {
58
+ return contrastedColor(el, bgcolor) ? '#ffffff' : '#000000'
59
+ }
60
+
61
+ const contrastWCAG21 = (rgb) => {
62
+ const [r, g, b] = rgb
63
+ // luminance of inputted colour
64
+ const lum = 0.2126 * colourMod(r) + 0.7152 * colourMod(g) + 0.0722 * colourMod(b)
65
+ // white has a luminance of 1
66
+ const whiteLum = 1
67
+ const contrast = (whiteLum + 0.05) / (lum + 0.05)
68
+ return contrast > 3 ? 1 : 0
69
+ }
70
+ const colorNameToHex = (str) => {
71
+ const ctx = document.createElement('canvas').getContext('2d')
72
+ ctx.fillStyle = str
73
+ return ctx.fillStyle
74
+ }
75
+ export const hexToRGB = (hex) => {
76
+ return hex
77
+ .replace(
78
+ /^#?([a-f\d])([a-f\d])([a-f\d])$/i,
79
+ (m, r, g, b) => `#${r}${r}${g}${g}${b}${b}`
80
+ )
81
+ .substring(1)
82
+ .match(/.{2}/g)
83
+ .map((x) => Number.parseInt(x, 16))
84
+ }
85
+
86
+ const CACHE_CONTRAST = {}
87
+ export const contrastedColor = (el, bgcolor) => {
88
+ // Return 0 for black and 1 for white
89
+ // bgcolor is a human color, it can be a any keyword (purple…)
90
+ if (typeof CACHE_CONTRAST[bgcolor] !== 'undefined') return CACHE_CONTRAST[bgcolor]
91
+ let rgb = window.getComputedStyle(el).getPropertyValue('background-color')
92
+ rgb = RGBRegex.exec(rgb)
93
+ if (rgb && rgb.length === 4) {
94
+ rgb = [
95
+ Number.parseInt(rgb[1], 10),
96
+ Number.parseInt(rgb[2], 10),
97
+ Number.parseInt(rgb[3], 10),
98
+ ]
99
+ } else {
100
+ // The element may not yet be added to the DOM, so let's try
101
+ // another way
102
+ const hex = colorNameToHex(bgcolor)
103
+ rgb = hexToRGB(hex)
104
+ }
105
+ if (!rgb) return 1
106
+ const out = contrastWCAG21(rgb)
107
+ if (bgcolor) CACHE_CONTRAST[bgcolor] = out
108
+ return out
109
+ }