umap-project 2.6.3__py3-none-any.whl → 2.7.0b0__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 (104) hide show
  1. umap/__init__.py +1 -1
  2. umap/admin.py +64 -1
  3. umap/context_processors.py +1 -0
  4. umap/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  5. umap/locale/cs_CZ/LC_MESSAGES/django.po +96 -92
  6. umap/locale/de/LC_MESSAGES/django.mo +0 -0
  7. umap/locale/de/LC_MESSAGES/django.po +19 -18
  8. umap/locale/en/LC_MESSAGES/django.po +47 -43
  9. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  10. umap/locale/fr/LC_MESSAGES/django.po +51 -47
  11. umap/locale/pt/LC_MESSAGES/django.mo +0 -0
  12. umap/locale/pt/LC_MESSAGES/django.po +64 -60
  13. umap/management/commands/clean_tilelayer.py +152 -0
  14. umap/management/commands/purge_purgatory.py +28 -0
  15. umap/models.py +27 -2
  16. umap/settings/base.py +2 -0
  17. umap/static/umap/base.css +4 -4
  18. umap/static/umap/css/contextmenu.css +5 -0
  19. umap/static/umap/css/icon.css +7 -2
  20. umap/static/umap/img/16-white.svg +9 -2
  21. umap/static/umap/img/16.svg +3 -0
  22. umap/static/umap/img/source/16-white.svg +10 -3
  23. umap/static/umap/img/source/16.svg +4 -1
  24. umap/static/umap/js/modules/autocomplete.js +7 -3
  25. umap/static/umap/js/modules/browser.js +7 -1
  26. umap/static/umap/js/modules/caption.js +6 -1
  27. umap/static/umap/js/modules/data/features.js +176 -2
  28. umap/static/umap/js/modules/data/layer.js +31 -26
  29. umap/static/umap/js/modules/formatter.js +3 -2
  30. umap/static/umap/js/modules/global.js +2 -0
  31. umap/static/umap/js/modules/importers/communesfr.js +13 -1
  32. umap/static/umap/js/modules/permissions.js +123 -93
  33. umap/static/umap/js/modules/rendering/ui.js +37 -212
  34. umap/static/umap/js/modules/sync/engine.js +365 -14
  35. umap/static/umap/js/modules/sync/hlc.js +106 -0
  36. umap/static/umap/js/modules/sync/updaters.js +4 -4
  37. umap/static/umap/js/modules/sync/websocket.js +1 -1
  38. umap/static/umap/js/modules/ui/base.js +2 -2
  39. umap/static/umap/js/modules/ui/contextmenu.js +34 -17
  40. umap/static/umap/js/modules/urls.js +5 -1
  41. umap/static/umap/js/modules/utils.js +5 -1
  42. umap/static/umap/js/umap.controls.js +47 -47
  43. umap/static/umap/js/umap.core.js +3 -3
  44. umap/static/umap/js/umap.forms.js +3 -1
  45. umap/static/umap/js/umap.js +95 -112
  46. umap/static/umap/locale/br.js +13 -4
  47. umap/static/umap/locale/br.json +13 -4
  48. umap/static/umap/locale/ca.js +21 -12
  49. umap/static/umap/locale/ca.json +21 -12
  50. umap/static/umap/locale/cs_CZ.js +87 -78
  51. umap/static/umap/locale/cs_CZ.json +87 -78
  52. umap/static/umap/locale/de.js +17 -8
  53. umap/static/umap/locale/de.json +17 -8
  54. umap/static/umap/locale/en.js +9 -2
  55. umap/static/umap/locale/en.json +9 -2
  56. umap/static/umap/locale/eu.js +10 -3
  57. umap/static/umap/locale/eu.json +10 -3
  58. umap/static/umap/locale/fa_IR.js +11 -4
  59. umap/static/umap/locale/fa_IR.json +11 -4
  60. umap/static/umap/locale/fr.js +11 -4
  61. umap/static/umap/locale/fr.json +11 -4
  62. umap/static/umap/locale/hu.js +10 -3
  63. umap/static/umap/locale/hu.json +10 -3
  64. umap/static/umap/locale/pt.js +17 -8
  65. umap/static/umap/locale/pt.json +17 -8
  66. umap/static/umap/locale/pt_PT.js +13 -4
  67. umap/static/umap/locale/pt_PT.json +13 -4
  68. umap/static/umap/locale/zh_TW.js +13 -4
  69. umap/static/umap/locale/zh_TW.json +13 -4
  70. umap/static/umap/map.css +7 -22
  71. umap/static/umap/unittests/hlc.js +158 -0
  72. umap/static/umap/unittests/sync.js +321 -15
  73. umap/static/umap/unittests/utils.js +23 -0
  74. umap/static/umap/vendors/georsstogeojson/GeoRSSToGeoJSON.js +111 -80
  75. umap/templates/umap/dashboard_menu.html +4 -2
  76. umap/templates/umap/js.html +0 -4
  77. umap/tests/integration/test_anonymous_owned_map.py +1 -0
  78. umap/tests/integration/test_basics.py +1 -1
  79. umap/tests/integration/test_circles_layer.py +12 -0
  80. umap/tests/integration/test_datalayer.py +5 -0
  81. umap/tests/integration/test_draw_polygon.py +17 -9
  82. umap/tests/integration/test_draw_polyline.py +12 -8
  83. umap/tests/integration/test_edit_datalayer.py +4 -6
  84. umap/tests/integration/test_edit_map.py +1 -1
  85. umap/tests/integration/test_import.py +5 -0
  86. umap/tests/integration/test_map.py +5 -0
  87. umap/tests/integration/test_owned_map.py +1 -1
  88. umap/tests/integration/test_view_polygon.py +12 -12
  89. umap/tests/integration/test_websocket_sync.py +65 -3
  90. umap/tests/test_clean_tilelayer.py +83 -0
  91. umap/tests/test_datalayer.py +24 -0
  92. umap/tests/test_map_views.py +1 -0
  93. umap/tests/test_purge_purgatory.py +25 -0
  94. umap/tests/test_websocket_server.py +22 -0
  95. umap/urls.py +5 -1
  96. umap/views.py +6 -3
  97. umap/websocket_server.py +130 -27
  98. {umap_project-2.6.3.dist-info → umap_project-2.7.0b0.dist-info}/METADATA +9 -9
  99. {umap_project-2.6.3.dist-info → umap_project-2.7.0b0.dist-info}/RECORD +102 -97
  100. umap/static/umap/vendors/contextmenu/leaflet.contextmenu.min.css +0 -1
  101. umap/static/umap/vendors/contextmenu/leaflet.contextmenu.min.js +0 -7
  102. {umap_project-2.6.3.dist-info → umap_project-2.7.0b0.dist-info}/WHEEL +0 -0
  103. {umap_project-2.6.3.dist-info → umap_project-2.7.0b0.dist-info}/entry_points.txt +0 -0
  104. {umap_project-2.6.3.dist-info → umap_project-2.7.0b0.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,46 @@
1
1
  import { DataLayerUpdater, FeatureUpdater, MapUpdater } from './updaters.js'
2
2
  import { WebSocketTransport } from './websocket.js'
3
+ import { HybridLogicalClock } from './hlc.js'
4
+ import * as Utils from '../utils.js'
3
5
 
6
+ /**
7
+ * The syncEngine exposes an API to sync messages between peers over the network.
8
+ *
9
+ * It's taking care of initializing the `transport` layer (sending and receiving
10
+ * messages over websocket), the `operations` list (to store them locally),
11
+ * and the `updaters` to apply messages to the map.
12
+ *
13
+ * You can use the `update`, `upsert` and `delete` methods.
14
+ *
15
+ * @example
16
+ *
17
+ * ```
18
+ * const sync = new SyncEngine(map)
19
+ *
20
+ * // Get the authentication token from the umap server
21
+ * sync.authenticate(tokenURI, webSocketURI, server)
22
+ *
23
+ * // Alternatively, start the engine manually with
24
+ * sync.start(webSocketURI, authToken)
25
+ *
26
+ * // Then use the `upsert`, `update` and `delete` methods.
27
+ * let {metadata, subject} = object.getSyncMetadata()
28
+ * sync.upsert(subject, metadata, "value")
29
+ * sync.update(subject, metadata, "key", "value")
30
+ * sync.delete(subject, metadata, "key")
31
+ * ```
32
+ *
33
+ * A `proxy()` method is also exposed, making it easier to use without having
34
+ * to specify `subject` and `metadata` fields on each call:
35
+ *
36
+ * @example
37
+ * ```
38
+ * // Or using the `proxy()` method:
39
+ * let syncProxy = sync.proxy(object)
40
+ * syncProxy.upsert("value")
41
+ * syncProxy.update("key", "value")
42
+ * ```
43
+ */
4
44
  export class SyncEngine {
5
45
  constructor(map) {
6
46
  this.updaters = {
@@ -9,6 +49,7 @@ export class SyncEngine {
9
49
  datalayer: new DataLayerUpdater(map),
10
50
  }
11
51
  this.transport = undefined
52
+ this._operations = new Operations()
12
53
  }
13
54
 
14
55
  async authenticate(tokenURI, webSocketURI, server) {
@@ -27,6 +68,27 @@ export class SyncEngine {
27
68
  this.transport = undefined
28
69
  }
29
70
 
71
+ upsert(subject, metadata, value) {
72
+ this._send({ verb: 'upsert', subject, metadata, value })
73
+ }
74
+
75
+ update(subject, metadata, key, value) {
76
+ this._send({ verb: 'update', subject, metadata, key, value })
77
+ }
78
+
79
+ delete(subject, metadata, key) {
80
+ this._send({ verb: 'delete', subject, metadata, key })
81
+ }
82
+
83
+ _send(inputMessage) {
84
+ let message = this._operations.addLocal(inputMessage)
85
+
86
+ if (this.offline) return
87
+ if (this.transport) {
88
+ this.transport.send('OperationMessage', message)
89
+ }
90
+ }
91
+
30
92
  _getUpdater(subject, metadata) {
31
93
  if (Object.keys(this.updaters).includes(subject)) {
32
94
  return this.updaters[subject]
@@ -34,32 +96,164 @@ export class SyncEngine {
34
96
  throw new Error(`Unknown updater ${subject}, ${metadata}`)
35
97
  }
36
98
 
37
- // This method is called by the transport layer on new messages
99
+ _applyOperation(operation) {
100
+ const updater = this._getUpdater(operation.subject, operation.metadata)
101
+ updater.applyMessage(operation)
102
+ }
103
+
104
+ /**
105
+ * This is called by the transport layer on new messages,
106
+ * and dispatches the different "on*" methods.
107
+ */
38
108
  receive({ kind, ...payload }) {
39
- if (kind === 'operation') {
40
- const updater = this._getUpdater(payload.subject, payload.metadata)
41
- updater.applyMessage(payload)
109
+ if (kind === 'OperationMessage') {
110
+ this.onOperationMessage(payload)
111
+ } else if (kind === 'JoinResponse') {
112
+ this.onJoinResponse(payload)
113
+ } else if (kind === 'ListPeersResponse') {
114
+ this.onListPeersResponse(payload)
115
+ } else if (kind === 'PeerMessage') {
116
+ debug('received peermessage', payload)
117
+ if (payload.message.verb === 'ListOperationsRequest') {
118
+ this.onListOperationsRequest(payload)
119
+ } else if (payload.message.verb === 'ListOperationsResponse') {
120
+ this.onListOperationsResponse(payload)
121
+ }
42
122
  } else {
43
- throw new Error(`Unknown dispatch kind: ${kind}`)
123
+ throw new Error(`Received unknown message from the websocket server: ${kind}`)
44
124
  }
45
125
  }
46
126
 
47
- _send(message) {
48
- if (this.transport) {
49
- this.transport.send('operation', message)
127
+ /**
128
+ * Received when an operation has been performed by another peer.
129
+ *
130
+ * Stores the passed operation locally and apply it.
131
+ *
132
+ * @param {Object} payload
133
+ */
134
+ onOperationMessage(payload) {
135
+ this._operations.storeRemoteOperations([payload])
136
+ this._applyOperation(payload)
137
+ }
138
+
139
+ /**
140
+ * Received when the server acknowledges the `join` for this peer.
141
+ *
142
+ * @param {Object} payload
143
+ * @param {string} payload.uuid The server-assigned uuid for this peer
144
+ * @param {string[]} payload.peers The list of peers uuids
145
+ */
146
+ onJoinResponse({ uuid, peers }) {
147
+ debug('received join response', { uuid, peers })
148
+ this.uuid = uuid
149
+ this.peers = peers
150
+
151
+ // Get one peer at random
152
+ let randomPeer = this._getRandomPeer()
153
+
154
+ if (randomPeer) {
155
+ // Retrieve the operations which happened before join.
156
+ this.sendToPeer(randomPeer, 'ListOperationsRequest', {
157
+ lastKnownHLC: this._operations.getLastKnownHLC(),
158
+ })
50
159
  }
51
160
  }
52
161
 
53
- upsert(subject, metadata, value) {
54
- this._send({ verb: 'upsert', subject, metadata, value })
162
+ /**
163
+ * Received when the list of peers has changed.
164
+ *
165
+ * @param {Object} payload
166
+ * @param {string[]} payload.peers The list of peers uuids
167
+ */
168
+ onListPeersResponse({ peers }) {
169
+ debug('received peerinfo', { peers })
170
+ this.peers = peers
55
171
  }
56
172
 
57
- update(subject, metadata, key, value) {
58
- this._send({ verb: 'update', subject, metadata, key, value })
173
+ /**
174
+ * Received when another peer asks for the list of operations.
175
+ *
176
+ * @param {Object} payload
177
+ * @param {string} payload.sender the uuid of the requesting peer
178
+ * @param {string} payload.latestKnownHLC the latest known HLC of the requesting peer
179
+ */
180
+ onListOperationsRequest({ sender, lastKnownHLC }) {
181
+ this.sendToPeer(sender, 'ListOperationsResponse', {
182
+ operations: this._operations.getOperationsSince(lastKnownHLC),
183
+ })
59
184
  }
60
185
 
61
- delete(subject, metadata, key) {
62
- this._send({ verb: 'delete', subject, metadata, key })
186
+ /**
187
+ * Received when another peer sends the list of operations.
188
+ *
189
+ * When receiving this message, operations are filtered and applied
190
+ *
191
+ * @param {*} operations The list of (encoded operations)
192
+ */
193
+ onListOperationsResponse({ sender, message }) {
194
+ debug(`received operations from peer ${sender}`, message.operations)
195
+
196
+ if (message.operations.length === 0) return
197
+
198
+ // Get the list of stored operations before this message.
199
+ const remoteOperations = Operations.sort(message.operations)
200
+ this._operations.storeRemoteOperations(remoteOperations)
201
+
202
+ // Sort the local operations only once, see below.
203
+ for (const remote of remoteOperations) {
204
+ if (this._operations.shouldBypassOperation(remote)) {
205
+ debug(
206
+ 'Skipping the following operation, because a newer one has been found locally',
207
+ remote
208
+ )
209
+ } else {
210
+ this._applyOperation(remote)
211
+ }
212
+ }
213
+
214
+ // TODO: compact the changes here?
215
+ // e.g. we might want to :
216
+ // - group cases of multiple updates
217
+ // - not apply changes where we have a more recent version (but store them nevertheless)
218
+
219
+ // 1. Get the list of fields that changed (in the incoming operations)
220
+ // 2. For each field, get the last version
221
+ // 3. Check if we should apply the changes.
222
+
223
+ // For each operation
224
+ // Get the updated key hlc
225
+ // If key.local_hlc > key.remote_hlc: drop
226
+ // Else: apply
227
+ }
228
+
229
+ /**
230
+ * Send a message to another peer (via the transport layer)
231
+ *
232
+ * @param {*} recipient
233
+ * @param {*} verb
234
+ * @param {*} payload
235
+ */
236
+ sendToPeer(recipient, verb, payload) {
237
+ payload.verb = verb
238
+ this.transport.send('PeerMessage', {
239
+ sender: this.uuid,
240
+ recipient: recipient,
241
+ message: payload,
242
+ })
243
+ }
244
+
245
+ /**
246
+ * Selects a peer ID at random within the known ones.
247
+ *
248
+ * @returns {string|bool} the selected peer uuid, or False if none was found.
249
+ */
250
+ _getRandomPeer() {
251
+ let otherPeers = this.peers.filter((p) => p !== this.uuid)
252
+ if (otherPeers.length > 0) {
253
+ const random = Math.floor(Math.random() * otherPeers.length)
254
+ return otherPeers[random]
255
+ }
256
+ return false
63
257
  }
64
258
 
65
259
  /**
@@ -91,3 +285,160 @@ export class SyncEngine {
91
285
  return new Proxy(this, handler)
92
286
  }
93
287
  }
288
+
289
+ /**
290
+ * Registry of local and remote operations, keeping a constant ordering.
291
+ */
292
+ export class Operations {
293
+ constructor() {
294
+ this._hlc = new HybridLogicalClock()
295
+ this._operations = new Array()
296
+ }
297
+
298
+ /**
299
+ * Tick the clock and add store the passed message in the operations list.
300
+ *
301
+ * @param {*} inputMessage
302
+ * @returns {*} clock-aware message
303
+ */
304
+ addLocal(inputMessage) {
305
+ let message = { ...inputMessage, hlc: this._hlc.tick() }
306
+ this._operations.push(message)
307
+ return message
308
+ }
309
+
310
+ /**
311
+ * Returns the current list of operations ordered by their HLC.
312
+ *
313
+ * This DOES NOT modify the list in place, but instead return a new copy.
314
+ *
315
+ * @returns {Array}
316
+ */
317
+ sorted() {
318
+ return Operations.sort(this._operations)
319
+ }
320
+
321
+ /**
322
+ * Static method to order the given list of operations by their HCL.
323
+ *
324
+ * @param {Object[]} operations
325
+ * @returns an ordered copy
326
+ */
327
+ static sort(operations) {
328
+ const copy = [...operations]
329
+ copy.sort((a, b) => (a.hlc < b.hlc ? -1 : 1))
330
+ return copy
331
+ }
332
+
333
+ /**
334
+ * Store a list of remote operations locally
335
+ *
336
+ * Note that operations are not applied as part of this method.
337
+ *
338
+ * - Updates the list of operations with the remote ones.
339
+ * - Updates the clock to reflect these changes.
340
+ *
341
+ * @param {Array} remoteOperations
342
+ */
343
+ storeRemoteOperations(remoteOperations) {
344
+ // get the highest date from the passed operations
345
+ let greatestHLC = remoteOperations
346
+ .map((op) => op.hlc)
347
+ .reduce((max, current) => (current > max ? current : max))
348
+
349
+ // Bump the current HLC.
350
+ this._hlc.receive(greatestHLC)
351
+ this._operations.push(...remoteOperations)
352
+ }
353
+
354
+ /**
355
+ * Get operations that happened since a specific clock tick.
356
+ */
357
+ getOperationsSince(hlc) {
358
+ if (!hlc) return this._operations
359
+ // first get the position of the clock that was sent
360
+ const start = this._operations.findIndex((op) => op.hlc === hlc)
361
+ this._operations.slice(start)
362
+ return this._operations.filter((op) => op.hlc > hlc)
363
+ }
364
+
365
+ /**
366
+ * Returns the last known HLC value.
367
+ */
368
+ getLastKnownHLC() {
369
+ return this._operations.at(-1)?.hlc
370
+ }
371
+
372
+ /**
373
+ * Checks if a given operation should be bypassed.
374
+ *
375
+ * Note that this doesn't only check the clock, but also if the operation share
376
+ * on the same context (subject + metadata).
377
+ *
378
+ * @param {Object} remote the remote operation to compare to
379
+ * @returns bool
380
+ */
381
+ shouldBypassOperation(remote) {
382
+ const sortedLocalOperations = this.sorted()
383
+ // No operations are stored, no need to check
384
+ if (sortedLocalOperations.length <= 0) {
385
+ debug('No operations are stored, no need to check')
386
+ return false
387
+ }
388
+
389
+ // Latest local operation is older than the remote one
390
+ const latest = sortedLocalOperations.at(-1)
391
+ if (latest.hlc < remote.hlc) {
392
+ debug('Latest local operation is older than the remote one')
393
+ return false
394
+ }
395
+
396
+ // Skip operations enabling the sync engine:
397
+ // If we receive something, we are already connected.
398
+ if (
399
+ remote.hasOwnProperty('key') &&
400
+ remote.key === 'options.syncEnabled' &&
401
+ remote.value === true
402
+ ) {
403
+ return true
404
+ }
405
+ for (const local of sortedLocalOperations) {
406
+ if (
407
+ local.hlc > remote.hlc &&
408
+ Operations.haveSameContext(local, remote) &&
409
+ // For now (and until we fix the conflict between updates and upsert)
410
+ // upsert always have priority over other operations
411
+ remote.verb !== 'upsert'
412
+ ) {
413
+ debug('this is newer:', local)
414
+ return true
415
+ }
416
+ }
417
+ return false
418
+ }
419
+
420
+ /**
421
+ * Compares two operations to see if they share the same context.
422
+ *
423
+ * @param {Object} local
424
+ * @param {Object} remote
425
+ * @return {bool} true if the two operations share the same context.
426
+ */
427
+ static haveSameContext(local, remote) {
428
+ const shouldCheckKey =
429
+ local.hasOwnProperty('key') &&
430
+ remote.hasOwnProperty('key') &&
431
+ typeof local.key !== 'undefined' &&
432
+ typeof remote.key !== 'undefined'
433
+
434
+ return (
435
+ Utils.deepEqual(local.subject, remote.subject) &&
436
+ Utils.deepEqual(local.metadata, remote.metadata) &&
437
+ (!shouldCheckKey || (shouldCheckKey && local.key == remote.key))
438
+ )
439
+ }
440
+ }
441
+
442
+ function debug(...args) {
443
+ console.debug('SYNC ⇆', ...args)
444
+ }
@@ -0,0 +1,106 @@
1
+ import * as Utils from '../utils.js'
2
+
3
+ /**
4
+ * This is an implementation of a Hybrid Logical Clock (HLC).
5
+ *
6
+ * There are three parts in the clock:
7
+ *
8
+ * - walltime: the relative clock of each of the peers
9
+ * - NN: a local counter that gets incremented in case of ties.
10
+ * - id: to identify the peer
11
+ *
12
+ * HLCs are used to order operations consistently in distributed systems.
13
+ */
14
+ export class HybridLogicalClock {
15
+ constructor(walltime = Date.now(), nn = 0, id = Utils.generateId()) {
16
+ this._current = { walltime, nn, id }
17
+ }
18
+
19
+ /**
20
+ * Return a serialized version of the current clock
21
+ */
22
+ serialize(clock = this._current) {
23
+ const { walltime, nn, id } = clock
24
+ return `${walltime}:${nn}:${id}`
25
+ }
26
+
27
+ /**
28
+ * Parse a serialized time and return a JS object.
29
+ * @param string raw
30
+ * @returns object
31
+ */
32
+ parse(raw) {
33
+ let tokens = raw.split(':')
34
+
35
+ if (tokens.length !== 3) {
36
+ throw new SyntaxError(`Unable to parse ${raw}`)
37
+ }
38
+ let [walltime, rawNN, id] = tokens
39
+
40
+ let nn = Number.parseInt(rawNN)
41
+ if (Number.isNaN(nn)) {
42
+ nn = 0
43
+ }
44
+ return { walltime, nn, id }
45
+ }
46
+
47
+ /**
48
+ * Increment the current clock by one tick.
49
+ *
50
+ * - If the current time is greater than the last known tip, increment it.
51
+ * - Otherwise, increment the `nn` counter by 1.
52
+ *
53
+ * This allows each tick to be different from each other.
54
+ *
55
+ * @returns a serialized clock
56
+ */
57
+ tick() {
58
+ // Copy the current value of the hlc to avoid concurrency issues
59
+ const current = { ...this._current }
60
+ const now = Date.now()
61
+
62
+ let nextValue
63
+
64
+ if (now > current.walltime) {
65
+ nextValue = { ...current, walltime: now, nn: 0 }
66
+ } else {
67
+ nextValue = { ...current, nn: current.nn + 1 }
68
+ }
69
+
70
+ this._current = nextValue
71
+ return this.serialize(this._current)
72
+ }
73
+
74
+ /**
75
+ * Receive a remote clock info, and update the local clock.
76
+ *
77
+ * - If the current wall time is greater than both local and remote wall time, use the local one.
78
+ * - If the current wall time is the same, increment max (local, remote) `nn` counter by 1.
79
+ * - If remote time is greater, keep the remote time and increment `nn`
80
+ * - Otherwise, keep local values and increment `nn`
81
+ *
82
+ * This allows to take into account clock drifting, when clocks on different peers are getting
83
+ * out of sync.
84
+ **/
85
+ receive(remoteRaw) {
86
+ const local = { ...this._current }
87
+ const remote = this.parse(remoteRaw)
88
+ const now = Date.now()
89
+
90
+ let nextValue
91
+
92
+ if (now > local.walltime && now > remote.walltime) {
93
+ nextValue = { ...local, walltime: now }
94
+ } else if (local.walltime == remote.walltime) {
95
+ let nn = Math.max(local.nn, remote.nn) + 1
96
+ nextValue = { ...local, nn: nn }
97
+ } else if (remote.walltime > local.walltime) {
98
+ nextValue = { ...remote, id: local.id, nn: remote.nn + 1 }
99
+ } else {
100
+ nextValue = { ...local, nn: local.nn + 1 }
101
+ }
102
+
103
+ this._current = nextValue
104
+ return this._current
105
+ }
106
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * This file contains the updaters: classes that are able to convert messages
3
- * received from another party (or the server) to changes on the map.
2
+ * Updaters are classes able to convert messages
3
+ * received from other peers (or from the server) to changes on the map.
4
4
  */
5
5
 
6
6
  class BaseUpdater {
@@ -76,7 +76,7 @@ export class FeatureUpdater extends BaseUpdater {
76
76
  if (feature) {
77
77
  feature.geometry = value.geometry
78
78
  } else {
79
- datalayer.makeFeature(value)
79
+ datalayer.makeFeature(value, false)
80
80
  }
81
81
  }
82
82
 
@@ -85,9 +85,9 @@ export class FeatureUpdater extends BaseUpdater {
85
85
  const feature = this.getFeatureFromMetadata(metadata)
86
86
  if (feature === undefined) {
87
87
  console.error(`Unable to find feature with id = ${metadata.id}.`)
88
+ return
88
89
  }
89
90
  if (key === 'geometry') {
90
- const datalayer = this.getDataLayerFromID(metadata.layerId)
91
91
  const feature = this.getFeatureFromMetadata(metadata, value)
92
92
  feature.geometry = value
93
93
  } else {
@@ -2,7 +2,7 @@ export class WebSocketTransport {
2
2
  constructor(webSocketURI, authToken, messagesReceiver) {
3
3
  this.websocket = new WebSocket(webSocketURI)
4
4
  this.websocket.onopen = () => {
5
- this.send('join', { token: authToken })
5
+ this.send('JoinRequest', { token: authToken })
6
6
  }
7
7
  this.websocket.addEventListener('message', this.onMessage.bind(this))
8
8
  this.receiver = messagesReceiver
@@ -72,9 +72,9 @@ export class Positioned {
72
72
  left = x - this.container.offsetWidth
73
73
  }
74
74
  if (y < window.innerHeight / 2) {
75
- top = y
75
+ top = Math.min(y, window.innerHeight - this.container.offsetHeight)
76
76
  } else {
77
- top = y - this.container.offsetHeight
77
+ top = Math.max(0, y - this.container.offsetHeight)
78
78
  }
79
79
  this.setPosition({ left, top })
80
80
  }
@@ -15,27 +15,44 @@ export default class ContextMenu extends Positioned {
15
15
  })
16
16
  }
17
17
 
18
- open([x, y], items) {
18
+ open([left, top], items) {
19
19
  this.container.innerHTML = ''
20
20
  for (const item of items) {
21
- const li = loadTemplate(
22
- `<li class="${item.className || ''}"><button tabindex="0" class="flat">${item.label}</button></li>`
23
- )
24
- li.addEventListener('click', () => {
25
- this.close()
26
- item.action()
27
- })
28
- this.container.appendChild(li)
21
+ if (item === '-') {
22
+ this.container.appendChild(document.createElement('hr'))
23
+ } else if (typeof item.action === 'string') {
24
+ const li = loadTemplate(
25
+ `<li class="${item.className || ''}"><a tabindex="0" href="${item.action}">${item.label}</a></li>`
26
+ )
27
+ this.container.appendChild(li)
28
+ } else {
29
+ const li = loadTemplate(
30
+ `<li class="${item.className || ''}"><button tabindex="0" class="flat">${item.label}</button></li>`
31
+ )
32
+ li.addEventListener('click', () => {
33
+ this.close()
34
+ item.action()
35
+ })
36
+ this.container.appendChild(li)
37
+ }
29
38
  }
30
39
  document.body.appendChild(this.container)
31
- this.computePosition([x, y])
32
- this.container.querySelector('button').focus()
33
- this.container.addEventListener('keydown', (event) => {
34
- if (event.key === 'Escape') {
35
- event.stopPropagation()
36
- this.close()
37
- }
38
- })
40
+ if (this.options.fixed) {
41
+ this.setPosition({ left, top })
42
+ } else {
43
+ this.computePosition([left, top])
44
+ }
45
+ this.container.querySelector('li > *').focus()
46
+ this.container.addEventListener(
47
+ 'keydown',
48
+ (event) => {
49
+ if (event.key === 'Escape') {
50
+ event.stopPropagation()
51
+ this.close()
52
+ }
53
+ },
54
+ { once: true }
55
+ )
39
56
  }
40
57
 
41
58
  close() {
@@ -5,10 +5,14 @@ export default class URLs {
5
5
  this.urls = serverUrls
6
6
  }
7
7
 
8
+ has(urlName) {
9
+ return urlName in this.urls
10
+ }
11
+
8
12
  get(urlName, params) {
9
13
  if (typeof this[urlName] === 'function') return this[urlName](params)
10
14
 
11
- if (this.urls.hasOwnProperty(urlName)) {
15
+ if (this.has(urlName)) {
12
16
  return template(this.urls[urlName], params)
13
17
  }
14
18
  throw `Unable to find a URL for route ${urlName}`
@@ -178,7 +178,7 @@ export function toHTML(r, options) {
178
178
  }
179
179
 
180
180
  export function isObject(what) {
181
- return typeof what === 'object' && what !== null
181
+ return typeof what === 'object' && what !== null && !Array.isArray(what)
182
182
  }
183
183
 
184
184
  export function CopyJSON(geojson) {
@@ -406,3 +406,7 @@ export class WithTemplate {
406
406
  return this.element
407
407
  }
408
408
  }
409
+
410
+ export function deepEqual(object1, object2){
411
+ return JSON.stringify(object1) === JSON.stringify(object2)
412
+ }