umap-project 2.8.0b0__py3-none-any.whl → 2.8.1__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (112) hide show
  1. umap/__init__.py +1 -1
  2. umap/decorators.py +3 -1
  3. umap/locale/ar/LC_MESSAGES/django.mo +0 -0
  4. umap/locale/ar/LC_MESSAGES/django.po +45 -30
  5. umap/locale/br/LC_MESSAGES/django.mo +0 -0
  6. umap/locale/br/LC_MESSAGES/django.po +49 -34
  7. umap/locale/ca/LC_MESSAGES/django.mo +0 -0
  8. umap/locale/ca/LC_MESSAGES/django.po +45 -30
  9. umap/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  10. umap/locale/cs_CZ/LC_MESSAGES/django.po +52 -37
  11. umap/locale/da/LC_MESSAGES/django.mo +0 -0
  12. umap/locale/da/LC_MESSAGES/django.po +45 -30
  13. umap/locale/de/LC_MESSAGES/django.mo +0 -0
  14. umap/locale/de/LC_MESSAGES/django.po +45 -30
  15. umap/locale/el/LC_MESSAGES/django.mo +0 -0
  16. umap/locale/el/LC_MESSAGES/django.po +45 -30
  17. umap/locale/en/LC_MESSAGES/django.po +44 -29
  18. umap/locale/es/LC_MESSAGES/django.mo +0 -0
  19. umap/locale/es/LC_MESSAGES/django.po +45 -30
  20. umap/locale/et/LC_MESSAGES/django.mo +0 -0
  21. umap/locale/et/LC_MESSAGES/django.po +45 -30
  22. umap/locale/eu/LC_MESSAGES/django.mo +0 -0
  23. umap/locale/eu/LC_MESSAGES/django.po +65 -50
  24. umap/locale/fa_IR/LC_MESSAGES/django.mo +0 -0
  25. umap/locale/fa_IR/LC_MESSAGES/django.po +45 -30
  26. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  27. umap/locale/fr/LC_MESSAGES/django.po +45 -30
  28. umap/locale/gl/LC_MESSAGES/django.mo +0 -0
  29. umap/locale/gl/LC_MESSAGES/django.po +45 -30
  30. umap/locale/he/LC_MESSAGES/django.mo +0 -0
  31. umap/locale/he/LC_MESSAGES/django.po +45 -30
  32. umap/locale/hu/LC_MESSAGES/django.mo +0 -0
  33. umap/locale/hu/LC_MESSAGES/django.po +45 -30
  34. umap/locale/is/LC_MESSAGES/django.mo +0 -0
  35. umap/locale/is/LC_MESSAGES/django.po +45 -30
  36. umap/locale/it/LC_MESSAGES/django.mo +0 -0
  37. umap/locale/it/LC_MESSAGES/django.po +45 -30
  38. umap/locale/ja/LC_MESSAGES/django.mo +0 -0
  39. umap/locale/ja/LC_MESSAGES/django.po +45 -30
  40. umap/locale/ko/LC_MESSAGES/django.mo +0 -0
  41. umap/locale/ko/LC_MESSAGES/django.po +45 -30
  42. umap/locale/lt/LC_MESSAGES/django.mo +0 -0
  43. umap/locale/lt/LC_MESSAGES/django.po +45 -30
  44. umap/locale/ms/LC_MESSAGES/django.mo +0 -0
  45. umap/locale/ms/LC_MESSAGES/django.po +45 -30
  46. umap/locale/nl/LC_MESSAGES/django.mo +0 -0
  47. umap/locale/nl/LC_MESSAGES/django.po +45 -30
  48. umap/locale/pl/LC_MESSAGES/django.mo +0 -0
  49. umap/locale/pl/LC_MESSAGES/django.po +45 -30
  50. umap/locale/pt/LC_MESSAGES/django.mo +0 -0
  51. umap/locale/pt/LC_MESSAGES/django.po +45 -30
  52. umap/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
  53. umap/locale/pt_BR/LC_MESSAGES/django.po +45 -30
  54. umap/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
  55. umap/locale/pt_PT/LC_MESSAGES/django.po +45 -30
  56. umap/locale/ru/LC_MESSAGES/django.mo +0 -0
  57. umap/locale/ru/LC_MESSAGES/django.po +45 -30
  58. umap/locale/sk_SK/LC_MESSAGES/django.mo +0 -0
  59. umap/locale/sk_SK/LC_MESSAGES/django.po +45 -30
  60. umap/locale/sl/LC_MESSAGES/django.mo +0 -0
  61. umap/locale/sl/LC_MESSAGES/django.po +45 -30
  62. umap/locale/sr/LC_MESSAGES/django.mo +0 -0
  63. umap/locale/sr/LC_MESSAGES/django.po +45 -30
  64. umap/locale/sv/LC_MESSAGES/django.mo +0 -0
  65. umap/locale/sv/LC_MESSAGES/django.po +45 -30
  66. umap/locale/th_TH/LC_MESSAGES/django.mo +0 -0
  67. umap/locale/th_TH/LC_MESSAGES/django.po +45 -30
  68. umap/locale/tr/LC_MESSAGES/django.mo +0 -0
  69. umap/locale/tr/LC_MESSAGES/django.po +45 -30
  70. umap/locale/uk_UA/LC_MESSAGES/django.mo +0 -0
  71. umap/locale/uk_UA/LC_MESSAGES/django.po +45 -30
  72. umap/locale/zh_TW/LC_MESSAGES/django.mo +0 -0
  73. umap/locale/zh_TW/LC_MESSAGES/django.po +50 -35
  74. umap/static/umap/content.css +18 -13
  75. umap/static/umap/css/bar.css +4 -0
  76. umap/static/umap/img/logo_lightcyan.svg +4 -0
  77. umap/static/umap/js/modules/caption.js +73 -73
  78. umap/static/umap/js/modules/data/features.js +8 -5
  79. umap/static/umap/js/modules/data/layer.js +13 -8
  80. umap/static/umap/js/modules/drop.js +55 -0
  81. umap/static/umap/js/modules/importer.js +10 -5
  82. umap/static/umap/js/modules/rendering/map.js +2 -1
  83. umap/static/umap/js/modules/rendering/popup.js +9 -10
  84. umap/static/umap/js/modules/rendering/template.js +53 -9
  85. umap/static/umap/js/modules/schema.js +1 -0
  86. umap/static/umap/js/modules/sync/engine.js +56 -13
  87. umap/static/umap/js/modules/sync/updaters.js +4 -1
  88. umap/static/umap/js/modules/sync/websocket.js +47 -2
  89. umap/static/umap/js/modules/ui/bar.js +1 -1
  90. umap/static/umap/js/modules/umap.js +37 -21
  91. umap/static/umap/js/modules/utils.js +2 -0
  92. umap/static/umap/js/umap.controls.js +1 -51
  93. umap/static/umap/locale/cs_CZ.js +13 -11
  94. umap/static/umap/locale/cs_CZ.json +13 -11
  95. umap/static/umap/locale/en.js +2 -1
  96. umap/static/umap/locale/en.json +2 -1
  97. umap/static/umap/locale/fr.js +2 -1
  98. umap/static/umap/locale/fr.json +2 -1
  99. umap/static/umap/locale/zh_TW.js +13 -11
  100. umap/static/umap/locale/zh_TW.json +13 -11
  101. umap/static/umap/unittests/sync.js +4 -1
  102. umap/templates/403.html +12 -0
  103. umap/templates/404.html +4 -13
  104. umap/templates/40x.html +9 -0
  105. umap/tests/integration/conftest.py +3 -3
  106. umap/tests/integration/test_websocket_sync.py +69 -0
  107. umap/websocket_server.py +8 -1
  108. {umap_project-2.8.0b0.dist-info → umap_project-2.8.1.dist-info}/METADATA +5 -5
  109. {umap_project-2.8.0b0.dist-info → umap_project-2.8.1.dist-info}/RECORD +112 -108
  110. {umap_project-2.8.0b0.dist-info → umap_project-2.8.1.dist-info}/WHEEL +0 -0
  111. {umap_project-2.8.0b0.dist-info → umap_project-2.8.1.dist-info}/entry_points.txt +0 -0
  112. {umap_project-2.8.0b0.dist-info → umap_project-2.8.1.dist-info}/licenses/LICENSE +0 -0
