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