umap-project 2.8.0a2__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 (134) 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/settings/local_s3.py +45 -0
  75. umap/static/umap/content.css +18 -13
  76. umap/static/umap/css/bar.css +4 -0
  77. umap/static/umap/css/form.css +3 -0
  78. umap/static/umap/img/logo_lightcyan.svg +4 -0
  79. umap/static/umap/js/modules/caption.js +73 -73
  80. umap/static/umap/js/modules/data/features.js +20 -5
  81. umap/static/umap/js/modules/data/layer.js +17 -14
  82. umap/static/umap/js/modules/drop.js +55 -0
  83. umap/static/umap/js/modules/importer.js +20 -10
  84. umap/static/umap/js/modules/rendering/icon.js +2 -1
  85. umap/static/umap/js/modules/rendering/map.js +9 -8
  86. umap/static/umap/js/modules/rendering/popup.js +9 -10
  87. umap/static/umap/js/modules/rendering/template.js +53 -9
  88. umap/static/umap/js/modules/rendering/ui.js +6 -2
  89. umap/static/umap/js/modules/request.js +2 -2
  90. umap/static/umap/js/modules/schema.js +1 -0
  91. umap/static/umap/js/modules/sync/engine.js +56 -13
  92. umap/static/umap/js/modules/sync/updaters.js +4 -1
  93. umap/static/umap/js/modules/sync/websocket.js +47 -2
  94. umap/static/umap/js/modules/ui/bar.js +1 -1
  95. umap/static/umap/js/modules/ui/dialog.js +5 -0
  96. umap/static/umap/js/modules/umap.js +62 -25
  97. umap/static/umap/js/modules/utils.js +2 -0
  98. umap/static/umap/js/umap.controls.js +8 -55
  99. umap/static/umap/js/umap.forms.js +44 -0
  100. umap/static/umap/locale/cs_CZ.js +13 -11
  101. umap/static/umap/locale/cs_CZ.json +13 -11
  102. umap/static/umap/locale/en.js +2 -1
  103. umap/static/umap/locale/en.json +2 -1
  104. umap/static/umap/locale/fr.js +2 -1
  105. umap/static/umap/locale/fr.json +2 -1
  106. umap/static/umap/locale/zh_TW.js +13 -11
  107. umap/static/umap/locale/zh_TW.json +13 -11
  108. umap/static/umap/map.css +34 -166
  109. umap/static/umap/unittests/sync.js +4 -1
  110. umap/static/umap/vars.css +0 -1
  111. umap/templates/403.html +12 -0
  112. umap/templates/404.html +4 -13
  113. umap/templates/40x.html +9 -0
  114. umap/templates/base.html +2 -0
  115. umap/templates/umap/components/alerts/alert.html +4 -0
  116. umap/templates/umap/css.html +3 -0
  117. umap/templates/umap/js.html +2 -0
  118. umap/templates/umap/map_init.html +2 -0
  119. umap/templates/umap/user_dashboard.html +2 -0
  120. umap/tests/fixtures/test_upload_simple_marker.json +19 -0
  121. umap/tests/integration/conftest.py +3 -3
  122. umap/tests/integration/test_edit_datalayer.py +11 -0
  123. umap/tests/integration/test_import.py +20 -1
  124. umap/tests/integration/test_websocket_sync.py +69 -0
  125. umap/tests/test_dashboard.py +82 -0
  126. umap/tests/test_team_views.py +35 -1
  127. umap/tests/test_views.py +0 -74
  128. umap/views.py +5 -1
  129. umap/websocket_server.py +8 -1
  130. {umap_project-2.8.0a2.dist-info → umap_project-2.8.1.dist-info}/METADATA +5 -5
  131. {umap_project-2.8.0a2.dist-info → umap_project-2.8.1.dist-info}/RECORD +134 -127
  132. {umap_project-2.8.0a2.dist-info → umap_project-2.8.1.dist-info}/WHEEL +0 -0
  133. {umap_project-2.8.0a2.dist-info → umap_project-2.8.1.dist-info}/entry_points.txt +0 -0
  134. {umap_project-2.8.0a2.dist-info → umap_project-2.8.1.dist-info}/licenses/LICENSE +0 -0
