umap-project 2.7.3__py3-none-any.whl → 2.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. umap/__init__.py +1 -1
  2. umap/forms.py +4 -14
  3. umap/locale/am_ET/LC_MESSAGES/django.mo +0 -0
  4. umap/locale/am_ET/LC_MESSAGES/django.po +278 -151
  5. umap/locale/ar/LC_MESSAGES/django.mo +0 -0
  6. umap/locale/ar/LC_MESSAGES/django.po +335 -141
  7. umap/locale/bg/LC_MESSAGES/django.mo +0 -0
  8. umap/locale/bg/LC_MESSAGES/django.po +279 -152
  9. umap/locale/br/LC_MESSAGES/django.mo +0 -0
  10. umap/locale/br/LC_MESSAGES/django.po +95 -79
  11. umap/locale/ca/LC_MESSAGES/django.mo +0 -0
  12. umap/locale/ca/LC_MESSAGES/django.po +85 -68
  13. umap/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  14. umap/locale/cs_CZ/LC_MESSAGES/django.po +78 -66
  15. umap/locale/da/LC_MESSAGES/django.mo +0 -0
  16. umap/locale/da/LC_MESSAGES/django.po +280 -153
  17. umap/locale/de/LC_MESSAGES/django.mo +0 -0
  18. umap/locale/de/LC_MESSAGES/django.po +80 -64
  19. umap/locale/el/LC_MESSAGES/django.mo +0 -0
  20. umap/locale/el/LC_MESSAGES/django.po +82 -66
  21. umap/locale/en/LC_MESSAGES/django.po +73 -61
  22. umap/locale/es/LC_MESSAGES/django.mo +0 -0
  23. umap/locale/es/LC_MESSAGES/django.po +75 -63
  24. umap/locale/et/LC_MESSAGES/django.mo +0 -0
  25. umap/locale/et/LC_MESSAGES/django.po +280 -153
  26. umap/locale/eu/LC_MESSAGES/django.mo +0 -0
  27. umap/locale/eu/LC_MESSAGES/django.po +82 -66
  28. umap/locale/fa_IR/LC_MESSAGES/django.mo +0 -0
  29. umap/locale/fa_IR/LC_MESSAGES/django.po +80 -64
  30. umap/locale/fi/LC_MESSAGES/django.mo +0 -0
  31. umap/locale/fi/LC_MESSAGES/django.po +278 -151
  32. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  33. umap/locale/fr/LC_MESSAGES/django.po +75 -63
  34. umap/locale/gl/LC_MESSAGES/django.mo +0 -0
  35. umap/locale/gl/LC_MESSAGES/django.po +280 -153
  36. umap/locale/he/LC_MESSAGES/django.mo +0 -0
  37. umap/locale/he/LC_MESSAGES/django.po +281 -154
  38. umap/locale/hu/LC_MESSAGES/django.mo +0 -0
  39. umap/locale/hu/LC_MESSAGES/django.po +80 -64
  40. umap/locale/is/LC_MESSAGES/django.mo +0 -0
  41. umap/locale/is/LC_MESSAGES/django.po +280 -153
  42. umap/locale/it/LC_MESSAGES/django.mo +0 -0
  43. umap/locale/it/LC_MESSAGES/django.po +82 -66
  44. umap/locale/ja/LC_MESSAGES/django.mo +0 -0
  45. umap/locale/ja/LC_MESSAGES/django.po +280 -153
  46. umap/locale/ko/LC_MESSAGES/django.mo +0 -0
  47. umap/locale/ko/LC_MESSAGES/django.po +280 -153
  48. umap/locale/lt/LC_MESSAGES/django.mo +0 -0
  49. umap/locale/lt/LC_MESSAGES/django.po +280 -153
  50. umap/locale/ms/LC_MESSAGES/django.mo +0 -0
  51. umap/locale/ms/LC_MESSAGES/django.po +82 -66
  52. umap/locale/nl/LC_MESSAGES/django.mo +0 -0
  53. umap/locale/nl/LC_MESSAGES/django.po +280 -153
  54. umap/locale/pl/LC_MESSAGES/django.mo +0 -0
  55. umap/locale/pl/LC_MESSAGES/django.po +82 -66
  56. umap/locale/pt/LC_MESSAGES/django.mo +0 -0
  57. umap/locale/pt/LC_MESSAGES/django.po +75 -63
  58. umap/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
  59. umap/locale/pt_BR/LC_MESSAGES/django.po +280 -153
  60. umap/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
  61. umap/locale/pt_PT/LC_MESSAGES/django.po +280 -153
  62. umap/locale/ru/LC_MESSAGES/django.mo +0 -0
  63. umap/locale/ru/LC_MESSAGES/django.po +280 -153
  64. umap/locale/sk_SK/LC_MESSAGES/django.mo +0 -0
  65. umap/locale/sk_SK/LC_MESSAGES/django.po +280 -153
  66. umap/locale/sl/LC_MESSAGES/django.mo +0 -0
  67. umap/locale/sl/LC_MESSAGES/django.po +280 -153
  68. umap/locale/sr/LC_MESSAGES/django.mo +0 -0
  69. umap/locale/sr/LC_MESSAGES/django.po +280 -153
  70. umap/locale/sv/LC_MESSAGES/django.mo +0 -0
  71. umap/locale/sv/LC_MESSAGES/django.po +81 -65
  72. umap/locale/th_TH/LC_MESSAGES/django.mo +0 -0
  73. umap/locale/th_TH/LC_MESSAGES/django.po +257 -185
  74. umap/locale/tr/LC_MESSAGES/django.mo +0 -0
  75. umap/locale/tr/LC_MESSAGES/django.po +280 -153
  76. umap/locale/uk_UA/LC_MESSAGES/django.mo +0 -0
  77. umap/locale/uk_UA/LC_MESSAGES/django.po +280 -153
  78. umap/locale/vi/LC_MESSAGES/django.mo +0 -0
  79. umap/locale/vi/LC_MESSAGES/django.po +278 -151
  80. umap/locale/zh/LC_MESSAGES/django.mo +0 -0
  81. umap/locale/zh/LC_MESSAGES/django.po +278 -151
  82. umap/locale/zh_TW/LC_MESSAGES/django.mo +0 -0
  83. umap/locale/zh_TW/LC_MESSAGES/django.po +97 -81
  84. umap/management/commands/empty_trash.py +35 -0
  85. umap/management/commands/migrate_to_S3.py +29 -0
  86. umap/migrations/0023_alter_datalayer_uuid.py +19 -0
  87. umap/migrations/0024_alter_map_share_status.py +30 -0
  88. umap/migrations/0025_alter_datalayer_geojson.py +24 -0
  89. umap/models.py +68 -116
  90. umap/settings/base.py +23 -3
  91. umap/settings/local_s3.py +45 -0
  92. umap/static/umap/base.css +3 -603
  93. umap/static/umap/content.css +5 -3
  94. umap/static/umap/css/bar.css +202 -0
  95. umap/static/umap/css/form.css +620 -0
  96. umap/static/umap/css/icon.css +21 -1
  97. umap/static/umap/css/popup.css +125 -0
  98. umap/static/umap/img/16-white.svg +16 -4
  99. umap/static/umap/img/16.svg +1 -1
  100. umap/static/umap/img/source/16-white.svg +46 -45
  101. umap/static/umap/img/source/16.svg +1 -753
  102. umap/static/umap/js/components/fragment.js +3 -1
  103. umap/static/umap/js/modules/browser.js +20 -19
  104. umap/static/umap/js/modules/caption.js +21 -22
  105. umap/static/umap/js/modules/data/features.js +120 -78
  106. umap/static/umap/js/modules/data/layer.js +195 -153
  107. umap/static/umap/js/modules/facets.js +9 -9
  108. umap/static/umap/js/modules/formatter.js +5 -5
  109. umap/static/umap/js/modules/global.js +4 -52
  110. umap/static/umap/js/modules/help.js +18 -21
  111. umap/static/umap/js/modules/importer.js +133 -56
  112. umap/static/umap/js/modules/importers/cadastrefr.js +4 -0
  113. umap/static/umap/js/modules/importers/geodatamine.js +3 -3
  114. umap/static/umap/js/modules/importers/overpass.js +5 -0
  115. umap/static/umap/js/modules/permissions.js +85 -87
  116. umap/static/umap/js/modules/rendering/icon.js +2 -1
  117. umap/static/umap/js/modules/rendering/layers/base.js +15 -15
  118. umap/static/umap/js/modules/rendering/layers/classified.js +1 -1
  119. umap/static/umap/js/modules/rendering/layers/cluster.js +1 -1
  120. umap/static/umap/js/modules/rendering/layers/heat.js +1 -1
  121. umap/static/umap/js/modules/rendering/map.js +390 -0
  122. umap/static/umap/js/modules/rendering/popup.js +19 -19
  123. umap/static/umap/js/modules/rendering/template.js +88 -21
  124. umap/static/umap/js/modules/rendering/ui.js +63 -14
  125. umap/static/umap/js/modules/request.js +2 -2
  126. umap/static/umap/js/modules/rules.js +22 -25
  127. umap/static/umap/js/modules/saving.js +47 -0
  128. umap/static/umap/js/modules/schema.js +6 -0
  129. umap/static/umap/js/modules/share.js +21 -24
  130. umap/static/umap/js/modules/slideshow.js +24 -20
  131. umap/static/umap/js/modules/sync/updaters.js +7 -9
  132. umap/static/umap/js/modules/tableeditor.js +20 -19
  133. umap/static/umap/js/modules/ui/bar.js +196 -0
  134. umap/static/umap/js/modules/ui/dialog.js +5 -0
  135. umap/static/umap/js/modules/ui/panel.js +10 -9
  136. umap/static/umap/js/modules/umap.js +1691 -0
  137. umap/static/umap/js/modules/urls.js +2 -2
  138. umap/static/umap/js/modules/utils.js +22 -6
  139. umap/static/umap/js/umap.controls.js +81 -305
  140. umap/static/umap/js/umap.core.js +29 -50
  141. umap/static/umap/js/umap.forms.js +78 -27
  142. umap/static/umap/keycloak.png +0 -0
  143. umap/static/umap/locale/am_ET.js +26 -10
  144. umap/static/umap/locale/am_ET.json +26 -10
  145. umap/static/umap/locale/ar.js +26 -10
  146. umap/static/umap/locale/ar.json +26 -10
  147. umap/static/umap/locale/ast.js +26 -10
  148. umap/static/umap/locale/ast.json +26 -10
  149. umap/static/umap/locale/bg.js +26 -10
  150. umap/static/umap/locale/bg.json +26 -10
  151. umap/static/umap/locale/br.js +27 -20
  152. umap/static/umap/locale/br.json +27 -20
  153. umap/static/umap/locale/ca.js +32 -29
  154. umap/static/umap/locale/ca.json +32 -29
  155. umap/static/umap/locale/cs_CZ.js +24 -17
  156. umap/static/umap/locale/cs_CZ.json +24 -17
  157. umap/static/umap/locale/da.js +26 -10
  158. umap/static/umap/locale/da.json +26 -10
  159. umap/static/umap/locale/de.js +21 -14
  160. umap/static/umap/locale/de.json +21 -14
  161. umap/static/umap/locale/el.js +28 -12
  162. umap/static/umap/locale/el.json +28 -12
  163. umap/static/umap/locale/en.js +14 -9
  164. umap/static/umap/locale/en.json +14 -9
  165. umap/static/umap/locale/en_US.json +26 -10
  166. umap/static/umap/locale/es.js +16 -13
  167. umap/static/umap/locale/es.json +16 -13
  168. umap/static/umap/locale/et.js +26 -10
  169. umap/static/umap/locale/et.json +26 -10
  170. umap/static/umap/locale/eu.js +16 -9
  171. umap/static/umap/locale/eu.json +16 -9
  172. umap/static/umap/locale/fa_IR.js +16 -9
  173. umap/static/umap/locale/fa_IR.json +16 -9
  174. umap/static/umap/locale/fi.js +26 -10
  175. umap/static/umap/locale/fi.json +26 -10
  176. umap/static/umap/locale/fr.js +14 -9
  177. umap/static/umap/locale/fr.json +14 -9
  178. umap/static/umap/locale/gl.js +26 -10
  179. umap/static/umap/locale/gl.json +26 -10
  180. umap/static/umap/locale/he.js +26 -10
  181. umap/static/umap/locale/he.json +26 -10
  182. umap/static/umap/locale/hr.js +26 -10
  183. umap/static/umap/locale/hr.json +26 -10
  184. umap/static/umap/locale/hu.js +16 -9
  185. umap/static/umap/locale/hu.json +16 -9
  186. umap/static/umap/locale/id.js +26 -10
  187. umap/static/umap/locale/id.json +26 -10
  188. umap/static/umap/locale/is.js +26 -10
  189. umap/static/umap/locale/is.json +26 -10
  190. umap/static/umap/locale/it.js +26 -10
  191. umap/static/umap/locale/it.json +26 -10
  192. umap/static/umap/locale/ja.js +26 -10
  193. umap/static/umap/locale/ja.json +26 -10
  194. umap/static/umap/locale/ko.js +26 -10
  195. umap/static/umap/locale/ko.json +26 -10
  196. umap/static/umap/locale/lt.js +26 -10
  197. umap/static/umap/locale/lt.json +26 -10
  198. umap/static/umap/locale/ms.js +28 -12
  199. umap/static/umap/locale/ms.json +28 -12
  200. umap/static/umap/locale/nl.js +28 -12
  201. umap/static/umap/locale/nl.json +28 -12
  202. umap/static/umap/locale/no.js +26 -10
  203. umap/static/umap/locale/no.json +26 -10
  204. umap/static/umap/locale/pl.js +28 -12
  205. umap/static/umap/locale/pl.json +28 -12
  206. umap/static/umap/locale/pl_PL.json +26 -10
  207. umap/static/umap/locale/pt.js +16 -9
  208. umap/static/umap/locale/pt.json +16 -9
  209. umap/static/umap/locale/pt_BR.js +26 -10
  210. umap/static/umap/locale/pt_BR.json +26 -10
  211. umap/static/umap/locale/pt_PT.js +16 -9
  212. umap/static/umap/locale/pt_PT.json +16 -9
  213. umap/static/umap/locale/ro.js +26 -10
  214. umap/static/umap/locale/ro.json +26 -10
  215. umap/static/umap/locale/ru.js +26 -10
  216. umap/static/umap/locale/ru.json +26 -10
  217. umap/static/umap/locale/si.js +7 -7
  218. umap/static/umap/locale/si.json +7 -7
  219. umap/static/umap/locale/sk_SK.js +26 -10
  220. umap/static/umap/locale/sk_SK.json +26 -10
  221. umap/static/umap/locale/sl.js +26 -10
  222. umap/static/umap/locale/sl.json +26 -10
  223. umap/static/umap/locale/sr.js +26 -10
  224. umap/static/umap/locale/sr.json +26 -10
  225. umap/static/umap/locale/sv.js +27 -11
  226. umap/static/umap/locale/sv.json +27 -11
  227. umap/static/umap/locale/th_TH.js +28 -12
  228. umap/static/umap/locale/th_TH.json +28 -12
  229. umap/static/umap/locale/tr.js +26 -10
  230. umap/static/umap/locale/tr.json +26 -10
  231. umap/static/umap/locale/uk_UA.js +26 -10
  232. umap/static/umap/locale/uk_UA.json +26 -10
  233. umap/static/umap/locale/vi.js +26 -10
  234. umap/static/umap/locale/vi.json +26 -10
  235. umap/static/umap/locale/vi_VN.json +26 -10
  236. umap/static/umap/locale/zh.js +26 -10
  237. umap/static/umap/locale/zh.json +26 -10
  238. umap/static/umap/locale/zh_CN.json +26 -10
  239. umap/static/umap/locale/zh_TW.Big5.json +26 -10
  240. umap/static/umap/locale/zh_TW.js +34 -27
  241. umap/static/umap/locale/zh_TW.json +34 -27
  242. umap/static/umap/map.css +39 -530
  243. umap/static/umap/unittests/URLs.js +15 -15
  244. umap/static/umap/unittests/utils.js +23 -1
  245. umap/static/umap/vars.css +2 -1
  246. umap/static/umap/vendors/formbuilder/Leaflet.FormBuilder.js +5 -1
  247. umap/storage/__init__.py +3 -0
  248. umap/storage/fs.py +101 -0
  249. umap/storage/s3.py +61 -0
  250. umap/templates/base.html +2 -0
  251. umap/templates/registration/login.html +7 -6
  252. umap/templates/umap/components/alerts/alert.html +4 -0
  253. umap/templates/umap/css.html +6 -0
  254. umap/templates/umap/js.html +3 -2
  255. umap/templates/umap/map_init.html +6 -5
  256. umap/templates/umap/user_dashboard.html +20 -19
  257. umap/tests/base.py +5 -1
  258. umap/tests/fixtures/test_upload_simple_marker.json +19 -0
  259. umap/tests/integration/conftest.py +2 -1
  260. umap/tests/integration/test_anonymous_owned_map.py +18 -10
  261. umap/tests/integration/test_browser.py +16 -1
  262. umap/tests/integration/test_dashboard.py +1 -1
  263. umap/tests/integration/test_edit_datalayer.py +29 -7
  264. umap/tests/integration/test_import.py +28 -6
  265. umap/tests/integration/test_optimistic_merge.py +31 -8
  266. umap/tests/integration/test_owned_map.py +22 -16
  267. umap/tests/integration/test_popup.py +44 -0
  268. umap/tests/integration/test_save.py +35 -0
  269. umap/tests/integration/test_view_marker.py +12 -0
  270. umap/tests/integration/test_view_polyline.py +257 -0
  271. umap/tests/integration/test_websocket_sync.py +81 -9
  272. umap/tests/test_dashboard.py +82 -0
  273. umap/tests/test_datalayer.py +6 -7
  274. umap/tests/test_datalayer_s3.py +135 -0
  275. umap/tests/test_datalayer_views.py +28 -10
  276. umap/tests/test_empty_trash.py +34 -0
  277. umap/tests/test_map.py +12 -3
  278. umap/tests/test_map_views.py +69 -37
  279. umap/tests/test_statics.py +1 -1
  280. umap/tests/test_team_views.py +35 -1
  281. umap/tests/test_views.py +31 -52
  282. umap/urls.py +3 -3
  283. umap/views.py +126 -90
  284. {umap_project-2.7.3.dist-info → umap_project-2.8.0.dist-info}/METADATA +16 -13
  285. {umap_project-2.7.3.dist-info → umap_project-2.8.0.dist-info}/RECORD +289 -269
  286. umap/management/commands/purge_purgatory.py +0 -28
  287. umap/static/umap/js/umap.js +0 -1903
  288. umap/tests/test_purge_purgatory.py +0 -25
  289. /umap/{storage.py → storage/staticfiles.py} +0 -0
  290. {umap_project-2.7.3.dist-info → umap_project-2.8.0.dist-info}/WHEEL +0 -0
  291. {umap_project-2.7.3.dist-info → umap_project-2.8.0.dist-info}/entry_points.txt +0 -0
  292. {umap_project-2.7.3.dist-info → umap_project-2.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1691 @@
1
+ import {
2
+ DomUtil,
3
+ Util as LeafletUtil,
4
+ stamp,
5
+ latLngBounds,
6
+ } from '../../vendors/leaflet/leaflet-src.esm.js'
7
+ import { translate, setLocale, getLocale } from './i18n.js'
8
+ import * as Utils from './utils.js'
9
+ import { ServerStored } from './saving.js'
10
+ import * as SAVEMANAGER from './saving.js'
11
+ import { SyncEngine } from './sync/engine.js'
12
+ import { LeafletMap } from './rendering/map.js'
13
+ import URLs from './urls.js'
14
+ import { Panel, EditPanel, FullPanel } from './ui/panel.js'
15
+ import Dialog from './ui/dialog.js'
16
+ import { BottomBar, TopBar } from './ui/bar.js'
17
+ import Tooltip from './ui/tooltip.js'
18
+ import ContextMenu from './ui/contextmenu.js'
19
+ import { Request, ServerRequest } from './request.js'
20
+ import Help from './help.js'
21
+ import { Formatter } from './formatter.js'
22
+ import Slideshow from './slideshow.js'
23
+ import { MapPermissions } from './permissions.js'
24
+ import { SCHEMA } from './schema.js'
25
+ import { DataLayer } from './data/layer.js'
26
+ import Facets from './facets.js'
27
+ import Browser from './browser.js'
28
+ import Caption from './caption.js'
29
+ import Importer from './importer.js'
30
+ import Rules from './rules.js'
31
+ import Share from './share.js'
32
+ import {
33
+ uMapAlertCreation as AlertCreation,
34
+ uMapAlert as Alert,
35
+ } from '../components/alerts/alert.js'
36
+ import Orderable from './orderable.js'
37
+
38
+ export default class Umap extends ServerStored {
39
+ constructor(element, geojson) {
40
+ super()
41
+ // We need to call async function in the init process,
42
+ // the init itself does not need to be awaited, but some calls
43
+ // in the process must be blocker
44
+ this.init(element, geojson)
45
+ }
46
+
47
+ get id() {
48
+ return this.properties.id
49
+ }
50
+
51
+ async init(element, geojson) {
52
+ this.properties = Object.assign(
53
+ {
54
+ enableMarkerDraw: true,
55
+ enablePolygonDraw: true,
56
+ enablePolylineDraw: true,
57
+ hash: true,
58
+ limitBounds: {},
59
+ },
60
+ geojson.properties
61
+ )
62
+ this.searchParams = new URLSearchParams(window.location.search)
63
+
64
+ this.sync_engine = new SyncEngine(this)
65
+ this.sync = this.sync_engine.proxy(this)
66
+ // Locale name (pt_PT, en_US…)
67
+ // To be used for Django localization
68
+ if (geojson.properties.locale) setLocale(geojson.properties.locale)
69
+
70
+ // Language code (pt-pt, en-us…)
71
+ // To be used in javascript APIs
72
+ if (geojson.properties.lang) U.lang = geojson.properties.lang
73
+
74
+ // Make it available to utils, without needing a reference to `Umap`.
75
+ U.LABEL_KEYS = geojson.properties.defaultLabelKeys || []
76
+ U.DEFAULT_LABEL_KEY = U.LABEL_KEYS[0] || 'name'
77
+
78
+ this.setPropertiesFromQueryString()
79
+
80
+ // Needed for actions labels
81
+ this.help = new Help(this)
82
+ // Prevent default creation of controls
83
+ const zoomControl = this.properties.zoomControl
84
+ const fullscreenControl = this.properties.fullscreenControl
85
+ const center = geojson.geometry
86
+ this.properties.zoomControl = false
87
+ this.properties.fullscreenControl = false
88
+
89
+ this._leafletMap = new LeafletMap(this, element)
90
+
91
+ this.properties.zoomControl = zoomControl !== undefined ? zoomControl : true
92
+ this.properties.fullscreenControl =
93
+ fullscreenControl !== undefined ? fullscreenControl : true
94
+
95
+ if (center) {
96
+ this._leafletMap.options.center = this._leafletMap.latLng(center)
97
+ }
98
+
99
+ // Needed to render controls
100
+ this.permissions = new MapPermissions(this)
101
+ this.urls = new URLs(this.properties.urls)
102
+ this.slideshow = new Slideshow(this, this._leafletMap)
103
+
104
+ this._leafletMap.setup()
105
+
106
+ if (geojson.properties.schema) this.overrideSchema(geojson.properties.schema)
107
+
108
+ this.panel = new Panel(this, this._leafletMap)
109
+ this.dialog = new Dialog({ className: 'dark' })
110
+ this.topBar = new TopBar(this, this._leafletMap._controlContainer)
111
+ this.bottomBar = new BottomBar(
112
+ this,
113
+ this.slideshow,
114
+ this._leafletMap._controlContainer
115
+ )
116
+ this.tooltip = new Tooltip(this._leafletMap._controlContainer)
117
+ this.contextmenu = new ContextMenu()
118
+ this.server = new ServerRequest()
119
+ this.request = new Request()
120
+ this.facets = new Facets(this)
121
+ this.browser = new Browser(this, this._leafletMap)
122
+ this.caption = new Caption(this, this._leafletMap)
123
+ this.importer = new Importer(this)
124
+ this.share = new Share(this)
125
+ this.rules = new Rules(this)
126
+
127
+ if (this.hasEditMode()) {
128
+ this.editPanel = new EditPanel(this, this._leafletMap)
129
+ this.fullPanel = new FullPanel(this, this._leafletMap)
130
+ this._leafletMap.initEditTools()
131
+ this.topBar.setup()
132
+ }
133
+
134
+ this.datalayersFromQueryString = this.searchParams.get('datalayers')
135
+ if (this.datalayersFromQueryString) {
136
+ this.datalayersFromQueryString = this.datalayersFromQueryString
137
+ .toString()
138
+ .split(',')
139
+ }
140
+
141
+ // Retrocompat
142
+ if (
143
+ this.properties.slideshow?.delay &&
144
+ this.properties.slideshow.active === undefined
145
+ ) {
146
+ this.properties.slideshow.active = true
147
+ }
148
+ if (this.properties.advancedFilterKey) {
149
+ this.properties.facetKey = this.properties.advancedFilterKey
150
+ delete this.properties.advancedFilterKey
151
+ }
152
+
153
+ // Global storage for retrieving datalayers and features.
154
+ this.datalayers = {} // All datalayers, including deleted.
155
+ this.datalayersIndex = [] // Datalayers actually on the map and ordered.
156
+ this.featuresIndex = {}
157
+
158
+ this.formatter = new Formatter(this)
159
+
160
+ this.initDataLayers()
161
+
162
+ if (this.properties.displayCaptionOnLoad) {
163
+ // Retrocompat
164
+ if (!this.properties.onLoadPanel) {
165
+ this.properties.onLoadPanel = 'caption'
166
+ }
167
+ delete this.properties.displayCaptionOnLoad
168
+ }
169
+ if (this.properties.displayDataBrowserOnLoad) {
170
+ // Retrocompat
171
+ if (!this.properties.onLoadPanel) {
172
+ this.properties.onLoadPanel = 'databrowser'
173
+ }
174
+ delete this.properties.displayDataBrowserOnLoad
175
+ }
176
+ if (this.properties.datalayersControl === 'expanded') {
177
+ if (!this.properties.onLoadPanel) {
178
+ this.properties.onLoadPanel = 'datalayers'
179
+ }
180
+ delete this.properties.datalayersControl
181
+ }
182
+ if (this.properties.onLoadPanel === 'facet') {
183
+ this.properties.onLoadPanel = 'datafilters'
184
+ }
185
+
186
+ // Creation mode
187
+ if (!this.id) {
188
+ if (!this.properties.preview) {
189
+ this.isDirty = true
190
+ this.enableEdit()
191
+ }
192
+ this._defaultExtent = true
193
+ this.properties.name = translate('Untitled map')
194
+ await this.loadDataFromQueryString()
195
+ }
196
+
197
+ if (!this.properties.noControl) {
198
+ this.initShortcuts()
199
+ this._leafletMap.on('contextmenu', (e) => this.onContextMenu(e))
200
+ this.onceDataLoaded(this.setViewFromQueryString)
201
+ this.bottomBar.setup()
202
+ this.propagate()
203
+ }
204
+
205
+ window.onbeforeunload = () => (this.editEnabled && SAVEMANAGER.isDirty) || null
206
+ this.backup()
207
+ }
208
+
209
+ get editedFeature() {
210
+ return this._editedFeature
211
+ }
212
+
213
+ set editedFeature(feature) {
214
+ if (this._editedFeature && this._editedFeature !== feature) {
215
+ this._editedFeature.endEdit()
216
+ }
217
+ this._editedFeature = feature
218
+ this.fire('seteditedfeature')
219
+ }
220
+
221
+ get activeFeature() {
222
+ return this._activeFeature
223
+ }
224
+
225
+ set activeFeature(feature) {
226
+ this._activeFeature = feature
227
+ }
228
+
229
+ setPropertiesFromQueryString() {
230
+ const asBoolean = (key) => {
231
+ const value = this.searchParams.get(key)
232
+ if (value !== undefined && value !== null) {
233
+ this.properties[key] = value === '1' || value === 'true'
234
+ }
235
+ }
236
+ const asNullableBoolean = (key) => {
237
+ if (this.searchParams.has(key)) {
238
+ let value = this.searchParams.get(key)
239
+ if (value === 'null') value = null
240
+ else if (value === '0' || value === 'false') value = false
241
+ else value = true
242
+ this.properties[key] = value
243
+ }
244
+ }
245
+ const asNumber = (key) => {
246
+ const value = +this.searchParams.get(key)
247
+ if (!Number.isNaN(value)) this.properties[name] = value
248
+ }
249
+ // FIXME retrocompat
250
+ asBoolean('displayDataBrowserOnLoad')
251
+ asBoolean('displayCaptionOnLoad')
252
+ for (const [key, schema] of Object.entries(SCHEMA)) {
253
+ switch (schema.type) {
254
+ case Boolean:
255
+ if (schema.nullable) asNullableBoolean(key)
256
+ else asBoolean(key)
257
+ break
258
+ case Number:
259
+ asNumber(key)
260
+ break
261
+ case String: {
262
+ if (this.searchParams.has(key)) {
263
+ const value = this.searchParams.get(key)
264
+ if (value !== undefined) this.properties[key] = value
265
+ break
266
+ }
267
+ }
268
+ }
269
+ }
270
+ // Specific case for datalayersControl
271
+ // which accepts "expanded" value, on top of true/false/null
272
+ if (this.searchParams.get('datalayersControl') === 'expanded') {
273
+ if (!this.properties.onLoadPanel) {
274
+ this.properties.onLoadPanel = 'datalayers'
275
+ }
276
+ }
277
+ }
278
+
279
+ async setViewFromQueryString() {
280
+ if (this.properties.noControl) return
281
+ // TODO: move to a "initPanel" function
282
+ if (this.searchParams.has('share')) {
283
+ this.share.open()
284
+ } else if (this.properties.onLoadPanel === 'databrowser') {
285
+ this.panel.setDefaultMode('expanded')
286
+ this.openBrowser('data')
287
+ } else if (this.properties.onLoadPanel === 'datalayers') {
288
+ this.panel.setDefaultMode('condensed')
289
+ this.openBrowser('layers')
290
+ } else if (this.properties.onLoadPanel === 'datafilters') {
291
+ this.panel.setDefaultMode('expanded')
292
+ this.openBrowser('filters')
293
+ } else if (this.properties.onLoadPanel === 'caption') {
294
+ this.panel.setDefaultMode('condensed')
295
+ this.openCaption()
296
+ }
297
+ // Comes after default panels, so if it opens in a panel it will
298
+ // take precedence.
299
+ const slug = this.searchParams.get('feature')
300
+ if (slug && this.featuresIndex[slug]) this.featuresIndex[slug].view()
301
+ if (this.searchParams.has('edit')) {
302
+ if (this.hasEditMode()) this.enableEdit()
303
+ // Sometimes users share the ?edit link by mistake, let's remove
304
+ // this search parameter from URL to prevent this
305
+ const url = new URL(window.location)
306
+ url.searchParams.delete('edit')
307
+ history.pushState({}, '', url)
308
+ }
309
+ if (this.searchParams.has('download')) {
310
+ const download_url = this.urls.get('map_download', {
311
+ map_id: this.id,
312
+ })
313
+ window.location = download_url
314
+ }
315
+ }
316
+
317
+ async loadDataFromQueryString() {
318
+ let data = this.searchParams.get('data')
319
+ const dataUrls = this.searchParams.getAll('dataUrl')
320
+ const dataFormat = this.searchParams.get('dataFormat') || 'geojson'
321
+ if (dataUrls.length) {
322
+ for (let dataUrl of dataUrls) {
323
+ dataUrl = decodeURIComponent(dataUrl)
324
+ dataUrl = this.renderUrl(dataUrl)
325
+ dataUrl = this.proxyUrl(dataUrl)
326
+ const datalayer = this.createDataLayer()
327
+ await datalayer
328
+ .importFromUrl(dataUrl, dataFormat)
329
+ .then(() => datalayer.zoomTo())
330
+ }
331
+ } else if (data) {
332
+ data = decodeURIComponent(data)
333
+ const datalayer = this.createDataLayer()
334
+ await datalayer.importRaw(data, dataFormat).then(() => datalayer.zoomTo())
335
+ }
336
+ }
337
+
338
+ getOwnContextMenuItems(event) {
339
+ const items = []
340
+ if (this.hasEditMode()) {
341
+ if (this.editEnabled) {
342
+ if (!SAVEMANAGER.isDirty) {
343
+ items.push({
344
+ label: this.help.displayLabel('STOP_EDIT'),
345
+ action: () => this.disableEdit(),
346
+ })
347
+ }
348
+ if (this.properties.enableMarkerDraw) {
349
+ items.push({
350
+ label: this.help.displayLabel('DRAW_MARKER'),
351
+ action: () => this._leafletMap.startMarker(event),
352
+ })
353
+ }
354
+ if (this.properties.enablePolylineDraw) {
355
+ items.push({
356
+ label: this.help.displayLabel('DRAW_POLYGON'),
357
+ action: () => this._leafletMap.startPolygon(event),
358
+ })
359
+ }
360
+ if (this.properties.enablePolygonDraw) {
361
+ items.push({
362
+ label: this.help.displayLabel('DRAW_LINE'),
363
+ action: () => this._leafletMap.startPolyline(event),
364
+ })
365
+ }
366
+ items.push('-')
367
+ items.push({
368
+ label: translate('Help'),
369
+ action: () => this.help.show('edit'),
370
+ })
371
+ } else {
372
+ items.push({
373
+ label: this.help.displayLabel('TOGGLE_EDIT'),
374
+ action: () => this.enableEdit(),
375
+ })
376
+ }
377
+ }
378
+ if (items.length) {
379
+ items.push('-')
380
+ }
381
+ items.push(
382
+ {
383
+ label: translate('Open browser'),
384
+ action: () => this.openBrowser('layers'),
385
+ },
386
+ {
387
+ label: translate('Browse data'),
388
+ action: () => this.openBrowser('data'),
389
+ }
390
+ )
391
+ if (this.properties.facetKey) {
392
+ items.push({
393
+ label: translate('Filter data'),
394
+ action: () => this.openBrowser('filters'),
395
+ })
396
+ }
397
+ items.push(
398
+ {
399
+ label: translate('Open caption'),
400
+ action: () => this.openCaption(),
401
+ },
402
+ {
403
+ label: this.help.displayLabel('SEARCH'),
404
+ action: () => this.search(),
405
+ }
406
+ )
407
+ return items
408
+ }
409
+
410
+ getSharedContextMenuItems(event) {
411
+ const items = []
412
+ if (this.properties.urls.routing) {
413
+ items.push('-', {
414
+ label: translate('Directions from here'),
415
+ action: () => this.openExternalRouting(event),
416
+ })
417
+ }
418
+ if (this.properties.urls.edit_in_osm) {
419
+ items.push('-', {
420
+ label: translate('Edit in OpenStreetMap'),
421
+ action: () => this.editInOSM(event),
422
+ })
423
+ }
424
+ return items
425
+ }
426
+
427
+ onContextMenu(event) {
428
+ const items = this.getOwnContextMenuItems(event).concat(
429
+ this.getSharedContextMenuItems(event)
430
+ )
431
+ this.contextmenu.open(event.originalEvent, items)
432
+ }
433
+
434
+ // Merge the given schema with the default one
435
+ // Missing keys inside the schema are merged with the default ones.
436
+ overrideSchema(schema) {
437
+ for (const [key, extra] of Object.entries(schema)) {
438
+ SCHEMA[key] = Object.assign({}, SCHEMA[key], extra)
439
+ }
440
+ }
441
+
442
+ search() {
443
+ if (this._leafletMap._controls.search) this._leafletMap._controls.search.open()
444
+ }
445
+
446
+ hasEditMode() {
447
+ const editMode = this.properties.editMode
448
+ return editMode === 'simple' || editMode === 'advanced'
449
+ }
450
+
451
+ getProperty(key, feature) {
452
+ if (feature) {
453
+ const value = this.rules.getOption(key, feature)
454
+ if (value !== undefined) return value
455
+ }
456
+ if (Utils.usableOption(this.properties, key)) return this.properties[key]
457
+ return SCHEMA[key]?.default
458
+ }
459
+
460
+ getOption(key, feature) {
461
+ // TODO: remove when umap.forms.js is refactored and does not call blindly
462
+ // obj.getOption anymore
463
+ return this.getProperty(key, feature)
464
+ }
465
+
466
+ getGeoContext() {
467
+ const bounds = this._leafletMap.getBounds()
468
+ const center = this._leafletMap.getCenter()
469
+ const context = {
470
+ bbox: bounds.toBBoxString(),
471
+ north: bounds.getNorthEast().lat,
472
+ east: bounds.getNorthEast().lng,
473
+ south: bounds.getSouthWest().lat,
474
+ west: bounds.getSouthWest().lng,
475
+ lat: center.lat,
476
+ lng: center.lng,
477
+ zoom: this._leafletMap.getZoom(),
478
+ }
479
+ context.left = context.west
480
+ context.bottom = context.south
481
+ context.right = context.east
482
+ context.top = context.north
483
+ return context
484
+ }
485
+
486
+ renderUrl(url) {
487
+ return Utils.greedyTemplate(url, this.getGeoContext(), true)
488
+ }
489
+
490
+ initShortcuts() {
491
+ const globalShortcuts = (event) => {
492
+ if (event.key === 'Escape') {
493
+ if (this.importer.dialog.visible) {
494
+ this.importer.dialog.close()
495
+ } else if (this.editEnabled && this._leafletMap.editTools.drawing()) {
496
+ this._leafletMap.editTools.onEscape()
497
+ } else if (this._leafletMap.measureTools.enabled()) {
498
+ this._leafletMap.measureTools.stopDrawing()
499
+ } else if (this.fullPanel?.isOpen()) {
500
+ this.fullPanel?.close()
501
+ } else if (this.editPanel?.isOpen()) {
502
+ this.editPanel?.close()
503
+ } else if (this.panel.isOpen()) {
504
+ this.panel.close()
505
+ }
506
+ }
507
+
508
+ // From now on, only ctrl/meta shortcut
509
+ if (!(event.ctrlKey || event.metaKey) || event.shiftKey) return
510
+
511
+ if (event.key === 'f') {
512
+ event.stopPropagation()
513
+ event.preventDefault()
514
+ this.search()
515
+ }
516
+
517
+ /* Edit mode only shortcuts */
518
+ if (!this.hasEditMode()) return
519
+
520
+ // Edit mode Off
521
+ if (!this.editEnabled) {
522
+ switch (event.key) {
523
+ case 'e':
524
+ event.stopPropagation()
525
+ event.preventDefault()
526
+ this.enableEdit()
527
+ break
528
+ }
529
+ return
530
+ }
531
+
532
+ // Edit mode on
533
+ let used = true
534
+ switch (event.key) {
535
+ case 'e':
536
+ if (!SAVEMANAGER.isDirty) this.disableEdit()
537
+ break
538
+ case 's':
539
+ if (SAVEMANAGER.isDirty) this.saveAll()
540
+ break
541
+ case 'z':
542
+ if (SAVEMANAGER.isDirty) this.askForReset()
543
+ break
544
+ case 'm':
545
+ this._leafletMap.editTools.startMarker()
546
+ break
547
+ case 'p':
548
+ this._leafletMap.editTools.startPolygon()
549
+ break
550
+ case 'l':
551
+ this._leafletMap.editTools.startPolyline()
552
+ break
553
+ case 'i':
554
+ this.importer.open()
555
+ break
556
+ case 'o':
557
+ this.importer.openFiles()
558
+ break
559
+ case 'h':
560
+ this.help.showGetStarted()
561
+ break
562
+ default:
563
+ used = false
564
+ }
565
+ if (used) {
566
+ event.stopPropagation()
567
+ event.preventDefault()
568
+ }
569
+ }
570
+ document.addEventListener('keydown', globalShortcuts)
571
+ }
572
+
573
+ async initDataLayers(datalayers) {
574
+ datalayers = datalayers || this.properties.datalayers
575
+ for (const options of datalayers) {
576
+ // `false` to not propagate syncing elements served from uMap
577
+ this.createDataLayer(options, false)
578
+ }
579
+ this.datalayersLoaded = true
580
+ this.fire('datalayersloaded')
581
+ const toLoad = []
582
+ for (const datalayer of this.datalayersIndex) {
583
+ if (datalayer.showAtLoad()) toLoad.push(datalayer.show())
584
+ }
585
+ await Promise.all(toLoad)
586
+ this.dataloaded = true
587
+ this.fire('dataloaded')
588
+ }
589
+
590
+ createDataLayer(options = {}, sync = true) {
591
+ options.name =
592
+ options.name || `${translate('Layer')} ${this.datalayersIndex.length + 1}`
593
+ const datalayer = new DataLayer(this, this._leafletMap, options)
594
+
595
+ if (sync !== false) {
596
+ datalayer.sync.upsert(datalayer.options)
597
+ }
598
+ return datalayer
599
+ }
600
+
601
+ newDataLayer() {
602
+ const datalayer = this.createDataLayer({})
603
+ datalayer.edit()
604
+ }
605
+
606
+ reindexDataLayers() {
607
+ this.eachDataLayer((datalayer) => datalayer.reindex())
608
+ this.onDataLayersChanged()
609
+ }
610
+
611
+ indexDatalayers() {
612
+ const panes = this._leafletMap.getPane('overlayPane')
613
+
614
+ this.datalayersIndex = []
615
+ for (const pane of panes.children) {
616
+ if (!pane.dataset || !pane.dataset.id) continue
617
+ this.datalayersIndex.push(this.datalayers[pane.dataset.id])
618
+ }
619
+ this.onDataLayersChanged()
620
+ }
621
+
622
+ onceDatalayersLoaded(callback, context) {
623
+ // Once datalayers **metadata** have been loaded
624
+ if (this.datalayersLoaded) {
625
+ callback.call(context || this, this)
626
+ } else {
627
+ this._leafletMap.once('datalayersloaded', callback, context)
628
+ }
629
+ return this
630
+ }
631
+
632
+ onceDataLoaded(callback, context) {
633
+ // Once datalayers **data** have been loaded
634
+ if (this.dataloaded) {
635
+ callback.call(context || this, this)
636
+ } else {
637
+ this._leafletMap.once('dataloaded', callback, context || this)
638
+ }
639
+ return this
640
+ }
641
+
642
+ onDataLayersChanged() {
643
+ if (this.browser) this.browser.update()
644
+ this.caption.refresh()
645
+ }
646
+
647
+ async saveAll() {
648
+ if (!SAVEMANAGER.isDirty) return
649
+ if (this._defaultExtent) this._setCenterAndZoom()
650
+ this.backup()
651
+ await SAVEMANAGER.save()
652
+ // Do a blind render for now, as we are not sure what could
653
+ // have changed, we'll be more subtil when we'll remove the
654
+ // save action
655
+ this.render(['name', 'user', 'permissions'])
656
+ if (!this._leafletMap.listens('saved')) {
657
+ // When we save only layers, we don't have the map feedback message
658
+ this._leafletMap.on('saved', () => {
659
+ Alert.success(translate('Map has been saved!'))
660
+ })
661
+ }
662
+ this.fire('saved')
663
+ }
664
+
665
+ getDisplayName() {
666
+ return this.properties.name || translate('Untitled map')
667
+ }
668
+
669
+ backup() {
670
+ this.backupProperties()
671
+ this._datalayersIndex_bk = [].concat(this.datalayersIndex)
672
+ }
673
+
674
+ backupProperties() {
675
+ this._backupProperties = Object.assign({}, this.properties)
676
+ this._backupProperties.tilelayer = Object.assign({}, this.properties.tilelayer)
677
+ this._backupProperties.limitBounds = Object.assign({}, this.properties.limitBounds)
678
+ this._backupProperties.permissions = Object.assign({}, this.permissions.properties)
679
+ }
680
+
681
+ resetProperties() {
682
+ this.properties = Object.assign({}, this._backupProperties)
683
+ this.properties.tilelayer = Object.assign({}, this._backupProperties.tilelayer)
684
+ this.permissions.properties = Object.assign({}, this._backupProperties.permissions)
685
+ }
686
+
687
+ setProperties(newProperties) {
688
+ for (const key of Object.keys(SCHEMA)) {
689
+ if (newProperties[key] !== undefined) {
690
+ this.properties[key] = newProperties[key]
691
+ if (key === 'rules') this.rules.load()
692
+ if (key === 'slideshow') this.slideshow.load()
693
+ // TODO: sync ?
694
+ }
695
+ }
696
+ }
697
+
698
+ hasData() {
699
+ for (const datalayer of this.datalayersIndex) {
700
+ if (datalayer.hasData()) return true
701
+ }
702
+ }
703
+
704
+ hasLayers() {
705
+ return Boolean(this.datalayersIndex.length)
706
+ }
707
+
708
+ allProperties() {
709
+ return [].concat(...this.datalayersIndex.map((dl) => dl.allProperties()))
710
+ }
711
+
712
+ sortedValues(property) {
713
+ return []
714
+ .concat(...this.datalayersIndex.map((dl) => dl.sortedValues(property)))
715
+ .filter((val, idx, arr) => arr.indexOf(val) === idx)
716
+ .sort(U.Utils.naturalSort)
717
+ }
718
+
719
+ editCaption() {
720
+ if (!this.editEnabled) return
721
+ if (this.properties.editMode !== 'advanced') return
722
+ const container = DomUtil.create('div', 'umap-edit-container')
723
+ const metadataFields = ['properties.name', 'properties.description']
724
+
725
+ DomUtil.createTitle(container, translate('Edit map details'), 'icon-caption')
726
+ const builder = new U.FormBuilder(this, metadataFields, {
727
+ className: 'map-metadata',
728
+ umap: this,
729
+ })
730
+ const form = builder.build()
731
+ container.appendChild(form)
732
+
733
+ const credits = DomUtil.createFieldset(container, translate('Credits'))
734
+ const creditsFields = [
735
+ 'properties.licence',
736
+ 'properties.shortCredit',
737
+ 'properties.longCredit',
738
+ 'properties.permanentCredit',
739
+ 'properties.permanentCreditBackground',
740
+ ]
741
+ const creditsBuilder = new U.FormBuilder(this, creditsFields, { umap: this })
742
+ credits.appendChild(creditsBuilder.build())
743
+ this.editPanel.open({ content: container })
744
+ }
745
+
746
+ _editControls(container) {
747
+ let UIFields = []
748
+ for (const name of this._leafletMap.HIDDABLE_CONTROLS) {
749
+ UIFields.push(`properties.${name}Control`)
750
+ }
751
+ UIFields = UIFields.concat([
752
+ 'properties.moreControl',
753
+ 'properties.scrollWheelZoom',
754
+ 'properties.miniMap',
755
+ 'properties.scaleControl',
756
+ 'properties.onLoadPanel',
757
+ 'properties.defaultView',
758
+ 'properties.displayPopupFooter',
759
+ 'properties.captionBar',
760
+ 'properties.captionMenus',
761
+ ])
762
+ const builder = new U.FormBuilder(this, UIFields, { umap: this })
763
+ const controlsOptions = DomUtil.createFieldset(
764
+ container,
765
+ translate('User interface options')
766
+ )
767
+ controlsOptions.appendChild(builder.build())
768
+ }
769
+
770
+ _editShapeProperties(container) {
771
+ const shapeOptions = [
772
+ 'properties.color',
773
+ 'properties.iconClass',
774
+ 'properties.iconUrl',
775
+ 'properties.iconOpacity',
776
+ 'properties.opacity',
777
+ 'properties.weight',
778
+ 'properties.fill',
779
+ 'properties.fillColor',
780
+ 'properties.fillOpacity',
781
+ 'properties.smoothFactor',
782
+ 'properties.dashArray',
783
+ ]
784
+
785
+ const builder = new U.FormBuilder(this, shapeOptions, { umap: this })
786
+ const defaultShapeProperties = DomUtil.createFieldset(
787
+ container,
788
+ translate('Default shape properties')
789
+ )
790
+ defaultShapeProperties.appendChild(builder.build())
791
+ }
792
+
793
+ _editDefaultProperties(container) {
794
+ const optionsFields = [
795
+ 'properties.zoomTo',
796
+ 'properties.easing',
797
+ 'properties.labelKey',
798
+ 'properties.sortKey',
799
+ 'properties.filterKey',
800
+ 'properties.facetKey',
801
+ 'properties.slugKey',
802
+ ]
803
+
804
+ const builder = new U.FormBuilder(this, optionsFields, { umap: this })
805
+ const defaultProperties = DomUtil.createFieldset(
806
+ container,
807
+ translate('Default properties')
808
+ )
809
+ defaultProperties.appendChild(builder.build())
810
+ }
811
+
812
+ _editInteractionsProperties(container) {
813
+ const popupFields = [
814
+ 'properties.popupShape',
815
+ 'properties.popupTemplate',
816
+ 'properties.popupContentTemplate',
817
+ 'properties.showLabel',
818
+ 'properties.labelDirection',
819
+ 'properties.labelInteractive',
820
+ 'properties.outlinkTarget',
821
+ ]
822
+ const builder = new U.FormBuilder(this, popupFields, { umap: this })
823
+ const popupFieldset = DomUtil.createFieldset(
824
+ container,
825
+ translate('Default interaction options')
826
+ )
827
+ popupFieldset.appendChild(builder.build())
828
+ }
829
+
830
+ _editTilelayer(container) {
831
+ if (!Utils.isObject(this.properties.tilelayer)) {
832
+ this.properties.tilelayer = {}
833
+ }
834
+ const tilelayerFields = [
835
+ [
836
+ 'properties.tilelayer.name',
837
+ { handler: 'BlurInput', placeholder: translate('display name') },
838
+ ],
839
+ [
840
+ 'properties.tilelayer.url_template',
841
+ {
842
+ handler: 'BlurInput',
843
+ helpText: `${translate('Supported scheme')}: http://{s}.domain.com/{z}/{x}/{y}.png`,
844
+ placeholder: 'url',
845
+ type: 'url',
846
+ },
847
+ ],
848
+ [
849
+ 'properties.tilelayer.maxZoom',
850
+ {
851
+ handler: 'BlurIntInput',
852
+ placeholder: translate('max zoom'),
853
+ min: 0,
854
+ max: this.properties.maxZoomLimit,
855
+ },
856
+ ],
857
+ [
858
+ 'properties.tilelayer.minZoom',
859
+ {
860
+ handler: 'BlurIntInput',
861
+ placeholder: translate('min zoom'),
862
+ min: 0,
863
+ max: this.properties.maxZoomLimit,
864
+ },
865
+ ],
866
+ [
867
+ 'properties.tilelayer.attribution',
868
+ { handler: 'BlurInput', placeholder: translate('attribution') },
869
+ ],
870
+ [
871
+ 'properties.tilelayer.tms',
872
+ { handler: 'Switch', label: translate('TMS format') },
873
+ ],
874
+ ]
875
+ const customTilelayer = DomUtil.createFieldset(
876
+ container,
877
+ translate('Custom background')
878
+ )
879
+ const builder = new U.FormBuilder(this, tilelayerFields, { umap: this })
880
+ customTilelayer.appendChild(builder.build())
881
+ }
882
+
883
+ _editOverlay(container) {
884
+ if (!Utils.isObject(this.properties.overlay)) {
885
+ this.properties.overlay = {}
886
+ }
887
+ const overlayFields = [
888
+ [
889
+ 'properties.overlay.url_template',
890
+ {
891
+ handler: 'BlurInput',
892
+ helpText: `${translate('Supported scheme')}: http://{s}.domain.com/{z}/{x}/{y}.png`,
893
+ placeholder: 'url',
894
+ label: translate('Background overlay url'),
895
+ type: 'url',
896
+ },
897
+ ],
898
+ [
899
+ 'properties.overlay.maxZoom',
900
+ {
901
+ handler: 'BlurIntInput',
902
+ placeholder: translate('max zoom'),
903
+ min: 0,
904
+ max: this.properties.maxZoomLimit,
905
+ },
906
+ ],
907
+ [
908
+ 'properties.overlay.minZoom',
909
+ {
910
+ handler: 'BlurIntInput',
911
+ placeholder: translate('min zoom'),
912
+ min: 0,
913
+ max: this.properties.maxZoomLimit,
914
+ },
915
+ ],
916
+ [
917
+ 'properties.overlay.attribution',
918
+ { handler: 'BlurInput', placeholder: translate('attribution') },
919
+ ],
920
+ [
921
+ 'properties.overlay.opacity',
922
+ { handler: 'Range', min: 0, max: 1, step: 0.1, label: translate('Opacity') },
923
+ ],
924
+ ['properties.overlay.tms', { handler: 'Switch', label: translate('TMS format') }],
925
+ ]
926
+ const overlay = DomUtil.createFieldset(container, translate('Custom overlay'))
927
+ const builder = new U.FormBuilder(this, overlayFields, { umap: this })
928
+ overlay.appendChild(builder.build())
929
+ }
930
+
931
+ _editBounds(container) {
932
+ if (!Utils.isObject(this.properties.limitBounds)) {
933
+ this.properties.limitBounds = {}
934
+ }
935
+ const limitBounds = DomUtil.createFieldset(container, translate('Limit bounds'))
936
+ const boundsFields = [
937
+ [
938
+ 'properties.limitBounds.south',
939
+ { handler: 'BlurFloatInput', placeholder: translate('max South') },
940
+ ],
941
+ [
942
+ 'properties.limitBounds.west',
943
+ { handler: 'BlurFloatInput', placeholder: translate('max West') },
944
+ ],
945
+ [
946
+ 'properties.limitBounds.north',
947
+ { handler: 'BlurFloatInput', placeholder: translate('max North') },
948
+ ],
949
+ [
950
+ 'properties.limitBounds.east',
951
+ { handler: 'BlurFloatInput', placeholder: translate('max East') },
952
+ ],
953
+ ]
954
+ const boundsBuilder = new U.FormBuilder(this, boundsFields, { umap: this })
955
+ limitBounds.appendChild(boundsBuilder.build())
956
+ const boundsButtons = DomUtil.create('div', 'button-bar half', limitBounds)
957
+ DomUtil.createButton(
958
+ 'button',
959
+ boundsButtons,
960
+ translate('Use current bounds'),
961
+ function () {
962
+ const bounds = this._leafletMap.getBounds()
963
+ this.properties.limitBounds.south = LeafletUtil.formatNum(bounds.getSouth())
964
+ this.properties.limitBounds.west = LeafletUtil.formatNum(bounds.getWest())
965
+ this.properties.limitBounds.north = LeafletUtil.formatNum(bounds.getNorth())
966
+ this.properties.limitBounds.east = LeafletUtil.formatNum(bounds.getEast())
967
+ boundsBuilder.fetchAll()
968
+
969
+ this.sync.update(this, 'properties.limitBounds', this.properties.limitBounds)
970
+ this.isDirty = true
971
+ this._leafletMap.handleLimitBounds()
972
+ },
973
+ this
974
+ )
975
+ DomUtil.createButton(
976
+ 'button',
977
+ boundsButtons,
978
+ translate('Empty'),
979
+ function () {
980
+ this.properties.limitBounds.south = null
981
+ this.properties.limitBounds.west = null
982
+ this.properties.limitBounds.north = null
983
+ this.properties.limitBounds.east = null
984
+ boundsBuilder.fetchAll()
985
+ this.isDirty = true
986
+ this._leafletMap.handleLimitBounds()
987
+ },
988
+ this
989
+ )
990
+ }
991
+
992
+ _editSlideshow(container) {
993
+ const slideshow = DomUtil.createFieldset(container, translate('Slideshow'))
994
+ const slideshowFields = [
995
+ [
996
+ 'properties.slideshow.active',
997
+ { handler: 'Switch', label: translate('Activate slideshow mode') },
998
+ ],
999
+ [
1000
+ 'properties.slideshow.delay',
1001
+ {
1002
+ handler: 'SlideshowDelay',
1003
+ helpText: translate('Delay between two transitions when in play mode'),
1004
+ },
1005
+ ],
1006
+ [
1007
+ 'properties.slideshow.easing',
1008
+ {
1009
+ handler: 'Switch',
1010
+ label: translate('Animated transitions'),
1011
+ inheritable: true,
1012
+ },
1013
+ ],
1014
+ [
1015
+ 'properties.slideshow.autoplay',
1016
+ { handler: 'Switch', label: translate('Autostart when map is loaded') },
1017
+ ],
1018
+ ]
1019
+ const slideshowBuilder = new U.FormBuilder(this, slideshowFields, {
1020
+ callback: () => {
1021
+ this.slideshow.load()
1022
+ // FIXME when we refactor formbuilder: this callback is called in a 'postsync'
1023
+ // event, which comes after the call of `setter` method, which will call the
1024
+ // map.render method, which should do this redraw.
1025
+ this.bottomBar.redraw()
1026
+ },
1027
+ umap: this,
1028
+ })
1029
+ slideshow.appendChild(slideshowBuilder.build())
1030
+ }
1031
+
1032
+ _editSync(container) {
1033
+ const sync = DomUtil.createFieldset(container, translate('Real-time collaboration'))
1034
+ const builder = new U.FormBuilder(this, ['properties.syncEnabled'], { umap: this })
1035
+ sync.appendChild(builder.build())
1036
+ }
1037
+
1038
+ _advancedActions(container) {
1039
+ const advancedActions = DomUtil.createFieldset(
1040
+ container,
1041
+ translate('Advanced actions')
1042
+ )
1043
+ const advancedButtons = DomUtil.create('div', 'button-bar half', advancedActions)
1044
+ if (this.permissions.isOwner()) {
1045
+ const deleteButton = Utils.loadTemplate(`
1046
+ <button class="button" type="button">
1047
+ <i class="icon icon-24 icon-delete"></i>${translate('Delete')}
1048
+ </button>`)
1049
+ deleteButton.addEventListener('click', () => this.del())
1050
+ advancedButtons.appendChild(deleteButton)
1051
+
1052
+ DomUtil.createButton(
1053
+ 'button umap-empty',
1054
+ advancedButtons,
1055
+ translate('Clear data'),
1056
+ this.emptyDataLayers,
1057
+ this
1058
+ )
1059
+ DomUtil.createButton(
1060
+ 'button umap-empty',
1061
+ advancedButtons,
1062
+ translate('Remove layers'),
1063
+ this.removeDataLayers,
1064
+ this
1065
+ )
1066
+ }
1067
+ DomUtil.createButton(
1068
+ 'button umap-clone',
1069
+ advancedButtons,
1070
+ translate('Clone this map'),
1071
+ this.clone,
1072
+ this
1073
+ )
1074
+ DomUtil.createButton(
1075
+ 'button umap-download',
1076
+ advancedButtons,
1077
+ translate('Open share & download panel'),
1078
+ this.share.open,
1079
+ this.share
1080
+ )
1081
+ }
1082
+
1083
+ edit() {
1084
+ if (!this.editEnabled) return
1085
+ if (this.properties.editMode !== 'advanced') return
1086
+ const container = DomUtil.create('div')
1087
+ DomUtil.createTitle(
1088
+ container,
1089
+ translate('Map advanced properties'),
1090
+ 'icon-settings'
1091
+ )
1092
+ this._editControls(container)
1093
+ this._editShapeProperties(container)
1094
+ this._editDefaultProperties(container)
1095
+ this._editInteractionsProperties(container)
1096
+ this.rules.edit(container)
1097
+ this._editTilelayer(container)
1098
+ this._editOverlay(container)
1099
+ this._editBounds(container)
1100
+ this._editSlideshow(container)
1101
+ if (this.properties.websocketEnabled) {
1102
+ this._editSync(container)
1103
+ }
1104
+ this._advancedActions(container)
1105
+
1106
+ this.editPanel.open({ content: container, className: 'dark' })
1107
+ }
1108
+
1109
+ reset() {
1110
+ if (this._leafletMap.editTools) this._leafletMap.editTools.stopDrawing()
1111
+ this.resetProperties()
1112
+ this.datalayersIndex = [].concat(this._datalayersIndex_bk)
1113
+ // Iter over all datalayers, including deleted if any.
1114
+ for (const datalayer of Object.values(this.datalayers)) {
1115
+ if (datalayer.isDeleted) datalayer.connectToMap()
1116
+ if (datalayer.isDirty) datalayer.reset()
1117
+ }
1118
+ this.ensurePanesOrder()
1119
+ this._leafletMap.initTileLayers()
1120
+ this.onDataLayersChanged()
1121
+ this.isDirty = !this.id
1122
+ }
1123
+
1124
+ async save() {
1125
+ this.rules.commit()
1126
+ const geojson = {
1127
+ type: 'Feature',
1128
+ geometry: this.geometry(),
1129
+ properties: this.exportProperties(),
1130
+ }
1131
+ const formData = new FormData()
1132
+ formData.append('name', this.properties.name)
1133
+ formData.append('center', JSON.stringify(this.geometry()))
1134
+ formData.append('settings', JSON.stringify(geojson))
1135
+ const uri = this.urls.get('map_save', { map_id: this.id })
1136
+ const [data, _, error] = await this.server.post(uri, {}, formData)
1137
+ // FIXME: login_required response will not be an error, so it will not
1138
+ // stop code while it should
1139
+ if (error) {
1140
+ return
1141
+ }
1142
+ // TOOD: map.save may not always be the first call during save process
1143
+ // since SAVEMANAGER refactor
1144
+ if (data.login_required) {
1145
+ window.onLogin = () => this.saveAll()
1146
+ window.open(data.login_required)
1147
+ return
1148
+ }
1149
+ this.properties.user = data.user
1150
+ if (!this.id) {
1151
+ this.properties.id = data.id
1152
+ this.permissions.setProperties(data.permissions)
1153
+ this.permissions.commit()
1154
+ if (data.permissions?.anonymous_edit_url) {
1155
+ this._leafletMap.once('saved', () => {
1156
+ AlertCreation.info(
1157
+ translate('Your map has been created with an anonymous account!'),
1158
+ Number.Infinity,
1159
+ data.permissions.anonymous_edit_url,
1160
+ this.properties.urls.map_send_edit_link
1161
+ ? this.sendEditLinkEmail.bind(this)
1162
+ : null
1163
+ )
1164
+ })
1165
+ } else {
1166
+ this._leafletMap.once('saved', () => {
1167
+ Alert.success(translate('Congratulations, your map has been created!'))
1168
+ })
1169
+ }
1170
+ } else {
1171
+ if (!this.permissions.isDirty) {
1172
+ // Do not override local changes to permissions,
1173
+ // but update in case some other editors changed them in the meantime.
1174
+ this.permissions.setProperties(data.permissions)
1175
+ this.permissions.commit()
1176
+ }
1177
+ this._leafletMap.once('saved', () => {
1178
+ Alert.success(data.info || translate('Map has been saved!'))
1179
+ })
1180
+ }
1181
+ // Update URL in case the name has changed.
1182
+ if (history?.pushState) {
1183
+ history.pushState({}, this.properties.name, data.url)
1184
+ } else {
1185
+ window.location = data.url
1186
+ }
1187
+ return true
1188
+ }
1189
+
1190
+ exportProperties() {
1191
+ const properties = {}
1192
+ for (const key of Object.keys(SCHEMA)) {
1193
+ if (this.properties[key] !== undefined) {
1194
+ properties[key] = this.properties[key]
1195
+ }
1196
+ }
1197
+ return properties
1198
+ }
1199
+
1200
+ geometry() {
1201
+ /* Return a GeoJSON geometry Object */
1202
+ const latlng = this._leafletMap.latLng(
1203
+ this.properties.center || this._leafletMap.getCenter()
1204
+ )
1205
+ return {
1206
+ type: 'Point',
1207
+ coordinates: [latlng.lng, latlng.lat],
1208
+ }
1209
+ }
1210
+
1211
+ toGeoJSON() {
1212
+ let features = []
1213
+ this.eachDataLayer((datalayer) => {
1214
+ if (datalayer.isVisible()) {
1215
+ features = features.concat(datalayer.featuresToGeoJSON())
1216
+ }
1217
+ })
1218
+ const geojson = {
1219
+ type: 'FeatureCollection',
1220
+ features: features,
1221
+ }
1222
+ return geojson
1223
+ }
1224
+
1225
+ enableEdit() {
1226
+ document.body.classList.add('umap-edit-enabled')
1227
+ this.editEnabled = true
1228
+ this.drop.enable()
1229
+ this.fire('edit:enabled')
1230
+ this.initSyncEngine()
1231
+ }
1232
+
1233
+ disableEdit() {
1234
+ if (this.isDirty) return
1235
+ this.drop.disable()
1236
+ document.body.classList.remove('umap-edit-enabled')
1237
+ this.editedFeature = null
1238
+ this.editEnabled = false
1239
+ this.fire('edit:disabled')
1240
+ this.editPanel.close()
1241
+ this.fullPanel.close()
1242
+ this.sync.stop()
1243
+ this._leafletMap.closeInplaceToolbar()
1244
+ }
1245
+
1246
+ fire(name) {
1247
+ this._leafletMap.fire(name)
1248
+ }
1249
+
1250
+ askForReset(e) {
1251
+ this.dialog
1252
+ .confirm(translate('Are you sure you want to cancel your changes?'))
1253
+ .then(() => {
1254
+ this.reset()
1255
+ this.disableEdit()
1256
+ })
1257
+ }
1258
+
1259
+ async initSyncEngine() {
1260
+ if (this.properties.websocketEnabled === false) return
1261
+ if (this.properties.syncEnabled !== true) {
1262
+ this.sync.stop()
1263
+ } else {
1264
+ const ws_token_uri = this.urls.get('map_websocket_auth_token', {
1265
+ map_id: this.id,
1266
+ })
1267
+ await this.sync.authenticate(
1268
+ ws_token_uri,
1269
+ this.properties.websocketURI,
1270
+ this.server
1271
+ )
1272
+ }
1273
+ }
1274
+
1275
+ getSyncMetadata() {
1276
+ return {
1277
+ engine: this.sync,
1278
+ subject: 'map',
1279
+ }
1280
+ }
1281
+
1282
+ render(fields = []) {
1283
+ // Propagate will remove the fields it has already
1284
+ // processed
1285
+ fields = this.propagate(fields)
1286
+
1287
+ const impacts = Utils.getImpactsFromSchema(fields)
1288
+ for (const impact of impacts) {
1289
+ switch (impact) {
1290
+ case 'ui':
1291
+ this._leafletMap.renderUI()
1292
+ this.browser.redraw()
1293
+ this.topBar.redraw()
1294
+ this.bottomBar.redraw()
1295
+ break
1296
+ case 'data':
1297
+ this.eachVisibleDataLayer((datalayer) => {
1298
+ datalayer.redraw()
1299
+ })
1300
+ break
1301
+ case 'datalayer-index':
1302
+ this.reindexDataLayers()
1303
+ break
1304
+ case 'background':
1305
+ this._leafletMap.initTileLayers()
1306
+ break
1307
+ case 'bounds':
1308
+ this._leafletMap.handleLimitBounds()
1309
+ break
1310
+ case 'sync':
1311
+ this.initSyncEngine()
1312
+ }
1313
+ }
1314
+ }
1315
+
1316
+ // This method does a targeted update of the UI,
1317
+ // it whould be merged with `render`` method and the
1318
+ // SCHEMA at some point
1319
+ propagate(fields = []) {
1320
+ const impacts = {
1321
+ 'properties.name': () => {
1322
+ Utils.eachElement('.map-name', (el) => {
1323
+ el.textContent = this.getDisplayName()
1324
+ })
1325
+ },
1326
+ user: () => {
1327
+ Utils.eachElement('.umap-user .username', (el) => {
1328
+ if (this.properties.user?.id) {
1329
+ el.textContent = this.properties.user.name
1330
+ }
1331
+ })
1332
+ },
1333
+ 'properties.permissions': () => {
1334
+ const status = this.permissions.getShareStatusDisplay()
1335
+ if (status) {
1336
+ Utils.eachElement('.share-status', (el) => {
1337
+ el.textContent = translate('Visibility: {status}', {
1338
+ status: status,
1339
+ })
1340
+ })
1341
+ }
1342
+ this.topBar.redraw()
1343
+ },
1344
+ numberOfConnectedPeers: () => {
1345
+ Utils.eachElement('.connected-peers span', (el) => {
1346
+ el.textContent = this.sync.getNumberOfConnectedPeers()
1347
+ })
1348
+ },
1349
+ }
1350
+ for (const [field, impact] of Object.entries(impacts)) {
1351
+ if (!fields.length || fields.includes(field)) {
1352
+ impact()
1353
+ fields = fields.filter((item) => item !== field)
1354
+ }
1355
+ }
1356
+ return fields
1357
+ }
1358
+
1359
+ // TODO: allow to control the default datalayer
1360
+ // (edit and viewing)
1361
+ // cf https://github.com/umap-project/umap/issues/585
1362
+ defaultEditDataLayer() {
1363
+ let datalayer
1364
+ let fallback
1365
+ datalayer = this.lastUsedDataLayer
1366
+ if (
1367
+ datalayer &&
1368
+ !datalayer.isDataReadOnly() &&
1369
+ datalayer.isBrowsable() &&
1370
+ datalayer.isVisible()
1371
+ ) {
1372
+ return datalayer
1373
+ }
1374
+ datalayer = this.findDataLayer((datalayer) => {
1375
+ if (!datalayer.isDataReadOnly() && datalayer.isBrowsable()) {
1376
+ fallback = datalayer
1377
+ if (datalayer.isVisible()) return true
1378
+ }
1379
+ })
1380
+ if (datalayer) return datalayer
1381
+ if (fallback) {
1382
+ // No datalayer visible, let's force one
1383
+ fallback.show()
1384
+ return fallback
1385
+ }
1386
+ return this.createDataLayer()
1387
+ }
1388
+
1389
+ findDataLayer(method, context) {
1390
+ for (let i = this.datalayersIndex.length - 1; i >= 0; i--) {
1391
+ if (method.call(context, this.datalayersIndex[i])) {
1392
+ return this.datalayersIndex[i]
1393
+ }
1394
+ }
1395
+ }
1396
+
1397
+ eachDataLayer(method, context) {
1398
+ for (let i = 0; i < this.datalayersIndex.length; i++) {
1399
+ method.call(context, this.datalayersIndex[i])
1400
+ }
1401
+ }
1402
+
1403
+ eachDataLayerReverse(method, context, filter) {
1404
+ for (let i = this.datalayersIndex.length - 1; i >= 0; i--) {
1405
+ if (filter && !filter.call(context, this.datalayersIndex[i])) continue
1406
+ method.call(context, this.datalayersIndex[i])
1407
+ }
1408
+ }
1409
+
1410
+ eachBrowsableDataLayer(method, context) {
1411
+ this.eachDataLayerReverse(method, context, (d) => d.allowBrowse())
1412
+ }
1413
+
1414
+ eachVisibleDataLayer(method, context) {
1415
+ this.eachDataLayerReverse(method, context, (d) => d.isVisible())
1416
+ }
1417
+
1418
+ eachFeature(callback, context) {
1419
+ this.eachBrowsableDataLayer((datalayer) => {
1420
+ if (datalayer.isVisible()) datalayer.eachFeature(callback, context)
1421
+ })
1422
+ }
1423
+
1424
+ removeDataLayers() {
1425
+ this.eachDataLayerReverse((datalayer) => {
1426
+ datalayer._delete()
1427
+ })
1428
+ }
1429
+
1430
+ emptyDataLayers() {
1431
+ this.eachDataLayerReverse((datalayer) => {
1432
+ datalayer.empty()
1433
+ })
1434
+ }
1435
+
1436
+ editDatalayers() {
1437
+ if (!this.editEnabled) return
1438
+ const container = DomUtil.create('div')
1439
+ DomUtil.createTitle(container, translate('Manage layers'), 'icon-layers')
1440
+ const ul = DomUtil.create('ul', '', container)
1441
+ this.eachDataLayerReverse((datalayer) => {
1442
+ const row = DomUtil.create('li', 'orderable', ul)
1443
+ DomUtil.createIcon(row, 'icon-drag', translate('Drag to reorder'))
1444
+ datalayer.renderToolbox(row)
1445
+ const builder = new U.FormBuilder(
1446
+ datalayer,
1447
+ [['options.name', { handler: 'EditableText' }]],
1448
+ { className: 'umap-form-inline' }
1449
+ )
1450
+ const form = builder.build()
1451
+ row.appendChild(form)
1452
+ row.classList.toggle('off', !datalayer.isVisible())
1453
+ row.dataset.id = stamp(datalayer)
1454
+ })
1455
+ const onReorder = (src, dst, initialIndex, finalIndex) => {
1456
+ const movedLayer = this.datalayers[src.dataset.id]
1457
+ const targetLayer = this.datalayers[dst.dataset.id]
1458
+ const minIndex = Math.min(movedLayer.getRank(), targetLayer.getRank())
1459
+ const maxIndex = Math.max(movedLayer.getRank(), targetLayer.getRank())
1460
+ if (finalIndex === 0) movedLayer.bringToTop()
1461
+ else if (finalIndex > initialIndex) movedLayer.insertBefore(targetLayer)
1462
+ else movedLayer.insertAfter(targetLayer)
1463
+ this.eachDataLayerReverse((datalayer) => {
1464
+ if (datalayer.getRank() >= minIndex && datalayer.getRank() <= maxIndex)
1465
+ datalayer.isDirty = true
1466
+ })
1467
+ this.indexDatalayers()
1468
+ }
1469
+ const orderable = new Orderable(ul, onReorder)
1470
+
1471
+ const bar = DomUtil.create('div', 'button-bar', container)
1472
+ DomUtil.createButton(
1473
+ 'show-on-edit block add-datalayer button',
1474
+ bar,
1475
+ translate('Add a layer'),
1476
+ this.newDataLayer,
1477
+ this
1478
+ )
1479
+
1480
+ this.editPanel.open({ content: container })
1481
+ }
1482
+
1483
+ getDataLayerByUmapId(id) {
1484
+ const datalayer = this.findDataLayer((d) => d.id === id)
1485
+ if (!datalayer) throw new Error(`Can't find datalayer with id ${id}`)
1486
+ return datalayer
1487
+ }
1488
+
1489
+ firstVisibleDatalayer() {
1490
+ return this.findDataLayer((datalayer) => {
1491
+ if (datalayer.isVisible()) return true
1492
+ })
1493
+ }
1494
+
1495
+ ensurePanesOrder() {
1496
+ this.eachDataLayer((datalayer) => {
1497
+ datalayer.bringToTop()
1498
+ })
1499
+ }
1500
+
1501
+ openBrowser(mode) {
1502
+ this.onceDatalayersLoaded(() => this.browser.open(mode))
1503
+ }
1504
+
1505
+ openCaption() {
1506
+ this.onceDatalayersLoaded(() => this.caption.open())
1507
+ }
1508
+
1509
+ addAuthorLink(container) {
1510
+ const author = this.properties.author
1511
+ if (author?.name) {
1512
+ const el = Utils.loadTemplate(
1513
+ `<span class="umap-map-author"> ${translate('by')} <a href="${author.url}">${author.name}</a></span>`
1514
+ )
1515
+ container.appendChild(el)
1516
+ }
1517
+ }
1518
+
1519
+ async star() {
1520
+ if (!this.id) {
1521
+ return Alert.error(translate('Please save the map first'))
1522
+ }
1523
+ const url = this.urls.get('map_star', { map_id: this.id })
1524
+ const [data, response, error] = await this.server.post(url)
1525
+ if (error) {
1526
+ return
1527
+ }
1528
+ this.properties.starred = data.starred
1529
+ Alert.success(
1530
+ data.starred
1531
+ ? translate('Map has been starred')
1532
+ : translate('Map has been unstarred')
1533
+ )
1534
+ this.render(['starred'])
1535
+ }
1536
+
1537
+ processFileToImport(file, layer, type) {
1538
+ type = type || Utils.detectFileType(file)
1539
+ if (!type) {
1540
+ Alert.error(
1541
+ translate('Unable to detect format of file {filename}', {
1542
+ filename: file.name,
1543
+ })
1544
+ )
1545
+ return
1546
+ }
1547
+ if (type === 'umap') {
1548
+ this.importUmapFile(file, 'umap')
1549
+ } else {
1550
+ if (!layer) layer = this.createDataLayer({ name: file.name })
1551
+ layer.importFromFile(file, type)
1552
+ }
1553
+ }
1554
+
1555
+ async importFromUrl(uri) {
1556
+ const response = await this.request.get(uri)
1557
+ if (response?.ok) {
1558
+ this.importRaw(await response.text())
1559
+ }
1560
+ }
1561
+
1562
+ importRaw(rawData) {
1563
+ const importedData = JSON.parse(rawData)
1564
+
1565
+ this.setProperties(importedData.properties)
1566
+
1567
+ if (importedData.geometry) {
1568
+ this.properties.center = this._leafletMap.latLng(importedData.geometry)
1569
+ }
1570
+ for (const geojson of importedData.layers) {
1571
+ if (!geojson._umap_options && geojson._storage) {
1572
+ geojson._umap_options = geojson._storage
1573
+ delete geojson._storage
1574
+ }
1575
+ delete geojson._umap_options?.id // Never trust an id at this stage
1576
+ const dataLayer = this.createDataLayer(geojson._umap_options)
1577
+ dataLayer.fromUmapGeoJSON(geojson)
1578
+ }
1579
+
1580
+ // For now render->propagate expect a `properties.` prefix.
1581
+ // Remove this when we have refactored schema and render.
1582
+ const fields = Object.keys(importedData.properties).map(
1583
+ (field) => `properties.${field}`
1584
+ )
1585
+ this.render(fields)
1586
+ this._leafletMap._setDefaultCenter()
1587
+ this.isDirty = true
1588
+ }
1589
+
1590
+ importUmapFile(file) {
1591
+ const reader = new FileReader()
1592
+ reader.readAsText(file)
1593
+ reader.onload = (e) => {
1594
+ const rawData = e.target.result
1595
+ try {
1596
+ this.importRaw(rawData)
1597
+ } catch (e) {
1598
+ console.error('Error importing data', e)
1599
+ U.Alert.error(
1600
+ translate('Invalid umap data in {filename}', { filename: file.name })
1601
+ )
1602
+ }
1603
+ }
1604
+ }
1605
+
1606
+ async del() {
1607
+ this.dialog
1608
+ .confirm(translate('Are you sure you want to delete this map?'))
1609
+ .then(async () => {
1610
+ const url = this.urls.get('map_delete', { map_id: this.id })
1611
+ const [data, response, error] = await this.server.post(url)
1612
+ if (data.redirect) window.location = data.redirect
1613
+ })
1614
+ }
1615
+
1616
+ async clone() {
1617
+ this.dialog
1618
+ .confirm(
1619
+ translate('Are you sure you want to clone this map and all its datalayers?')
1620
+ )
1621
+ .then(async () => {
1622
+ const url = this.urls.get('map_clone', { map_id: this.id })
1623
+ const [data, response, error] = await this.server.post(url)
1624
+ if (data.redirect) window.location = data.redirect
1625
+ })
1626
+ }
1627
+
1628
+ async sendEditLinkEmail(formData) {
1629
+ const sendLink =
1630
+ this.properties.urls.map_send_edit_link &&
1631
+ this.urls.get('map_send_edit_link', {
1632
+ map_id: this.id,
1633
+ })
1634
+ await this.server.post(sendLink, {}, formData)
1635
+ }
1636
+
1637
+ getLayersBounds() {
1638
+ const bounds = new latLngBounds()
1639
+ this.eachBrowsableDataLayer((d) => {
1640
+ if (d.isVisible()) bounds.extend(d.layer.getBounds())
1641
+ })
1642
+ return bounds
1643
+ }
1644
+
1645
+ fitDataBounds() {
1646
+ const bounds = this.getLayersBounds()
1647
+ if (!this.hasData() || !bounds.isValid()) return false
1648
+ this._leafletMap.fitBounds(bounds)
1649
+ }
1650
+
1651
+ proxyUrl(url, ttl) {
1652
+ if (this.properties.urls.ajax_proxy) {
1653
+ url = Utils.greedyTemplate(this.properties.urls.ajax_proxy, {
1654
+ url: encodeURIComponent(url),
1655
+ ttl: ttl,
1656
+ })
1657
+ }
1658
+ return url
1659
+ }
1660
+
1661
+ openExternalRouting(event) {
1662
+ const url = this.urls.get('routing', {
1663
+ lat: event.latlng.lat,
1664
+ lng: event.latlng.lng,
1665
+ locale: getLocale(),
1666
+ zoom: this._leafletMap.getZoom(),
1667
+ })
1668
+ if (url) window.open(url)
1669
+ }
1670
+
1671
+ editInOSM(event) {
1672
+ const url = this.urls.get('edit_in_osm', {
1673
+ lat: event.latlng.lat,
1674
+ lng: event.latlng.lng,
1675
+ zoom: Math.max(this._leafletMap.getZoom(), 16),
1676
+ })
1677
+ if (url) window.open(url)
1678
+ }
1679
+
1680
+ setCenterAndZoom() {
1681
+ this._setCenterAndZoom()
1682
+ Alert.success(translate('The zoom and center have been modified.'))
1683
+ }
1684
+
1685
+ _setCenterAndZoom() {
1686
+ this.properties.center = this._leafletMap.getCenter()
1687
+ this.properties.zoom = this._leafletMap.getZoom()
1688
+ this.isDirty = true
1689
+ this._defaultExtent = false
1690
+ }
1691
+ }