umap-project 2.5.1__py3-none-any.whl → 2.6.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 (276) hide show
  1. umap/__init__.py +1 -1
  2. umap/admin.py +6 -1
  3. umap/context_processors.py +2 -1
  4. umap/decorators.py +13 -2
  5. umap/forms.py +26 -2
  6. umap/locale/br/LC_MESSAGES/django.mo +0 -0
  7. umap/locale/br/LC_MESSAGES/django.po +252 -146
  8. umap/locale/ca/LC_MESSAGES/django.mo +0 -0
  9. umap/locale/ca/LC_MESSAGES/django.po +274 -162
  10. umap/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  11. umap/locale/cs_CZ/LC_MESSAGES/django.po +261 -150
  12. umap/locale/de/LC_MESSAGES/django.mo +0 -0
  13. umap/locale/de/LC_MESSAGES/django.po +299 -187
  14. umap/locale/el/LC_MESSAGES/django.mo +0 -0
  15. umap/locale/el/LC_MESSAGES/django.po +215 -159
  16. umap/locale/en/LC_MESSAGES/django.po +211 -155
  17. umap/locale/es/LC_MESSAGES/django.mo +0 -0
  18. umap/locale/es/LC_MESSAGES/django.po +255 -144
  19. umap/locale/eu/LC_MESSAGES/django.mo +0 -0
  20. umap/locale/eu/LC_MESSAGES/django.po +254 -198
  21. umap/locale/fa_IR/LC_MESSAGES/django.mo +0 -0
  22. umap/locale/fa_IR/LC_MESSAGES/django.po +347 -235
  23. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  24. umap/locale/fr/LC_MESSAGES/django.po +216 -160
  25. umap/locale/hu/LC_MESSAGES/django.mo +0 -0
  26. umap/locale/hu/LC_MESSAGES/django.po +215 -159
  27. umap/locale/it/LC_MESSAGES/django.mo +0 -0
  28. umap/locale/it/LC_MESSAGES/django.po +252 -146
  29. umap/locale/ms/LC_MESSAGES/django.mo +0 -0
  30. umap/locale/ms/LC_MESSAGES/django.po +252 -146
  31. umap/locale/pl/LC_MESSAGES/django.mo +0 -0
  32. umap/locale/pl/LC_MESSAGES/django.po +254 -148
  33. umap/locale/pt/LC_MESSAGES/django.mo +0 -0
  34. umap/locale/pt/LC_MESSAGES/django.po +215 -159
  35. umap/locale/sv/LC_MESSAGES/django.mo +0 -0
  36. umap/locale/sv/LC_MESSAGES/django.po +254 -143
  37. umap/locale/th_TH/LC_MESSAGES/django.mo +0 -0
  38. umap/locale/th_TH/LC_MESSAGES/django.po +125 -70
  39. umap/locale/zh_TW/LC_MESSAGES/django.mo +0 -0
  40. umap/locale/zh_TW/LC_MESSAGES/django.po +256 -145
  41. umap/migrations/0022_add_team.py +94 -0
  42. umap/models.py +45 -10
  43. umap/settings/__init__.py +2 -0
  44. umap/settings/base.py +9 -2
  45. umap/static/umap/base.css +32 -41
  46. umap/static/umap/content.css +19 -25
  47. umap/static/umap/css/icon.css +63 -37
  48. umap/static/umap/css/importers.css +1 -1
  49. umap/static/umap/css/slideshow.css +7 -5
  50. umap/static/umap/css/tableeditor.css +4 -3
  51. umap/static/umap/img/16-white.svg +1 -4
  52. umap/static/umap/img/16.svg +2 -6
  53. umap/static/umap/img/24-white.svg +4 -4
  54. umap/static/umap/img/24.svg +6 -6
  55. umap/static/umap/img/source/16-white.svg +2 -5
  56. umap/static/umap/img/source/16.svg +3 -7
  57. umap/static/umap/img/source/24-white.svg +7 -14
  58. umap/static/umap/img/source/24.svg +10 -17
  59. umap/static/umap/js/components/alerts/alert.css +20 -8
  60. umap/static/umap/js/modules/autocomplete.js +8 -12
  61. umap/static/umap/js/modules/browser.js +4 -3
  62. umap/static/umap/js/modules/caption.js +9 -11
  63. umap/static/umap/js/modules/data/features.js +993 -0
  64. umap/static/umap/js/modules/data/layer.js +1210 -0
  65. umap/static/umap/js/modules/formatter.js +12 -3
  66. umap/static/umap/js/modules/global.js +21 -5
  67. umap/static/umap/js/modules/importers/overpass.js +22 -8
  68. umap/static/umap/js/modules/permissions.js +280 -0
  69. umap/static/umap/js/{umap.icon.js → modules/rendering/icon.js} +77 -56
  70. umap/static/umap/js/modules/rendering/layers/base.js +105 -0
  71. umap/static/umap/js/modules/rendering/layers/classified.js +484 -0
  72. umap/static/umap/js/modules/rendering/layers/cluster.js +103 -0
  73. umap/static/umap/js/modules/rendering/layers/heat.js +182 -0
  74. umap/static/umap/js/modules/rendering/popup.js +99 -0
  75. umap/static/umap/js/modules/rendering/template.js +217 -0
  76. umap/static/umap/js/modules/rendering/ui.js +610 -0
  77. umap/static/umap/js/modules/rules.js +16 -3
  78. umap/static/umap/js/modules/schema.js +25 -1
  79. umap/static/umap/js/modules/share.js +66 -45
  80. umap/static/umap/js/modules/sync/updaters.js +9 -10
  81. umap/static/umap/js/modules/tableeditor.js +7 -7
  82. umap/static/umap/js/modules/ui/dialog.js +8 -4
  83. umap/static/umap/js/modules/utils.js +22 -13
  84. umap/static/umap/js/umap.controls.js +80 -146
  85. umap/static/umap/js/umap.core.js +9 -9
  86. umap/static/umap/js/umap.forms.js +41 -17
  87. umap/static/umap/js/umap.js +72 -65
  88. umap/static/umap/locale/am_ET.js +8 -2
  89. umap/static/umap/locale/am_ET.json +8 -2
  90. umap/static/umap/locale/ar.js +8 -2
  91. umap/static/umap/locale/ar.json +8 -2
  92. umap/static/umap/locale/ast.js +8 -2
  93. umap/static/umap/locale/ast.json +8 -2
  94. umap/static/umap/locale/bg.js +8 -2
  95. umap/static/umap/locale/bg.json +8 -2
  96. umap/static/umap/locale/br.js +42 -36
  97. umap/static/umap/locale/br.json +42 -36
  98. umap/static/umap/locale/ca.js +67 -61
  99. umap/static/umap/locale/ca.json +67 -61
  100. umap/static/umap/locale/cs_CZ.js +8 -2
  101. umap/static/umap/locale/cs_CZ.json +8 -2
  102. umap/static/umap/locale/da.js +8 -2
  103. umap/static/umap/locale/da.json +8 -2
  104. umap/static/umap/locale/de.js +143 -137
  105. umap/static/umap/locale/de.json +143 -137
  106. umap/static/umap/locale/el.js +54 -48
  107. umap/static/umap/locale/el.json +54 -48
  108. umap/static/umap/locale/en.js +10 -2
  109. umap/static/umap/locale/en.json +10 -2
  110. umap/static/umap/locale/en_US.json +8 -2
  111. umap/static/umap/locale/es.js +8 -2
  112. umap/static/umap/locale/es.json +8 -2
  113. umap/static/umap/locale/et.js +8 -2
  114. umap/static/umap/locale/et.json +8 -2
  115. umap/static/umap/locale/eu.js +346 -338
  116. umap/static/umap/locale/eu.json +346 -338
  117. umap/static/umap/locale/fa_IR.js +415 -407
  118. umap/static/umap/locale/fa_IR.json +415 -407
  119. umap/static/umap/locale/fi.js +8 -2
  120. umap/static/umap/locale/fi.json +8 -2
  121. umap/static/umap/locale/fr.js +11 -3
  122. umap/static/umap/locale/fr.json +11 -3
  123. umap/static/umap/locale/gl.js +8 -2
  124. umap/static/umap/locale/gl.json +8 -2
  125. umap/static/umap/locale/he.js +8 -2
  126. umap/static/umap/locale/he.json +8 -2
  127. umap/static/umap/locale/hr.js +8 -2
  128. umap/static/umap/locale/hr.json +8 -2
  129. umap/static/umap/locale/hu.js +31 -23
  130. umap/static/umap/locale/hu.json +31 -23
  131. umap/static/umap/locale/id.js +8 -2
  132. umap/static/umap/locale/id.json +8 -2
  133. umap/static/umap/locale/is.js +8 -2
  134. umap/static/umap/locale/is.json +8 -2
  135. umap/static/umap/locale/it.js +8 -2
  136. umap/static/umap/locale/it.json +8 -2
  137. umap/static/umap/locale/ja.js +8 -2
  138. umap/static/umap/locale/ja.json +8 -2
  139. umap/static/umap/locale/ko.js +8 -2
  140. umap/static/umap/locale/ko.json +8 -2
  141. umap/static/umap/locale/lt.js +8 -2
  142. umap/static/umap/locale/lt.json +8 -2
  143. umap/static/umap/locale/ms.js +8 -2
  144. umap/static/umap/locale/ms.json +8 -2
  145. umap/static/umap/locale/nl.js +8 -2
  146. umap/static/umap/locale/nl.json +8 -2
  147. umap/static/umap/locale/no.js +8 -2
  148. umap/static/umap/locale/no.json +8 -2
  149. umap/static/umap/locale/pl.js +54 -48
  150. umap/static/umap/locale/pl.json +54 -48
  151. umap/static/umap/locale/pl_PL.json +8 -2
  152. umap/static/umap/locale/pt.js +24 -18
  153. umap/static/umap/locale/pt.json +24 -18
  154. umap/static/umap/locale/pt_BR.js +8 -2
  155. umap/static/umap/locale/pt_BR.json +8 -2
  156. umap/static/umap/locale/pt_PT.js +214 -208
  157. umap/static/umap/locale/pt_PT.json +214 -208
  158. umap/static/umap/locale/ro.js +8 -2
  159. umap/static/umap/locale/ro.json +8 -2
  160. umap/static/umap/locale/ru.js +8 -2
  161. umap/static/umap/locale/ru.json +8 -2
  162. umap/static/umap/locale/sk_SK.js +8 -2
  163. umap/static/umap/locale/sk_SK.json +8 -2
  164. umap/static/umap/locale/sl.js +8 -2
  165. umap/static/umap/locale/sl.json +8 -2
  166. umap/static/umap/locale/sr.js +8 -2
  167. umap/static/umap/locale/sr.json +8 -2
  168. umap/static/umap/locale/sv.js +8 -2
  169. umap/static/umap/locale/sv.json +8 -2
  170. umap/static/umap/locale/th_TH.js +33 -27
  171. umap/static/umap/locale/th_TH.json +33 -27
  172. umap/static/umap/locale/tr.js +8 -2
  173. umap/static/umap/locale/tr.json +8 -2
  174. umap/static/umap/locale/uk_UA.js +8 -2
  175. umap/static/umap/locale/uk_UA.json +8 -2
  176. umap/static/umap/locale/vi.js +8 -2
  177. umap/static/umap/locale/vi.json +8 -2
  178. umap/static/umap/locale/vi_VN.json +8 -2
  179. umap/static/umap/locale/zh.js +8 -2
  180. umap/static/umap/locale/zh.json +8 -2
  181. umap/static/umap/locale/zh_CN.json +8 -2
  182. umap/static/umap/locale/zh_TW.Big5.json +8 -2
  183. umap/static/umap/locale/zh_TW.js +102 -96
  184. umap/static/umap/locale/zh_TW.json +102 -96
  185. umap/static/umap/map.css +111 -108
  186. umap/static/umap/nav.css +19 -10
  187. umap/static/umap/unittests/utils.js +230 -107
  188. umap/static/umap/vars.css +1 -0
  189. umap/static/umap/vendors/csv2geojson/csv2geojson.js +62 -40
  190. umap/static/umap/vendors/editable/Leaflet.Editable.js +2079 -1937
  191. umap/storage.py +1 -0
  192. umap/templates/404.html +5 -1
  193. umap/templates/500.html +3 -1
  194. umap/templates/auth/user_detail.html +8 -2
  195. umap/templates/auth/user_form.html +19 -10
  196. umap/templates/auth/user_stars.html +8 -2
  197. umap/templates/base.html +1 -0
  198. umap/templates/registration/login.html +18 -3
  199. umap/templates/umap/about.html +1 -0
  200. umap/templates/umap/about_summary.html +22 -7
  201. umap/templates/umap/components/alerts/alert.html +42 -21
  202. umap/templates/umap/content.html +2 -0
  203. umap/templates/umap/content_footer.html +7 -3
  204. umap/templates/umap/css.html +1 -0
  205. umap/templates/umap/dashboard_menu.html +15 -0
  206. umap/templates/umap/home.html +14 -4
  207. umap/templates/umap/js.html +4 -9
  208. umap/templates/umap/login_popup_end.html +10 -4
  209. umap/templates/umap/map_detail.html +8 -2
  210. umap/templates/umap/map_fragment.html +3 -1
  211. umap/templates/umap/map_init.html +2 -1
  212. umap/templates/umap/map_list.html +6 -3
  213. umap/templates/umap/map_table.html +36 -12
  214. umap/templates/umap/messages.html +0 -1
  215. umap/templates/umap/navigation.html +2 -1
  216. umap/templates/umap/password_change.html +5 -1
  217. umap/templates/umap/password_change_done.html +8 -2
  218. umap/templates/umap/search.html +8 -2
  219. umap/templates/umap/search_bar.html +1 -0
  220. umap/templates/umap/team_confirm_delete.html +19 -0
  221. umap/templates/umap/team_detail.html +27 -0
  222. umap/templates/umap/team_form.html +60 -0
  223. umap/templates/umap/user_dashboard.html +7 -9
  224. umap/templates/umap/user_teams.html +51 -0
  225. umap/tests/base.py +8 -1
  226. umap/tests/conftest.py +6 -0
  227. umap/tests/fixtures/test_circles_layer.geojson +219 -0
  228. umap/tests/fixtures/test_upload_georss.xml +20 -0
  229. umap/tests/integration/conftest.py +18 -4
  230. umap/tests/integration/helpers.py +12 -0
  231. umap/tests/integration/test_anonymous_owned_map.py +23 -0
  232. umap/tests/integration/test_basics.py +29 -0
  233. umap/tests/integration/test_browser.py +20 -0
  234. umap/tests/integration/test_caption.py +20 -0
  235. umap/tests/integration/test_circles_layer.py +69 -0
  236. umap/tests/integration/test_conditional_rules.py +102 -17
  237. umap/tests/integration/test_draw_polygon.py +138 -13
  238. umap/tests/integration/test_draw_polyline.py +8 -18
  239. umap/tests/integration/test_edit_datalayer.py +3 -3
  240. umap/tests/integration/test_import.py +124 -5
  241. umap/tests/integration/test_owned_map.py +21 -13
  242. umap/tests/integration/test_querystring.py +7 -0
  243. umap/tests/integration/test_team.py +47 -0
  244. umap/tests/integration/test_tilelayer.py +19 -2
  245. umap/tests/integration/test_view_marker.py +28 -1
  246. umap/tests/integration/test_websocket_sync.py +5 -5
  247. umap/tests/test_datalayer.py +32 -7
  248. umap/tests/test_datalayer_views.py +1 -1
  249. umap/tests/test_map.py +30 -4
  250. umap/tests/test_map_views.py +2 -2
  251. umap/tests/test_statics.py +40 -0
  252. umap/tests/test_team_views.py +131 -0
  253. umap/tests/test_views.py +15 -1
  254. umap/urls.py +23 -13
  255. umap/views.py +116 -10
  256. {umap_project-2.5.1.dist-info → umap_project-2.6.0.dist-info}/METADATA +14 -14
  257. {umap_project-2.5.1.dist-info → umap_project-2.6.0.dist-info}/RECORD +260 -253
  258. umap/static/umap/js/umap.datalayer.permissions.js +0 -70
  259. umap/static/umap/js/umap.features.js +0 -1290
  260. umap/static/umap/js/umap.layer.js +0 -1837
  261. umap/static/umap/js/umap.permissions.js +0 -208
  262. umap/static/umap/js/umap.popup.js +0 -341
  263. umap/static/umap/test/TableEditor.js +0 -104
  264. umap/static/umap/vendors/leaflet/leaflet-src.js +0 -14512
  265. umap/static/umap/vendors/leaflet/leaflet-src.js.map +0 -1
  266. umap/static/umap/vendors/leaflet/leaflet.js +0 -6
  267. umap/static/umap/vendors/leaflet/leaflet.js.map +0 -1
  268. umap/static/umap/vendors/markercluster/WhereAreTheJavascriptFiles.txt +0 -5
  269. umap/static/umap/vendors/markercluster/leaflet.markercluster-src.js +0 -2718
  270. umap/static/umap/vendors/markercluster/leaflet.markercluster-src.js.map +0 -1
  271. umap/static/umap/vendors/toolbar/leaflet.toolbar-src.css +0 -117
  272. umap/static/umap/vendors/toolbar/leaflet.toolbar-src.js +0 -365
  273. umap/tests/integration/test_statics.py +0 -47
  274. {umap_project-2.5.1.dist-info → umap_project-2.6.0.dist-info}/WHEEL +0 -0
  275. {umap_project-2.5.1.dist-info → umap_project-2.6.0.dist-info}/entry_points.txt +0 -0
  276. {umap_project-2.5.1.dist-info → umap_project-2.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1210 @@
1
+ // Uses U.FormBuilder not available as ESM
2
+
3
+ // FIXME: this module should not depend on Leaflet
4
+ import {
5
+ DomUtil,
6
+ DomEvent,
7
+ stamp,
8
+ GeoJSON,
9
+ } from '../../../vendors/leaflet/leaflet-src.esm.js'
10
+ import * as Utils from '../utils.js'
11
+ import { Default as DefaultLayer } from '../rendering/layers/base.js'
12
+ import { Cluster } from '../rendering/layers/cluster.js'
13
+ import { Heat } from '../rendering/layers/heat.js'
14
+ import { Categorized, Choropleth, Circles } from '../rendering/layers/classified.js'
15
+ import {
16
+ uMapAlert as Alert,
17
+ uMapAlertConflict as AlertConflict,
18
+ } from '../../components/alerts/alert.js'
19
+ import { translate } from '../i18n.js'
20
+ import { DataLayerPermissions } from '../permissions.js'
21
+ import { Point, LineString, Polygon } from './features.js'
22
+ import TableEditor from '../tableeditor.js'
23
+
24
+ export const LAYER_TYPES = [
25
+ DefaultLayer,
26
+ Cluster,
27
+ Heat,
28
+ Choropleth,
29
+ Categorized,
30
+ Circles,
31
+ ]
32
+
33
+ const LAYER_MAP = LAYER_TYPES.reduce((acc, klass) => {
34
+ acc[klass.TYPE] = klass
35
+ return acc
36
+ }, {})
37
+
38
+ export class DataLayer {
39
+ constructor(map, data) {
40
+ this.map = map
41
+ this.sync = map.sync_engine.proxy(this)
42
+ this._index = Array()
43
+ this._features = {}
44
+ this._geojson = null
45
+ this._propertiesIndex = []
46
+ this._loaded = false // Are layer metadata loaded
47
+ this._dataloaded = false // Are layer data loaded
48
+
49
+ this.parentPane = this.map.getPane('overlayPane')
50
+ this.pane = this.map.createPane(`datalayer${stamp(this)}`, this.parentPane)
51
+ this.pane.dataset.id = stamp(this)
52
+ // FIXME: should be on layer
53
+ this.renderer = L.svg({ pane: this.pane })
54
+ this.defaultOptions = {
55
+ displayOnLoad: true,
56
+ inCaption: true,
57
+ browsable: true,
58
+ editMode: 'advanced',
59
+ }
60
+
61
+ this._isDirty = false
62
+ this._isDeleted = false
63
+ this.setUmapId(data.id)
64
+ this.setOptions(data)
65
+
66
+ if (!Utils.isObject(this.options.remoteData)) {
67
+ this.options.remoteData = {}
68
+ }
69
+ // Retrocompat
70
+ if (this.options.remoteData?.from) {
71
+ this.options.fromZoom = this.options.remoteData.from
72
+ delete this.options.remoteData.from
73
+ }
74
+ if (this.options.remoteData?.to) {
75
+ this.options.toZoom = this.options.remoteData.to
76
+ delete this.options.remoteData.to
77
+ }
78
+ this.backupOptions()
79
+ this.connectToMap()
80
+ this.permissions = new DataLayerPermissions(this)
81
+ if (!this.umap_id) {
82
+ if (this.showAtLoad()) this.show()
83
+ this.isDirty = true
84
+ }
85
+
86
+ // Only layers that are displayed on load must be hidden/shown
87
+ // Automatically, others will be shown manually, and thus will
88
+ // be in the "forced visibility" mode
89
+ if (this.isVisible()) this.propagateShow()
90
+ }
91
+
92
+ set isDirty(status) {
93
+ this._isDirty = status
94
+ if (status) {
95
+ this.map.addDirtyDatalayer(this)
96
+ // A layer can be made dirty by indirect action (like dragging layers)
97
+ // we need to have it loaded before saving it.
98
+ if (!this.isLoaded()) this.fetchData()
99
+ } else {
100
+ this.map.removeDirtyDatalayer(this)
101
+ this.isDeleted = false
102
+ }
103
+ }
104
+
105
+ get isDirty() {
106
+ return this._isDirty
107
+ }
108
+
109
+ set isDeleted(status) {
110
+ this._isDeleted = status
111
+ if (status) this.isDirty = status
112
+ }
113
+
114
+ get isDeleted() {
115
+ return this._isDeleted
116
+ }
117
+
118
+ getSyncMetadata() {
119
+ return {
120
+ subject: 'datalayer',
121
+ metadata: {
122
+ id: this.umap_id || null,
123
+ },
124
+ }
125
+ }
126
+
127
+ render(fields, builder) {
128
+ const impacts = Utils.getImpactsFromSchema(fields)
129
+
130
+ for (const impact of impacts) {
131
+ switch (impact) {
132
+ case 'ui':
133
+ this.map.onDataLayersChanged()
134
+ break
135
+ case 'data':
136
+ if (fields.includes('options.type')) {
137
+ this.resetLayer()
138
+ }
139
+ this.hide()
140
+ for (const field of fields) {
141
+ this.layer.onEdit(field, builder)
142
+ }
143
+ this.redraw()
144
+ this.show()
145
+ break
146
+ case 'remote-data':
147
+ this.fetchRemoteData()
148
+ break
149
+ }
150
+ }
151
+ }
152
+
153
+ showAtLoad() {
154
+ return this.autoLoaded() && this.showAtZoom()
155
+ }
156
+
157
+ autoLoaded() {
158
+ if (!this.map.datalayersFromQueryString) return this.options.displayOnLoad
159
+ const datalayerIds = this.map.datalayersFromQueryString
160
+ let loadMe = datalayerIds.includes(this.umap_id.toString())
161
+ if (this.options.old_id) {
162
+ loadMe = loadMe || datalayerIds.includes(this.options.old_id.toString())
163
+ }
164
+ return loadMe
165
+ }
166
+
167
+ insertBefore(other) {
168
+ if (!other) return
169
+ this.parentPane.insertBefore(this.pane, other.pane)
170
+ }
171
+
172
+ insertAfter(other) {
173
+ if (!other) return
174
+ this.parentPane.insertBefore(this.pane, other.pane.nextSibling)
175
+ }
176
+
177
+ bringToTop() {
178
+ this.parentPane.appendChild(this.pane)
179
+ }
180
+
181
+ hasDataVisible() {
182
+ return this.layer.hasDataVisible()
183
+ }
184
+
185
+ resetLayer(force) {
186
+ // Only reset if type is defined (undefined is the default) and different from current type
187
+ if (
188
+ this.layer &&
189
+ (!this.options.type || this.options.type === this.layer.getType()) &&
190
+ !force
191
+ ) {
192
+ return
193
+ }
194
+ const visible = this.isVisible()
195
+ if (this.layer) this.layer.clearLayers()
196
+ // delete this.layer?
197
+ if (visible) this.map.removeLayer(this.layer)
198
+ const Class = LAYER_MAP[this.options.type] || DefaultLayer
199
+ this.layer = new Class(this)
200
+ // Rendering layer changed, so let's force reset the feature rendering too.
201
+ this.eachFeature((feature) => feature.makeUI())
202
+ this.eachFeature(this.showFeature)
203
+ if (visible) this.show()
204
+ this.propagateRemote()
205
+ }
206
+
207
+ eachFeature(method, context) {
208
+ for (const idx of this._index) {
209
+ method.call(context || this, this._features[idx])
210
+ }
211
+ return this
212
+ }
213
+
214
+ async fetchData() {
215
+ if (!this.umap_id) return
216
+ if (this._loading) return
217
+ this._loading = true
218
+ const [geojson, response, error] = await this.map.server.get(this._dataUrl())
219
+ if (!error) {
220
+ this._reference_version = response.headers.get('X-Datalayer-Version')
221
+ // FIXME: for now this property is set dynamically from backend
222
+ // And thus it's not in the geojson file in the server
223
+ // So do not let all options to be reset
224
+ // Fix is a proper migration so all datalayers settings are
225
+ // in DB, and we remove it from geojson flat files.
226
+ if (geojson._umap_options) {
227
+ geojson._umap_options.editMode = this.options.editMode
228
+ }
229
+ // In case of maps pre 1.0 still around
230
+ if (geojson._storage) geojson._storage.editMode = this.options.editMode
231
+ await this.fromUmapGeoJSON(geojson)
232
+ this.backupOptions()
233
+ this._loading = false
234
+ }
235
+ }
236
+
237
+ dataChanged() {
238
+ this.map.onDataLayersChanged()
239
+ this.layer.dataChanged()
240
+ }
241
+
242
+ fromGeoJSON(geojson, sync = true) {
243
+ this.addData(geojson, sync)
244
+ this._geojson = geojson
245
+ this._dataloaded = true
246
+ this.dataChanged()
247
+ }
248
+
249
+ async fromUmapGeoJSON(geojson) {
250
+ if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat
251
+ if (geojson._umap_options) this.setOptions(geojson._umap_options)
252
+ if (this.isRemoteLayer()) await this.fetchRemoteData()
253
+ else this.fromGeoJSON(geojson, false)
254
+ this._loaded = true
255
+ }
256
+
257
+ clear() {
258
+ this.layer.clearLayers()
259
+ this._features = {}
260
+ this._index = Array()
261
+ if (this._geojson) {
262
+ this.backupData()
263
+ this._geojson = null
264
+ }
265
+ this.dataChanged()
266
+ }
267
+
268
+ backupData() {
269
+ this._geojson_bk = Utils.CopyJSON(this._geojson)
270
+ }
271
+
272
+ reindex() {
273
+ const features = Object.values(this._features)
274
+ Utils.sortFeatures(features, this.map.getOption('sortKey'), L.lang)
275
+ this._index = features.map((feature) => stamp(feature))
276
+ }
277
+
278
+ showAtZoom() {
279
+ const from = Number.parseInt(this.options.fromZoom, 10)
280
+ const to = Number.parseInt(this.options.toZoom, 10)
281
+ const zoom = this.map.getZoom()
282
+ return !((!Number.isNaN(from) && zoom < from) || (!Number.isNaN(to) && zoom > to))
283
+ }
284
+
285
+ hasDynamicData() {
286
+ return this.isRemoteLayer() && Boolean(this.options.remoteData?.dynamic)
287
+ }
288
+
289
+ async fetchRemoteData(force) {
290
+ if (!this.isRemoteLayer()) return
291
+ if (!this.hasDynamicData() && this.hasDataLoaded() && !force) return
292
+ if (!this.isVisible()) return
293
+ let url = this.map.localizeUrl(this.options.remoteData.url)
294
+ if (this.options.remoteData.proxy) {
295
+ url = this.map.proxyUrl(url, this.options.remoteData.ttl)
296
+ }
297
+ const response = await this.map.request.get(url)
298
+ if (response?.ok) {
299
+ this.clear()
300
+ this.map.formatter
301
+ .parse(await response.text(), this.options.remoteData.format)
302
+ .then((geojson) => this.fromGeoJSON(geojson))
303
+ }
304
+ }
305
+
306
+ isLoaded() {
307
+ return !this.umap_id || this._loaded
308
+ }
309
+
310
+ hasDataLoaded() {
311
+ return this._dataloaded
312
+ }
313
+
314
+ setUmapId(id) {
315
+ // Datalayer is null when listening creation form
316
+ if (!this.umap_id && id) this.umap_id = id
317
+ }
318
+
319
+ backupOptions() {
320
+ this._backupOptions = Utils.CopyJSON(this.options)
321
+ }
322
+
323
+ resetOptions() {
324
+ this.options = Utils.CopyJSON(this._backupOptions)
325
+ }
326
+
327
+ setOptions(options) {
328
+ delete options.geojson
329
+ this.options = Utils.CopyJSON(this.defaultOptions) // Start from fresh.
330
+ this.updateOptions(options)
331
+ }
332
+
333
+ updateOptions(options) {
334
+ this.options = Object.assign(this.options, options)
335
+ this.resetLayer()
336
+ }
337
+
338
+ connectToMap() {
339
+ const id = stamp(this)
340
+ if (!this.map.datalayers[id]) {
341
+ this.map.datalayers[id] = this
342
+ if (!this.map.datalayers_index.includes(this)) {
343
+ this.map.datalayers_index.push(this)
344
+ }
345
+ this.map.onDataLayersChanged()
346
+ }
347
+ }
348
+
349
+ _dataUrl() {
350
+ const template = this.map.options.urls.datalayer_view
351
+
352
+ let url = Utils.template(template, {
353
+ pk: this.umap_id,
354
+ map_id: this.map.options.umap_id,
355
+ })
356
+
357
+ // No browser cache for owners/editors.
358
+ if (this.map.hasEditMode()) url = `${url}?${Date.now()}`
359
+ return url
360
+ }
361
+
362
+ isRemoteLayer() {
363
+ return Boolean(this.options.remoteData?.url && this.options.remoteData.format)
364
+ }
365
+
366
+ isClustered() {
367
+ return this.options.type === 'Cluster'
368
+ }
369
+
370
+ showFeature(feature) {
371
+ if (feature.isFiltered()) return
372
+ this.layer.addLayer(feature.ui)
373
+ }
374
+
375
+ hideFeature(feature) {
376
+ this.layer.removeLayer(feature.ui)
377
+ }
378
+
379
+ addFeature(feature) {
380
+ const id = stamp(feature)
381
+ feature.connectToDataLayer(this)
382
+ this._index.push(id)
383
+ this._features[id] = feature
384
+ this.indexProperties(feature)
385
+ this.map.features_index[feature.getSlug()] = feature
386
+ this.showFeature(feature)
387
+ if (this.hasDataLoaded()) this.dataChanged()
388
+ }
389
+
390
+ removeFeature(feature, sync) {
391
+ const id = stamp(feature)
392
+ if (sync !== false) feature.sync.delete()
393
+ this.hideFeature(feature)
394
+ delete this.map.features_index[feature.getSlug()]
395
+ feature.disconnectFromDataLayer(this)
396
+ this._index.splice(this._index.indexOf(id), 1)
397
+ delete this._features[id]
398
+ if (this.hasDataLoaded() && this.isVisible()) this.dataChanged()
399
+ }
400
+
401
+ indexProperties(feature) {
402
+ for (const i in feature.properties)
403
+ if (typeof feature.properties[i] !== 'object') this.indexProperty(i)
404
+ }
405
+
406
+ indexProperty(name) {
407
+ if (!name) return
408
+ if (name.indexOf('_') === 0) return
409
+ if (!this._propertiesIndex.includes(name)) {
410
+ this._propertiesIndex.push(name)
411
+ this._propertiesIndex.sort()
412
+ }
413
+ }
414
+
415
+ deindexProperty(name) {
416
+ const idx = this._propertiesIndex.indexOf(name)
417
+ if (idx !== -1) this._propertiesIndex.splice(idx, 1)
418
+ }
419
+
420
+ sortedValues(property) {
421
+ return Object.values(this._features)
422
+ .map((feature) => feature.properties[property])
423
+ .filter((val, idx, arr) => arr.indexOf(val) === idx)
424
+ .sort(Utils.naturalSort)
425
+ }
426
+
427
+ addData(geojson, sync) {
428
+ try {
429
+ // Do not fail if remote data is somehow invalid,
430
+ // otherwise the layer becomes uneditable.
431
+ this.makeFeatures(geojson, sync)
432
+ } catch (err) {
433
+ console.log('Error with DataLayer', this.umap_id)
434
+ console.error(err)
435
+ }
436
+ }
437
+
438
+ makeFeatures(geojson = {}, sync = true) {
439
+ if (geojson.type === 'Feature' || geojson.coordinates) {
440
+ geojson = [geojson]
441
+ }
442
+ const collection = Array.isArray(geojson)
443
+ ? geojson
444
+ : geojson.features || geojson.geometries
445
+ Utils.sortFeatures(collection, this.map.getOption('sortKey'), L.lang)
446
+ for (const feature of collection) {
447
+ this.makeFeature(feature, sync)
448
+ }
449
+ }
450
+
451
+ makeFeature(geojson = {}, sync = true, id = null) {
452
+ // Both Feature and Geometry are valid geojson objects.
453
+ const geometry = geojson.geometry || geojson
454
+ let feature
455
+
456
+ switch (geometry.type) {
457
+ case 'Point':
458
+ // FIXME: deal with MultiPoint
459
+ feature = new Point(this, geojson, id)
460
+ break
461
+ case 'MultiLineString':
462
+ case 'LineString':
463
+ feature = new LineString(this, geojson, id)
464
+ break
465
+ case 'MultiPolygon':
466
+ case 'Polygon':
467
+ feature = new Polygon(this, geojson, id)
468
+ break
469
+ default:
470
+ console.log(geojson)
471
+ Alert.error(
472
+ translate('Skipping unknown geometry.type: {type}', {
473
+ type: geometry.type || 'undefined',
474
+ })
475
+ )
476
+ }
477
+ if (feature) {
478
+ this.addFeature(feature)
479
+ if (sync) feature.onCommit()
480
+ return feature
481
+ }
482
+ }
483
+
484
+ async importRaw(raw, format) {
485
+ this.map.formatter
486
+ .parse(raw, format)
487
+ .then((geojson) => this.addData(geojson))
488
+ .then(() => this.zoomTo())
489
+ this.isDirty = true
490
+ }
491
+
492
+ importFromFiles(files, type) {
493
+ for (const f of files) {
494
+ this.importFromFile(f, type)
495
+ }
496
+ }
497
+
498
+ importFromFile(f, type) {
499
+ const reader = new FileReader()
500
+ type = type || Utils.detectFileType(f)
501
+ reader.readAsText(f)
502
+ reader.onload = (e) => this.importRaw(e.target.result, type)
503
+ }
504
+
505
+ async importFromUrl(uri, type) {
506
+ uri = this.map.localizeUrl(uri)
507
+ const response = await this.map.request.get(uri)
508
+ if (response?.ok) {
509
+ this.importRaw(await response.text(), type)
510
+ }
511
+ }
512
+
513
+ getColor() {
514
+ return this.options.color || this.map.getOption('color')
515
+ }
516
+
517
+ getDeleteUrl() {
518
+ return Utils.template(this.map.options.urls.datalayer_delete, {
519
+ pk: this.umap_id,
520
+ map_id: this.map.options.umap_id,
521
+ })
522
+ }
523
+
524
+ getVersionsUrl() {
525
+ return Utils.template(this.map.options.urls.datalayer_versions, {
526
+ pk: this.umap_id,
527
+ map_id: this.map.options.umap_id,
528
+ })
529
+ }
530
+
531
+ getVersionUrl(name) {
532
+ return Utils.template(this.map.options.urls.datalayer_version, {
533
+ pk: this.umap_id,
534
+ map_id: this.map.options.umap_id,
535
+ name: name,
536
+ })
537
+ }
538
+
539
+ _delete() {
540
+ this.isDeleted = true
541
+ this.erase()
542
+ }
543
+
544
+ empty() {
545
+ if (this.isRemoteLayer()) return
546
+ this.clear()
547
+ this.isDirty = true
548
+ }
549
+
550
+ clone() {
551
+ const options = Utils.CopyJSON(this.options)
552
+ options.name = translate('Clone of {name}', { name: this.options.name })
553
+ delete options.id
554
+ const geojson = Utils.CopyJSON(this._geojson)
555
+ const datalayer = this.map.createDataLayer(options)
556
+ datalayer.fromGeoJSON(geojson)
557
+ return datalayer
558
+ }
559
+
560
+ erase() {
561
+ this.hide()
562
+ delete this.map.datalayers[stamp(this)]
563
+ this.map.datalayers_index.splice(this.getRank(), 1)
564
+ this.parentPane.removeChild(this.pane)
565
+ this.map.onDataLayersChanged()
566
+ this.layer.onDelete(this.map)
567
+ this.propagateDelete()
568
+ this._leaflet_events_bk = this._leaflet_events
569
+ this.clear()
570
+ delete this._loaded
571
+ delete this._dataloaded
572
+ }
573
+
574
+ reset() {
575
+ if (!this.umap_id) this.erase()
576
+
577
+ this.resetOptions()
578
+ this.parentPane.appendChild(this.pane)
579
+ if (this._leaflet_events_bk && !this._leaflet_events) {
580
+ this._leaflet_events = this._leaflet_events_bk
581
+ }
582
+ this.clear()
583
+ this.hide()
584
+ if (this.isRemoteLayer()) this.fetchRemoteData()
585
+ else if (this._geojson_bk) this.fromGeoJSON(this._geojson_bk)
586
+ this._loaded = true
587
+ this.show()
588
+ this.isDirty = false
589
+ }
590
+
591
+ redraw() {
592
+ if (!this.isVisible()) return
593
+ this.hide()
594
+ this.show()
595
+ }
596
+
597
+ edit() {
598
+ if (!this.map.editEnabled || !this.isLoaded()) {
599
+ return
600
+ }
601
+ const container = DomUtil.create('div', 'umap-layer-properties-container')
602
+ const metadataFields = [
603
+ 'options.name',
604
+ 'options.description',
605
+ [
606
+ 'options.type',
607
+ { handler: 'LayerTypeChooser', label: translate('Type of layer') },
608
+ ],
609
+ [
610
+ 'options.displayOnLoad',
611
+ { label: translate('Display on load'), handler: 'Switch' },
612
+ ],
613
+ [
614
+ 'options.browsable',
615
+ {
616
+ label: translate('Data is browsable'),
617
+ handler: 'Switch',
618
+ helpEntries: 'browsable',
619
+ },
620
+ ],
621
+ [
622
+ 'options.inCaption',
623
+ {
624
+ label: translate('Show this layer in the caption'),
625
+ handler: 'Switch',
626
+ },
627
+ ],
628
+ ]
629
+ DomUtil.createTitle(container, translate('Layer properties'), 'icon-layers')
630
+ let builder = new U.FormBuilder(this, metadataFields, {
631
+ callback(e) {
632
+ this.map.onDataLayersChanged()
633
+ if (e.helper.field === 'options.type') {
634
+ this.edit()
635
+ }
636
+ },
637
+ })
638
+ container.appendChild(builder.build())
639
+
640
+ const layerOptions = this.layer.getEditableOptions()
641
+
642
+ if (layerOptions.length) {
643
+ builder = new U.FormBuilder(this, layerOptions, {
644
+ id: 'datalayer-layer-properties',
645
+ })
646
+ const layerProperties = DomUtil.createFieldset(
647
+ container,
648
+ `${this.layer.getName()}: ${translate('settings')}`
649
+ )
650
+ layerProperties.appendChild(builder.build())
651
+ }
652
+
653
+ const shapeOptions = [
654
+ 'options.color',
655
+ 'options.iconClass',
656
+ 'options.iconUrl',
657
+ 'options.iconOpacity',
658
+ 'options.opacity',
659
+ 'options.stroke',
660
+ 'options.weight',
661
+ 'options.fill',
662
+ 'options.fillColor',
663
+ 'options.fillOpacity',
664
+ ]
665
+
666
+ builder = new U.FormBuilder(this, shapeOptions, {
667
+ id: 'datalayer-advanced-properties',
668
+ })
669
+ const shapeProperties = DomUtil.createFieldset(
670
+ container,
671
+ translate('Shape properties')
672
+ )
673
+ shapeProperties.appendChild(builder.build())
674
+
675
+ const optionsFields = [
676
+ 'options.smoothFactor',
677
+ 'options.dashArray',
678
+ 'options.zoomTo',
679
+ 'options.fromZoom',
680
+ 'options.toZoom',
681
+ 'options.labelKey',
682
+ ]
683
+
684
+ builder = new U.FormBuilder(this, optionsFields, {
685
+ id: 'datalayer-advanced-properties',
686
+ })
687
+ const advancedProperties = DomUtil.createFieldset(
688
+ container,
689
+ translate('Advanced properties')
690
+ )
691
+ advancedProperties.appendChild(builder.build())
692
+
693
+ const popupFields = [
694
+ 'options.popupShape',
695
+ 'options.popupTemplate',
696
+ 'options.popupContentTemplate',
697
+ 'options.showLabel',
698
+ 'options.labelDirection',
699
+ 'options.labelInteractive',
700
+ 'options.outlinkTarget',
701
+ 'options.interactive',
702
+ ]
703
+ builder = new U.FormBuilder(this, popupFields)
704
+ const popupFieldset = DomUtil.createFieldset(
705
+ container,
706
+ translate('Interaction options')
707
+ )
708
+ popupFieldset.appendChild(builder.build())
709
+
710
+ // XXX I'm not sure **why** this is needed (as it's set during `this.initialize`)
711
+ // but apparently it's needed.
712
+ if (!Utils.isObject(this.options.remoteData)) {
713
+ this.options.remoteData = {}
714
+ }
715
+
716
+ const remoteDataFields = [
717
+ [
718
+ 'options.remoteData.url',
719
+ { handler: 'Url', label: translate('Url'), helpEntries: 'formatURL' },
720
+ ],
721
+ [
722
+ 'options.remoteData.format',
723
+ { handler: 'DataFormat', label: translate('Format') },
724
+ ],
725
+ 'options.fromZoom',
726
+ 'options.toZoom',
727
+ [
728
+ 'options.remoteData.dynamic',
729
+ {
730
+ handler: 'Switch',
731
+ label: translate('Dynamic'),
732
+ helpEntries: 'dynamicRemoteData',
733
+ },
734
+ ],
735
+ [
736
+ 'options.remoteData.licence',
737
+ {
738
+ label: translate('Licence'),
739
+ helpText: translate('Please be sure the licence is compliant with your use.'),
740
+ },
741
+ ],
742
+ ]
743
+ if (this.map.options.urls.ajax_proxy) {
744
+ remoteDataFields.push([
745
+ 'options.remoteData.proxy',
746
+ {
747
+ handler: 'Switch',
748
+ label: translate('Proxy request'),
749
+ helpEntries: 'proxyRemoteData',
750
+ },
751
+ ])
752
+ remoteDataFields.push('options.remoteData.ttl')
753
+ }
754
+
755
+ const remoteDataContainer = DomUtil.createFieldset(
756
+ container,
757
+ translate('Remote data')
758
+ )
759
+ builder = new U.FormBuilder(this, remoteDataFields)
760
+ remoteDataContainer.appendChild(builder.build())
761
+ DomUtil.createButton(
762
+ 'button umap-verify',
763
+ remoteDataContainer,
764
+ translate('Verify remote URL'),
765
+ () => this.fetchRemoteData(true),
766
+ this
767
+ )
768
+
769
+ if (this.map.options.urls.datalayer_versions) this.buildVersionsFieldset(container)
770
+
771
+ const advancedActions = DomUtil.createFieldset(
772
+ container,
773
+ translate('Advanced actions')
774
+ )
775
+ const advancedButtons = DomUtil.create('div', 'button-bar half', advancedActions)
776
+ const deleteButton = Utils.loadTemplate(`
777
+ <button class="button" type="button">
778
+ <i class="icon icon-24 icon-delete"></i>${translate('Delete')}
779
+ </button>`)
780
+ deleteButton.addEventListener('click', () => {
781
+ this._delete()
782
+ this.map.editPanel.close()
783
+ })
784
+ advancedButtons.appendChild(deleteButton)
785
+
786
+ if (!this.isRemoteLayer()) {
787
+ const emptyLink = DomUtil.createButton(
788
+ 'button umap-empty',
789
+ advancedButtons,
790
+ translate('Empty'),
791
+ this.empty,
792
+ this
793
+ )
794
+ }
795
+ const cloneLink = DomUtil.createButton(
796
+ 'button umap-clone',
797
+ advancedButtons,
798
+ translate('Clone'),
799
+ function () {
800
+ const datalayer = this.clone()
801
+ datalayer.edit()
802
+ },
803
+ this
804
+ )
805
+ if (this.umap_id) {
806
+ const download = DomUtil.createLink(
807
+ 'button umap-download',
808
+ advancedButtons,
809
+ translate('Download'),
810
+ this._dataUrl(),
811
+ '_blank'
812
+ )
813
+ }
814
+ const backButton = DomUtil.createButtonIcon(
815
+ undefined,
816
+ 'icon-back',
817
+ translate('Back to layers')
818
+ )
819
+ // Fixme: remove me when this is merged and released
820
+ // https://github.com/Leaflet/Leaflet/pull/9052
821
+ DomEvent.disableClickPropagation(backButton)
822
+ DomEvent.on(backButton, 'click', this.map.editDatalayers, this.map)
823
+
824
+ this.map.editPanel.open({
825
+ content: container,
826
+ actions: [backButton],
827
+ })
828
+ }
829
+
830
+ getOwnOption(option) {
831
+ if (Utils.usableOption(this.options, option)) return this.options[option]
832
+ }
833
+
834
+ getOption(option, feature) {
835
+ if (this.layer?.getOption) {
836
+ const value = this.layer.getOption(option, feature)
837
+ if (value !== undefined) return value
838
+ }
839
+ if (this.getOwnOption(option) !== undefined) {
840
+ return this.getOwnOption(option)
841
+ }
842
+ if (this.layer?.defaults?.[option]) {
843
+ return this.layer.defaults[option]
844
+ }
845
+ return this.map.getOption(option, feature)
846
+ }
847
+
848
+ async buildVersionsFieldset(container) {
849
+ const appendVersion = (data) => {
850
+ const date = new Date(Number.parseInt(data.at, 10))
851
+ const content = `${date.toLocaleString(L.lang)} (${Number.parseInt(data.size) / 1000}Kb)`
852
+ const el = DomUtil.create('div', 'umap-datalayer-version', versionsContainer)
853
+ const button = DomUtil.createButton(
854
+ '',
855
+ el,
856
+ '',
857
+ () => this.restore(data.name),
858
+ this
859
+ )
860
+ button.title = translate('Restore this version')
861
+ DomUtil.add('span', '', el, content)
862
+ }
863
+
864
+ const versionsContainer = DomUtil.createFieldset(container, translate('Versions'), {
865
+ async callback() {
866
+ const [{ versions }, response, error] = await this.map.server.get(
867
+ this.getVersionsUrl()
868
+ )
869
+ if (!error) versions.forEach(appendVersion)
870
+ },
871
+ context: this,
872
+ })
873
+ }
874
+
875
+ async restore(version) {
876
+ if (!this.map.editEnabled) return
877
+ if (!confirm(translate('Are you sure you want to restore this version?'))) return
878
+ const [geojson, response, error] = await this.map.server.get(
879
+ this.getVersionUrl(version)
880
+ )
881
+ if (!error) {
882
+ if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat.
883
+ if (geojson._umap_options) this.setOptions(geojson._umap_options)
884
+ this.empty()
885
+ if (this.isRemoteLayer()) this.fetchRemoteData()
886
+ else this.addData(geojson)
887
+ this.isDirty = true
888
+ }
889
+ }
890
+
891
+ featuresToGeoJSON() {
892
+ const features = []
893
+ this.eachFeature((feature) => features.push(feature.toGeoJSON()))
894
+ return features
895
+ }
896
+
897
+ async show() {
898
+ this.map.addLayer(this.layer)
899
+ if (!this.isLoaded()) await this.fetchData()
900
+ this.propagateShow()
901
+ }
902
+
903
+ hide() {
904
+ this.map.removeLayer(this.layer)
905
+ this.propagateHide()
906
+ }
907
+
908
+ toggle() {
909
+ // From now on, do not try to how/hidedataChanged
910
+ // automatically this layer.
911
+ this._forcedVisibility = true
912
+ if (!this.isVisible()) this.show()
913
+ else this.hide()
914
+ }
915
+
916
+ zoomTo() {
917
+ if (!this.isVisible()) return
918
+ const bounds = this.layer.getBounds()
919
+ if (bounds.isValid()) {
920
+ const options = { maxZoom: this.getOption('zoomTo') }
921
+ this.map.fitBounds(bounds, options)
922
+ }
923
+ }
924
+
925
+ // Is this layer type browsable in theorie
926
+ isBrowsable() {
927
+ return this.layer?.browsable
928
+ }
929
+
930
+ // Is this layer browsable in theorie
931
+ // AND the user allows it
932
+ allowBrowse() {
933
+ return !!this.options.browsable && this.isBrowsable()
934
+ }
935
+
936
+ // Is this layer browsable in theorie
937
+ // AND the user allows it
938
+ // AND it makes actually sense (is visible, it has data…)
939
+ canBrowse() {
940
+ return this.allowBrowse() && this.isVisible() && this.hasData()
941
+ }
942
+
943
+ count() {
944
+ return this._index.length
945
+ }
946
+
947
+ hasData() {
948
+ return !!this._index.length
949
+ }
950
+
951
+ isVisible() {
952
+ return Boolean(this.layer && this.map.hasLayer(this.layer))
953
+ }
954
+
955
+ getFeatureByIndex(index) {
956
+ if (index === -1) index = this._index.length - 1
957
+ const id = this._index[index]
958
+ return this._features[id]
959
+ }
960
+
961
+ // TODO Add an index
962
+ // For now, iterate on all the features.
963
+ getFeatureById(id) {
964
+ return Object.values(this._features).find((feature) => feature.id === id)
965
+ }
966
+
967
+ getNextFeature(feature) {
968
+ const id = this._index.indexOf(stamp(feature))
969
+ const nextId = this._index[id + 1]
970
+ return nextId
971
+ ? this._features[nextId]
972
+ : this.getNextBrowsable().getFeatureByIndex(0)
973
+ }
974
+
975
+ getPreviousFeature(feature) {
976
+ if (this._index <= 1) {
977
+ return null
978
+ }
979
+ const id = this._index.indexOf(stamp(feature))
980
+ const previousId = this._index[id - 1]
981
+ return previousId
982
+ ? this._features[previousId]
983
+ : this.getPreviousBrowsable().getFeatureByIndex(-1)
984
+ }
985
+
986
+ getPreviousBrowsable() {
987
+ let id = this.getRank()
988
+ let next
989
+ const index = this.map.datalayers_index
990
+ while (((id = index[++id] ? id : 0), (next = index[id]))) {
991
+ if (next === this || next.canBrowse()) break
992
+ }
993
+ return next
994
+ }
995
+
996
+ getNextBrowsable() {
997
+ let id = this.getRank()
998
+ let prev
999
+ const index = this.map.datalayers_index
1000
+ while (((id = index[--id] ? id : index.length - 1), (prev = index[id]))) {
1001
+ if (prev === this || prev.canBrowse()) break
1002
+ }
1003
+ return prev
1004
+ }
1005
+
1006
+ umapGeoJSON() {
1007
+ return {
1008
+ type: 'FeatureCollection',
1009
+ features: this.isRemoteLayer() ? [] : this.featuresToGeoJSON(),
1010
+ _umap_options: this.options,
1011
+ }
1012
+ }
1013
+
1014
+ getRank() {
1015
+ return this.map.datalayers_index.indexOf(this)
1016
+ }
1017
+
1018
+ isReadOnly() {
1019
+ // isReadOnly must return true if unset
1020
+ return this.options.editMode === 'disabled'
1021
+ }
1022
+
1023
+ isDataReadOnly() {
1024
+ // This layer cannot accept features
1025
+ return this.isReadOnly() || this.isRemoteLayer()
1026
+ }
1027
+
1028
+ async save() {
1029
+ if (this.isDeleted) return this.saveDelete()
1030
+ if (!this.isLoaded()) {
1031
+ return
1032
+ }
1033
+ const geojson = this.umapGeoJSON()
1034
+ const formData = new FormData()
1035
+ formData.append('name', this.options.name)
1036
+ formData.append('display_on_load', !!this.options.displayOnLoad)
1037
+ formData.append('rank', this.getRank())
1038
+ formData.append('settings', JSON.stringify(this.options))
1039
+ // Filename support is shaky, don't do it for now.
1040
+ const blob = new Blob([JSON.stringify(geojson)], { type: 'application/json' })
1041
+ formData.append('geojson', blob)
1042
+ const saveUrl = this.map.urls.get('datalayer_save', {
1043
+ map_id: this.map.options.umap_id,
1044
+ pk: this.umap_id,
1045
+ })
1046
+ const headers = this._reference_version
1047
+ ? { 'X-Datalayer-Reference': this._reference_version }
1048
+ : {}
1049
+ await this._trySave(saveUrl, headers, formData)
1050
+ this._geojson = geojson
1051
+ }
1052
+
1053
+ async _trySave(url, headers, formData) {
1054
+ const [data, response, error] = await this.map.server.post(url, headers, formData)
1055
+ if (error) {
1056
+ if (response && response.status === 412) {
1057
+ AlertConflict.error(
1058
+ translate(
1059
+ 'Whoops! Other contributor(s) changed some of the same map elements as you. ' +
1060
+ 'This situation is tricky, you have to choose carefully which version is pertinent.'
1061
+ ),
1062
+ async () => {
1063
+ await this._trySave(url, {}, formData)
1064
+ }
1065
+ )
1066
+ }
1067
+ } else {
1068
+ // Response contains geojson only if save has conflicted and conflicts have
1069
+ // been resolved. So we need to reload to get extra data (added by someone else)
1070
+ if (data.geojson) {
1071
+ this.clear()
1072
+ this.fromGeoJSON(data.geojson)
1073
+ delete data.geojson
1074
+ }
1075
+ this._reference_version = response.headers.get('X-Datalayer-Version')
1076
+ this.sync.update('_reference_version', this._reference_version)
1077
+
1078
+ this.setUmapId(data.id)
1079
+ this.updateOptions(data)
1080
+ this.backupOptions()
1081
+ this.connectToMap()
1082
+ this._loaded = true
1083
+ this.redraw() // Needed for reordering features
1084
+ this.isDirty = false
1085
+ this.permissions.save()
1086
+ }
1087
+ }
1088
+
1089
+ async saveDelete() {
1090
+ if (this.umap_id) {
1091
+ await this.map.server.post(this.getDeleteUrl())
1092
+ }
1093
+ this.isDirty = false
1094
+ this.map.continueSaving()
1095
+ }
1096
+
1097
+ getMap() {
1098
+ return this.map
1099
+ }
1100
+
1101
+ getName() {
1102
+ return this.options.name || translate('Untitled layer')
1103
+ }
1104
+
1105
+ tableEdit() {
1106
+ if (!this.isVisible()) return
1107
+ const editor = new TableEditor(this)
1108
+ editor.open()
1109
+ }
1110
+
1111
+ getFilterKeys() {
1112
+ // This keys will be used to filter feature from the browser text input.
1113
+ // By default, it will we use the "name" property, which is also the one used as label in the features list.
1114
+ // When map owner has configured another label or sort key, we try to be smart and search in the same keys.
1115
+ if (this.map.options.filterKey) return this.map.options.filterKey
1116
+ if (this.getOption('labelKey')) return this.getOption('labelKey')
1117
+ if (this.map.options.sortKey) return this.map.options.sortKey
1118
+ return 'displayName'
1119
+ }
1120
+
1121
+ renderLegend(container) {
1122
+ if (this.layer.renderLegend) return this.layer.renderLegend(container)
1123
+ const color = DomUtil.create('span', 'datalayer-color', container)
1124
+ color.style.backgroundColor = this.getColor()
1125
+ }
1126
+
1127
+ renderToolbox(container) {
1128
+ const toggle = DomUtil.createButtonIcon(
1129
+ container,
1130
+ 'icon-eye',
1131
+ translate('Show/hide layer')
1132
+ )
1133
+ const zoomTo = DomUtil.createButtonIcon(
1134
+ container,
1135
+ 'icon-zoom',
1136
+ translate('Zoom to layer extent')
1137
+ )
1138
+ const edit = DomUtil.createButtonIcon(
1139
+ container,
1140
+ 'icon-edit show-on-edit',
1141
+ translate('Edit')
1142
+ )
1143
+ const table = DomUtil.createButtonIcon(
1144
+ container,
1145
+ 'icon-table show-on-edit',
1146
+ translate('Edit properties in a table')
1147
+ )
1148
+ const remove = DomUtil.createButtonIcon(
1149
+ container,
1150
+ 'icon-delete show-on-edit',
1151
+ translate('Delete layer')
1152
+ )
1153
+ if (this.isReadOnly()) {
1154
+ DomUtil.addClass(container, 'readonly')
1155
+ } else {
1156
+ DomEvent.on(edit, 'click', this.edit, this)
1157
+ DomEvent.on(table, 'click', this.tableEdit, this)
1158
+ DomEvent.on(
1159
+ remove,
1160
+ 'click',
1161
+ function () {
1162
+ if (!this.isVisible()) return
1163
+ if (!confirm(translate('Are you sure you want to delete this layer?'))) return
1164
+ this._delete()
1165
+ },
1166
+ this
1167
+ )
1168
+ }
1169
+ DomEvent.on(toggle, 'click', this.toggle, this)
1170
+ DomEvent.on(zoomTo, 'click', this.zoomTo, this)
1171
+ container.classList.add(this.getHidableClass())
1172
+ container.classList.toggle('off', !this.isVisible())
1173
+ }
1174
+
1175
+ getHidableElements() {
1176
+ return document.querySelectorAll(`.${this.getHidableClass()}`)
1177
+ }
1178
+
1179
+ getHidableClass() {
1180
+ return `show_with_datalayer_${stamp(this)}`
1181
+ }
1182
+
1183
+ propagateDelete() {
1184
+ const els = this.getHidableElements()
1185
+ for (const el of els) {
1186
+ DomUtil.remove(el)
1187
+ }
1188
+ }
1189
+
1190
+ propagateRemote() {
1191
+ const els = this.getHidableElements()
1192
+ for (const el of els) {
1193
+ el.classList.toggle('remotelayer', this.isRemoteLayer())
1194
+ }
1195
+ }
1196
+
1197
+ propagateHide() {
1198
+ const els = this.getHidableElements()
1199
+ for (let i = 0; i < els.length; i++) {
1200
+ DomUtil.addClass(els[i], 'off')
1201
+ }
1202
+ }
1203
+
1204
+ propagateShow() {
1205
+ const els = this.getHidableElements()
1206
+ for (let i = 0; i < els.length; i++) {
1207
+ DomUtil.removeClass(els[i], 'off')
1208
+ }
1209
+ }
1210
+ }