@@ -124,6 +124,11 @@ export default class Importer extends Utils.WithTemplate {
124
124
  return this.qs('[type=file]').files
125
125
  }
126
126
 
127
+ set files(files) {
128
+ this.qs('[type=file]').files = files
129
+ this.onFileChange()
130
+ }
131
+
127
132
  get raw() {
128
133
  return this.qs('textarea').value
129
134
  }
@@ -161,7 +166,7 @@ export default class Importer extends Utils.WithTemplate {
161
166
  get layer() {
162
167
  return (
163
168
  this._umap.datalayers[this.layerId] ||
164
- this._umap.createDataLayer({ name: this.layerName })
169
+ this._umap.createDirtyDataLayer({ name: this.layerName })
165
170
  )
166
171
  }
167
172
 
@@ -213,11 +218,11 @@ export default class Importer extends Utils.WithTemplate {
213
218
  this.qs('[name=submit').toggleAttribute('disabled', !this.canSubmit())
214
219
  }
215
220
 
216
- onFileChange(e) {
221
+ onFileChange() {
217
222
  let type = ''
218
223
  let newType
219
- for (const file of e.target.files) {
220
- newType = U.Utils.detectFileType(file)
224
+ for (const file of this.files) {
225
+ newType = Utils.detectFileType(file)
221
226
  if (!type && newType) type = newType
222
227
  if (type && newType !== type) {
223
228
  type = ''
@@ -382,7 +387,7 @@ export default class Importer extends Utils.WithTemplate {
382
387
  bounds.extend(featureBounds)
383
388
  }
384
389
  this.onSuccess(features.length)
385
- layer.zoomTo(bounds)
390
+ layer.zoomToBounds(bounds)
386
391
  }
387
392
  }
388
393
  }
@@ -12,6 +12,7 @@ import { translate } from '../i18n.js'
12
12
  import { uMapAlert as Alert } from '../../components/alerts/alert.js'
13
13
  import * as Utils from '../utils.js'
14
14
  import * as Icon from './icon.js'
15
+ import DropControl from '../drop.js'
15
16
 
16
17
  // Those options are not saved on the server, so they can live here
17
18
  // instead of in umap.properties
@@ -96,7 +97,7 @@ const ControlsMixin = {
96
97
  this._controls.more = new U.MoreControls()
97
98
  this._controls.scale = L.control.scale()
98
99
  this._controls.permanentCredit = new U.PermanentCreditsControl(this)
99
- this._umap.drop = new U.DropControl(this)
100
+ this._umap.drop = new DropControl(this._umap, this, this._container)
100
101
  this._controls.tilelayers = new U.TileLayerControl(this)
101
102
  },
102
103
 
@@ -21,23 +21,22 @@ export default function loadPopup(name) {
21
21
  const Popup = BasePopup.extend({
22
22
  initialize: function (feature) {
23
23
  this.feature = feature
24
- this.container = DomUtil.create('div', 'umap-popup')
25
- this.format()
26
- BasePopup.prototype.initialize.call(this, {}, feature)
27
- this.setContent(this.container)
24
+ BasePopup.prototype.initialize.call(this, {}, feature.ui)
28
25
  },
29
26
 
30
- format: function () {
27
+ loadContent: async function () {
28
+ const container = DomUtil.create('div', 'umap-popup')
31
29
  const name = this.feature.getOption('popupTemplate')
32
- this.content = loadTemplate(name, this.feature, this.container)
33
- const elements = this.container.querySelectorAll('img,iframe')
30
+ this.content = await loadTemplate(name, this.feature, container)
31
+ const elements = container.querySelectorAll('img,iframe')
34
32
  for (const element of elements) {
35
33
  this.onElementLoaded(element)
36
34
  }
37
- if (!elements.length && this.container.textContent.replace('\n', '') === '') {
38
- this.container.innerHTML = ''
39
- DomUtil.add('h3', '', this.container, this.feature.getDisplayName())
35
+ if (!elements.length && container.textContent.replace('\n', '') === '') {
36
+ container.innerHTML = ''
37
+ DomUtil.add('h3', '', container, this.feature.getDisplayName())
40
38
  }
39
+ this.setContent(container)
41
40
  },
42
41
 
43
42
  onElementLoaded: function (el) {
@@ -2,8 +2,9 @@ import { DomUtil, DomEvent } from '../../../vendors/leaflet/leaflet-src.esm.js'
2
2
  import { translate, getLocale } from '../i18n.js'
3
3
  import * as Utils from '../utils.js'
4
4
  import * as Icon from './icon.js'
5
+ import { Request } from '../request.js'
5
6
 
6
- export default function loadTemplate(name, feature, container) {
7
+ export default async function loadTemplate(name, feature, container) {
7
8
  let klass = PopupTemplate
8
9
  switch (name) {
9
10
  case 'GeoRSSLink':
@@ -18,9 +19,12 @@ export default function loadTemplate(name, feature, container) {
18
19
  case 'OSM':
19
20
  klass = OSM
20
21
  break
22
+ case 'Wikipedia':
23
+ klass = Wikipedia
24
+ break
21
25
  }
22
26
  const content = new klass()
23
- return content.render(feature, container)
27
+ return await content.render(feature, container)
24
28
  }
25
29
 
26
30
  class PopupTemplate {
@@ -76,10 +80,10 @@ class PopupTemplate {
76
80
  }
77
81
  }
78
82
 
79
- render(feature, container) {
83
+ async render(feature, container) {
80
84
  const title = this.renderTitle(feature)
81
85
  if (title) container.appendChild(title)
82
- const body = this.renderBody(feature)
86
+ const body = await this.renderBody(feature)
83
87
  if (body) DomUtil.add('div', 'umap-popup-content', container, body)
84
88
  const footer = this.renderFooter(feature)
85
89
  if (footer) container.appendChild(footer)
@@ -111,7 +115,7 @@ class Table extends TitleMixin(PopupTemplate) {
111
115
  )
112
116
  }
113
117
 
114
- renderBody(feature) {
118
+ async renderBody(feature) {
115
119
  const table = document.createElement('table')
116
120
 
117
121
  for (const key in feature.properties) {
@@ -125,7 +129,7 @@ class Table extends TitleMixin(PopupTemplate) {
125
129
  }
126
130
 
127
131
  class GeoRSSImage extends TitleMixin(PopupTemplate) {
128
- renderBody(feature) {
132
+ async renderBody(feature) {
129
133
  const body = DomUtil.create('a')
130
134
  body.href = feature.properties.link
131
135
  body.target = '_blank'
@@ -142,7 +146,7 @@ class GeoRSSImage extends TitleMixin(PopupTemplate) {
142
146
  }
143
147
 
144
148
  class GeoRSSLink extends PopupTemplate {
145
- renderBody(feature) {
149
+ async renderBody(feature) {
146
150
  if (feature.properties.link) {
147
151
  return Utils.loadTemplate(
148
152
  `<a href="${feature.properties.link}" target="_blank"><h3>${feature.getDisplayName()}</h3></a>`
@@ -151,7 +155,7 @@ class GeoRSSLink extends PopupTemplate {
151
155
  }
152
156
  }
153
157
 
154
- class OSM extends TitleMixin(PopupTemplate) {
158
+ class OSM extends PopupTemplate {
155
159
  renderTitle(feature) {
156
160
  const title = DomUtil.add('h3', 'popup-title')
157
161
  const color = feature.getPreviewColor()
@@ -172,7 +176,7 @@ class OSM extends TitleMixin(PopupTemplate) {
172
176
  return props.name
173
177
  }
174
178
 
175
- renderBody(feature) {
179
+ async renderBody(feature) {
176
180
  const props = feature.properties
177
181
  const body = document.createElement('div')
178
182
  const locale = getLocale()
@@ -238,3 +242,43 @@ class OSM extends TitleMixin(PopupTemplate) {
238
242
  return body
239
243
  }
240
244
  }
245
+
246
+ const _WIKIPEDIA_CACHE = {}
247
+
248
+ class Wikipedia extends PopupTemplate {
249
+ async callWikipedia(wikipedia) {
250
+ if (wikipedia && _WIKIPEDIA_CACHE[wikipedia]) return _WIKIPEDIA_CACHE[wikipedia]
251
+ // Wikipedia value should be in form of "{locale}:{title}", according to https://wiki.openstreetmap.org/wiki/Key:wikipedia
252
+ const [locale, page] = wikipedia.split(':')
253
+ const url = `https://${locale}.wikipedia.org/w/api.php?action=query&format=json&origin=*&pithumbsize=500&prop=extracts|pageimages&titles=${page}`
254
+ const request = new Request()
255
+ const response = await request.get(url)
256
+ if (response?.ok) {
257
+ const data = await response.json()
258
+ _WIKIPEDIA_CACHE[wikipedia] = data
259
+ return data
260
+ }
261
+ }
262
+
263
+ async renderBody(feature) {
264
+ const body = document.createElement('div')
265
+ const wikipedia = feature.properties.wikipedia
266
+ if (!wikipedia) return ''
267
+ const data = await this.callWikipedia(wikipedia)
268
+ if (data) {
269
+ const page = Object.values(data.query.pages)[0]
270
+ const title = page.title || feature.getDisplayName()
271
+ const extract = page.extract || ''
272
+ const thumbnail = page.thumbnail?.source
273
+ const [content, { image }] = Utils.loadTemplateWithRefs(
274
+ `<div><h3>${Utils.escapeHTML(title)}</h3><img data-ref="image" hidden src="" />${Utils.escapeHTML(extract)}</div>`
275
+ )
276
+ if (thumbnail) {
277
+ image.src = thumbnail
278
+ image.hidden = false
279
+ }
280
+ body.appendChild(content)
281
+ }
282
+ return body
283
+ }
284
+ }
@@ -404,6 +404,7 @@ export const SCHEMA = {
404
404
  ['GeoRSSImage', translate('GeoRSS (title + image)')],
405
405
  ['GeoRSSLink', translate('GeoRSS (only link)')],
406
406
  ['OSM', translate('OpenStreetMap')],
407
+ ['Wikipedia', translate('Wikipedia')],
407
408
  ],
408
409
  default: 'Default',
409
410
  },
@@ -3,6 +3,12 @@ import { HybridLogicalClock } from './hlc.js'
3
3
  import { DataLayerUpdater, FeatureUpdater, MapUpdater } from './updaters.js'
4
4
  import { WebSocketTransport } from './websocket.js'
5
5
 
6
+ // Start reconnecting after 2 seconds, then double the delay each time
7
+ // maxing out at 32 seconds.
8
+ const RECONNECT_DELAY = 2000
9
+ const RECONNECT_DELAY_FACTOR = 2
10
+ const MAX_RECONNECT_DELAY = 32000
11
+
6
12
  /**
7
13
  * The syncEngine exposes an API to sync messages between peers over the network.
8
14
  *
@@ -42,32 +48,65 @@ import { WebSocketTransport } from './websocket.js'
42
48
  * ```
43
49
  */
44
50
  export class SyncEngine {
45
- constructor(map) {
51
+ constructor(umap) {
52
+ this._umap = umap
46
53
  this.updaters = {
47
- map: new MapUpdater(map),
48
- feature: new FeatureUpdater(map),
49
- datalayer: new DataLayerUpdater(map),
54
+ map: new MapUpdater(umap),
55
+ feature: new FeatureUpdater(umap),
56
+ datalayer: new DataLayerUpdater(umap),
50
57
  }
51
58
  this.transport = undefined
52
59
  this._operations = new Operations()
60
+
61
+ this._reconnectTimeout = null
62
+ this._reconnectDelay = RECONNECT_DELAY
63
+ this.websocketConnected = false
53
64
  }
54
65
 
55
- async authenticate(tokenURI, webSocketURI, server) {
56
- const [response, _, error] = await server.get(tokenURI)
66
+ async authenticate() {
67
+ const websocketTokenURI = this._umap.urls.get('map_websocket_auth_token', {
68
+ map_id: this._umap.id,
69
+ })
70
+
71
+ const [response, _, error] = await this._umap.server.get(websocketTokenURI)
57
72
  if (!error) {
58
- this.start(webSocketURI, response.token)
73
+ this.start(response.token)
59
74
  }
60
75
  }
61
76
 
62
- start(webSocketURI, authToken) {
63
- this.transport = new WebSocketTransport(webSocketURI, authToken, this)
77
+ start(authToken) {
78
+ this.transport = new WebSocketTransport(
79
+ this._umap.properties.websocketURI,
80
+ authToken,
81
+ this
82
+ )
64
83
  }
65
84
 
66
85
  stop() {
67
- if (this.transport) this.transport.close()
86
+ if (this.transport) {
87
+ this.transport.close()
88
+ }
68
89
  this.transport = undefined
69
90
  }
70
91
 
92
+ onConnection() {
93
+ this._reconnectTimeout = null
94
+ this._reconnectDelay = RECONNECT_DELAY
95
+ this.websocketConnected = true
96
+ this.updaters.map.update({ key: 'numberOfConnectedPeers' })
97
+ }
98
+
99
+ reconnect() {
100
+ this.websocketConnected = false
101
+ this.updaters.map.update({ key: 'numberOfConnectedPeers' })
102
+
103
+ this._reconnectTimeout = setTimeout(() => {
104
+ if (this._reconnectDelay < MAX_RECONNECT_DELAY) {
105
+ this._reconnectDelay = this._reconnectDelay * RECONNECT_DELAY_FACTOR
106
+ }
107
+ this.authenticate()
108
+ }, this._reconnectDelay)
109
+ }
71
110
  upsert(subject, metadata, value) {
72
111
  this._send({ verb: 'upsert', subject, metadata, value })
73
112
  }
@@ -183,9 +222,13 @@ export class SyncEngine {
183
222
  * @param {string} payload.sender the uuid of the requesting peer
184
223
  * @param {string} payload.latestKnownHLC the latest known HLC of the requesting peer
185
224
  */
186
- onListOperationsRequest({ sender, lastKnownHLC }) {
225
+ onListOperationsRequest({ sender, message }) {
226
+ debug(
227
+ `received operations request from peer ${sender} (since ${message.lastKnownHLC})`
228
+ )
229
+
187
230
  this.sendToPeer(sender, 'ListOperationsResponse', {
188
- operations: this._operations.getOperationsSince(lastKnownHLC),
231
+ operations: this._operations.getOperationsSince(message.lastKnownHLC),
189
232
  })
190
233
  }
191
234
 
@@ -446,5 +489,5 @@ export class Operations {
446
489
  }
447
490
 
448
491
  function debug(...args) {
449
- console.debug('SYNC ⇆', ...args)
492
+ console.debug('SYNC ⇆', ...args.map((x) => JSON.stringify(x)))
450
493
  }
@@ -54,7 +54,10 @@ export class MapUpdater extends BaseUpdater {
54
54
  export class DataLayerUpdater extends BaseUpdater {
55
55
  upsert({ value }) {
56
56
  // Upsert only happens when a new datalayer is created.
57
- this._umap.createDataLayer(value, false)
57
+ const datalayer = this._umap.createDataLayer(value, false)
58
+ // Prevent the layer to get data from the server, as it will get it
59
+ // from the sync.
60
+ datalayer._loaded = true
58
61
  }
59
62
 
60
63
  update({ key, metadata, value }) {
@@ -1,15 +1,59 @@
1
+ const PONG_TIMEOUT = 5000
2
+ const PING_INTERVAL = 30000
3
+ const FIRST_CONNECTION_TIMEOUT = 2000
4
+
1
5
  export class WebSocketTransport {
2
6
  constructor(webSocketURI, authToken, messagesReceiver) {
7
+ this.receiver = messagesReceiver
8
+ this.closeRequested = false
9
+
3
10
  this.websocket = new WebSocket(webSocketURI)
11
+
4
12
  this.websocket.onopen = () => {
5
13
  this.send('JoinRequest', { token: authToken })
14
+ this.receiver.onConnection()
6
15
  }
7
16
  this.websocket.addEventListener('message', this.onMessage.bind(this))
8
- this.receiver = messagesReceiver
17
+ this.websocket.onclose = () => {
18
+ console.log('websocket closed')
19
+ if (!this.closeRequested) {
20
+ console.log('Not requested, reconnecting...')
21
+ this.receiver.reconnect()
22
+ }
23
+ }
24
+
25
+ this.ensureOpen = setInterval(() => {
26
+ if (this.websocket.readyState !== WebSocket.OPEN) {
27
+ this.websocket.close()
28
+ clearInterval(this.ensureOpen)
29
+ }
30
+ }, FIRST_CONNECTION_TIMEOUT)
31
+
32
+ // To ensure the connection is still alive, we send ping and expect pong back.
33
+ // Websocket provides a `ping` method to keep the connection alive, but it's
34
+ // unfortunately not possible to access it from the WebSocket object.
35
+ // See https://making.close.com/posts/reliable-websockets/ for more details.
36
+ this.pingInterval = setInterval(() => {
37
+ if (this.websocket.readyState === WebSocket.OPEN) {
38
+ this.websocket.send('ping')
39
+ this.pongReceived = false
40
+ setTimeout(() => {
41
+ if (!this.pongReceived) {
42
+ console.warn('No pong received, reconnecting...')
43
+ this.websocket.close()
44
+ clearInterval(this.pingInterval)
45
+ }
46
+ }, PONG_TIMEOUT)
47
+ }
48
+ }, PING_INTERVAL)
9
49
  }
10
50
 
11
51
  onMessage(wsMessage) {
12
- this.receiver.receive(JSON.parse(wsMessage.data))
52
+ if (wsMessage.data === 'pong') {
53
+ this.pongReceived = true
54
+ } else {
55
+ this.receiver.receive(JSON.parse(wsMessage.data))
56
+ }
13
57
  }
14
58
 
15
59
  send(kind, payload) {
@@ -20,6 +64,7 @@ export class WebSocketTransport {
20
64
  }
21
65
 
22
66
  close() {
67
+ this.closeRequested = true
23
68
  this.websocket.close()
24
69
  }
25
70
  }
@@ -110,7 +110,7 @@ export class TopBar extends WithTemplate {
110
110
  })
111
111
  })
112
112
 
113
- this.elements.help.addEventListener('click', () => this._umap.showGetStarted())
113
+ this.elements.help.addEventListener('click', () => this._umap.help.showGetStarted())
114
114
  this.elements.cancel.addEventListener('click', () => this._umap.askForReset())
115
115
  this.elements.cancel.addEventListener('mouseover', () => {
116
116
  this._umap.tooltip.open({
@@ -61,8 +61,6 @@ export default class Umap extends ServerStored {
61
61
  )
62
62
  this.searchParams = new URLSearchParams(window.location.search)
63
63
 
64
- this.sync_engine = new SyncEngine(this)
65
- this.sync = this.sync_engine.proxy(this)
66
64
  // Locale name (pt_PT, en_US…)
67
65
  // To be used for Django localization
68
66
  if (geojson.properties.locale) setLocale(geojson.properties.locale)
@@ -124,6 +122,9 @@ export default class Umap extends ServerStored {
124
122
  this.share = new Share(this)
125
123
  this.rules = new Rules(this)
126
124
 
125
+ this.syncEngine = new SyncEngine(this)
126
+ this.sync = this.syncEngine.proxy(this)
127
+
127
128
  if (this.hasEditMode()) {
128
129
  this.editPanel = new EditPanel(this, this._leafletMap)
129
130
  this.fullPanel = new FullPanel(this, this._leafletMap)
@@ -323,14 +324,14 @@ export default class Umap extends ServerStored {
323
324
  dataUrl = decodeURIComponent(dataUrl)
324
325
  dataUrl = this.renderUrl(dataUrl)
325
326
  dataUrl = this.proxyUrl(dataUrl)
326
- const datalayer = this.createDataLayer()
327
+ const datalayer = this.createDirtyDataLayer()
327
328
  await datalayer
328
329
  .importFromUrl(dataUrl, dataFormat)
329
330
  .then(() => datalayer.zoomTo())
330
331
  }
331
332
  } else if (data) {
332
333
  data = decodeURIComponent(data)
333
- const datalayer = this.createDataLayer()
334
+ const datalayer = this.createDirtyDataLayer()
334
335
  await datalayer.importRaw(data, dataFormat).then(() => datalayer.zoomTo())
335
336
  }
336
337
  }
@@ -580,9 +581,13 @@ export default class Umap extends ServerStored {
580
581
  this.fire('datalayersloaded')
581
582
  const toLoad = []
582
583
  for (const datalayer of this.datalayersIndex) {
583
- if (datalayer.showAtLoad()) toLoad.push(datalayer.show())
584
+ if (datalayer.showAtLoad()) toLoad.push(() => datalayer.show())
585
+ }
586
+ while (toLoad.length) {
587
+ const chunk = toLoad.splice(0, 10)
588
+ await Promise.all(chunk.map((func) => func()))
584
589
  }
585
- await Promise.all(toLoad)
590
+
586
591
  this.dataloaded = true
587
592
  this.fire('dataloaded')
588
593
  }
@@ -598,8 +603,14 @@ export default class Umap extends ServerStored {
598
603
  return datalayer
599
604
  }
600
605
 
606
+ createDirtyDataLayer(options) {
607
+ const datalayer = this.createDataLayer(options, true)
608
+ datalayer.isDirty = true
609
+ return datalayer
610
+ }
611
+
601
612
  newDataLayer() {
602
- const datalayer = this.createDataLayer({})
613
+ const datalayer = this.createDirtyDataLayer({})
603
614
  datalayer.edit()
604
615
  }
605
616
 
@@ -1117,8 +1128,8 @@ export default class Umap extends ServerStored {
1117
1128
  }
1118
1129
  this.ensurePanesOrder()
1119
1130
  this._leafletMap.initTileLayers()
1120
- this.isDirty = false
1121
1131
  this.onDataLayersChanged()
1132
+ this.isDirty = !this.id
1122
1133
  }
1123
1134
 
1124
1135
  async save() {
@@ -1257,18 +1268,13 @@ export default class Umap extends ServerStored {
1257
1268
  }
1258
1269
 
1259
1270
  async initSyncEngine() {
1271
+ // this.properties.websocketEnabled is set by the server admin
1260
1272
  if (this.properties.websocketEnabled === false) return
1273
+ // this.properties.syncEnabled is set by the user in the map settings
1261
1274
  if (this.properties.syncEnabled !== true) {
1262
1275
  this.sync.stop()
1263
1276
  } 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
- )
1277
+ await this.sync.authenticate()
1272
1278
  }
1273
1279
  }
1274
1280
 
@@ -1343,7 +1349,17 @@ export default class Umap extends ServerStored {
1343
1349
  },
1344
1350
  numberOfConnectedPeers: () => {
1345
1351
  Utils.eachElement('.connected-peers span', (el) => {
1346
- el.textContent = this.sync.getNumberOfConnectedPeers()
1352
+ if (this.sync.websocketConnected) {
1353
+ el.textContent = this.sync.getNumberOfConnectedPeers()
1354
+ } else {
1355
+ el.textContent = translate('Disconnected')
1356
+ }
1357
+ el.parentElement.classList.toggle('off', !this.sync.websocketConnected)
1358
+ })
1359
+ },
1360
+ 'properties.starred': () => {
1361
+ Utils.eachElement('.map-star', (el) => {
1362
+ el.classList.toggle('starred', this.properties.starred)
1347
1363
  })
1348
1364
  },
1349
1365
  }
@@ -1383,7 +1399,7 @@ export default class Umap extends ServerStored {
1383
1399
  fallback.show()
1384
1400
  return fallback
1385
1401
  }
1386
- return this.createDataLayer()
1402
+ return this.createDirtyDataLayer()
1387
1403
  }
1388
1404
 
1389
1405
  findDataLayer(method, context) {
@@ -1531,7 +1547,7 @@ export default class Umap extends ServerStored {
1531
1547
  ? translate('Map has been starred')
1532
1548
  : translate('Map has been unstarred')
1533
1549
  )
1534
- this.render(['starred'])
1550
+ this.render(['properties.starred'])
1535
1551
  }
1536
1552
 
1537
1553
  processFileToImport(file, layer, type) {
@@ -1547,7 +1563,7 @@ export default class Umap extends ServerStored {
1547
1563
  if (type === 'umap') {
1548
1564
  this.importUmapFile(file, 'umap')
1549
1565
  } else {
1550
- if (!layer) layer = this.createDataLayer({ name: file.name })
1566
+ if (!layer) layer = this.createDirtyDataLayer({ name: file.name })
1551
1567
  layer.importFromFile(file, type)
1552
1568
  }
1553
1569
  }
@@ -1573,7 +1589,7 @@ export default class Umap extends ServerStored {
1573
1589
  delete geojson._storage
1574
1590
  }
1575
1591
  delete geojson._umap_options?.id // Never trust an id at this stage
1576
- const dataLayer = this.createDataLayer(geojson._umap_options)
1592
+ const dataLayer = this.createDirtyDataLayer(geojson._umap_options)
1577
1593
  dataLayer.fromUmapGeoJSON(geojson)
1578
1594
  }
1579
1595
 
@@ -115,6 +115,8 @@ export function escapeHTML(s) {
115
115
  'span',
116
116
  'dt',
117
117
  'dd',
118
+ 'b',
119
+ 'i',
118
120
  ],
119
121
  ADD_ATTR: [
120
122
  'target',
@@ -337,52 +337,6 @@ U.DrawToolbar = L.Toolbar.Control.extend({
337
337
  },
338
338
  })
339
339
 
340
- U.DropControl = L.Class.extend({
341
- initialize: function (map) {
342
- this.map = map
343
- this.dropzone = map._container
344
- },
345
-
346
- enable: function () {
347
- L.DomEvent.on(this.dropzone, 'dragenter', this.dragenter, this)
348
- L.DomEvent.on(this.dropzone, 'dragover', this.dragover, this)
349
- L.DomEvent.on(this.dropzone, 'drop', this.drop, this)
350
- L.DomEvent.on(this.dropzone, 'dragleave', this.dragleave, this)
351
- },
352
-
353
- disable: function () {
354
- L.DomEvent.off(this.dropzone, 'dragenter', this.dragenter, this)
355
- L.DomEvent.off(this.dropzone, 'dragover', this.dragover, this)
356
- L.DomEvent.off(this.dropzone, 'drop', this.drop, this)
357
- L.DomEvent.off(this.dropzone, 'dragleave', this.dragleave, this)
358
- },
359
-
360
- dragenter: function (event) {
361
- L.DomEvent.stop(event)
362
- this.map.scrollWheelZoom.disable()
363
- this.dropzone.classList.add('umap-dragover')
364
- },
365
-
366
- dragover: (event) => {
367
- L.DomEvent.stop(event)
368
- },
369
-
370
- drop: function (event) {
371
- this.map.scrollWheelZoom.enable()
372
- this.dropzone.classList.remove('umap-dragover')
373
- L.DomEvent.stop(event)
374
- for (const file of event.dataTransfer.files) {
375
- this.map._umap.processFileToImport(file)
376
- }
377
- this.map._umap.onceDataLoaded(this.map._umap.fitDataBounds)
378
- },
379
-
380
- dragleave: function () {
381
- this.map.scrollWheelZoom.enable()
382
- this.dropzone.classList.remove('umap-dragover')
383
- },
384
- })
385
-
386
340
  U.EditControl = L.Control.extend({
387
341
  options: {
388
342
  position: 'topright',
@@ -541,11 +495,7 @@ U.StarControl = L.Control.Button.extend({
541
495
  options: {
542
496
  position: 'topleft',
543
497
  title: L._('Star this map'),
544
- },
545
-
546
- getClassName: function () {
547
- const status = this._umap.properties.starred ? ' starred' : ''
548
- return `leaflet-control-star umap-control${status}`
498
+ className: 'leaflet-control-star map-star umap-control',
549
499
  },
550
500
 
551
501
  onClick: function () {