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
@@ -1,1837 +0,0 @@
1
- U.Layer = {
2
- browsable: true,
3
-
4
- getType: function () {
5
- const proto = Object.getPrototypeOf(this)
6
- return proto.constructor.TYPE
7
- },
8
-
9
- getName: function () {
10
- const proto = Object.getPrototypeOf(this)
11
- return proto.constructor.NAME
12
- },
13
-
14
- getFeatures: function () {
15
- return this._layers
16
- },
17
-
18
- getEditableOptions: () => [],
19
-
20
- onEdit: () => {},
21
-
22
- hasDataVisible: function () {
23
- return !!Object.keys(this._layers).length
24
- },
25
- }
26
-
27
- U.Layer.Default = L.FeatureGroup.extend({
28
- statics: {
29
- NAME: L._('Default'),
30
- TYPE: 'Default',
31
- },
32
- includes: [U.Layer],
33
-
34
- initialize: function (datalayer) {
35
- this.datalayer = datalayer
36
- L.FeatureGroup.prototype.initialize.call(this)
37
- },
38
- })
39
-
40
- U.MarkerCluster = L.MarkerCluster.extend({
41
- // Custom class so we can call computeTextColor
42
- // when element is already on the DOM.
43
-
44
- _initIcon: function () {
45
- L.MarkerCluster.prototype._initIcon.call(this)
46
- const div = this._icon.querySelector('div')
47
- // Compute text color only when icon is added to the DOM.
48
- div.style.color = this._iconObj.computeTextColor(div)
49
- },
50
- })
51
-
52
- U.Layer.Cluster = L.MarkerClusterGroup.extend({
53
- statics: {
54
- NAME: L._('Clustered'),
55
- TYPE: 'Cluster',
56
- },
57
- includes: [U.Layer],
58
-
59
- initialize: function (datalayer) {
60
- this.datalayer = datalayer
61
- if (!U.Utils.isObject(this.datalayer.options.cluster)) {
62
- this.datalayer.options.cluster = {}
63
- }
64
- const options = {
65
- polygonOptions: {
66
- color: this.datalayer.getColor(),
67
- },
68
- iconCreateFunction: (cluster) => new U.Icon.Cluster(datalayer, cluster),
69
- }
70
- if (this.datalayer.options.cluster?.radius) {
71
- options.maxClusterRadius = this.datalayer.options.cluster.radius
72
- }
73
- L.MarkerClusterGroup.prototype.initialize.call(this, options)
74
- this._markerCluster = U.MarkerCluster
75
- this._layers = []
76
- },
77
-
78
- onRemove: function (map) {
79
- // In some situation, the onRemove is called before the layer is really
80
- // added to the map: basically when combining a defaultView=data + max/minZoom
81
- // and loading the map at a zoom outside of that zoom range.
82
- // FIXME: move this upstream (_unbindEvents should accept a map parameter
83
- // instead of relying on this._map)
84
- this._map = map
85
- return L.MarkerClusterGroup.prototype.onRemove.call(this, map)
86
- },
87
-
88
- addLayer: function (layer) {
89
- this._layers.push(layer)
90
- return L.MarkerClusterGroup.prototype.addLayer.call(this, layer)
91
- },
92
-
93
- removeLayer: function (layer) {
94
- this._layers.splice(this._layers.indexOf(layer), 1)
95
- return L.MarkerClusterGroup.prototype.removeLayer.call(this, layer)
96
- },
97
-
98
- getEditableOptions: () => [
99
- [
100
- 'options.cluster.radius',
101
- {
102
- handler: 'BlurIntInput',
103
- placeholder: L._('Clustering radius'),
104
- helpText: L._('Override clustering radius (default 80)'),
105
- },
106
- ],
107
- [
108
- 'options.cluster.textColor',
109
- {
110
- handler: 'TextColorPicker',
111
- placeholder: L._('Auto'),
112
- helpText: L._('Text color for the cluster label'),
113
- },
114
- ],
115
- ],
116
-
117
- onEdit: function (field, builder) {
118
- if (field === 'options.cluster.radius') {
119
- // No way to reset radius of an already instanciated MarkerClusterGroup...
120
- this.datalayer.resetLayer(true)
121
- return
122
- }
123
- if (field === 'options.color') {
124
- this.options.polygonOptions.color = this.datalayer.getColor()
125
- }
126
- },
127
- })
128
-
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({
200
- statics: {
201
- NAME: L._('Choropleth'),
202
- TYPE: 'Choropleth',
203
- },
204
- includes: [U.Layer],
205
- // Have defaults that better suit the choropleth mode.
206
- defaults: {
207
- color: 'white',
208
- fillColor: 'red',
209
- fillOpacity: 0.7,
210
- weight: 2,
211
- },
212
- MODES: {
213
- kmeans: L._('K-means'),
214
- equidistant: L._('Equidistant'),
215
- jenks: L._('Jenks-Fisher'),
216
- quantiles: L._('Quantiles'),
217
- manual: L._('Manual'),
218
- },
219
-
220
- _getValue: function (feature) {
221
- const key = this.datalayer.options.choropleth.property || 'value'
222
- const value = +feature.properties[key]
223
- if (!Number.isNaN(value)) return value
224
- },
225
-
226
- compute: function () {
227
- const values = this.getValues()
228
-
229
- if (!values.length) {
230
- this.options.breaks = []
231
- this.options.colors = []
232
- return
233
- }
234
- const mode = this.datalayer.options.choropleth.mode
235
- let classes = +this.datalayer.options.choropleth.classes || 5
236
- let breaks
237
- classes = Math.min(classes, values.length)
238
- if (mode === 'manual') {
239
- const manualBreaks = this.datalayer.options.choropleth.breaks
240
- if (manualBreaks) {
241
- breaks = manualBreaks
242
- .split(',')
243
- .map((b) => +b)
244
- .filter((b) => !Number.isNaN(b))
245
- }
246
- } else if (mode === 'equidistant') {
247
- breaks = ss.equalIntervalBreaks(values, classes)
248
- } else if (mode === 'jenks') {
249
- breaks = ss.jenks(values, classes)
250
- } else if (mode === 'quantiles') {
251
- const quantiles = [...Array(classes)].map((e, i) => i / classes).concat(1)
252
- breaks = ss.quantile(values, quantiles)
253
- } else {
254
- breaks = ss.ckmeans(values, classes).map((cluster) => cluster[0])
255
- breaks.push(ss.max(values)) // Needed for computing the legend
256
- }
257
- this.options.breaks = breaks || []
258
- this.datalayer.options.choropleth.breaks = this.options.breaks
259
- .map((b) => +b.toFixed(2))
260
- .join(',')
261
- const fillColor = this.datalayer.getOption('fillColor') || this.defaults.fillColor
262
- let colorScheme = this.datalayer.options.choropleth.brewer
263
- if (!colorbrewer[colorScheme]) colorScheme = 'Blues'
264
- this.options.colors = colorbrewer[colorScheme][this.options.breaks.length - 1] || []
265
- },
266
-
267
- getColor: function (feature) {
268
- if (!feature) return // FIXME should not happen
269
- const featureValue = this._getValue(feature)
270
- // Find the bucket/step/limit that this value is less than and give it that color
271
- for (let i = 1; i < this.options.breaks.length; i++) {
272
- if (featureValue <= this.options.breaks[i]) {
273
- return this.options.colors[i - 1]
274
- }
275
- }
276
- },
277
-
278
- onEdit: function (field, builder) {
279
- // Only compute the breaks if we're dealing with choropleth
280
- if (!field.startsWith('options.choropleth')) return
281
- // If user touches the breaks, then force manual mode
282
- if (field === 'options.choropleth.breaks') {
283
- this.datalayer.options.choropleth.mode = 'manual'
284
- if (builder) builder.helpers['options.choropleth.mode'].fetch()
285
- }
286
- this.compute()
287
- // If user changes the mode or the number of classes,
288
- // then update the breaks input value
289
- if (field === 'options.choropleth.mode' || field === 'options.choropleth.classes') {
290
- if (builder) builder.helpers['options.choropleth.breaks'].fetch()
291
- }
292
- },
293
-
294
- getEditableOptions: function () {
295
- return [
296
- [
297
- 'options.choropleth.property',
298
- {
299
- handler: 'Select',
300
- selectOptions: this.datalayer._propertiesIndex,
301
- label: L._('Choropleth property value'),
302
- },
303
- ],
304
- [
305
- 'options.choropleth.brewer',
306
- {
307
- handler: 'Select',
308
- label: L._('Choropleth color palette'),
309
- selectOptions: this.colorSchemes,
310
- },
311
- ],
312
- [
313
- 'options.choropleth.classes',
314
- {
315
- handler: 'Range',
316
- min: 3,
317
- max: 9,
318
- step: 1,
319
- label: L._('Choropleth classes'),
320
- helpText: L._('Number of desired classes (default 5)'),
321
- },
322
- ],
323
- [
324
- 'options.choropleth.breaks',
325
- {
326
- handler: 'BlurInput',
327
- label: L._('Choropleth breakpoints'),
328
- helpText: L._(
329
- 'Comma separated list of numbers, including min and max values.'
330
- ),
331
- },
332
- ],
333
- [
334
- 'options.choropleth.mode',
335
- {
336
- handler: 'MultiChoice',
337
- default: 'kmeans',
338
- choices: Object.entries(this.MODES),
339
- label: L._('Choropleth mode'),
340
- },
341
- ],
342
- ]
343
- },
344
-
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]]
478
- })
479
- },
480
- })
481
-
482
- U.Layer.Heat = L.HeatLayer.extend({
483
- statics: {
484
- NAME: L._('Heatmap'),
485
- TYPE: 'Heat',
486
- },
487
- includes: [U.Layer],
488
- browsable: false,
489
-
490
- initialize: function (datalayer) {
491
- this.datalayer = datalayer
492
- L.HeatLayer.prototype.initialize.call(this, [], this.datalayer.options.heat)
493
- if (!U.Utils.isObject(this.datalayer.options.heat)) {
494
- this.datalayer.options.heat = {}
495
- }
496
- },
497
-
498
- addLayer: function (layer) {
499
- if (layer instanceof L.Marker) {
500
- let latlng = layer.getLatLng()
501
- let alt
502
- if (this.datalayer.options.heat?.intensityProperty) {
503
- alt = Number.parseFloat(
504
- layer.properties[this.datalayer.options.heat.intensityProperty || 0]
505
- )
506
- latlng = new L.LatLng(latlng.lat, latlng.lng, alt)
507
- }
508
- this.addLatLng(latlng)
509
- }
510
- },
511
-
512
- clearLayers: function () {
513
- this.setLatLngs([])
514
- },
515
-
516
- getFeatures: () => ({}),
517
-
518
- getBounds: function () {
519
- return L.latLngBounds(this._latlngs)
520
- },
521
-
522
- getEditableOptions: () => [
523
- [
524
- 'options.heat.radius',
525
- {
526
- handler: 'Range',
527
- min: 10,
528
- max: 100,
529
- step: 5,
530
- label: L._('Heatmap radius'),
531
- helpText: L._('Override heatmap radius (default 25)'),
532
- },
533
- ],
534
- [
535
- 'options.heat.intensityProperty',
536
- {
537
- handler: 'BlurInput',
538
- placeholder: L._('Heatmap intensity property'),
539
- helpText: L._('Optional intensity property for heatmap'),
540
- },
541
- ],
542
- ],
543
-
544
- onEdit: function (field, builder) {
545
- if (field === 'options.heat.intensityProperty') {
546
- this.datalayer.resetLayer(true) // We need to repopulate the latlngs
547
- return
548
- }
549
- if (field === 'options.heat.radius') {
550
- this.options.radius = this.datalayer.options.heat.radius
551
- }
552
- this._updateOptions()
553
- },
554
-
555
- redraw: function () {
556
- // setlalngs call _redraw through setAnimFrame, thus async, so this
557
- // can ends with race condition if we remove the layer very faslty after.
558
- // TODO: PR in upstream Leaflet.heat
559
- if (!this._map) return
560
- L.HeatLayer.prototype.redraw.call(this)
561
- },
562
-
563
- _redraw: function () {
564
- // Import patch from https://github.com/Leaflet/Leaflet.heat/pull/78
565
- // Remove me when this get merged and released.
566
- if (!this._map) {
567
- return
568
- }
569
- const data = []
570
- const r = this._heat._r
571
- const size = this._map.getSize()
572
- const bounds = new L.Bounds(L.point([-r, -r]), size.add([r, r]))
573
- const cellSize = r / 2
574
- const grid = []
575
- const panePos = this._map._getMapPanePos()
576
- const offsetX = panePos.x % cellSize
577
- const offsetY = panePos.y % cellSize
578
- let i
579
- let len
580
- let p
581
- let cell
582
- let x
583
- let y
584
- let j
585
- let len2
586
-
587
- this._max = 1
588
-
589
- for (i = 0, len = this._latlngs.length; i < len; i++) {
590
- p = this._map.latLngToContainerPoint(this._latlngs[i])
591
- x = Math.floor((p.x - offsetX) / cellSize) + 2
592
- y = Math.floor((p.y - offsetY) / cellSize) + 2
593
-
594
- const alt =
595
- this._latlngs[i].alt !== undefined
596
- ? this._latlngs[i].alt
597
- : this._latlngs[i][2] !== undefined
598
- ? +this._latlngs[i][2]
599
- : 1
600
-
601
- grid[y] = grid[y] || []
602
- cell = grid[y][x]
603
-
604
- if (!cell) {
605
- cell = grid[y][x] = [p.x, p.y, alt]
606
- cell.p = p
607
- } else {
608
- cell[0] = (cell[0] * cell[2] + p.x * alt) / (cell[2] + alt) // x
609
- cell[1] = (cell[1] * cell[2] + p.y * alt) / (cell[2] + alt) // y
610
- cell[2] += alt // cumulated intensity value
611
- }
612
-
613
- // Set the max for the current zoom level
614
- if (cell[2] > this._max) {
615
- this._max = cell[2]
616
- }
617
- }
618
-
619
- this._heat.max(this._max)
620
-
621
- for (i = 0, len = grid.length; i < len; i++) {
622
- if (grid[i]) {
623
- for (j = 0, len2 = grid[i].length; j < len2; j++) {
624
- cell = grid[i][j]
625
- if (cell && bounds.contains(cell.p)) {
626
- data.push([
627
- Math.round(cell[0]),
628
- Math.round(cell[1]),
629
- Math.min(cell[2], this._max),
630
- ])
631
- }
632
- }
633
- }
634
- }
635
-
636
- this._heat.data(data).draw(this.options.minOpacity)
637
-
638
- this._frame = null
639
- },
640
- })
641
-
642
- U.DataLayer = L.Evented.extend({
643
- options: {
644
- displayOnLoad: true,
645
- inCaption: true,
646
- browsable: true,
647
- editMode: 'advanced',
648
- },
649
-
650
- initialize: function (map, data) {
651
- this.map = map
652
- this.sync = map.sync_engine.proxy(this)
653
- this._index = Array()
654
- this._layers = {}
655
- this._geojson = null
656
- this._propertiesIndex = []
657
- this._loaded = false // Are layer metadata loaded
658
- this._dataloaded = false // Are layer data loaded
659
-
660
- this.parentPane = this.map.getPane('overlayPane')
661
- this.pane = this.map.createPane(`datalayer${L.stamp(this)}`, this.parentPane)
662
- this.pane.dataset.id = L.stamp(this)
663
- this.renderer = L.svg({ pane: this.pane })
664
-
665
- let isDirty = false
666
- let isDeleted = false
667
- try {
668
- Object.defineProperty(this, 'isDirty', {
669
- get: () => isDirty,
670
- set: (status) => {
671
- if (!isDirty && status) this.fire('dirty')
672
- isDirty = status
673
- if (status) {
674
- this.map.addDirtyDatalayer(this)
675
- // A layer can be made dirty by indirect action (like dragging layers)
676
- // we need to have it loaded before saving it.
677
- if (!this.isLoaded()) this.fetchData()
678
- } else {
679
- this.map.removeDirtyDatalayer(this)
680
- this.isDeleted = false
681
- }
682
- },
683
- })
684
- } catch (e) {
685
- // Certainly IE8, which has a limited version of defineProperty
686
- }
687
- try {
688
- Object.defineProperty(this, 'isDeleted', {
689
- get: () => isDeleted,
690
- set: (status) => {
691
- if (!isDeleted && status) this.fire('deleted')
692
- isDeleted = status
693
- if (status) this.isDirty = status
694
- },
695
- })
696
- } catch (e) {
697
- // Certainly IE8, which has a limited version of defineProperty
698
- }
699
- this.setUmapId(data.id)
700
- this.setOptions(data)
701
-
702
- if (!U.Utils.isObject(this.options.remoteData)) {
703
- this.options.remoteData = {}
704
- }
705
- // Retrocompat
706
- if (this.options.remoteData?.from) {
707
- this.options.fromZoom = this.options.remoteData.from
708
- delete this.options.remoteData.from
709
- }
710
- if (this.options.remoteData?.to) {
711
- this.options.toZoom = this.options.remoteData.to
712
- delete this.options.remoteData.to
713
- }
714
- this.backupOptions()
715
- this.connectToMap()
716
- this.permissions = new U.DataLayerPermissions(this)
717
- if (!this.umap_id) {
718
- if (this.showAtLoad()) this.show()
719
- this.isDirty = true
720
- }
721
-
722
- this.onceLoaded(function () {
723
- this.map.on('moveend', this.onMoveEnd, this)
724
- })
725
- // Only layers that are displayed on load must be hidden/shown
726
- // Automatically, others will be shown manually, and thus will
727
- // be in the "forced visibility" mode
728
- if (this.autoLoaded()) this.map.on('zoomend', this.onZoomEnd, this)
729
- this.on('datachanged', this.map.onDataLayersChanged, this.map)
730
- },
731
-
732
- getSyncMetadata: function () {
733
- return {
734
- subject: 'datalayer',
735
- metadata: {
736
- id: this.umap_id || null,
737
- },
738
- }
739
- },
740
-
741
- render: function (fields, builder) {
742
- const impacts = U.Utils.getImpactsFromSchema(fields)
743
-
744
- for (const impact of impacts) {
745
- switch (impact) {
746
- case 'ui':
747
- this.map.onDataLayersChanged()
748
- break
749
- case 'data':
750
- if (fields.includes('options.type')) {
751
- this.resetLayer()
752
- }
753
- this.hide()
754
- for (const field of fields) {
755
- this.layer.onEdit(field, builder)
756
- }
757
- this.redraw()
758
- this.show()
759
- break
760
- case 'remote-data':
761
- this.fetchRemoteData()
762
- break
763
- }
764
- }
765
- },
766
-
767
- onMoveEnd: function (e) {
768
- if (this.isRemoteLayer() && this.showAtZoom()) this.fetchRemoteData()
769
- },
770
-
771
- onZoomEnd: function (e) {
772
- if (this._forcedVisibility) return
773
- if (!this.showAtZoom() && this.isVisible()) this.hide()
774
- if (this.showAtZoom() && !this.isVisible()) this.show()
775
- },
776
-
777
- showAtLoad: function () {
778
- return this.autoLoaded() && this.showAtZoom()
779
- },
780
-
781
- autoLoaded: function () {
782
- if (!this.map.datalayersFromQueryString) return this.options.displayOnLoad
783
- const datalayerIds = this.map.datalayersFromQueryString
784
- let loadMe = datalayerIds.includes(this.umap_id.toString())
785
- if (this.options.old_id) {
786
- loadMe = loadMe || datalayerIds.includes(this.options.old_id.toString())
787
- }
788
- return loadMe
789
- },
790
-
791
- insertBefore: function (other) {
792
- if (!other) return
793
- this.parentPane.insertBefore(this.pane, other.pane)
794
- },
795
-
796
- insertAfter: function (other) {
797
- if (!other) return
798
- this.parentPane.insertBefore(this.pane, other.pane.nextSibling)
799
- },
800
-
801
- bringToTop: function () {
802
- this.parentPane.appendChild(this.pane)
803
- },
804
-
805
- hasDataVisible: function () {
806
- return this.layer.hasDataVisible()
807
- },
808
-
809
- resetLayer: function (force) {
810
- // Only reset if type is defined (undefined is the default) and different from current type
811
- if (
812
- this.layer &&
813
- (!this.options.type || this.options.type === this.layer.getType()) &&
814
- !force
815
- ) {
816
- return
817
- }
818
- const visible = this.isVisible()
819
- if (this.layer) this.layer.clearLayers()
820
- // delete this.layer?
821
- if (visible) this.map.removeLayer(this.layer)
822
- const Class = U.Layer[this.options.type] || U.Layer.Default
823
- this.layer = new Class(this)
824
- this.eachLayer(this.showFeature)
825
- if (visible) this.show()
826
- this.propagateRemote()
827
- },
828
-
829
- eachLayer: function (method, context) {
830
- for (const i in this._layers) {
831
- method.call(context || this, this._layers[i])
832
- }
833
- return this
834
- },
835
-
836
- eachFeature: function (method, context) {
837
- if (this.isBrowsable()) {
838
- for (let i = 0; i < this._index.length; i++) {
839
- method.call(context || this, this._layers[this._index[i]])
840
- }
841
- }
842
- return this
843
- },
844
-
845
- fetchData: async function () {
846
- if (!this.umap_id) return
847
- if (this._loading) return
848
- this._loading = true
849
- const [geojson, response, error] = await this.map.server.get(this._dataUrl())
850
- if (!error) {
851
- this._reference_version = response.headers.get('X-Datalayer-Version')
852
- // FIXME: for now this property is set dynamically from backend
853
- // And thus it's not in the geojson file in the server
854
- // So do not let all options to be reset
855
- // Fix is a proper migration so all datalayers settings are
856
- // in DB, and we remove it from geojson flat files.
857
- if (geojson._umap_options) {
858
- geojson._umap_options.editMode = this.options.editMode
859
- }
860
- // In case of maps pre 1.0 still around
861
- if (geojson._storage) geojson._storage.editMode = this.options.editMode
862
- await this.fromUmapGeoJSON(geojson)
863
- this.backupOptions()
864
- this.fire('loaded')
865
- this._loading = false
866
- }
867
- },
868
-
869
- fromGeoJSON: function (geojson, sync = true) {
870
- this.addData(geojson, sync)
871
- this._geojson = geojson
872
- this._dataloaded = true
873
- this.fire('datachanged')
874
- this.fire('dataloaded')
875
- },
876
-
877
- fromUmapGeoJSON: async function (geojson) {
878
- if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat
879
- if (geojson._umap_options) this.setOptions(geojson._umap_options)
880
- if (this.isRemoteLayer()) await this.fetchRemoteData()
881
- else this.fromGeoJSON(geojson, false)
882
- this._loaded = true
883
- },
884
-
885
- clear: function () {
886
- this.layer.clearLayers()
887
- this._layers = {}
888
- this._index = Array()
889
- if (this._geojson) {
890
- this.backupData()
891
- this._geojson = null
892
- }
893
- this.fire('datachanged')
894
- },
895
-
896
- backupData: function () {
897
- this._geojson_bk = U.Utils.CopyJSON(this._geojson)
898
- },
899
-
900
- reindex: function () {
901
- const features = []
902
- this.eachFeature((feature) => features.push(feature))
903
- U.Utils.sortFeatures(features, this.map.getOption('sortKey'), L.lang)
904
- this._index = []
905
- for (let i = 0; i < features.length; i++) {
906
- this._index.push(L.Util.stamp(features[i]))
907
- }
908
- },
909
-
910
- showAtZoom: function () {
911
- const from = Number.parseInt(this.options.fromZoom, 10)
912
- const to = Number.parseInt(this.options.toZoom, 10)
913
- const zoom = this.map.getZoom()
914
- return !((!Number.isNaN(from) && zoom < from) || (!Number.isNaN(to) && zoom > to))
915
- },
916
-
917
- hasDynamicData: function () {
918
- return !!this.options.remoteData?.dynamic
919
- },
920
-
921
- fetchRemoteData: async function (force) {
922
- if (!this.isRemoteLayer()) return
923
- if (!this.hasDynamicData() && this.hasDataLoaded() && !force) return
924
- if (!this.isVisible()) return
925
- let url = this.map.localizeUrl(this.options.remoteData.url)
926
- if (this.options.remoteData.proxy) {
927
- url = this.map.proxyUrl(url, this.options.remoteData.ttl)
928
- }
929
- const response = await this.map.request.get(url)
930
- if (response?.ok) {
931
- this.clear()
932
- this.map.formatter
933
- .parse(await response.text(), this.options.remoteData.format)
934
- .then((geojson) => this.fromGeoJSON(geojson))
935
- }
936
- },
937
-
938
- onceLoaded: function (callback, context) {
939
- if (this.isLoaded()) callback.call(context || this, this)
940
- else this.once('loaded', callback, context)
941
- return this
942
- },
943
-
944
- onceDataLoaded: function (callback, context) {
945
- if (this.hasDataLoaded()) callback.call(context || this, this)
946
- else this.once('dataloaded', callback, context)
947
- return this
948
- },
949
-
950
- isLoaded: function () {
951
- return !this.umap_id || this._loaded
952
- },
953
-
954
- hasDataLoaded: function () {
955
- return this._dataloaded
956
- },
957
-
958
- setUmapId: function (id) {
959
- // Datalayer is null when listening creation form
960
- if (!this.umap_id && id) this.umap_id = id
961
- },
962
-
963
- backupOptions: function () {
964
- this._backupOptions = U.Utils.CopyJSON(this.options)
965
- },
966
-
967
- resetOptions: function () {
968
- this.options = U.Utils.CopyJSON(this._backupOptions)
969
- },
970
-
971
- setOptions: function (options) {
972
- delete options.geojson
973
- this.options = U.Utils.CopyJSON(U.DataLayer.prototype.options) // Start from fresh.
974
- this.updateOptions(options)
975
- },
976
-
977
- updateOptions: function (options) {
978
- L.Util.setOptions(this, options)
979
- this.resetLayer()
980
- },
981
-
982
- connectToMap: function () {
983
- const id = L.stamp(this)
984
- if (!this.map.datalayers[id]) {
985
- this.map.datalayers[id] = this
986
- if (L.Util.indexOf(this.map.datalayers_index, this) === -1)
987
- this.map.datalayers_index.push(this)
988
- this.map.onDataLayersChanged()
989
- }
990
- },
991
-
992
- _dataUrl: function () {
993
- const template = this.map.options.urls.datalayer_view
994
-
995
- let url = U.Utils.template(template, {
996
- pk: this.umap_id,
997
- map_id: this.map.options.umap_id,
998
- })
999
-
1000
- // No browser cache for owners/editors.
1001
- if (this.map.hasEditMode()) url = `${url}?${Date.now()}`
1002
- return url
1003
- },
1004
-
1005
- isRemoteLayer: function () {
1006
- return Boolean(this.options.remoteData?.url && this.options.remoteData.format)
1007
- },
1008
-
1009
- isClustered: function () {
1010
- return this.options.type === 'Cluster'
1011
- },
1012
-
1013
- showFeature: function (feature) {
1014
- if (feature.isFiltered()) return
1015
- this.layer.addLayer(feature)
1016
- },
1017
-
1018
- addLayer: function (feature) {
1019
- const id = L.stamp(feature)
1020
- feature.connectToDataLayer(this)
1021
- this._index.push(id)
1022
- this._layers[id] = feature
1023
- this.indexProperties(feature)
1024
- this.map.features_index[feature.getSlug()] = feature
1025
- this.showFeature(feature)
1026
- if (this.hasDataLoaded()) this.fire('datachanged')
1027
- },
1028
-
1029
- removeLayer: function (feature, sync) {
1030
- const id = L.stamp(feature)
1031
- if (sync !== false) feature.sync.delete()
1032
- this.layer.removeLayer(feature)
1033
- feature.disconnectFromDataLayer(this)
1034
- this._index.splice(this._index.indexOf(id), 1)
1035
- delete this._layers[id]
1036
- delete this.map.features_index[feature.getSlug()]
1037
- if (this.hasDataLoaded() && this.isVisible()) this.fire('datachanged')
1038
- },
1039
-
1040
- indexProperties: function (feature) {
1041
- for (const i in feature.properties)
1042
- if (typeof feature.properties[i] !== 'object') this.indexProperty(i)
1043
- },
1044
-
1045
- indexProperty: function (name) {
1046
- if (!name) return
1047
- if (name.indexOf('_') === 0) return
1048
- if (L.Util.indexOf(this._propertiesIndex, name) !== -1) return
1049
- this._propertiesIndex.push(name)
1050
- this._propertiesIndex.sort()
1051
- },
1052
-
1053
- deindexProperty: function (name) {
1054
- const idx = this._propertiesIndex.indexOf(name)
1055
- if (idx !== -1) this._propertiesIndex.splice(idx, 1)
1056
- },
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
-
1065
- addData: function (geojson, sync) {
1066
- try {
1067
- // Do not fail if remote data is somehow invalid,
1068
- // otherwise the layer becomes uneditable.
1069
- this.geojsonToFeatures(geojson, sync)
1070
- } catch (err) {
1071
- console.log('Error with DataLayer', this.umap_id)
1072
- console.error(err)
1073
- }
1074
- },
1075
-
1076
- // The choice of the name is not ours, because it is required by Leaflet.
1077
- // It is misleading, as the returned objects are uMap objects, and not
1078
- // GeoJSON features.
1079
- geojsonToFeatures: function (geojson, sync) {
1080
- if (!geojson) return
1081
- const features = Array.isArray(geojson) ? geojson : geojson.features
1082
- let i
1083
- let len
1084
-
1085
- if (features) {
1086
- U.Utils.sortFeatures(features, this.map.getOption('sortKey'), L.lang)
1087
- for (i = 0, len = features.length; i < len; i++) {
1088
- this.geojsonToFeatures(features[i])
1089
- }
1090
- return this // Why returning "this" ?
1091
- }
1092
-
1093
- const geometry = geojson.type === 'Feature' ? geojson.geometry : geojson
1094
-
1095
- const feature = this.geoJSONToLeaflet({ geometry, geojson })
1096
- if (feature) {
1097
- this.addLayer(feature)
1098
- if (sync) feature.onCommit()
1099
- return feature
1100
- }
1101
- },
1102
-
1103
- /**
1104
- * Create or update Leaflet features from GeoJSON geometries.
1105
- *
1106
- * If no `feature` is provided, a new feature will be created.
1107
- * If `feature` is provided, it will be updated with the passed geometry.
1108
- *
1109
- * GeoJSON and Leaflet use incompatible formats to encode coordinates.
1110
- * This method takes care of the convertion.
1111
- *
1112
- * @param geometry GeoJSON geometry field
1113
- * @param geojson Enclosing GeoJSON. If none is provided, a new one will
1114
- * be created
1115
- * @param id Id of the feature
1116
- * @param feature Leaflet feature that should be updated with the new geometry
1117
- * @returns Leaflet feature.
1118
- */
1119
- geoJSONToLeaflet: function ({
1120
- geometry,
1121
- geojson = null,
1122
- id = null,
1123
- feature = null,
1124
- } = {}) {
1125
- if (!geometry) return // null geometry is valid geojson.
1126
- const coords = geometry.coordinates
1127
- let latlng
1128
- let latlngs
1129
-
1130
- // Create a default geojson if none is provided
1131
- if (geojson === undefined) geojson = { type: 'Feature', geometry: geometry }
1132
-
1133
- switch (geometry.type) {
1134
- case 'Point':
1135
- try {
1136
- latlng = L.GeoJSON.coordsToLatLng(coords)
1137
- } catch (e) {
1138
- console.error('Invalid latlng object from', coords)
1139
- break
1140
- }
1141
- if (feature) {
1142
- feature.setLatLng(latlng)
1143
- return feature
1144
- }
1145
- return this._pointToLayer(geojson, latlng, id)
1146
-
1147
- case 'MultiLineString':
1148
- case 'LineString':
1149
- latlngs = L.GeoJSON.coordsToLatLngs(
1150
- coords,
1151
- geometry.type === 'LineString' ? 0 : 1
1152
- )
1153
- if (!latlngs.length) break
1154
- if (feature) {
1155
- feature.setLatLngs(latlngs)
1156
- return feature
1157
- }
1158
- return this._lineToLayer(geojson, latlngs, id)
1159
-
1160
- case 'MultiPolygon':
1161
- case 'Polygon':
1162
- latlngs = L.GeoJSON.coordsToLatLngs(coords, geometry.type === 'Polygon' ? 1 : 2)
1163
- if (feature) {
1164
- feature.setLatLngs(latlngs)
1165
- return feature
1166
- }
1167
- return this._polygonToLayer(geojson, latlngs, id)
1168
- case 'GeometryCollection':
1169
- return this.geojsonToFeatures(geometry.geometries)
1170
-
1171
- default:
1172
- U.Alert.error(
1173
- L._('Skipping unknown geometry.type: {type}', {
1174
- type: geometry.type || 'undefined',
1175
- })
1176
- )
1177
- }
1178
- },
1179
-
1180
- _pointToLayer: function (geojson, latlng, id) {
1181
- return new U.Marker(this.map, latlng, { geojson: geojson, datalayer: this }, id)
1182
- },
1183
-
1184
- _lineToLayer: function (geojson, latlngs, id) {
1185
- return new U.Polyline(
1186
- this.map,
1187
- latlngs,
1188
- {
1189
- geojson: geojson,
1190
- datalayer: this,
1191
- color: null,
1192
- },
1193
- id
1194
- )
1195
- },
1196
-
1197
- _polygonToLayer: function (geojson, latlngs, id) {
1198
- // Ensure no empty hole
1199
- // for (let i = latlngs.length - 1; i > 0; i--) {
1200
- // if (!latlngs.slice()[i].length) latlngs.splice(i, 1);
1201
- // }
1202
- return new U.Polygon(this.map, latlngs, { geojson: geojson, datalayer: this }, id)
1203
- },
1204
-
1205
- importRaw: async function (raw, format) {
1206
- this.map.formatter
1207
- .parse(raw, format)
1208
- .then((geojson) => this.addData(geojson))
1209
- .then(() => this.zoomTo())
1210
- this.isDirty = true
1211
- },
1212
-
1213
- importFromFiles: function (files, type) {
1214
- for (let i = 0, f; (f = files[i]); i++) {
1215
- this.importFromFile(f, type)
1216
- }
1217
- },
1218
-
1219
- importFromFile: function (f, type) {
1220
- const reader = new FileReader()
1221
- type = type || U.Utils.detectFileType(f)
1222
- reader.readAsText(f)
1223
- reader.onload = (e) => this.importRaw(e.target.result, type)
1224
- },
1225
-
1226
- importFromUrl: async function (uri, type) {
1227
- uri = this.map.localizeUrl(uri)
1228
- const response = await this.map.request.get(uri)
1229
- if (response?.ok) {
1230
- this.importRaw(await response.text(), type)
1231
- }
1232
- },
1233
-
1234
- getColor: function () {
1235
- return this.options.color || this.map.getOption('color')
1236
- },
1237
-
1238
- getDeleteUrl: function () {
1239
- return U.Utils.template(this.map.options.urls.datalayer_delete, {
1240
- pk: this.umap_id,
1241
- map_id: this.map.options.umap_id,
1242
- })
1243
- },
1244
-
1245
- getVersionsUrl: function () {
1246
- return U.Utils.template(this.map.options.urls.datalayer_versions, {
1247
- pk: this.umap_id,
1248
- map_id: this.map.options.umap_id,
1249
- })
1250
- },
1251
-
1252
- getVersionUrl: function (name) {
1253
- return U.Utils.template(this.map.options.urls.datalayer_version, {
1254
- pk: this.umap_id,
1255
- map_id: this.map.options.umap_id,
1256
- name: name,
1257
- })
1258
- },
1259
-
1260
- _delete: function () {
1261
- this.isDeleted = true
1262
- this.erase()
1263
- },
1264
-
1265
- empty: function () {
1266
- if (this.isRemoteLayer()) return
1267
- this.clear()
1268
- this.isDirty = true
1269
- },
1270
-
1271
- clone: function () {
1272
- const options = U.Utils.CopyJSON(this.options)
1273
- options.name = L._('Clone of {name}', { name: this.options.name })
1274
- delete options.id
1275
- const geojson = U.Utils.CopyJSON(this._geojson)
1276
- const datalayer = this.map.createDataLayer(options)
1277
- datalayer.fromGeoJSON(geojson)
1278
- return datalayer
1279
- },
1280
-
1281
- erase: function () {
1282
- this.hide()
1283
- delete this.map.datalayers[L.stamp(this)]
1284
- this.map.datalayers_index.splice(this.getRank(), 1)
1285
- this.parentPane.removeChild(this.pane)
1286
- this.map.onDataLayersChanged()
1287
- this.off('datachanged', this.map.onDataLayersChanged, this.map)
1288
- this.fire('erase')
1289
- this._leaflet_events_bk = this._leaflet_events
1290
- this.map.off('moveend', this.onMoveEnd, this)
1291
- this.map.off('zoomend', this.onZoomEnd, this)
1292
- this.off()
1293
- this.clear()
1294
- delete this._loaded
1295
- delete this._dataloaded
1296
- },
1297
-
1298
- reset: function () {
1299
- if (!this.umap_id) this.erase()
1300
-
1301
- this.resetOptions()
1302
- this.parentPane.appendChild(this.pane)
1303
- if (this._leaflet_events_bk && !this._leaflet_events) {
1304
- this._leaflet_events = this._leaflet_events_bk
1305
- }
1306
- this.clear()
1307
- this.hide()
1308
- if (this.isRemoteLayer()) this.fetchRemoteData()
1309
- else if (this._geojson_bk) this.fromGeoJSON(this._geojson_bk)
1310
- this._loaded = true
1311
- this.show()
1312
- this.isDirty = false
1313
- },
1314
-
1315
- redraw: function () {
1316
- if (!this.isVisible()) return
1317
- this.hide()
1318
- this.show()
1319
- },
1320
-
1321
- edit: function () {
1322
- if (!this.map.editEnabled || !this.isLoaded()) {
1323
- return
1324
- }
1325
- const container = L.DomUtil.create('div', 'umap-layer-properties-container')
1326
- const metadataFields = [
1327
- 'options.name',
1328
- 'options.description',
1329
- ['options.type', { handler: 'LayerTypeChooser', label: L._('Type of layer') }],
1330
- ['options.displayOnLoad', { label: L._('Display on load'), handler: 'Switch' }],
1331
- [
1332
- 'options.browsable',
1333
- {
1334
- label: L._('Data is browsable'),
1335
- handler: 'Switch',
1336
- helpEntries: 'browsable',
1337
- },
1338
- ],
1339
- [
1340
- 'options.inCaption',
1341
- {
1342
- label: L._('Show this layer in the caption'),
1343
- handler: 'Switch',
1344
- },
1345
- ],
1346
- ]
1347
- L.DomUtil.createTitle(container, L._('Layer properties'), 'icon-layers')
1348
- let builder = new U.FormBuilder(this, metadataFields, {
1349
- callback: function (e) {
1350
- this.map.onDataLayersChanged()
1351
- if (e.helper.field === 'options.type') {
1352
- this.edit()
1353
- }
1354
- },
1355
- })
1356
- container.appendChild(builder.build())
1357
-
1358
- const layerOptions = this.layer.getEditableOptions()
1359
-
1360
- if (layerOptions.length) {
1361
- builder = new U.FormBuilder(this, layerOptions, {
1362
- id: 'datalayer-layer-properties',
1363
- })
1364
- const layerProperties = L.DomUtil.createFieldset(
1365
- container,
1366
- `${this.layer.getName()}: ${L._('settings')}`
1367
- )
1368
- layerProperties.appendChild(builder.build())
1369
- }
1370
-
1371
- const shapeOptions = [
1372
- 'options.color',
1373
- 'options.iconClass',
1374
- 'options.iconUrl',
1375
- 'options.iconOpacity',
1376
- 'options.opacity',
1377
- 'options.stroke',
1378
- 'options.weight',
1379
- 'options.fill',
1380
- 'options.fillColor',
1381
- 'options.fillOpacity',
1382
- ]
1383
-
1384
- builder = new U.FormBuilder(this, shapeOptions, {
1385
- id: 'datalayer-advanced-properties',
1386
- })
1387
- const shapeProperties = L.DomUtil.createFieldset(container, L._('Shape properties'))
1388
- shapeProperties.appendChild(builder.build())
1389
-
1390
- const optionsFields = [
1391
- 'options.smoothFactor',
1392
- 'options.dashArray',
1393
- 'options.zoomTo',
1394
- 'options.fromZoom',
1395
- 'options.toZoom',
1396
- 'options.labelKey',
1397
- ]
1398
-
1399
- builder = new U.FormBuilder(this, optionsFields, {
1400
- id: 'datalayer-advanced-properties',
1401
- })
1402
- const advancedProperties = L.DomUtil.createFieldset(
1403
- container,
1404
- L._('Advanced properties')
1405
- )
1406
- advancedProperties.appendChild(builder.build())
1407
-
1408
- const popupFields = [
1409
- 'options.popupShape',
1410
- 'options.popupTemplate',
1411
- 'options.popupContentTemplate',
1412
- 'options.showLabel',
1413
- 'options.labelDirection',
1414
- 'options.labelInteractive',
1415
- 'options.outlinkTarget',
1416
- 'options.interactive',
1417
- ]
1418
- builder = new U.FormBuilder(this, popupFields)
1419
- const popupFieldset = L.DomUtil.createFieldset(
1420
- container,
1421
- L._('Interaction options')
1422
- )
1423
- popupFieldset.appendChild(builder.build())
1424
-
1425
- // XXX I'm not sure **why** this is needed (as it's set during `this.initialize`)
1426
- // but apparently it's needed.
1427
- if (!U.Utils.isObject(this.options.remoteData)) {
1428
- this.options.remoteData = {}
1429
- }
1430
-
1431
- const remoteDataFields = [
1432
- [
1433
- 'options.remoteData.url',
1434
- { handler: 'Url', label: L._('Url'), helpEntries: 'formatURL' },
1435
- ],
1436
- ['options.remoteData.format', { handler: 'DataFormat', label: L._('Format') }],
1437
- 'options.fromZoom',
1438
- 'options.toZoom',
1439
- [
1440
- 'options.remoteData.dynamic',
1441
- { handler: 'Switch', label: L._('Dynamic'), helpEntries: 'dynamicRemoteData' },
1442
- ],
1443
- [
1444
- 'options.remoteData.licence',
1445
- {
1446
- label: L._('Licence'),
1447
- helpText: L._('Please be sure the licence is compliant with your use.'),
1448
- },
1449
- ],
1450
- ]
1451
- if (this.map.options.urls.ajax_proxy) {
1452
- remoteDataFields.push([
1453
- 'options.remoteData.proxy',
1454
- {
1455
- handler: 'Switch',
1456
- label: L._('Proxy request'),
1457
- helpEntries: 'proxyRemoteData',
1458
- },
1459
- ])
1460
- remoteDataFields.push('options.remoteData.ttl')
1461
- }
1462
-
1463
- const remoteDataContainer = L.DomUtil.createFieldset(container, L._('Remote data'))
1464
- builder = new U.FormBuilder(this, remoteDataFields)
1465
- remoteDataContainer.appendChild(builder.build())
1466
- L.DomUtil.createButton(
1467
- 'button umap-verify',
1468
- remoteDataContainer,
1469
- L._('Verify remote URL'),
1470
- () => this.fetchRemoteData(true),
1471
- this
1472
- )
1473
-
1474
- if (this.map.options.urls.datalayer_versions) this.buildVersionsFieldset(container)
1475
-
1476
- const advancedActions = L.DomUtil.createFieldset(container, L._('Advanced actions'))
1477
- const advancedButtons = L.DomUtil.create('div', 'button-bar half', advancedActions)
1478
- const deleteLink = L.DomUtil.createButton(
1479
- 'button delete_datalayer_button umap-delete',
1480
- advancedButtons,
1481
- L._('Delete'),
1482
- function () {
1483
- this._delete()
1484
- this.map.editPanel.close()
1485
- },
1486
- this
1487
- )
1488
- if (!this.isRemoteLayer()) {
1489
- const emptyLink = L.DomUtil.createButton(
1490
- 'button umap-empty',
1491
- advancedButtons,
1492
- L._('Empty'),
1493
- this.empty,
1494
- this
1495
- )
1496
- }
1497
- const cloneLink = L.DomUtil.createButton(
1498
- 'button umap-clone',
1499
- advancedButtons,
1500
- L._('Clone'),
1501
- function () {
1502
- const datalayer = this.clone()
1503
- datalayer.edit()
1504
- },
1505
- this
1506
- )
1507
- if (this.umap_id) {
1508
- const download = L.DomUtil.createLink(
1509
- 'button umap-download',
1510
- advancedButtons,
1511
- L._('Download'),
1512
- this._dataUrl(),
1513
- '_blank'
1514
- )
1515
- }
1516
- const backButton = L.DomUtil.createButtonIcon(
1517
- undefined,
1518
- 'icon-back',
1519
- L._('Back to layers')
1520
- )
1521
- // Fixme: remove me when this is merged and released
1522
- // https://github.com/Leaflet/Leaflet/pull/9052
1523
- L.DomEvent.disableClickPropagation(backButton)
1524
- L.DomEvent.on(backButton, 'click', this.map.editDatalayers, this.map)
1525
-
1526
- this.map.editPanel.open({
1527
- content: container,
1528
- actions: [backButton],
1529
- })
1530
- },
1531
-
1532
- getOwnOption: function (option) {
1533
- if (U.Utils.usableOption(this.options, option)) return this.options[option]
1534
- },
1535
-
1536
- getOption: function (option, feature) {
1537
- if (this.layer?.getOption) {
1538
- const value = this.layer.getOption(option, feature)
1539
- if (typeof value !== 'undefined') return value
1540
- }
1541
- if (typeof this.getOwnOption(option) !== 'undefined') {
1542
- return this.getOwnOption(option)
1543
- }
1544
- if (this.layer?.defaults?.[option]) {
1545
- return this.layer.defaults[option]
1546
- }
1547
- return this.map.getOption(option, feature)
1548
- },
1549
-
1550
- buildVersionsFieldset: async function (container) {
1551
- const appendVersion = (data) => {
1552
- const date = new Date(Number.parseInt(data.at, 10))
1553
- const content = `${date.toLocaleString(L.lang)} (${Number.parseInt(data.size) / 1000}Kb)`
1554
- const el = L.DomUtil.create('div', 'umap-datalayer-version', versionsContainer)
1555
- const button = L.DomUtil.createButton(
1556
- '',
1557
- el,
1558
- '',
1559
- () => this.restore(data.name),
1560
- this
1561
- )
1562
- button.title = L._('Restore this version')
1563
- L.DomUtil.add('span', '', el, content)
1564
- }
1565
-
1566
- const versionsContainer = L.DomUtil.createFieldset(container, L._('Versions'), {
1567
- callback: async function () {
1568
- const [{ versions }, response, error] = await this.map.server.get(
1569
- this.getVersionsUrl()
1570
- )
1571
- if (!error) versions.forEach(appendVersion)
1572
- },
1573
- context: this,
1574
- })
1575
- },
1576
-
1577
- restore: async function (version) {
1578
- if (!this.map.editEnabled) return
1579
- if (!confirm(L._('Are you sure you want to restore this version?'))) return
1580
- const [geojson, response, error] = await this.map.server.get(
1581
- this.getVersionUrl(version)
1582
- )
1583
- if (!error) {
1584
- if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat.
1585
- if (geojson._umap_options) this.setOptions(geojson._umap_options)
1586
- this.empty()
1587
- if (this.isRemoteLayer()) this.fetchRemoteData()
1588
- else this.addData(geojson)
1589
- this.isDirty = true
1590
- }
1591
- },
1592
-
1593
- featuresToGeoJSON: function () {
1594
- const features = []
1595
- this.eachLayer((layer) => features.push(layer.toGeoJSON()))
1596
- return features
1597
- },
1598
-
1599
- show: async function () {
1600
- this.map.addLayer(this.layer)
1601
- if (!this.isLoaded()) await this.fetchData()
1602
- this.fire('show')
1603
- },
1604
-
1605
- hide: function () {
1606
- this.map.removeLayer(this.layer)
1607
- this.fire('hide')
1608
- },
1609
-
1610
- toggle: function () {
1611
- // From now on, do not try to how/hide
1612
- // automatically this layer.
1613
- this._forcedVisibility = true
1614
- if (!this.isVisible()) this.show()
1615
- else this.hide()
1616
- },
1617
-
1618
- zoomTo: function () {
1619
- if (!this.isVisible()) return
1620
- const bounds = this.layer.getBounds()
1621
- if (bounds.isValid()) {
1622
- const options = { maxZoom: this.getOption('zoomTo') }
1623
- this.map.fitBounds(bounds, options)
1624
- }
1625
- },
1626
-
1627
- // Is this layer type browsable in theorie
1628
- isBrowsable: function () {
1629
- return this.layer?.browsable
1630
- },
1631
-
1632
- // Is this layer browsable in theorie
1633
- // AND the user allows it
1634
- allowBrowse: function () {
1635
- return !!this.options.browsable && this.isBrowsable()
1636
- },
1637
-
1638
- // Is this layer browsable in theorie
1639
- // AND the user allows it
1640
- // AND it makes actually sense (is visible, it has data…)
1641
- canBrowse: function () {
1642
- return this.allowBrowse() && this.isVisible() && this.hasData()
1643
- },
1644
-
1645
- count: function () {
1646
- return this._index.length
1647
- },
1648
-
1649
- hasData: function () {
1650
- return !!this._index.length
1651
- },
1652
-
1653
- isVisible: function () {
1654
- return Boolean(this.layer && this.map.hasLayer(this.layer))
1655
- },
1656
-
1657
- getFeatureByIndex: function (index) {
1658
- if (index === -1) index = this._index.length - 1
1659
- const id = this._index[index]
1660
- return this._layers[id]
1661
- },
1662
-
1663
- // TODO Add an index
1664
- // For now, iterate on all the features.
1665
- getFeatureById: function (id) {
1666
- return Object.values(this._layers).find((feature) => feature.id === id)
1667
- },
1668
-
1669
- getNextFeature: function (feature) {
1670
- const id = this._index.indexOf(L.stamp(feature))
1671
- const nextId = this._index[id + 1]
1672
- return nextId ? this._layers[nextId] : this.getNextBrowsable().getFeatureByIndex(0)
1673
- },
1674
-
1675
- getPreviousFeature: function (feature) {
1676
- if (this._index <= 1) {
1677
- return null
1678
- }
1679
- const id = this._index.indexOf(L.stamp(feature))
1680
- const previousId = this._index[id - 1]
1681
- return previousId
1682
- ? this._layers[previousId]
1683
- : this.getPreviousBrowsable().getFeatureByIndex(-1)
1684
- },
1685
-
1686
- getPreviousBrowsable: function () {
1687
- let id = this.getRank()
1688
- let next
1689
- const index = this.map.datalayers_index
1690
- while (((id = index[++id] ? id : 0), (next = index[id]))) {
1691
- if (next === this || next.canBrowse()) break
1692
- }
1693
- return next
1694
- },
1695
-
1696
- getNextBrowsable: function () {
1697
- let id = this.getRank()
1698
- let prev
1699
- const index = this.map.datalayers_index
1700
- while (((id = index[--id] ? id : index.length - 1), (prev = index[id]))) {
1701
- if (prev === this || prev.canBrowse()) break
1702
- }
1703
- return prev
1704
- },
1705
-
1706
- umapGeoJSON: function () {
1707
- return {
1708
- type: 'FeatureCollection',
1709
- features: this.isRemoteLayer() ? [] : this.featuresToGeoJSON(),
1710
- _umap_options: this.options,
1711
- }
1712
- },
1713
-
1714
- getRank: function () {
1715
- return this.map.datalayers_index.indexOf(this)
1716
- },
1717
-
1718
- isReadOnly: function () {
1719
- // isReadOnly must return true if unset
1720
- return this.options.editMode === 'disabled'
1721
- },
1722
-
1723
- isDataReadOnly: function () {
1724
- // This layer cannot accept features
1725
- return this.isReadOnly() || this.isRemoteLayer()
1726
- },
1727
-
1728
- save: async function () {
1729
- if (this.isDeleted) return this.saveDelete()
1730
- if (!this.isLoaded()) {
1731
- return
1732
- }
1733
- const geojson = this.umapGeoJSON()
1734
- const formData = new FormData()
1735
- formData.append('name', this.options.name)
1736
- formData.append('display_on_load', !!this.options.displayOnLoad)
1737
- formData.append('rank', this.getRank())
1738
- formData.append('settings', JSON.stringify(this.options))
1739
- // Filename support is shaky, don't do it for now.
1740
- const blob = new Blob([JSON.stringify(geojson)], { type: 'application/json' })
1741
- formData.append('geojson', blob)
1742
- const saveUrl = this.map.urls.get('datalayer_save', {
1743
- map_id: this.map.options.umap_id,
1744
- pk: this.umap_id,
1745
- })
1746
- const headers = this._reference_version
1747
- ? { 'X-Datalayer-Reference': this._reference_version }
1748
- : {}
1749
- await this._trySave(saveUrl, headers, formData)
1750
- this._geojson = geojson
1751
- },
1752
-
1753
- _trySave: async function (url, headers, formData) {
1754
- const [data, response, error] = await this.map.server.post(url, headers, formData)
1755
- if (error) {
1756
- if (response && response.status === 412) {
1757
- U.AlertConflict.error(
1758
- L._(
1759
- 'Whoops! Other contributor(s) changed some of the same map elements as you. ' +
1760
- 'This situation is tricky, you have to choose carefully which version is pertinent.'
1761
- ),
1762
- async () => {
1763
- await this._trySave(url, {}, formData)
1764
- }
1765
- )
1766
- }
1767
- } else {
1768
- // Response contains geojson only if save has conflicted and conflicts have
1769
- // been resolved. So we need to reload to get extra data (added by someone else)
1770
- if (data.geojson) {
1771
- this.clear()
1772
- this.fromGeoJSON(data.geojson)
1773
- delete data.geojson
1774
- }
1775
- this._reference_version = response.headers.get('X-Datalayer-Version')
1776
- this.sync.update('_reference_version', this._reference_version)
1777
-
1778
- this.setUmapId(data.id)
1779
- this.updateOptions(data)
1780
- this.backupOptions()
1781
- this.connectToMap()
1782
- this._loaded = true
1783
- this.redraw() // Needed for reordering features
1784
- this.isDirty = false
1785
- this.permissions.save()
1786
- }
1787
- },
1788
-
1789
- saveDelete: async function () {
1790
- if (this.umap_id) {
1791
- await this.map.server.post(this.getDeleteUrl())
1792
- }
1793
- this.isDirty = false
1794
- this.map.continueSaving()
1795
- },
1796
-
1797
- getMap: function () {
1798
- return this.map
1799
- },
1800
-
1801
- getName: function () {
1802
- return this.options.name || L._('Untitled layer')
1803
- },
1804
-
1805
- tableEdit: function () {
1806
- if (!this.isVisible()) return
1807
- const editor = new U.TableEditor(this)
1808
- editor.open()
1809
- },
1810
-
1811
- getFilterKeys: function () {
1812
- // This keys will be used to filter feature from the browser text input.
1813
- // By default, it will we use the "name" property, which is also the one used as label in the features list.
1814
- // When map owner has configured another label or sort key, we try to be smart and search in the same keys.
1815
- if (this.map.options.filterKey) return this.map.options.filterKey
1816
- if (this.options.labelKey) return this.options.labelKey
1817
- if (this.map.options.sortKey) return this.map.options.sortKey
1818
- return 'name'
1819
- },
1820
- })
1821
-
1822
- L.TileLayer.include({
1823
- toJSON: function () {
1824
- return {
1825
- minZoom: this.options.minZoom,
1826
- maxZoom: this.options.maxZoom,
1827
- attribution: this.options.attribution,
1828
- url_template: this._url,
1829
- name: this.options.name,
1830
- tms: this.options.tms,
1831
- }
1832
- },
1833
-
1834
- getAttribution: function () {
1835
- return U.Utils.toHTML(this.options.attribution)
1836
- },
1837
- })