umap-project 2.4.2__py3-none-any.whl → 2.5.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 (177) hide show
  1. umap/__init__.py +1 -1
  2. umap/locale/el/LC_MESSAGES/django.mo +0 -0
  3. umap/locale/eu/LC_MESSAGES/django.mo +0 -0
  4. umap/locale/hu/LC_MESSAGES/django.mo +0 -0
  5. umap/locale/hu/LC_MESSAGES/django.po +100 -50
  6. umap/static/umap/base.css +4 -1
  7. umap/static/umap/css/contextmenu.css +11 -0
  8. umap/static/umap/css/dialog.css +24 -3
  9. umap/static/umap/css/panel.css +4 -2
  10. umap/static/umap/css/slideshow.css +69 -0
  11. umap/static/umap/css/tableeditor.css +69 -0
  12. umap/static/umap/css/tooltip.css +3 -3
  13. umap/static/umap/img/16-white.svg +4 -0
  14. umap/static/umap/img/source/16-white.svg +5 -1
  15. umap/static/umap/js/components/alerts/alert.css +10 -10
  16. umap/static/umap/js/modules/autocomplete.js +23 -1
  17. umap/static/umap/js/modules/browser.js +14 -8
  18. umap/static/umap/js/modules/facets.js +40 -10
  19. umap/static/umap/js/modules/formatter.js +153 -0
  20. umap/static/umap/js/modules/global.js +10 -1
  21. umap/static/umap/js/modules/help.js +25 -25
  22. umap/static/umap/js/modules/importer.js +4 -4
  23. umap/static/umap/js/modules/importers/communesfr.js +3 -1
  24. umap/static/umap/js/modules/importers/datasets.js +8 -6
  25. umap/static/umap/js/modules/importers/geodatamine.js +10 -10
  26. umap/static/umap/js/modules/importers/overpass.js +18 -14
  27. umap/static/umap/js/modules/rules.js +13 -1
  28. umap/static/umap/js/modules/schema.js +16 -12
  29. umap/static/umap/js/{umap.share.js → modules/share.js} +60 -99
  30. umap/static/umap/js/modules/slideshow.js +141 -0
  31. umap/static/umap/js/modules/tableeditor.js +329 -0
  32. umap/static/umap/js/modules/ui/base.js +93 -0
  33. umap/static/umap/js/modules/ui/contextmenu.js +50 -0
  34. umap/static/umap/js/modules/ui/dialog.js +169 -31
  35. umap/static/umap/js/modules/ui/panel.js +6 -4
  36. umap/static/umap/js/modules/ui/tooltip.js +5 -75
  37. umap/static/umap/js/modules/utils.js +20 -0
  38. umap/static/umap/js/umap.controls.js +1 -1
  39. umap/static/umap/js/umap.features.js +22 -14
  40. umap/static/umap/js/umap.forms.js +157 -154
  41. umap/static/umap/js/umap.js +48 -34
  42. umap/static/umap/js/umap.layer.js +232 -164
  43. umap/static/umap/js/umap.permissions.js +1 -1
  44. umap/static/umap/js/umap.popup.js +1 -1
  45. umap/static/umap/locale/am_ET.js +22 -5
  46. umap/static/umap/locale/am_ET.json +19 -5
  47. umap/static/umap/locale/ar.js +22 -5
  48. umap/static/umap/locale/ar.json +19 -5
  49. umap/static/umap/locale/ast.js +22 -5
  50. umap/static/umap/locale/ast.json +19 -5
  51. umap/static/umap/locale/bg.js +22 -5
  52. umap/static/umap/locale/bg.json +19 -5
  53. umap/static/umap/locale/br.js +22 -5
  54. umap/static/umap/locale/br.json +19 -5
  55. umap/static/umap/locale/ca.js +56 -39
  56. umap/static/umap/locale/ca.json +53 -39
  57. umap/static/umap/locale/cs_CZ.js +22 -5
  58. umap/static/umap/locale/cs_CZ.json +19 -5
  59. umap/static/umap/locale/da.js +22 -5
  60. umap/static/umap/locale/da.json +19 -5
  61. umap/static/umap/locale/de.js +22 -5
  62. umap/static/umap/locale/de.json +19 -5
  63. umap/static/umap/locale/el.js +27 -10
  64. umap/static/umap/locale/el.json +19 -5
  65. umap/static/umap/locale/en.js +22 -6
  66. umap/static/umap/locale/en.json +19 -5
  67. umap/static/umap/locale/en_US.json +19 -5
  68. umap/static/umap/locale/es.js +22 -6
  69. umap/static/umap/locale/es.json +19 -5
  70. umap/static/umap/locale/et.js +22 -5
  71. umap/static/umap/locale/et.json +19 -5
  72. umap/static/umap/locale/eu.js +167 -150
  73. umap/static/umap/locale/eu.json +167 -150
  74. umap/static/umap/locale/fa_IR.js +22 -5
  75. umap/static/umap/locale/fa_IR.json +19 -5
  76. umap/static/umap/locale/fi.js +22 -5
  77. umap/static/umap/locale/fi.json +19 -5
  78. umap/static/umap/locale/fr.js +22 -6
  79. umap/static/umap/locale/fr.json +19 -5
  80. umap/static/umap/locale/gl.js +22 -5
  81. umap/static/umap/locale/gl.json +19 -5
  82. umap/static/umap/locale/he.js +22 -5
  83. umap/static/umap/locale/he.json +19 -5
  84. umap/static/umap/locale/hr.js +22 -5
  85. umap/static/umap/locale/hr.json +19 -5
  86. umap/static/umap/locale/hu.js +89 -72
  87. umap/static/umap/locale/hu.json +89 -75
  88. umap/static/umap/locale/id.js +22 -5
  89. umap/static/umap/locale/id.json +19 -5
  90. umap/static/umap/locale/is.js +22 -5
  91. umap/static/umap/locale/is.json +19 -5
  92. umap/static/umap/locale/it.js +22 -5
  93. umap/static/umap/locale/it.json +19 -5
  94. umap/static/umap/locale/ja.js +22 -5
  95. umap/static/umap/locale/ja.json +19 -5
  96. umap/static/umap/locale/ko.js +22 -5
  97. umap/static/umap/locale/ko.json +19 -5
  98. umap/static/umap/locale/lt.js +22 -5
  99. umap/static/umap/locale/lt.json +19 -5
  100. umap/static/umap/locale/ms.js +22 -5
  101. umap/static/umap/locale/ms.json +19 -5
  102. umap/static/umap/locale/nl.js +22 -5
  103. umap/static/umap/locale/nl.json +19 -5
  104. umap/static/umap/locale/no.js +22 -5
  105. umap/static/umap/locale/no.json +19 -5
  106. umap/static/umap/locale/pl.js +22 -5
  107. umap/static/umap/locale/pl.json +19 -5
  108. umap/static/umap/locale/pl_PL.json +19 -5
  109. umap/static/umap/locale/pt.js +22 -6
  110. umap/static/umap/locale/pt.json +21 -7
  111. umap/static/umap/locale/pt_BR.js +22 -5
  112. umap/static/umap/locale/pt_BR.json +19 -5
  113. umap/static/umap/locale/pt_PT.js +22 -5
  114. umap/static/umap/locale/pt_PT.json +19 -5
  115. umap/static/umap/locale/ro.js +22 -5
  116. umap/static/umap/locale/ro.json +19 -5
  117. umap/static/umap/locale/ru.js +22 -5
  118. umap/static/umap/locale/ru.json +19 -5
  119. umap/static/umap/locale/sk_SK.js +22 -5
  120. umap/static/umap/locale/sk_SK.json +19 -5
  121. umap/static/umap/locale/sl.js +22 -5
  122. umap/static/umap/locale/sl.json +19 -5
  123. umap/static/umap/locale/sr.js +22 -5
  124. umap/static/umap/locale/sr.json +19 -5
  125. umap/static/umap/locale/sv.js +22 -5
  126. umap/static/umap/locale/sv.json +19 -5
  127. umap/static/umap/locale/th_TH.js +22 -5
  128. umap/static/umap/locale/th_TH.json +19 -5
  129. umap/static/umap/locale/tr.js +22 -5
  130. umap/static/umap/locale/tr.json +19 -5
  131. umap/static/umap/locale/uk_UA.js +22 -5
  132. umap/static/umap/locale/uk_UA.json +19 -5
  133. umap/static/umap/locale/vi.js +22 -5
  134. umap/static/umap/locale/vi.json +19 -5
  135. umap/static/umap/locale/vi_VN.json +19 -5
  136. umap/static/umap/locale/zh.js +22 -5
  137. umap/static/umap/locale/zh.json +19 -5
  138. umap/static/umap/locale/zh_CN.json +19 -5
  139. umap/static/umap/locale/zh_TW.Big5.json +19 -5
  140. umap/static/umap/locale/zh_TW.js +22 -5
  141. umap/static/umap/locale/zh_TW.json +19 -5
  142. umap/static/umap/map.css +2 -145
  143. umap/static/umap/vars.css +5 -0
  144. umap/static/umap/vendors/formbuilder/Leaflet.FormBuilder.js +410 -428
  145. umap/static/umap/vendors/geojson-to-gpx/index.js +155 -0
  146. umap/static/umap/vendors/osmtogeojson/osmtogeojson.js +1 -2
  147. umap/static/umap/vendors/togeojson/togeojson.es.js +1109 -0
  148. umap/static/umap/vendors/togeojson/{togeojson.umd.js.map → togeojson.es.mjs.map} +1 -1
  149. umap/static/umap/vendors/tokml/tokml.es.js +895 -0
  150. umap/static/umap/vendors/tokml/tokml.es.mjs.map +1 -0
  151. umap/storage.py +6 -2
  152. umap/templates/umap/components/alerts/alert.html +3 -3
  153. umap/templates/umap/css.html +3 -0
  154. umap/templates/umap/js.html +0 -6
  155. umap/tests/fixtures/categorized_highway.geojson +1 -0
  156. umap/tests/fixtures/test_import_osm_relation.json +130 -0
  157. umap/tests/integration/conftest.py +8 -1
  158. umap/tests/integration/test_browser.py +3 -2
  159. umap/tests/integration/test_categorized_layer.py +141 -0
  160. umap/tests/integration/test_conditional_rules.py +21 -0
  161. umap/tests/integration/test_datalayer.py +9 -4
  162. umap/tests/integration/test_edit_datalayer.py +1 -0
  163. umap/tests/integration/test_edit_polygon.py +1 -1
  164. umap/tests/integration/test_export_map.py +2 -3
  165. umap/tests/integration/test_import.py +22 -0
  166. umap/tests/integration/test_tableeditor.py +158 -4
  167. umap/tests/integration/test_websocket_sync.py +2 -2
  168. {umap_project-2.4.2.dist-info → umap_project-2.5.1.dist-info}/METADATA +8 -8
  169. {umap_project-2.4.2.dist-info → umap_project-2.5.1.dist-info}/RECORD +172 -162
  170. umap/static/umap/js/umap.slideshow.js +0 -163
  171. umap/static/umap/js/umap.tableeditor.js +0 -118
  172. umap/static/umap/vendors/togeojson/togeojson.umd.js +0 -2
  173. umap/static/umap/vendors/togpx/togpx.js +0 -547
  174. umap/static/umap/vendors/tokml/tokml.js +0 -343
  175. {umap_project-2.4.2.dist-info → umap_project-2.5.1.dist-info}/WHEEL +0 -0
  176. {umap_project-2.4.2.dist-info → umap_project-2.5.1.dist-info}/entry_points.txt +0 -0
  177. {umap_project-2.4.2.dist-info → umap_project-2.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -29,7 +29,7 @@ L.Map.mergeOptions({
29
29
  U.Map = L.Map.extend({
30
30
  includes: [ControlsMixin],
31
31
 
32
- initialize: function (el, geojson) {
32
+ initialize: async function (el, geojson) {
33
33
  this.sync_engine = new U.SyncEngine(this)
34
34
  this.sync = this.sync_engine.proxy(this)
35
35
  // Locale name (pt_PT, en_US…)
@@ -56,8 +56,8 @@ U.Map = L.Map.extend({
56
56
  this.urls = new U.URLs(this.options.urls)
57
57
 
58
58
  this.panel = new U.Panel(this)
59
+ this.dialog = new U.Dialog({ className: 'dark' })
59
60
  this.tooltip = new U.Tooltip(this._controlContainer)
60
- this.dialog = new U.Dialog(this._controlContainer)
61
61
  if (this.hasEditMode()) {
62
62
  this.editPanel = new U.EditPanel(this)
63
63
  this.fullPanel = new U.FullPanel(this)
@@ -116,6 +116,8 @@ U.Map = L.Map.extend({
116
116
  // Needed for actions labels
117
117
  this.help = new U.Help(this)
118
118
 
119
+ this.formatter = new U.Formatter(this)
120
+
119
121
  this.initControls()
120
122
  // Needs locate control and hash to exist
121
123
  this.initCenter()
@@ -174,23 +176,7 @@ U.Map = L.Map.extend({
174
176
  }
175
177
  this._default_extent = true
176
178
  this.options.name = L._('Untitled map')
177
- let data = L.Util.queryString('data', null)
178
- const url = new URL(window.location.href)
179
- const dataUrls = new URLSearchParams(url.search).getAll('dataUrl')
180
- const dataFormat = L.Util.queryString('dataFormat', 'geojson')
181
- if (dataUrls.length) {
182
- for (let dataUrl of dataUrls) {
183
- dataUrl = decodeURIComponent(dataUrl)
184
- dataUrl = this.localizeUrl(dataUrl)
185
- dataUrl = this.proxyUrl(dataUrl)
186
- const datalayer = this.createDataLayer()
187
- datalayer.importFromUrl(dataUrl, dataFormat)
188
- }
189
- } else if (data) {
190
- data = decodeURIComponent(data)
191
- const datalayer = this.createDataLayer()
192
- datalayer.importRaw(data, dataFormat)
193
- }
179
+ await this.loadDataFromQueryString()
194
180
  }
195
181
 
196
182
  this.slideshow = new U.Slideshow(this, this.options.slideshow)
@@ -201,6 +187,7 @@ U.Map = L.Map.extend({
201
187
  }
202
188
 
203
189
  this.initShortcuts()
190
+ if (!this.options.noControl) this.initCaptionBar()
204
191
  this.onceDataLoaded(this.setViewFromQueryString)
205
192
 
206
193
  window.onbeforeunload = () => (this.editEnabled && this.isDirty) || null
@@ -295,9 +282,28 @@ U.Map = L.Map.extend({
295
282
  }
296
283
  },
297
284
 
298
- setViewFromQueryString: function () {
285
+ loadDataFromQueryString: async function () {
286
+ let data = L.Util.queryString('data', null)
287
+ const url = new URL(window.location.href)
288
+ const dataUrls = new URLSearchParams(url.search).getAll('dataUrl')
289
+ const dataFormat = L.Util.queryString('dataFormat', 'geojson')
290
+ if (dataUrls.length) {
291
+ for (let dataUrl of dataUrls) {
292
+ dataUrl = decodeURIComponent(dataUrl)
293
+ dataUrl = this.localizeUrl(dataUrl)
294
+ dataUrl = this.proxyUrl(dataUrl)
295
+ const datalayer = this.createDataLayer()
296
+ await datalayer.importFromUrl(dataUrl, dataFormat)
297
+ }
298
+ } else if (data) {
299
+ data = decodeURIComponent(data)
300
+ const datalayer = this.createDataLayer()
301
+ await datalayer.importRaw(data, dataFormat)
302
+ }
303
+ },
304
+
305
+ setViewFromQueryString: async function () {
299
306
  if (this.options.noControl) return
300
- this.initCaptionBar()
301
307
  if (L.Util.queryString('share')) {
302
308
  this.share.open()
303
309
  } else if (this.options.onLoadPanel === 'databrowser') {
@@ -539,18 +545,16 @@ U.Map = L.Map.extend({
539
545
  initShortcuts: function () {
540
546
  const globalShortcuts = function (e) {
541
547
  if (e.key === 'Escape') {
542
- if (this.dialog.visible) {
543
- this.dialog.close()
544
- } else if (this.importer.dialog.visible) {
548
+ if (this.importer.dialog.visible) {
545
549
  this.importer.dialog.close()
546
550
  } else if (this.editEnabled && this.editTools.drawing()) {
547
551
  this.editTools.stopDrawing()
548
552
  } else if (this.measureTools.enabled()) {
549
553
  this.measureTools.stopDrawing()
550
- } else if (this.editPanel?.isOpen()) {
551
- this.editPanel?.close()
552
554
  } else if (this.fullPanel?.isOpen()) {
553
555
  this.fullPanel?.close()
556
+ } else if (this.editPanel?.isOpen()) {
557
+ this.editPanel?.close()
554
558
  } else if (this.panel.isOpen()) {
555
559
  this.panel.close()
556
560
  }
@@ -1465,12 +1469,8 @@ U.Map = L.Map.extend({
1465
1469
  { handler: 'Switch', label: L._('Autostart when map is loaded') },
1466
1470
  ],
1467
1471
  ]
1468
- const slideshowHandler = function () {
1469
- this.slideshow.setOptions(this.options.slideshow)
1470
- }
1471
1472
  const slideshowBuilder = new U.FormBuilder(this, slideshowFields, {
1472
- callback: slideshowHandler,
1473
- callbackContext: this,
1473
+ callback: () => this.slideshow.setOptions(this.options.slideshow),
1474
1474
  })
1475
1475
  slideshow.appendChild(slideshowBuilder.build())
1476
1476
  },
@@ -1642,9 +1642,12 @@ U.Map = L.Map.extend({
1642
1642
  },
1643
1643
 
1644
1644
  askForReset: function (e) {
1645
- if (!confirm(L._('Are you sure you want to cancel your changes?'))) return
1646
- this.reset()
1647
- this.disableEdit()
1645
+ this.dialog
1646
+ .confirm(L._('Are you sure you want to cancel your changes?'))
1647
+ .then(() => {
1648
+ this.reset()
1649
+ this.disableEdit()
1650
+ })
1648
1651
  },
1649
1652
 
1650
1653
  startMarker: function () {
@@ -1888,4 +1891,15 @@ U.Map = L.Map.extend({
1888
1891
  })
1889
1892
  await this.server.post(sendLink, {}, formData)
1890
1893
  },
1894
+
1895
+ allProperties: function () {
1896
+ return [].concat(...this.datalayers_index.map((dl) => dl._propertiesIndex))
1897
+ },
1898
+
1899
+ sortedValues: function (property) {
1900
+ return []
1901
+ .concat(...this.datalayers_index.map((dl) => dl.sortedValues(property)))
1902
+ .filter((val, idx, arr) => arr.indexOf(val) === idx)
1903
+ .sort(U.Utils.naturalSort)
1904
+ },
1891
1905
  })
@@ -126,7 +126,77 @@ U.Layer.Cluster = L.MarkerClusterGroup.extend({
126
126
  },
127
127
  })
128
128
 
129
- U.Layer.Choropleth = L.FeatureGroup.extend({
129
+ // Layer where each feature color is relative to the others,
130
+ // so we need all features before behing able to set one
131
+ // feature layer
132
+ U.RelativeColorLayer = L.FeatureGroup.extend({
133
+ initialize: function (datalayer) {
134
+ this.datalayer = datalayer
135
+ this.colorSchemes = Object.keys(colorbrewer)
136
+ .filter((k) => k !== 'schemeGroups')
137
+ .sort()
138
+ const key = this.getType().toLowerCase()
139
+ if (!U.Utils.isObject(this.datalayer.options[key])) {
140
+ this.datalayer.options[key] = {}
141
+ }
142
+ L.FeatureGroup.prototype.initialize.call(this, [], this.datalayer.options[key])
143
+ this.datalayer.onceDataLoaded(() => {
144
+ this.redraw()
145
+ this.datalayer.on('datachanged', this.redraw, this)
146
+ })
147
+ },
148
+
149
+ redraw: function () {
150
+ this.compute()
151
+ if (this._map) this.eachLayer(this._map.addLayer, this._map)
152
+ },
153
+
154
+ getOption: function (option, feature) {
155
+ if (feature && option === feature.staticOptions.mainColor) {
156
+ return this.getColor(feature)
157
+ }
158
+ },
159
+
160
+ addLayer: function (layer) {
161
+ // Do not add yet the layer to the map
162
+ // wait for datachanged event, so we can compute breaks only once
163
+ const id = this.getLayerId(layer)
164
+ this._layers[id] = layer
165
+ return this
166
+ },
167
+
168
+ onAdd: function (map) {
169
+ this.compute()
170
+ L.FeatureGroup.prototype.onAdd.call(this, map)
171
+ },
172
+
173
+ getValues: function () {
174
+ const values = []
175
+ this.datalayer.eachLayer((layer) => {
176
+ const value = this._getValue(layer)
177
+ if (value !== undefined) values.push(value)
178
+ })
179
+ return values
180
+ },
181
+
182
+ renderLegend: function (container) {
183
+ const parent = L.DomUtil.create('ul', '', container)
184
+ const items = this.getLegendItems()
185
+ for (const [color, label] of items) {
186
+ const li = L.DomUtil.create('li', '', parent)
187
+ const colorEl = L.DomUtil.create('span', 'datalayer-color', li)
188
+ colorEl.style.backgroundColor = color
189
+ const labelEl = L.DomUtil.create('span', '', li)
190
+ labelEl.textContent = label
191
+ }
192
+ },
193
+
194
+ getColorSchemes: function (classes) {
195
+ return this.colorSchemes.filter((scheme) => Boolean(colorbrewer[scheme][classes]))
196
+ },
197
+ })
198
+
199
+ U.Layer.Choropleth = U.RelativeColorLayer.extend({
130
200
  statics: {
131
201
  NAME: L._('Choropleth'),
132
202
  TYPE: 'Choropleth',
@@ -147,42 +217,13 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
147
217
  manual: L._('Manual'),
148
218
  },
149
219
 
150
- initialize: function (datalayer) {
151
- this.datalayer = datalayer
152
- if (!U.Utils.isObject(this.datalayer.options.choropleth)) {
153
- this.datalayer.options.choropleth = {}
154
- }
155
- L.FeatureGroup.prototype.initialize.call(
156
- this,
157
- [],
158
- this.datalayer.options.choropleth
159
- )
160
- this.datalayer.onceDataLoaded(() => {
161
- this.redraw()
162
- this.datalayer.on('datachanged', this.redraw, this)
163
- })
164
- },
165
-
166
- redraw: function () {
167
- this.computeBreaks()
168
- if (this._map) this.eachLayer(this._map.addLayer, this._map)
169
- },
170
-
171
220
  _getValue: function (feature) {
172
221
  const key = this.datalayer.options.choropleth.property || 'value'
173
- return +feature.properties[key] // TODO: should we catch values non castable to int ?
174
- },
175
-
176
- getValues: function () {
177
- const values = []
178
- this.datalayer.eachLayer((layer) => {
179
- const value = this._getValue(layer)
180
- if (!Number.isNaN(value)) values.push(value)
181
- })
182
- return values
222
+ const value = +feature.properties[key]
223
+ if (!Number.isNaN(value)) return value
183
224
  },
184
225
 
185
- computeBreaks: function () {
226
+ compute: function () {
186
227
  const values = this.getValues()
187
228
 
188
229
  if (!values.length) {
@@ -224,7 +265,7 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
224
265
  },
225
266
 
226
267
  getColor: function (feature) {
227
- if (!feature) return // FIXME shold not happen
268
+ if (!feature) return // FIXME should not happen
228
269
  const featureValue = this._getValue(feature)
229
270
  // Find the bucket/step/limit that this value is less than and give it that color
230
271
  for (let i = 1; i < this.options.breaks.length; i++) {
@@ -234,25 +275,6 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
234
275
  }
235
276
  },
236
277
 
237
- getOption: function (option, feature) {
238
- if (feature && option === feature.staticOptions.mainColor) {
239
- return this.getColor(feature)
240
- }
241
- },
242
-
243
- addLayer: function (layer) {
244
- // Do not add yet the layer to the map
245
- // wait for datachanged event, so we want compute breaks once
246
- const id = this.getLayerId(layer)
247
- this._layers[id] = layer
248
- return this
249
- },
250
-
251
- onAdd: function (map) {
252
- this.computeBreaks()
253
- L.FeatureGroup.prototype.onAdd.call(this, map)
254
- },
255
-
256
278
  onEdit: function (field, builder) {
257
279
  // Only compute the breaks if we're dealing with choropleth
258
280
  if (!field.startsWith('options.choropleth')) return
@@ -261,7 +283,7 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
261
283
  this.datalayer.options.choropleth.mode = 'manual'
262
284
  if (builder) builder.helpers['options.choropleth.mode'].fetch()
263
285
  }
264
- this.computeBreaks()
286
+ this.compute()
265
287
  // If user changes the mode or the number of classes,
266
288
  // then update the breaks input value
267
289
  if (field === 'options.choropleth.mode' || field === 'options.choropleth.classes') {
@@ -270,10 +292,6 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
270
292
  },
271
293
 
272
294
  getEditableOptions: function () {
273
- const brewerSchemes = Object.keys(colorbrewer)
274
- .filter((k) => k !== 'schemeGroups')
275
- .sort()
276
-
277
295
  return [
278
296
  [
279
297
  'options.choropleth.property',
@@ -288,7 +306,7 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
288
306
  {
289
307
  handler: 'Select',
290
308
  label: L._('Choropleth color palette'),
291
- selectOptions: brewerSchemes,
309
+ selectOptions: this.colorSchemes,
292
310
  },
293
311
  ],
294
312
  [
@@ -324,20 +342,139 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
324
342
  ]
325
343
  },
326
344
 
327
- renderLegend: function (container) {
328
- const parent = L.DomUtil.create('ul', '', container)
329
- let li
330
- let color
331
- let label
332
-
333
- this.options.breaks.slice(0, -1).forEach((limit, index) => {
334
- li = L.DomUtil.create('li', '', parent)
335
- color = L.DomUtil.create('span', 'datalayer-color', li)
336
- color.style.backgroundColor = this.options.colors[index]
337
- label = L.DomUtil.create('span', '', li)
338
- label.textContent = `${+this.options.breaks[index].toFixed(
339
- 1
340
- )} - ${+this.options.breaks[index + 1].toFixed(1)}`
345
+ getLegendItems: function () {
346
+ return this.options.breaks.slice(0, -1).map((el, index) => {
347
+ const from = +this.options.breaks[index].toFixed(1)
348
+ const to = +this.options.breaks[index + 1].toFixed(1)
349
+ return [this.options.colors[index], `${from} - ${to}`]
350
+ })
351
+ },
352
+ })
353
+
354
+ U.Layer.Categorized = U.RelativeColorLayer.extend({
355
+ statics: {
356
+ NAME: L._('Categorized'),
357
+ TYPE: 'Categorized',
358
+ },
359
+ includes: [U.Layer],
360
+ MODES: {
361
+ manual: L._('Manual'),
362
+ alpha: L._('Alphabetical'),
363
+ },
364
+ defaults: {
365
+ color: 'white',
366
+ fillColor: 'red',
367
+ fillOpacity: 0.7,
368
+ weight: 2,
369
+ },
370
+
371
+ _getValue: function (feature) {
372
+ const key =
373
+ this.datalayer.options.categorized.property || this.datalayer._propertiesIndex[0]
374
+ return feature.properties[key]
375
+ },
376
+
377
+ getColor: function (feature) {
378
+ if (!feature) return // FIXME should not happen
379
+ const featureValue = this._getValue(feature)
380
+ for (let i = 0; i < this.options.categories.length; i++) {
381
+ if (featureValue === this.options.categories[i]) {
382
+ return this.options.colors[i]
383
+ }
384
+ }
385
+ },
386
+
387
+ compute: function () {
388
+ const values = this.getValues()
389
+
390
+ if (!values.length) {
391
+ this.options.categories = []
392
+ this.options.colors = []
393
+ return
394
+ }
395
+ const mode = this.datalayer.options.categorized.mode
396
+ let categories = []
397
+ if (mode === 'manual') {
398
+ const manualCategories = this.datalayer.options.categorized.categories
399
+ if (manualCategories) {
400
+ categories = manualCategories.split(',')
401
+ }
402
+ } else {
403
+ categories = values
404
+ .filter((val, idx, arr) => arr.indexOf(val) === idx)
405
+ .sort(U.Utils.naturalSort)
406
+ }
407
+ this.options.categories = categories
408
+ this.datalayer.options.categorized.categories = this.options.categories.join(',')
409
+ const fillColor = this.datalayer.getOption('fillColor') || this.defaults.fillColor
410
+ const colorScheme = this.datalayer.options.categorized.brewer
411
+ this._classes = this.options.categories.length
412
+ if (colorbrewer[colorScheme]?.[this._classes]) {
413
+ this.options.colors = colorbrewer[colorScheme][this._classes]
414
+ } else {
415
+ this.options.colors = colorbrewer?.Accent[this._classes]
416
+ ? colorbrewer?.Accent[this._classes]
417
+ : U.COLORS
418
+ }
419
+ },
420
+
421
+ getEditableOptions: function () {
422
+ return [
423
+ [
424
+ 'options.categorized.property',
425
+ {
426
+ handler: 'Select',
427
+ selectOptions: this.datalayer._propertiesIndex,
428
+ label: L._('Category property'),
429
+ },
430
+ ],
431
+ [
432
+ 'options.categorized.brewer',
433
+ {
434
+ handler: 'Select',
435
+ label: L._('Color palette'),
436
+ selectOptions: this.getColorSchemes(this._classes),
437
+ },
438
+ ],
439
+ [
440
+ 'options.categorized.categories',
441
+ {
442
+ handler: 'BlurInput',
443
+ label: L._('Categories'),
444
+ helpText: L._('Comma separated list of categories.'),
445
+ },
446
+ ],
447
+ [
448
+ 'options.categorized.mode',
449
+ {
450
+ handler: 'MultiChoice',
451
+ default: 'alpha',
452
+ choices: Object.entries(this.MODES),
453
+ label: L._('Categories mode'),
454
+ },
455
+ ],
456
+ ]
457
+ },
458
+
459
+ onEdit: function (field, builder) {
460
+ // Only compute the categories if we're dealing with categorized
461
+ if (!field.startsWith('options.categorized')) return
462
+ // If user touches the categories, then force manual mode
463
+ if (field === 'options.categorized.categories') {
464
+ this.datalayer.options.categorized.mode = 'manual'
465
+ if (builder) builder.helpers['options.categorized.mode'].fetch()
466
+ }
467
+ this.compute()
468
+ // If user changes the mode
469
+ // then update the categories input value
470
+ if (field === 'options.categorized.mode') {
471
+ if (builder) builder.helpers['options.categorized.categories'].fetch()
472
+ }
473
+ },
474
+
475
+ getLegendItems: function () {
476
+ return this.options.categories.map((limit, index) => {
477
+ return [this.options.colors[index], this.options.categories[index]]
341
478
  })
342
479
  },
343
480
  })
@@ -614,9 +751,9 @@ U.DataLayer = L.Evented.extend({
614
751
  this.resetLayer()
615
752
  }
616
753
  this.hide()
617
- fields.forEach((field) => {
754
+ for (const field of fields) {
618
755
  this.layer.onEdit(field, builder)
619
- })
756
+ }
620
757
  this.redraw()
621
758
  this.show()
622
759
  break
@@ -733,8 +870,8 @@ U.DataLayer = L.Evented.extend({
733
870
  this.addData(geojson, sync)
734
871
  this._geojson = geojson
735
872
  this._dataloaded = true
736
- this.fire('dataloaded')
737
873
  this.fire('datachanged')
874
+ this.fire('dataloaded')
738
875
  },
739
876
 
740
877
  fromUmapGeoJSON: async function (geojson) {
@@ -792,11 +929,9 @@ U.DataLayer = L.Evented.extend({
792
929
  const response = await this.map.request.get(url)
793
930
  if (response?.ok) {
794
931
  this.clear()
795
- this.rawToGeoJSON(
796
- await response.text(),
797
- this.options.remoteData.format,
798
- (geojson) => this.fromGeoJSON(geojson)
799
- )
932
+ this.map.formatter
933
+ .parse(await response.text(), this.options.remoteData.format)
934
+ .then((geojson) => this.fromGeoJSON(geojson))
800
935
  }
801
936
  },
802
937
 
@@ -899,7 +1034,7 @@ U.DataLayer = L.Evented.extend({
899
1034
  this._index.splice(this._index.indexOf(id), 1)
900
1035
  delete this._layers[id]
901
1036
  delete this.map.features_index[feature.getSlug()]
902
- if (this.hasDataLoaded()) this.fire('datachanged')
1037
+ if (this.hasDataLoaded() && this.isVisible()) this.fire('datachanged')
903
1038
  },
904
1039
 
905
1040
  indexProperties: function (feature) {
@@ -912,6 +1047,7 @@ U.DataLayer = L.Evented.extend({
912
1047
  if (name.indexOf('_') === 0) return
913
1048
  if (L.Util.indexOf(this._propertiesIndex, name) !== -1) return
914
1049
  this._propertiesIndex.push(name)
1050
+ this._propertiesIndex.sort()
915
1051
  },
916
1052
 
917
1053
  deindexProperty: function (name) {
@@ -919,6 +1055,13 @@ U.DataLayer = L.Evented.extend({
919
1055
  if (idx !== -1) this._propertiesIndex.splice(idx, 1)
920
1056
  },
921
1057
 
1058
+ sortedValues: function (property) {
1059
+ return Object.values(this._layers)
1060
+ .map((feature) => feature.properties[property])
1061
+ .filter((val, idx, arr) => arr.indexOf(val) === idx)
1062
+ .sort(U.Utils.naturalSort)
1063
+ },
1064
+
922
1065
  addData: function (geojson, sync) {
923
1066
  try {
924
1067
  // Do not fail if remote data is somehow invalid,
@@ -930,83 +1073,6 @@ U.DataLayer = L.Evented.extend({
930
1073
  }
931
1074
  },
932
1075
 
933
- addRawData: function (c, type) {
934
- this.rawToGeoJSON(c, type, (geojson) => this.addData(geojson))
935
- },
936
-
937
- rawToGeoJSON: (c, type, callback) => {
938
- const toDom = (x) => {
939
- const doc = new DOMParser().parseFromString(x, 'text/xml')
940
- const errorNode = doc.querySelector('parsererror')
941
- if (errorNode) {
942
- U.Alert.error(L._('Cannot parse data'))
943
- }
944
- return doc
945
- }
946
-
947
- // TODO add a duck typing guessType
948
- if (type === 'csv') {
949
- csv2geojson.csv2geojson(
950
- c,
951
- {
952
- delimiter: 'auto',
953
- includeLatLon: false,
954
- },
955
- (err, result) => {
956
- // csv2geojson fallback to null geometries when it cannot determine
957
- // lat or lon columns. This is valid geojson, but unwanted from a user
958
- // point of view.
959
- if (result?.features.length) {
960
- if (result.features[0].geometry === null) {
961
- err = {
962
- type: 'Error',
963
- message: L._('Cannot determine latitude and longitude columns.'),
964
- }
965
- }
966
- }
967
- if (err) {
968
- let message
969
- if (err.type === 'Error') {
970
- message = err.message
971
- } else {
972
- message = L._('{count} errors during import: {message}', {
973
- count: err.length,
974
- message: err[0].message,
975
- })
976
- }
977
- U.Alert.error(message, 10000)
978
- console.error(err)
979
- }
980
- if (result?.features.length) {
981
- callback(result)
982
- }
983
- }
984
- )
985
- } else if (type === 'gpx') {
986
- callback(toGeoJSON.gpx(toDom(c)))
987
- } else if (type === 'georss') {
988
- callback(GeoRSSToGeoJSON(toDom(c)))
989
- } else if (type === 'kml') {
990
- callback(toGeoJSON.kml(toDom(c)))
991
- } else if (type === 'osm') {
992
- let d
993
- try {
994
- d = JSON.parse(c)
995
- } catch (e) {
996
- d = toDom(c)
997
- }
998
- callback(osmtogeojson(d, { flatProperties: true }))
999
- } else if (type === 'geojson') {
1000
- try {
1001
- const gj = JSON.parse(c)
1002
- callback(gj)
1003
- } catch (err) {
1004
- U.Alert.error(`Invalid JSON file: ${err}`)
1005
- return
1006
- }
1007
- }
1008
- },
1009
-
1010
1076
  // The choice of the name is not ours, because it is required by Leaflet.
1011
1077
  // It is misleading, as the returned objects are uMap objects, and not
1012
1078
  // GeoJSON features.
@@ -1136,10 +1202,12 @@ U.DataLayer = L.Evented.extend({
1136
1202
  return new U.Polygon(this.map, latlngs, { geojson: geojson, datalayer: this }, id)
1137
1203
  },
1138
1204
 
1139
- importRaw: function (raw, type) {
1140
- this.addRawData(raw, type)
1205
+ importRaw: async function (raw, format) {
1206
+ this.map.formatter
1207
+ .parse(raw, format)
1208
+ .then((geojson) => this.addData(geojson))
1209
+ .then(() => this.zoomTo())
1141
1210
  this.isDirty = true
1142
- this.zoomTo()
1143
1211
  },
1144
1212
 
1145
1213
  importFromFiles: function (files, type) {
@@ -1735,9 +1803,9 @@ U.DataLayer = L.Evented.extend({
1735
1803
  },
1736
1804
 
1737
1805
  tableEdit: function () {
1738
- if (this.isRemoteLayer() || !this.isVisible()) return
1806
+ if (!this.isVisible()) return
1739
1807
  const editor = new U.TableEditor(this)
1740
- editor.edit()
1808
+ editor.open()
1741
1809
  },
1742
1810
 
1743
1811
  getFilterKeys: function () {
@@ -116,7 +116,7 @@ U.MapPermissions = L.Class.extend({
116
116
  L._('Advanced actions')
117
117
  )
118
118
  const advancedButtons = L.DomUtil.create('div', 'button-bar', advancedActions)
119
- const download = L.DomUtil.createButton(
119
+ L.DomUtil.createButton(
120
120
  'button',
121
121
  advancedButtons,
122
122
  L._('Attach the map to my account'),
@@ -265,7 +265,7 @@ U.PopupTemplate.OSM = U.PopupTemplate.Default.extend({
265
265
  const props = this.feature.properties
266
266
  const container = L.DomUtil.add('div')
267
267
  const title = L.DomUtil.add('h3', 'popup-title', container)
268
- const color = this.feature.getDynamicOption('color')
268
+ const color = this.feature.getPreviewColor()
269
269
  title.style.backgroundColor = color
270
270
  const iconUrl = this.feature.getDynamicOption('iconUrl')
271
271
  const icon = U.Icon.makeIconElement(iconUrl, title)