@@ -41,7 +41,7 @@ export class DataLayer extends ServerStored {
41
41
  constructor(umap, leafletMap, data = {}) {
42
42
  super()
43
43
  this._umap = umap
44
- this.sync = umap.sync_engine.proxy(this)
44
+ this.sync = umap.syncEngine.proxy(this)
45
45
  this._index = Array()
46
46
  this._features = {}
47
47
  this._geojson = null
@@ -88,7 +88,6 @@ export class DataLayer extends ServerStored {
88
88
 
89
89
  if (!this.createdOnServer) {
90
90
  if (this.showAtLoad()) this.show()
91
- this.isDirty = true
92
91
  }
93
92
 
94
93
  // Only layers that are displayed on load must be hidden/shown
@@ -151,7 +150,6 @@ export class DataLayer extends ServerStored {
151
150
  for (const field of fields) {
152
151
  this.layer.onEdit(field, builder)
153
152
  }
154
- this.redraw()
155
153
  this.show()
156
154
  break
157
155
  case 'remote-data':
@@ -526,15 +524,13 @@ export class DataLayer extends ServerStored {
526
524
  }
527
525
 
528
526
  async importFromFiles(files, type) {
529
- let all = []
527
+ const toLoad = []
530
528
  for (const file of files) {
531
- const features = await this.importFromFile(file, type)
532
- if (features) {
533
- all = all.concat(features)
534
- }
529
+ toLoad.push(this.importFromFile(file, type))
535
530
  }
531
+ const features = await Promise.all(toLoad)
536
532
  return new Promise((resolve) => {
537
- resolve(all)
533
+ resolve([].concat(...features))
538
534
  })
539
535
  }
540
536
 
@@ -594,7 +590,7 @@ export class DataLayer extends ServerStored {
594
590
  options.name = translate('Clone of {name}', { name: this.options.name })
595
591
  delete options.id
596
592
  const geojson = Utils.CopyJSON(this._geojson)
597
- const datalayer = this._umap.createDataLayer(options)
593
+ const datalayer = this._umap.createDirtyDataLayer(options)
598
594
  datalayer.fromGeoJSON(geojson)
599
595
  return datalayer
600
596
  }
@@ -613,7 +609,10 @@ export class DataLayer extends ServerStored {
613
609
  }
614
610
 
615
611
  reset() {
616
- if (!this.createdOnServer) this.erase()
612
+ if (!this.createdOnServer) {
613
+ this.erase()
614
+ return
615
+ }
617
616
 
618
617
  this.resetOptions()
619
618
  this.parentPane.appendChild(this.pane)
@@ -951,9 +950,13 @@ export class DataLayer extends ServerStored {
951
950
  else this.hide()
952
951
  }
953
952
 
954
- zoomTo(bounds) {
953
+ zoomTo() {
955
954
  if (!this.isVisible()) return
956
- bounds = bounds || this.layer.getBounds()
955
+ const bounds = this.layer.getBounds()
956
+ this.zoomToBounds(bounds)
957
+ }
958
+
959
+ zoomToBounds(bounds) {
957
960
  if (bounds.isValid()) {
958
961
  const options = { maxZoom: this.getOption('zoomTo') }
959
962
  this._leafletMap.fitBounds(bounds, options)
@@ -1065,7 +1068,7 @@ export class DataLayer extends ServerStored {
1065
1068
 
1066
1069
  setReferenceVersion({ response, sync }) {
1067
1070
  this._referenceVersion = response.headers.get('X-Datalayer-Version')
1068
- this.sync.update('_referenceVersion', this._referenceVersion)
1071
+ if (sync) this.sync.update('_referenceVersion', this._referenceVersion)
1069
1072
  }
1070
1073
 
1071
1074
  async save() {
@@ -0,0 +1,55 @@
1
+ export default class DropControl {
2
+ constructor(umap, leafletMap, dropzone) {
3
+ this._umap = umap
4
+ this._leafletMap = leafletMap
5
+ this.dropzone = dropzone
6
+ }
7
+
8
+ enable() {
9
+ this.controller = new AbortController()
10
+ this.dropzone.addEventListener('dragenter', (e) => this.dragenter(e), {
11
+ signal: this.controller.signal,
12
+ })
13
+ this.dropzone.addEventListener('dragover', (e) => this.dragover(e), {
14
+ signal: this.controller.signal,
15
+ })
16
+ this.dropzone.addEventListener('drop', (e) => this.drop(e), {
17
+ signal: this.controller.signal,
18
+ })
19
+ this.dropzone.addEventListener('dragleave', (e) => this.dragleave(e), {
20
+ signal: this.controller.signal,
21
+ })
22
+ }
23
+
24
+ disable() {
25
+ this.controller.abort()
26
+ }
27
+
28
+ dragenter(event) {
29
+ event.stopPropagation()
30
+ event.preventDefault()
31
+ this._leafletMap.scrollWheelZoom.disable()
32
+ this.dropzone.classList.add('umap-dragover')
33
+ }
34
+
35
+ dragover(event) {
36
+ event.stopPropagation()
37
+ event.preventDefault()
38
+ }
39
+
40
+ drop(event) {
41
+ this._leafletMap.scrollWheelZoom.enable()
42
+ this.dropzone.classList.remove('umap-dragover')
43
+ event.stopPropagation()
44
+ event.preventDefault()
45
+ const importer = this._umap.importer
46
+ importer.build()
47
+ importer.files = event.dataTransfer.files
48
+ importer.submit()
49
+ }
50
+
51
+ dragleave() {
52
+ this._leafletMap.scrollWheelZoom.enable()
53
+ this.dropzone.classList.remove('umap-dragover')
54
+ }
55
+ }
@@ -64,7 +64,10 @@ export default class Importer extends Utils.WithTemplate {
64
64
  this.TYPES = ['geojson', 'csv', 'gpx', 'kml', 'osm', 'georss', 'umap']
65
65
  this.IMPORTERS = []
66
66
  this.loadImporters()
67
- this.dialog = new Dialog({ className: 'importers dark' })
67
+ this.dialog = new Dialog({
68
+ className: 'importers dark',
69
+ back: () => this.showImporters(),
70
+ })
68
71
  }
69
72
 
70
73
  loadImporters() {
@@ -121,6 +124,11 @@ export default class Importer extends Utils.WithTemplate {
121
124
  return this.qs('[type=file]').files
122
125
  }
123
126
 
127
+ set files(files) {
128
+ this.qs('[type=file]').files = files
129
+ this.onFileChange()
130
+ }
131
+
124
132
  get raw() {
125
133
  return this.qs('textarea').value
126
134
  }
@@ -158,7 +166,7 @@ export default class Importer extends Utils.WithTemplate {
158
166
  get layer() {
159
167
  return (
160
168
  this._umap.datalayers[this.layerId] ||
161
- this._umap.createDataLayer({ name: this.layerName })
169
+ this._umap.createDirtyDataLayer({ name: this.layerName })
162
170
  )
163
171
  }
164
172
 
@@ -172,7 +180,7 @@ export default class Importer extends Utils.WithTemplate {
172
180
  button.addEventListener('click', () => plugin.open(this))
173
181
  grid.appendChild(button)
174
182
  }
175
- this.dialog.open({ template: element, cancel: false, accept: false })
183
+ this.dialog.open({ template: element, cancel: false, accept: false, back: false })
176
184
  }
177
185
 
178
186
  build() {
@@ -210,11 +218,11 @@ export default class Importer extends Utils.WithTemplate {
210
218
  this.qs('[name=submit').toggleAttribute('disabled', !this.canSubmit())
211
219
  }
212
220
 
213
- onFileChange(e) {
221
+ onFileChange() {
214
222
  let type = ''
215
223
  let newType
216
- for (const file of e.target.files) {
217
- newType = U.Utils.detectFileType(file)
224
+ for (const file of this.files) {
225
+ newType = Utils.detectFileType(file)
218
226
  if (!type && newType) type = newType
219
227
  if (type && newType !== type) {
220
228
  type = ''
@@ -355,9 +363,11 @@ export default class Importer extends Utils.WithTemplate {
355
363
 
356
364
  onSuccess(count) {
357
365
  if (count) {
358
- Alert.success(translate('Successfully imported {count} feature(s)'), {
359
- count: count,
360
- })
366
+ Alert.success(
367
+ translate('Successfully imported {count} feature(s)', {
368
+ count: count,
369
+ })
370
+ )
361
371
  } else {
362
372
  Alert.success(translate('Data successfully imported!'))
363
373
  }
@@ -377,7 +387,7 @@ export default class Importer extends Utils.WithTemplate {
377
387
  bounds.extend(featureBounds)
378
388
  }
379
389
  this.onSuccess(features.length)
380
- layer.zoomTo(bounds)
390
+ layer.zoomToBounds(bounds)
381
391
  }
382
392
  }
383
393
  }
@@ -22,7 +22,7 @@ export function getClass(name) {
22
22
 
23
23
  export const RECENT = []
24
24
 
25
- const BaseIcon = L.DivIcon.extend({
25
+ const BaseIcon = DivIcon.extend({
26
26
  initialize: function (options) {
27
27
  const default_options = {
28
28
  iconSize: null, // Made in css
@@ -86,6 +86,7 @@ const DefaultIcon = BaseIcon.extend({
86
86
  },
87
87
 
88
88
  _setIconStyles: function (img, name) {
89
+ if (this.feature.isActive()) this.options.className += ' umap-icon-active'
89
90
  BaseIcon.prototype._setIconStyles.call(this, img, name)
90
91
  const color = this._getColor()
91
92
  const opacity = this._getOpacity()
@@ -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
 
@@ -233,12 +234,8 @@ const ManageTilelayerMixin = {
233
234
  },
234
235
 
235
236
  updateTileLayers: function () {
236
- const callback = (tilelayer) => {
237
- this.options.tilelayer = tilelayer.toJSON()
238
- this._umap.isDirty = true
239
- }
240
237
  if (this._controls.tilelayersChooser) {
241
- this._controls.tilelayersChooser.openSwitcher({ callback, edit: true })
238
+ this._controls.tilelayersChooser.openSwitcher({ edit: true })
242
239
  }
243
240
  },
244
241
  }
@@ -263,8 +260,12 @@ export const LeafletMap = BaseMap.extend({
263
260
  this.loader.onAdd(this)
264
261
 
265
262
  if (!this.options.noControl) {
266
- DomEvent.on(document.body, 'dataloading', (e) => this.fire('dataloading', e))
267
- DomEvent.on(document.body, 'dataload', (e) => this.fire('dataload', e))
263
+ DomEvent.on(document.body, 'dataloading', (event) =>
264
+ this.fire('dataloading', event.detail)
265
+ )
266
+ DomEvent.on(document.body, 'dataload', (event) =>
267
+ this.fire('dataload', event.detail)
268
+ )
268
269
  this.on('click', this.closeInplaceToolbar)
269
270
  }
270
271
 
@@ -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
+ }
@@ -238,11 +238,15 @@ export const LeafletMarker = Marker.extend({
238
238
  },
239
239
 
240
240
  highlight: function () {
241
- DomUtil.addClass(this.options.icon.elements.main, 'umap-icon-active')
241
+ this.feature.activate()
242
+ this._redraw()
243
+ this._bringToFront()
242
244
  },
243
245
 
244
246
  resetHighlight: function () {
245
- DomUtil.removeClass(this.options.icon.elements.main, 'umap-icon-active')
247
+ this.feature.deactivate()
248
+ this._redraw()
249
+ this._resetZIndex()
246
250
  },
247
251
 
248
252
  getPopupToolbarAnchor: function () {
@@ -47,8 +47,8 @@ class BaseRequest {
47
47
  // In case of error, an alert is sent, but non 20X status are not handled
48
48
  // The consumer must check the response status by hand
49
49
  export class Request extends BaseRequest {
50
- fire(name, params) {
51
- document.body.dispatchEvent(new CustomEvent(name, params))
50
+ fire(name, detail) {
51
+ document.body.dispatchEvent(new CustomEvent(name, { detail }))
52
52
  }
53
53
 
54
54
  async _fetch(method, uri, headers, data) {
@@ -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
  }