umap-project 3.1.2__py3-none-any.whl → 3.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of umap-project might be problematic. Click here for more details.

Files changed (199) hide show
  1. umap/__init__.py +1 -1
  2. umap/locale/en/LC_MESSAGES/django.mo +0 -0
  3. umap/locale/en/LC_MESSAGES/django.po +22 -18
  4. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  5. umap/locale/fr/LC_MESSAGES/django.po +21 -17
  6. umap/management/commands/export_pictogram.py +29 -0
  7. umap/management/commands/migrate_to_S3.py +5 -1
  8. umap/management/commands/purge_old_versions.py +8 -6
  9. umap/settings/__init__.py +21 -0
  10. umap/settings/base.py +3 -0
  11. umap/static/umap/content.css +7 -2
  12. umap/static/umap/css/contextmenu.css +58 -2
  13. umap/static/umap/css/form.css +175 -45
  14. umap/static/umap/css/icon.css +97 -3
  15. umap/static/umap/css/panel.css +31 -1
  16. umap/static/umap/img/16-white.svg +21 -40
  17. umap/static/umap/img/16.svg +1 -1
  18. umap/static/umap/img/24-white.svg +9 -9
  19. umap/static/umap/img/24.svg +23 -10
  20. umap/static/umap/img/source/16-white.svg +23 -41
  21. umap/static/umap/img/source/16.svg +1 -1
  22. umap/static/umap/img/source/24-white.svg +11 -11
  23. umap/static/umap/img/source/24.svg +25 -12
  24. umap/static/umap/js/modules/browser.js +1 -1
  25. umap/static/umap/js/modules/caption.js +8 -0
  26. umap/static/umap/js/modules/data/features.js +331 -202
  27. umap/static/umap/js/modules/data/layer.js +263 -152
  28. umap/static/umap/js/modules/facets.js +2 -2
  29. umap/static/umap/js/modules/form/builder.js +11 -7
  30. umap/static/umap/js/modules/form/fields.js +66 -26
  31. umap/static/umap/js/modules/formatter.js +78 -28
  32. umap/static/umap/js/modules/importer.js +6 -1
  33. umap/static/umap/js/modules/importers/opendata.js +138 -33
  34. umap/static/umap/js/modules/importers/openrouteservice.js +140 -0
  35. umap/static/umap/js/modules/managers.js +67 -0
  36. umap/static/umap/js/modules/printer.js +107 -0
  37. umap/static/umap/js/modules/rendering/controls.js +78 -2
  38. umap/static/umap/js/modules/rendering/icon.js +116 -87
  39. umap/static/umap/js/modules/rendering/layers/classified.js +8 -7
  40. umap/static/umap/js/modules/rendering/layers/cluster.js +199 -63
  41. umap/static/umap/js/modules/rendering/map.js +6 -2
  42. umap/static/umap/js/modules/rendering/template.js +71 -1
  43. umap/static/umap/js/modules/rendering/ui.js +111 -34
  44. umap/static/umap/js/modules/rules.js +76 -23
  45. umap/static/umap/js/modules/schema.js +27 -0
  46. umap/static/umap/js/modules/share.js +19 -12
  47. umap/static/umap/js/modules/slideshow.js +1 -1
  48. umap/static/umap/js/modules/sync/updaters.js +1 -6
  49. umap/static/umap/js/modules/tableeditor.js +13 -37
  50. umap/static/umap/js/modules/templates.js +7 -6
  51. umap/static/umap/js/modules/ui/bar.js +6 -1
  52. umap/static/umap/js/modules/ui/base.js +24 -9
  53. umap/static/umap/js/modules/ui/contextmenu.js +17 -7
  54. umap/static/umap/js/modules/ui/dialog.js +7 -4
  55. umap/static/umap/js/modules/ui/panel.js +7 -0
  56. umap/static/umap/js/modules/umap.js +84 -67
  57. umap/static/umap/js/modules/utils.js +8 -7
  58. umap/static/umap/js/umap.controls.js +22 -57
  59. umap/static/umap/locale/am_ET.js +81 -9
  60. umap/static/umap/locale/am_ET.json +81 -9
  61. umap/static/umap/locale/ar.js +81 -9
  62. umap/static/umap/locale/ar.json +81 -9
  63. umap/static/umap/locale/ast.js +81 -9
  64. umap/static/umap/locale/ast.json +81 -9
  65. umap/static/umap/locale/bg.js +81 -9
  66. umap/static/umap/locale/bg.json +81 -9
  67. umap/static/umap/locale/br.js +68 -29
  68. umap/static/umap/locale/br.json +68 -29
  69. umap/static/umap/locale/ca.js +88 -16
  70. umap/static/umap/locale/ca.json +88 -16
  71. umap/static/umap/locale/cs_CZ.js +81 -9
  72. umap/static/umap/locale/cs_CZ.json +81 -9
  73. umap/static/umap/locale/da.js +48 -9
  74. umap/static/umap/locale/da.json +48 -9
  75. umap/static/umap/locale/de.js +48 -9
  76. umap/static/umap/locale/de.json +48 -9
  77. umap/static/umap/locale/el.js +58 -13
  78. umap/static/umap/locale/el.json +58 -13
  79. umap/static/umap/locale/en.js +48 -9
  80. umap/static/umap/locale/en.json +48 -9
  81. umap/static/umap/locale/en_US.json +81 -9
  82. umap/static/umap/locale/es.js +48 -9
  83. umap/static/umap/locale/es.json +48 -9
  84. umap/static/umap/locale/et.js +81 -9
  85. umap/static/umap/locale/et.json +81 -9
  86. umap/static/umap/locale/eu.js +97 -25
  87. umap/static/umap/locale/eu.json +97 -25
  88. umap/static/umap/locale/fa_IR.js +81 -9
  89. umap/static/umap/locale/fa_IR.json +81 -9
  90. umap/static/umap/locale/fi.js +81 -9
  91. umap/static/umap/locale/fi.json +81 -9
  92. umap/static/umap/locale/fr.js +48 -9
  93. umap/static/umap/locale/fr.json +48 -9
  94. umap/static/umap/locale/gl.js +81 -9
  95. umap/static/umap/locale/gl.json +81 -9
  96. umap/static/umap/locale/he.js +81 -9
  97. umap/static/umap/locale/he.json +81 -9
  98. umap/static/umap/locale/hr.js +81 -9
  99. umap/static/umap/locale/hr.json +81 -9
  100. umap/static/umap/locale/hu.js +72 -27
  101. umap/static/umap/locale/hu.json +72 -27
  102. umap/static/umap/locale/id.js +81 -9
  103. umap/static/umap/locale/id.json +81 -9
  104. umap/static/umap/locale/is.js +81 -9
  105. umap/static/umap/locale/is.json +81 -9
  106. umap/static/umap/locale/it.js +48 -9
  107. umap/static/umap/locale/it.json +48 -9
  108. umap/static/umap/locale/ja.js +81 -9
  109. umap/static/umap/locale/ja.json +81 -9
  110. umap/static/umap/locale/ko.js +81 -9
  111. umap/static/umap/locale/ko.json +81 -9
  112. umap/static/umap/locale/lt.js +81 -9
  113. umap/static/umap/locale/lt.json +81 -9
  114. umap/static/umap/locale/ms.js +81 -9
  115. umap/static/umap/locale/ms.json +81 -9
  116. umap/static/umap/locale/nl.js +48 -9
  117. umap/static/umap/locale/nl.json +48 -9
  118. umap/static/umap/locale/no.js +81 -9
  119. umap/static/umap/locale/no.json +81 -9
  120. umap/static/umap/locale/pl.js +81 -9
  121. umap/static/umap/locale/pl.json +81 -9
  122. umap/static/umap/locale/pl_PL.json +81 -9
  123. umap/static/umap/locale/pt.js +81 -9
  124. umap/static/umap/locale/pt.json +81 -9
  125. umap/static/umap/locale/pt_BR.js +91 -19
  126. umap/static/umap/locale/pt_BR.json +91 -19
  127. umap/static/umap/locale/pt_PT.js +81 -9
  128. umap/static/umap/locale/pt_PT.json +81 -9
  129. umap/static/umap/locale/ro.js +81 -9
  130. umap/static/umap/locale/ro.json +81 -9
  131. umap/static/umap/locale/ru.js +81 -9
  132. umap/static/umap/locale/ru.json +81 -9
  133. umap/static/umap/locale/sk_SK.js +81 -9
  134. umap/static/umap/locale/sk_SK.json +81 -9
  135. umap/static/umap/locale/sl.js +81 -9
  136. umap/static/umap/locale/sl.json +81 -9
  137. umap/static/umap/locale/sr.js +81 -9
  138. umap/static/umap/locale/sr.json +81 -9
  139. umap/static/umap/locale/sv.js +81 -9
  140. umap/static/umap/locale/sv.json +81 -9
  141. umap/static/umap/locale/th_TH.js +81 -9
  142. umap/static/umap/locale/th_TH.json +81 -9
  143. umap/static/umap/locale/tr.js +81 -9
  144. umap/static/umap/locale/tr.json +81 -9
  145. umap/static/umap/locale/uk_UA.js +81 -9
  146. umap/static/umap/locale/uk_UA.json +81 -9
  147. umap/static/umap/locale/vi.js +81 -9
  148. umap/static/umap/locale/vi.json +81 -9
  149. umap/static/umap/locale/vi_VN.json +81 -9
  150. umap/static/umap/locale/zh.js +81 -9
  151. umap/static/umap/locale/zh.json +81 -9
  152. umap/static/umap/locale/zh_CN.json +81 -9
  153. umap/static/umap/locale/zh_TW.Big5.json +81 -9
  154. umap/static/umap/locale/zh_TW.js +98 -26
  155. umap/static/umap/locale/zh_TW.json +98 -26
  156. umap/static/umap/map.css +325 -102
  157. umap/static/umap/vars.css +1 -0
  158. umap/static/umap/vendors/betterknown/betterknown.mjs +287 -0
  159. umap/static/umap/vendors/editable/Leaflet.Editable.js +3 -1
  160. umap/static/umap/vendors/openrouteservice/ors-js-client.js +521 -0
  161. umap/static/umap/vendors/openrouteservice/ors-js-client.js.map +1 -0
  162. umap/static/umap/vendors/simple-elevation-chart/elevation.js +63 -0
  163. umap/static/umap/vendors/simple-elevation-chart/elevation.svg +8 -0
  164. umap/static/umap/vendors/snapdom/snapdom.min.mjs +3 -0
  165. umap/storage/fs.py +3 -2
  166. umap/storage/staticfiles.py +12 -0
  167. umap/templates/base.html +4 -1
  168. umap/templates/umap/css.html +0 -4
  169. umap/templates/umap/js.html +1 -3
  170. umap/tests/base.py +9 -1
  171. umap/tests/integration/test_basics.py +3 -1
  172. umap/tests/integration/test_conditional_rules.py +79 -37
  173. umap/tests/integration/test_datalayer.py +1 -1
  174. umap/tests/integration/test_draw_polygon.py +3 -5
  175. umap/tests/integration/test_draw_polyline.py +4 -6
  176. umap/tests/integration/test_draw_route.py +178 -0
  177. umap/tests/integration/test_edit_datalayer.py +1 -1
  178. umap/tests/integration/test_edit_map.py +1 -1
  179. umap/tests/integration/test_edit_marker.py +8 -8
  180. umap/tests/integration/test_edit_polygon.py +2 -2
  181. umap/tests/integration/test_export_map.py +84 -10
  182. umap/tests/integration/test_import.py +140 -0
  183. umap/tests/integration/test_map_preview.py +1 -1
  184. umap/tests/integration/test_optimistic_merge.py +72 -12
  185. umap/tests/integration/test_share.py +1 -1
  186. umap/tests/integration/test_tableeditor.py +10 -7
  187. umap/tests/integration/test_websocket_sync.py +4 -4
  188. umap/utils.py +37 -0
  189. umap/views.py +18 -2
  190. umap_project-3.3.0.dist-info/METADATA +76 -0
  191. {umap_project-3.1.2.dist-info → umap_project-3.3.0.dist-info}/RECORD +194 -188
  192. umap/static/umap/vendors/markercluster/MarkerCluster.Default.css +0 -60
  193. umap/static/umap/vendors/markercluster/MarkerCluster.css +0 -14
  194. umap/static/umap/vendors/markercluster/leaflet.markercluster.js +0 -2
  195. umap/static/umap/vendors/markercluster/leaflet.markercluster.js.map +0 -1
  196. umap_project-3.1.2.dist-info/METADATA +0 -68
  197. {umap_project-3.1.2.dist-info → umap_project-3.3.0.dist-info}/WHEEL +0 -0
  198. {umap_project-3.1.2.dist-info → umap_project-3.3.0.dist-info}/entry_points.txt +0 -0
  199. {umap_project-3.1.2.dist-info → umap_project-3.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,4 @@
1
- import { DomEvent, DomUtil, stamp } from '../../vendors/leaflet/leaflet-src.esm.js'
1
+ import { DomEvent, DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
2
2
  import { translate } from './i18n.js'
3
3
  import * as Utils from './utils.js'
4
4
 
@@ -25,7 +25,7 @@ export default class Facets {
25
25
  }
26
26
 
27
27
  this._umap.datalayers.browsable().map((datalayer) => {
28
- datalayer.eachFeature((feature) => {
28
+ datalayer.features.forEach((feature) => {
29
29
  for (const name of names) {
30
30
  let value = feature.properties[name]
31
31
  const type = defined.get(name).type
@@ -70,7 +70,11 @@ export class Form extends Utils.WithEvents {
70
70
  }
71
71
 
72
72
  setter(field, value) {
73
- Utils.setObjectValue(this.obj, field, value)
73
+ if ('setter' in this.obj) {
74
+ this.obj.setter(field, value)
75
+ } else {
76
+ Utils.setObjectValue(this.obj, field, value)
77
+ }
74
78
  }
75
79
 
76
80
  restoreField(field) {
@@ -104,9 +108,13 @@ export class Form extends Utils.WithEvents {
104
108
  finish() {}
105
109
 
106
110
  getTemplate(helper) {
111
+ let tpl = helper.getTemplate()
112
+ if (helper.properties.label && !tpl.includes(helper.properties.label)) {
113
+ tpl = `<label>${helper.properties.label}${tpl}</label>`
114
+ }
107
115
  return `
108
116
  <div class="formbox" data-ref=container>
109
- ${helper.getTemplate()}
117
+ ${tpl}
110
118
  <small class="help-text" data-ref=helpText></small>
111
119
  </div>`
112
120
  }
@@ -171,11 +179,7 @@ export class MutatingForm extends Form {
171
179
 
172
180
  setter(field, value) {
173
181
  const oldValue = this.getter(field)
174
- if ('setter' in this.obj) {
175
- this.obj.setter(field, value)
176
- } else {
177
- super.setter(field, value)
178
- }
182
+ super.setter(field, value)
179
183
  if ('render' in this.obj) {
180
184
  this.obj.render([field], this)
181
185
  }
@@ -131,12 +131,13 @@ class BaseElement {
131
131
 
132
132
  Fields.Textarea = class extends BaseElement {
133
133
  getTemplate() {
134
- return `<textarea placeholder="${this.properties.placeholder || ''}" data-ref=textarea></textarea>`
134
+ return `<textarea placeholder="${this.properties.placeholder || ''}" name="${this.name}" data-ref=textarea></textarea>`
135
135
  }
136
136
 
137
137
  build() {
138
138
  super.build()
139
139
  this.textarea = this.elements.textarea
140
+ this.input = this.textarea
140
141
  this.fetch()
141
142
  this.textarea.addEventListener(
142
143
  'input',
@@ -363,6 +364,9 @@ Fields.Select = class extends BaseElement {
363
364
  }
364
365
 
365
366
  getOptions() {
367
+ if (this.properties.getOptions) {
368
+ return this.properties.getOptions()
369
+ }
366
370
  return this.properties.selectOptions
367
371
  }
368
372
 
@@ -421,7 +425,7 @@ Fields.IntSelect = class extends Fields.Select {
421
425
 
422
426
  Fields.EditableText = class extends BaseElement {
423
427
  getTemplate() {
424
- return `<span contentEditable class="${this.properties.className || ''}" data-ref=input></span>`
428
+ return `<span class="${this.properties.className || ''}" data-ref=input></span>`
425
429
  }
426
430
 
427
431
  buildTemplate() {
@@ -435,6 +439,24 @@ Fields.EditableText = class extends BaseElement {
435
439
  this.fetch()
436
440
  this.input.addEventListener('input', () => this.sync())
437
441
  this.input.addEventListener('keypress', (event) => this.onKeyPress(event))
442
+ this.input.addEventListener('dblclick', () => {
443
+ if (this.input.contentEditable !== true) {
444
+ this.input.contentEditable = true
445
+ this.input.focus()
446
+ }
447
+ })
448
+ this.input.addEventListener('blur', () => {
449
+ this.input.contentEditable = false
450
+ })
451
+ this.input.addEventListener('mouseover', () => {
452
+ this.builder._umap.tooltip.open({
453
+ content: translate('Double click to edit the name'),
454
+ anchor: this.input,
455
+ position: 'bottom',
456
+ delay: 500,
457
+ duration: 5000,
458
+ })
459
+ })
438
460
  }
439
461
 
440
462
  value() {
@@ -560,6 +582,9 @@ Fields.SlideshowDelay = class extends Fields.IntSelect {
560
582
  Fields.DataLayerSwitcher = class extends Fields.Select {
561
583
  getOptions() {
562
584
  const options = []
585
+ if (this.properties.allowEmpty) {
586
+ options.push([null, translate('Import in a new layer')])
587
+ }
563
588
  this.builder._umap.datalayers.reverse().map((datalayer) => {
564
589
  if (
565
590
  datalayer.isLoaded() &&
@@ -573,7 +598,7 @@ Fields.DataLayerSwitcher = class extends Fields.Select {
573
598
  }
574
599
 
575
600
  toHTML() {
576
- return this.obj.datalayer.id
601
+ return this.obj.datalayer?.id
577
602
  }
578
603
 
579
604
  toJS() {
@@ -582,7 +607,7 @@ Fields.DataLayerSwitcher = class extends Fields.Select {
582
607
 
583
608
  set() {
584
609
  this.builder._umap.lastUsedDataLayer = this.toJS()
585
- this.obj.changeDataLayer(this.toJS())
610
+ this.builder.setter(this.field, this.toJS())
586
611
  }
587
612
  }
588
613
 
@@ -654,7 +679,7 @@ Fields.PropertyInput = class extends Fields.BlurInput {
654
679
  super.build()
655
680
  const autocomplete = new AutocompleteDatalist(this.input)
656
681
  // Will be used on Umap and DataLayer
657
- const properties = this.builder.obj.allProperties()
682
+ const properties = this.builder.obj.fieldKeys
658
683
  autocomplete.suggestions = properties
659
684
  }
660
685
  }
@@ -691,10 +716,10 @@ Fields.IconUrl = class extends Fields.BlurInput {
691
716
 
692
717
  async onDefine() {
693
718
  this.footer.innerHTML = ''
694
- const [{ pictogram_list }, response, error] = await this.builder._umap.server.get(
719
+ const [{ data }, response, error] = await this.builder._umap.server.get(
695
720
  this.builder._umap.properties.urls.pictogram_list_json
696
721
  )
697
- if (!error) this.pictogram_list = pictogram_list
722
+ if (!error) this.pictogramCollections = data
698
723
  this.buildTabs()
699
724
  const value = this.value()
700
725
  if (Icon.RECENT.length) this.showRecentTab()
@@ -807,36 +832,52 @@ Fields.IconUrl = class extends Fields.BlurInput {
807
832
  this.updatePreview()
808
833
  }
809
834
 
810
- addCategory(items, name) {
835
+ addCategory(items, name, parent, attribution = null) {
811
836
  const hidden = name ? '' : ' hidden'
812
- const [parent, { grid }] = Utils.loadTemplateWithRefs(`
837
+ const [container, { grid }] = Utils.loadTemplateWithRefs(`
813
838
  <div class="umap-pictogram-category">
814
839
  <h6${hidden}>${name}</h6>
815
840
  <div class="umap-pictogram-grid" data-ref=grid></div>
816
841
  </div>
817
842
  `)
818
843
  let hasIcons = false
819
- for (const item of items) {
844
+ const sorted = items.sort((a, b) => Utils.naturalSort(a.name, b.name, U.lang))
845
+ for (const item of sorted) {
846
+ item.attribution ??= attribution
820
847
  hasIcons = this.addIconPreview(item, grid) || hasIcons
821
848
  }
822
- if (hasIcons) this.grid.appendChild(parent)
849
+ if (hasIcons) parent.appendChild(container)
823
850
  }
824
851
 
825
852
  buildSymbolsList() {
826
853
  this.grid.innerHTML = ''
827
854
  const categories = {}
828
855
  let category
829
- for (const props of this.pictogram_list) {
830
- category = props.category || translate('Generic')
831
- categories[category] = categories[category] || []
832
- categories[category].push(props)
833
- }
834
- const sorted = Object.entries(categories).sort(([a], [b]) =>
835
- Utils.naturalSort(a, b, U.lang)
856
+ const collectionsNames = Object.keys(this.pictogramCollections)
857
+ const [container, { select, icons }] = Utils.loadTemplateWithRefs(
858
+ '<div><select data-ref="select"></select><div data-ref="icons"></div></div>'
836
859
  )
837
- for (const [name, items] of sorted) {
838
- this.addCategory(items, name)
860
+ for (const name of collectionsNames) {
861
+ const option = Utils.loadTemplate(`<option value="${name}">${name}</option>`)
862
+ select.appendChild(option)
863
+ }
864
+ this.grid.appendChild(container)
865
+ select.hidden = collectionsNames.length === 1
866
+ const loadCollection = (name) => {
867
+ icons.innerHTML = ''
868
+ const collection = this.pictogramCollections[name || collectionsNames[0]]
869
+ if (!collection) return
870
+ const sorted = Object.entries(collection.categories).sort(([a], [b]) =>
871
+ Utils.naturalSort(a, b, U.lang)
872
+ )
873
+ for (const [name, items] of sorted) {
874
+ this.addCategory(items, name, icons, collection.attribution)
875
+ }
839
876
  }
877
+ loadCollection()
878
+ select.addEventListener('change', (event) => {
879
+ loadCollection(event.target.value)
880
+ })
840
881
  }
841
882
 
842
883
  buildRecentList() {
@@ -844,7 +885,7 @@ Fields.IconUrl = class extends Fields.BlurInput {
844
885
  const items = U.Icon.RECENT.map((src) => ({
845
886
  src,
846
887
  }))
847
- this.addCategory(items)
888
+ this.addCategory(items, null, this.grid)
848
889
  }
849
890
 
850
891
  isDefault() {
@@ -1266,11 +1307,10 @@ Fields.Range = class extends Fields.FloatInput {
1266
1307
  const step = this.properties.step || 1
1267
1308
  const digits = step < 1 ? 1 : 0
1268
1309
  const id = `range-${this.properties.label || this.name}`
1269
- for (
1270
- let i = this.properties.min;
1271
- i <= this.properties.max;
1272
- i += (this.properties.max - this.properties.min) / 10
1273
- ) {
1310
+ const range = this.properties.max - this.properties.min
1311
+ const ticks = this.properties.ticks || Math.min(20, range / step)
1312
+ const tickStep = range / ticks
1313
+ for (let i = this.properties.min; i <= this.properties.max; i += tickStep) {
1274
1314
  const ii = i.toFixed(digits)
1275
1315
  options += `<option value="${ii}" label="${ii}"></option>`
1276
1316
  }
@@ -2,35 +2,33 @@ import { uMapAlert as Alert } from '../components/alerts/alert.js'
2
2
  /* Uses globals for: csv2geojson, osmtogeojson (not available as ESM) */
3
3
  import { translate } from './i18n.js'
4
4
 
5
+ const parseTextGeom = async (geom) => {
6
+ try {
7
+ return JSON.parse(geom)
8
+ } catch (e) {
9
+ try {
10
+ const betterknown = await import('../../vendors/betterknown/betterknown.mjs')
11
+ return betterknown.wktToGeoJSON(geom)
12
+ } catch {
13
+ return null
14
+ }
15
+ }
16
+ }
17
+
5
18
  export const EXPORT_FORMATS = {
6
19
  geojson: {
7
- formatter: async (umap) => JSON.stringify(umap.toGeoJSON(), null, 2),
8
20
  ext: '.geojson',
9
21
  filetype: 'application/json',
10
22
  },
11
23
  gpx: {
12
- formatter: async (umap) => await umap.formatter.toGPX(umap.toGeoJSON()),
13
24
  ext: '.gpx',
14
25
  filetype: 'application/gpx+xml',
15
26
  },
16
27
  kml: {
17
- formatter: async (umap) => await umap.formatter.toKML(umap.toGeoJSON()),
18
28
  ext: '.kml',
19
29
  filetype: 'application/vnd.google-earth.kml+xml',
20
30
  },
21
31
  csv: {
22
- formatter: async (umap) => {
23
- const table = []
24
- umap.eachFeature((feature) => {
25
- const row = feature.toGeoJSON().properties
26
- const center = feature.center
27
- delete row._umap_options
28
- row.Latitude = center.lat
29
- row.Longitude = center.lng
30
- table.push(row)
31
- })
32
- return csv2geojson.dsv.csvFormat(table)
33
- },
34
32
  ext: '.csv',
35
33
  filetype: 'text/csv',
36
34
  },
@@ -81,15 +79,30 @@ export class Formatter {
81
79
  sexagesimal: false,
82
80
  parseLatLon: (raw) => Number.parseFloat(raw.toString().replace(',', '.')),
83
81
  },
84
- (err, result) => {
85
- // csv2geojson fallback to null geometries when it cannot determine
86
- // lat or lon columns. This is valid geojson, but unwanted from a user
87
- // point of view.
82
+ async (err, result) => {
88
83
  if (result?.features.length) {
89
- if (result.features[0].geometry === null) {
90
- err = {
91
- type: 'Error',
92
- message: translate('Cannot determine latitude and longitude columns.'),
84
+ const first = result.features[0]
85
+ if (first.geometry === null) {
86
+ const geomFields = ['geom', 'geometry', 'wkt', 'geojson']
87
+ for (const field of geomFields) {
88
+ if (first.properties[field]) {
89
+ for (const feature of result.features) {
90
+ feature.geometry = await parseTextGeom(feature.properties[field])
91
+ delete feature.properties[field]
92
+ }
93
+ break
94
+ }
95
+ }
96
+ if (first.geometry === null) {
97
+ // csv2geojson fallback to null geometries when it cannot determine
98
+ // lat or lon columns. This is valid geojson, but unwanted from a user
99
+ // point of view.
100
+ err = {
101
+ type: 'Error',
102
+ message: translate(
103
+ 'No geo column found: must be either `lat(itude)` and `lon(gitude)` or `geom(etry)`.'
104
+ ),
105
+ }
93
106
  }
94
107
  }
95
108
  }
@@ -152,17 +165,54 @@ export class Formatter {
152
165
  }
153
166
  }
154
167
 
155
- async toGPX(geojson) {
168
+ async stringify(features, format) {
169
+ switch (format) {
170
+ case 'csv':
171
+ return await this.toCSV(features)
172
+ case 'gpx':
173
+ return await this.toGPX(features)
174
+ case 'kml':
175
+ return await this.toKML(features)
176
+ case 'geojson':
177
+ return await this.toGeoJSON(features)
178
+ }
179
+ }
180
+
181
+ async toGPX(features) {
156
182
  const togpx = await import('../../vendors/geojson-to-gpx/index.js')
157
- for (const feature of geojson.features) {
183
+ for (const feature of features) {
158
184
  feature.properties.desc = feature.properties.description
159
185
  }
160
- const gpx = togpx.default(geojson)
186
+ const gpx = togpx.default(this.toFeatureCollection(features))
161
187
  return new XMLSerializer().serializeToString(gpx)
162
188
  }
163
189
 
164
- async toKML(geojson) {
190
+ async toKML(features) {
165
191
  const tokml = await import('../../vendors/tokml/tokml.es.js')
166
- return tokml.toKML(geojson)
192
+ return tokml.toKML(this.toFeatureCollection(features))
193
+ }
194
+
195
+ toFeatureCollection(features) {
196
+ return {
197
+ type: 'FeatureCollection',
198
+ features: features.map((f) => f.toGeoJSON()),
199
+ }
200
+ }
201
+
202
+ async toGeoJSON(features) {
203
+ return JSON.stringify(this.toFeatureCollection(features), null, 2)
204
+ }
205
+
206
+ async toCSV(features) {
207
+ const table = []
208
+ for (const feature of features) {
209
+ const row = feature.toGeoJSON().properties
210
+ const center = feature.center
211
+ delete row._umap_options
212
+ row.Latitude = center.lat
213
+ row.Longitude = center.lng
214
+ table.push(row)
215
+ }
216
+ return csv2geojson.dsv.csvFormat(table)
167
217
  }
168
218
  }
@@ -17,7 +17,7 @@ const TEMPLATE = `
17
17
  <input type="file" multiple autofocus onchange />
18
18
  <textarea onchange placeholder="${translate('Paste your data here')}"></textarea>
19
19
  <input class="highlightable" type="url" placeholder="${translate('Provide an URL here')}" onchange />
20
- <button class="flat importers" hidden data-ref="importersButton"><i class="icon icon-16 icon-magic"></i>${translate('Import helpers')}</button>
20
+ <button type=button class="importers" hidden data-ref="importersButton"><i class="icon icon-16 icon-magic"></i>${translate('Import helpers')}</button>
21
21
  </fieldset>
22
22
  <fieldset class="formbox">
23
23
  <legend class="counter" data-help="importFormats">${translate(
@@ -169,8 +169,13 @@ export default class Importer extends Utils.WithTemplate {
169
169
  this.onChange()
170
170
  }
171
171
 
172
+ set layer(layer) {
173
+ this._layer = layer
174
+ }
175
+
172
176
  get layer() {
173
177
  return (
178
+ this._layer ||
174
179
  this._umap.datalayers[this.layerId] ||
175
180
  this._umap.createDirtyDataLayer({ name: this.layerName })
176
181
  )
@@ -8,11 +8,21 @@ const PORTALS = [
8
8
  url: 'https://data.ampmetropole.fr',
9
9
  platform: 'opendatasoft',
10
10
  },
11
+ {
12
+ name: 'Auverge-Rhône-Alpes',
13
+ url: 'https://admin.open-datara.fr',
14
+ platform: 'prodige',
15
+ },
11
16
  {
12
17
  name: 'Bordeaux Métropole',
13
18
  url: 'https://opendata.bordeaux-metropole.fr',
14
19
  platform: 'opendatasoft',
15
20
  },
21
+ {
22
+ name: 'Nouvelle Aquitaine',
23
+ url: 'https://admin.sigena.fr',
24
+ platform: 'prodige',
25
+ },
16
26
  {
17
27
  name: 'Région Centre-Val de Loire',
18
28
  url: 'https://data.centrevaldeloire.fr',
@@ -33,6 +43,21 @@ const PORTALS = [
33
43
  url: 'https://data.iledefrance.fr',
34
44
  platform: 'opendatasoft',
35
45
  },
46
+ {
47
+ name: 'Martinique',
48
+ url: 'https://admin.geomartinique.fr',
49
+ platform: 'prodige',
50
+ },
51
+ {
52
+ name: 'Région Pays de la Loire',
53
+ url: 'https://admin.sigloire.fr',
54
+ platform: 'prodige',
55
+ },
56
+ {
57
+ name: 'Saint-Pierre et Miquelon',
58
+ url: 'https://admin.geospm.com',
59
+ platform: 'prodige',
60
+ },
36
61
  {
37
62
  name: 'Toulouse Métropole',
38
63
  url: 'https://data.toulouse-metropole.fr',
@@ -57,11 +82,84 @@ const TEMPLATE = `
57
82
  <option disabled selected value="">${translate('Choose a dataset')}</option>
58
83
  </select>
59
84
  <input type="hidden" name="geofield" data-ref="geofield">
60
- <label><input type="checkbox" name="in_bbox">${translate('Limit results to current map view')}</label>
85
+ <label data-ref="in_bbox" hidden><input type="checkbox" name="in_bbox">${translate('Limit results to current map view')}</label>
61
86
  </div>
62
87
  </div>
63
88
  `
64
89
 
90
+ class Connector {
91
+ constructor(umap, baseUrl) {
92
+ this.umap = umap
93
+ this.baseUrl = baseUrl
94
+ }
95
+ }
96
+
97
+ class Prodige extends Connector {
98
+ async datasets() {
99
+ const datasets = []
100
+ const endpoint = this.umap.proxyUrl(
101
+ `${this.baseUrl}/api/ogc-features/collections.json`,
102
+ 3600
103
+ )
104
+ const response = await this.umap.request.get(endpoint)
105
+ if (!response?.ok) return datasets
106
+ const data = await response.json()
107
+ for (const dataset of data.collections) {
108
+ let url
109
+ for (const link of dataset.links) {
110
+ if (link.type === 'application/geo+json') url = link.href
111
+ }
112
+ if (!url) continue
113
+ datasets.push({
114
+ label: dataset.title,
115
+ url: this.umap.proxyUrl(url, 3600),
116
+ })
117
+ }
118
+ return datasets.sort((a, b) => Utils.naturalSort(a.label, b.label, U.lang))
119
+ }
120
+ }
121
+
122
+ class OpenDataSoft extends Connector {
123
+ async datasets() {
124
+ let results = []
125
+ let total = null
126
+ const hardLimit = 500
127
+ while (total === null || results.length < total) {
128
+ const offset = results.length
129
+ const response = await this.umap.request.get(
130
+ `${this.baseUrl}/api/explore/v2.1/catalog/datasets?where=features%20in%20%28%22geo%22%29&limit=100&offset=${offset}&order_by=title asc`
131
+ )
132
+ if (!response?.ok) break
133
+ const data = await response.json()
134
+ if (total === null) {
135
+ total = data.total_count
136
+ }
137
+ results = results.concat(data.results)
138
+ if (total === null || results.length > hardLimit) break
139
+ }
140
+ const datasets = []
141
+ for (const result of results) {
142
+ const fields = result.fields.filter((field) => field.type === 'geo_point_2d')
143
+ if (!fields.length) {
144
+ console.debug('No geofield found for', result)
145
+ continue
146
+ }
147
+ if (fields.length > 1) {
148
+ console.debug('More than one geofield found for', result)
149
+ }
150
+ const url = `${this.baseUrl}/api/explore/v2.1/catalog/datasets/${result.dataset_id}/exports/geojson?select=%2A&limit=-1&timezone=UTC&use_labels=false&epsg=4326`
151
+ const bbox_url = `${url}&where=in_bbox%28${fields[0].name}%2C%20{south},{west},{north},{east}%29`
152
+ datasets.push({
153
+ id: result.dataset_id,
154
+ label: `${result.metas.default.title} (${result.metas.default.records_count})`,
155
+ url,
156
+ bbox_url,
157
+ })
158
+ }
159
+ return datasets
160
+ }
161
+ }
162
+
65
163
  export class Importer {
66
164
  constructor(umap, options = {}) {
67
165
  this.umap = umap
@@ -70,48 +168,54 @@ export class Importer {
70
168
  this.portals = options.choices || PORTALS
71
169
  }
72
170
 
171
+ resetSelect(select) {
172
+ Array.from(select.children).forEach((option) => {
173
+ if (!option.disabled) {
174
+ option.remove()
175
+ } else {
176
+ option.selected = true
177
+ }
178
+ })
179
+ }
180
+
73
181
  async open(importer) {
74
- let fields_map = {}
75
- const [container, { portals, datasets, geofield }] =
182
+ const [container, { portals, datasets, in_bbox }] =
76
183
  Utils.loadTemplateWithRefs(TEMPLATE)
184
+ datasets.addEventListener('change', (event) => {
185
+ const select = event.target
186
+ const selected = select.options[select.selectedIndex]
187
+ const bbox_url = selected.dataset.bbox_url
188
+ in_bbox.checked = false
189
+ in_bbox.hidden = !bbox_url
190
+ })
77
191
  portals.addEventListener('change', async (event) => {
78
- const response = await this.umap.request.get(
79
- `${event.target.value}/api/explore/v2.1/catalog/datasets?where=features%20in%20%28%22geo%22%29&limit=-1&offset=0&timezone=UTC`
80
- )
81
- if (response.ok) {
82
- fields_map = {}
83
- Array.from(datasets.children).forEach((option) => {
84
- if (!option.disabled) {
85
- option.remove()
86
- } else {
87
- option.selected = true
88
- }
89
- })
90
- const data = await response.json()
91
- for (const result of data.results) {
92
- const fields = result.fields.filter((field) => field.type === 'geo_point_2d')
93
- if (!fields.length) {
94
- console.debug('No geofield found for', result)
95
- continue
96
- }
97
- if (fields.length > 1) {
98
- console.debug('More than one geofield found for', result)
99
- }
100
- fields_map[result.dataset_id] = fields[0].name
192
+ const select = event.target
193
+ const selected = select.options[select.selectedIndex]
194
+ const platform = selected.dataset.platform
195
+ let connector
196
+ if (platform === 'opendatasoft') {
197
+ connector = new OpenDataSoft(this.umap, selected.value)
198
+ } else if (platform === 'prodige') {
199
+ connector = new Prodige(this.umap, selected.value)
200
+ } else {
201
+ console.error('Unknown platform', platform)
202
+ return
203
+ }
204
+ const results = await connector.datasets(event.target.value)
205
+ if (results) {
206
+ this.resetSelect(datasets)
207
+ for (const result of results) {
101
208
  const el = Utils.loadTemplate(
102
- `<option value="${result.dataset_id}">${result.metas.default.title} (${result.metas.default.records_count})</option>`
209
+ `<option value="${result.url}" data-url="${result.url}" data-bbox_url="${result.bbox_url || ''}">${result.label}</option>`
103
210
  )
104
211
  datasets.appendChild(el)
105
212
  }
106
213
  datasets.hidden = false
107
214
  }
108
215
  })
109
- datasets.addEventListener('change', (event) => {
110
- geofield.value = fields_map[event.target.value]
111
- })
112
216
  for (const instance of this.portals) {
113
217
  const el = Utils.loadTemplate(
114
- `<option value="${instance.url}">${instance.name}</option>`
218
+ `<option value="${instance.url}" data-platform="${instance.platform}">${instance.name}</option>`
115
219
  )
116
220
  portals.appendChild(el)
117
221
  }
@@ -121,9 +225,10 @@ export class Importer {
121
225
  Alert.error(translate('Please choose an instance first.'))
122
226
  return
123
227
  }
124
- let url = `${form.instance}/api/explore/v2.1/catalog/datasets/${form.dataset}/exports/geojson?select=%2A&limit=-1&timezone=UTC&use_labels=false&epsg=4326`
228
+ let url = form.dataset
125
229
  if (form.in_bbox) {
126
- url += `&where=in_bbox%28${form.geofield}%2C%20{south},{west},{north},{east}%29`
230
+ const selected = datasets.options[datasets.selectedIndex]
231
+ url = selected.dataset.bbox_url
127
232
  }
128
233
  importer.url = url
129
234
  importer.format = 'geojson'