umap-project 2.8.2__py3-none-any.whl → 2.9.0__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (260) hide show
  1. umap/__init__.py +1 -1
  2. umap/admin.py +15 -2
  3. umap/asgi.py +12 -7
  4. umap/context_processors.py +1 -0
  5. umap/locale/br/LC_MESSAGES/django.mo +0 -0
  6. umap/locale/br/LC_MESSAGES/django.po +111 -67
  7. umap/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  8. umap/locale/cs_CZ/LC_MESSAGES/django.po +110 -66
  9. umap/locale/el/LC_MESSAGES/django.mo +0 -0
  10. umap/locale/el/LC_MESSAGES/django.po +129 -85
  11. umap/locale/en/LC_MESSAGES/django.po +103 -60
  12. umap/locale/es/LC_MESSAGES/django.mo +0 -0
  13. umap/locale/es/LC_MESSAGES/django.po +114 -69
  14. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  15. umap/locale/fr/LC_MESSAGES/django.po +105 -61
  16. umap/locale/gl/LC_MESSAGES/django.mo +0 -0
  17. umap/locale/gl/LC_MESSAGES/django.po +216 -171
  18. umap/locale/it/LC_MESSAGES/django.mo +0 -0
  19. umap/locale/it/LC_MESSAGES/django.po +142 -98
  20. umap/locale/nl/LC_MESSAGES/django.mo +0 -0
  21. umap/locale/nl/LC_MESSAGES/django.po +196 -151
  22. umap/locale/pt/LC_MESSAGES/django.mo +0 -0
  23. umap/locale/pt/LC_MESSAGES/django.po +115 -71
  24. umap/locale/zh_TW/LC_MESSAGES/django.mo +0 -0
  25. umap/locale/zh_TW/LC_MESSAGES/django.po +109 -65
  26. umap/management/commands/empty_trash.py +12 -1
  27. umap/migrations/0026_datalayer_modified_at_datalayer_share_status.py +26 -0
  28. umap/models.py +43 -13
  29. umap/settings/base.py +5 -2
  30. umap/static/umap/base.css +5 -2
  31. umap/static/umap/content.css +2 -22
  32. umap/static/umap/css/bar.css +39 -10
  33. umap/static/umap/css/contextmenu.css +14 -2
  34. umap/static/umap/css/form.css +33 -39
  35. umap/static/umap/css/icon.css +47 -5
  36. umap/static/umap/css/panel.css +20 -2
  37. umap/static/umap/css/popup.css +0 -1
  38. umap/static/umap/css/tooltip.css +33 -31
  39. umap/static/umap/img/16-white.svg +5 -3
  40. umap/static/umap/img/16.svg +1 -1
  41. umap/static/umap/img/24-white.svg +17 -16
  42. umap/static/umap/img/24.svg +29 -18
  43. umap/static/umap/img/providers/bitbucket.png +0 -0
  44. umap/static/umap/img/providers/github.png +0 -0
  45. umap/static/umap/img/providers/keycloak.png +0 -0
  46. umap/static/umap/img/providers/openstreetmap-oauth2.png +0 -0
  47. umap/static/umap/img/providers/twitter-oauth2.png +0 -0
  48. umap/static/umap/img/source/16-white.svg +6 -4
  49. umap/static/umap/img/source/16.svg +1 -1
  50. umap/static/umap/img/source/24-white.svg +20 -18
  51. umap/static/umap/img/source/24.svg +30 -19
  52. umap/static/umap/js/components/alerts/alert.js +4 -1
  53. umap/static/umap/js/modules/browser.js +8 -8
  54. umap/static/umap/js/modules/caption.js +30 -7
  55. umap/static/umap/js/modules/data/features.js +101 -56
  56. umap/static/umap/js/modules/data/layer.js +108 -83
  57. umap/static/umap/js/modules/form/builder.js +242 -0
  58. umap/static/umap/js/modules/form/fields.js +1346 -0
  59. umap/static/umap/js/modules/formatter.js +9 -8
  60. umap/static/umap/js/modules/help.js +20 -24
  61. umap/static/umap/js/modules/importer.js +6 -3
  62. umap/static/umap/js/modules/permissions.js +11 -6
  63. umap/static/umap/js/modules/rendering/icon.js +5 -1
  64. umap/static/umap/js/modules/rendering/layers/classified.js +12 -8
  65. umap/static/umap/js/modules/rendering/layers/cluster.js +11 -1
  66. umap/static/umap/js/modules/rendering/map.js +1 -23
  67. umap/static/umap/js/modules/rendering/ui.js +20 -38
  68. umap/static/umap/js/modules/rules.js +3 -2
  69. umap/static/umap/js/modules/saving.js +5 -0
  70. umap/static/umap/js/modules/schema.js +8 -6
  71. umap/static/umap/js/modules/share.js +3 -3
  72. umap/static/umap/js/modules/sync/engine.js +56 -26
  73. umap/static/umap/js/modules/sync/updaters.js +15 -6
  74. umap/static/umap/js/modules/sync/websocket.js +50 -37
  75. umap/static/umap/js/modules/tableeditor.js +3 -2
  76. umap/static/umap/js/modules/ui/bar.js +101 -9
  77. umap/static/umap/js/modules/ui/base.js +7 -24
  78. umap/static/umap/js/modules/ui/contextmenu.js +9 -2
  79. umap/static/umap/js/modules/ui/panel.js +5 -1
  80. umap/static/umap/js/modules/ui/tooltip.js +19 -11
  81. umap/static/umap/js/modules/umap.js +121 -68
  82. umap/static/umap/js/modules/utils.js +196 -12
  83. umap/static/umap/js/umap.controls.js +11 -353
  84. umap/static/umap/locale/am_ET.js +17 -5
  85. umap/static/umap/locale/am_ET.json +17 -5
  86. umap/static/umap/locale/ar.js +17 -5
  87. umap/static/umap/locale/ar.json +17 -5
  88. umap/static/umap/locale/ast.js +17 -5
  89. umap/static/umap/locale/ast.json +17 -5
  90. umap/static/umap/locale/bg.js +17 -5
  91. umap/static/umap/locale/bg.json +17 -5
  92. umap/static/umap/locale/br.js +33 -20
  93. umap/static/umap/locale/br.json +33 -20
  94. umap/static/umap/locale/ca.js +17 -5
  95. umap/static/umap/locale/ca.json +17 -5
  96. umap/static/umap/locale/cs_CZ.js +15 -5
  97. umap/static/umap/locale/cs_CZ.json +15 -5
  98. umap/static/umap/locale/da.js +17 -5
  99. umap/static/umap/locale/da.json +17 -5
  100. umap/static/umap/locale/de.js +17 -5
  101. umap/static/umap/locale/de.json +17 -5
  102. umap/static/umap/locale/el.js +63 -51
  103. umap/static/umap/locale/el.json +63 -51
  104. umap/static/umap/locale/en.js +15 -5
  105. umap/static/umap/locale/en.json +15 -5
  106. umap/static/umap/locale/en_US.json +17 -5
  107. umap/static/umap/locale/es.js +25 -13
  108. umap/static/umap/locale/es.json +25 -13
  109. umap/static/umap/locale/et.js +17 -5
  110. umap/static/umap/locale/et.json +17 -5
  111. umap/static/umap/locale/eu.js +17 -5
  112. umap/static/umap/locale/eu.json +17 -5
  113. umap/static/umap/locale/fa_IR.js +17 -5
  114. umap/static/umap/locale/fa_IR.json +17 -5
  115. umap/static/umap/locale/fi.js +17 -5
  116. umap/static/umap/locale/fi.json +17 -5
  117. umap/static/umap/locale/fr.js +16 -6
  118. umap/static/umap/locale/fr.json +16 -6
  119. umap/static/umap/locale/gl.js +357 -345
  120. umap/static/umap/locale/gl.json +357 -345
  121. umap/static/umap/locale/he.js +17 -5
  122. umap/static/umap/locale/he.json +17 -5
  123. umap/static/umap/locale/hr.js +17 -5
  124. umap/static/umap/locale/hr.json +17 -5
  125. umap/static/umap/locale/hu.js +14 -5
  126. umap/static/umap/locale/hu.json +14 -5
  127. umap/static/umap/locale/id.js +17 -5
  128. umap/static/umap/locale/id.json +17 -5
  129. umap/static/umap/locale/is.js +17 -5
  130. umap/static/umap/locale/is.json +17 -5
  131. umap/static/umap/locale/it.js +125 -113
  132. umap/static/umap/locale/it.json +125 -113
  133. umap/static/umap/locale/ja.js +17 -5
  134. umap/static/umap/locale/ja.json +17 -5
  135. umap/static/umap/locale/ko.js +17 -5
  136. umap/static/umap/locale/ko.json +17 -5
  137. umap/static/umap/locale/lt.js +17 -5
  138. umap/static/umap/locale/lt.json +17 -5
  139. umap/static/umap/locale/ms.js +17 -5
  140. umap/static/umap/locale/ms.json +17 -5
  141. umap/static/umap/locale/nl.js +132 -119
  142. umap/static/umap/locale/nl.json +132 -119
  143. umap/static/umap/locale/no.js +17 -5
  144. umap/static/umap/locale/no.json +17 -5
  145. umap/static/umap/locale/pl.js +17 -5
  146. umap/static/umap/locale/pl.json +17 -5
  147. umap/static/umap/locale/pl_PL.json +17 -5
  148. umap/static/umap/locale/pt.js +38 -25
  149. umap/static/umap/locale/pt.json +38 -25
  150. umap/static/umap/locale/pt_BR.js +17 -5
  151. umap/static/umap/locale/pt_BR.json +17 -5
  152. umap/static/umap/locale/pt_PT.js +17 -5
  153. umap/static/umap/locale/pt_PT.json +17 -5
  154. umap/static/umap/locale/ro.js +17 -5
  155. umap/static/umap/locale/ro.json +17 -5
  156. umap/static/umap/locale/ru.js +17 -5
  157. umap/static/umap/locale/ru.json +17 -5
  158. umap/static/umap/locale/sk_SK.js +17 -5
  159. umap/static/umap/locale/sk_SK.json +17 -5
  160. umap/static/umap/locale/sl.js +17 -5
  161. umap/static/umap/locale/sl.json +17 -5
  162. umap/static/umap/locale/sr.js +17 -5
  163. umap/static/umap/locale/sr.json +17 -5
  164. umap/static/umap/locale/sv.js +17 -5
  165. umap/static/umap/locale/sv.json +17 -5
  166. umap/static/umap/locale/th_TH.js +17 -5
  167. umap/static/umap/locale/th_TH.json +17 -5
  168. umap/static/umap/locale/tr.js +17 -5
  169. umap/static/umap/locale/tr.json +17 -5
  170. umap/static/umap/locale/uk_UA.js +17 -5
  171. umap/static/umap/locale/uk_UA.json +17 -5
  172. umap/static/umap/locale/vi.js +17 -5
  173. umap/static/umap/locale/vi.json +17 -5
  174. umap/static/umap/locale/vi_VN.json +17 -5
  175. umap/static/umap/locale/zh.js +17 -5
  176. umap/static/umap/locale/zh.json +17 -5
  177. umap/static/umap/locale/zh_CN.json +17 -5
  178. umap/static/umap/locale/zh_TW.Big5.json +17 -5
  179. umap/static/umap/locale/zh_TW.js +15 -5
  180. umap/static/umap/locale/zh_TW.json +15 -5
  181. umap/static/umap/map.css +29 -76
  182. umap/static/umap/nav.css +6 -3
  183. umap/static/umap/unittests/utils.js +14 -0
  184. umap/static/umap/vars.css +3 -0
  185. umap/static/umap/vendors/dompurify/purify.es.js +138 -354
  186. umap/static/umap/vendors/dompurify/purify.es.mjs.map +1 -1
  187. umap/static/umap/vendors/editable/Leaflet.Editable.js +1 -0
  188. umap/sync/__init__.py +0 -0
  189. umap/sync/app.py +187 -0
  190. umap/sync/payloads.py +56 -0
  191. umap/templates/auth/user_detail.html +4 -0
  192. umap/templates/auth/user_form.html +9 -6
  193. umap/templates/auth/user_stars.html +4 -0
  194. umap/templates/base.html +1 -1
  195. umap/templates/registration/login.html +2 -5
  196. umap/templates/umap/about.html +5 -0
  197. umap/templates/umap/about_summary.html +2 -2
  198. umap/templates/umap/components/provider.html +8 -0
  199. umap/templates/umap/content_footer.html +1 -1
  200. umap/templates/umap/css.html +0 -2
  201. umap/templates/umap/js.html +0 -4
  202. umap/templates/umap/map_detail.html +1 -1
  203. umap/templates/umap/password_change.html +4 -0
  204. umap/templates/umap/password_change_done.html +4 -0
  205. umap/templates/umap/search.html +4 -0
  206. umap/templates/umap/search_bar.html +1 -0
  207. umap/templates/umap/team_confirm_delete.html +4 -0
  208. umap/templates/umap/team_detail.html +4 -0
  209. umap/templates/umap/team_form.html +4 -0
  210. umap/templates/umap/user_dashboard.html +1 -1
  211. umap/templates/umap/user_teams.html +4 -0
  212. umap/tests/base.py +3 -1
  213. umap/tests/integration/conftest.py +16 -23
  214. umap/tests/integration/test_anonymous_owned_map.py +2 -2
  215. umap/tests/integration/test_basics.py +4 -7
  216. umap/tests/integration/test_caption.py +1 -0
  217. umap/tests/integration/test_categorized_layer.py +4 -8
  218. umap/tests/integration/test_choropleth.py +1 -1
  219. umap/tests/integration/test_conditional_rules.py +3 -3
  220. umap/tests/integration/test_draw_polygon.py +14 -22
  221. umap/tests/integration/test_draw_polyline.py +6 -14
  222. umap/tests/integration/test_edit_datalayer.py +11 -11
  223. umap/tests/integration/test_edit_map.py +30 -4
  224. umap/tests/integration/test_edit_marker.py +5 -5
  225. umap/tests/integration/test_edit_polygon.py +6 -6
  226. umap/tests/integration/test_features_id_generation.py +2 -6
  227. umap/tests/integration/test_import.py +115 -29
  228. umap/tests/integration/test_optimistic_merge.py +1 -0
  229. umap/tests/integration/test_owned_map.py +1 -1
  230. umap/tests/integration/test_picto.py +8 -8
  231. umap/tests/integration/test_save.py +3 -2
  232. umap/tests/integration/test_star.py +13 -9
  233. umap/tests/integration/test_tableeditor.py +8 -7
  234. umap/tests/integration/test_view_marker.py +10 -0
  235. umap/tests/integration/test_websocket_sync.py +239 -64
  236. umap/tests/settings.py +2 -0
  237. umap/tests/test_datalayer.py +2 -3
  238. umap/tests/test_datalayer_views.py +20 -1
  239. umap/tests/test_empty_trash.py +10 -3
  240. umap/tests/test_map_views.py +11 -0
  241. umap/utils.py +27 -11
  242. umap/views.py +37 -6
  243. {umap_project-2.8.2.dist-info → umap_project-2.9.0.dist-info}/METADATA +22 -22
  244. {umap_project-2.8.2.dist-info → umap_project-2.9.0.dist-info}/RECORD +247 -248
  245. {umap_project-2.8.2.dist-info → umap_project-2.9.0.dist-info}/WHEEL +1 -1
  246. umap/management/commands/run_websocket_server.py +0 -23
  247. umap/settings/local_s3.py +0 -45
  248. umap/static/umap/bitbucket.png +0 -0
  249. umap/static/umap/github.png +0 -0
  250. umap/static/umap/js/umap.forms.js +0 -1242
  251. umap/static/umap/keycloak.png +0 -0
  252. umap/static/umap/openstreetmap.png +0 -0
  253. umap/static/umap/twitter.png +0 -0
  254. umap/static/umap/vendors/formbuilder/Leaflet.FormBuilder.js +0 -468
  255. umap/static/umap/vendors/toolbar/leaflet.toolbar.css +0 -1
  256. umap/static/umap/vendors/toolbar/leaflet.toolbar.js +0 -1
  257. umap/tests/test_websocket_server.py +0 -22
  258. umap/websocket_server.py +0 -202
  259. {umap_project-2.8.2.dist-info → umap_project-2.9.0.dist-info}/entry_points.txt +0 -0
  260. {umap_project-2.8.2.dist-info → umap_project-2.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,6 +2,7 @@ import * as Utils from '../utils.js'
2
2
  import { HybridLogicalClock } from './hlc.js'
3
3
  import { DataLayerUpdater, FeatureUpdater, MapUpdater } from './updaters.js'
4
4
  import { WebSocketTransport } from './websocket.js'
5
+ import * as SaveManager from '../saving.js'
5
6
 
6
7
  // Start reconnecting after 2 seconds, then double the delay each time
7
8
  // maxing out at 32 seconds.
@@ -61,25 +62,39 @@ export class SyncEngine {
61
62
  this._reconnectTimeout = null
62
63
  this._reconnectDelay = RECONNECT_DELAY
63
64
  this.websocketConnected = false
65
+ this.closeRequested = false
66
+ this.peerId = Utils.generateId()
67
+ }
68
+
69
+ get isOpen() {
70
+ return this.transport?.isOpen
64
71
  }
65
72
 
66
73
  async authenticate() {
74
+ if (this.isOpen) return
67
75
  const websocketTokenURI = this._umap.urls.get('map_websocket_auth_token', {
68
76
  map_id: this._umap.id,
69
77
  })
70
78
 
71
79
  const [response, _, error] = await this._umap.server.get(websocketTokenURI)
72
- if (!error) {
73
- this.start(response.token)
80
+ if (error) {
81
+ this.reconnect()
82
+ return
74
83
  }
84
+ await this.start(response.token)
75
85
  }
76
86
 
77
- start(authToken) {
78
- this.transport = new WebSocketTransport(
79
- this._umap.properties.websocketURI,
87
+ async start(authToken) {
88
+ const path = this._umap.urls.get('ws_sync', { map_id: this._umap.id })
89
+ const protocol = window.location.protocol === 'http:' ? 'ws:' : 'wss:'
90
+ this.transport = new WebSocketTransport(this)
91
+ await this.transport.connect(
92
+ `${protocol}//${window.location.host}${path}`,
80
93
  authToken,
81
- this
94
+ this.peerId,
95
+ this._umap.properties.user?.name
82
96
  )
97
+ this.onConnection()
83
98
  }
84
99
 
85
100
  stop() {
@@ -100,11 +115,11 @@ export class SyncEngine {
100
115
  this.websocketConnected = false
101
116
  this.updaters.map.update({ key: 'numberOfConnectedPeers' })
102
117
 
103
- this._reconnectTimeout = setTimeout(() => {
118
+ this._reconnectTimeout = setTimeout(async () => {
104
119
  if (this._reconnectDelay < MAX_RECONNECT_DELAY) {
105
120
  this._reconnectDelay = this._reconnectDelay * RECONNECT_DELAY_FACTOR
106
121
  }
107
- this.authenticate()
122
+ await this.authenticate()
108
123
  }, this._reconnectDelay)
109
124
  }
110
125
  upsert(subject, metadata, value) {
@@ -119,12 +134,22 @@ export class SyncEngine {
119
134
  this._send({ verb: 'delete', subject, metadata, key })
120
135
  }
121
136
 
137
+ saved() {
138
+ if (this.offline) return
139
+ if (this.transport) {
140
+ this.transport.send('SavedMessage', {
141
+ sender: this.peerId,
142
+ lastKnownHLC: this._operations.getLastKnownHLC(),
143
+ })
144
+ }
145
+ }
146
+
122
147
  _send(inputMessage) {
123
148
  const message = this._operations.addLocal(inputMessage)
124
149
 
125
150
  if (this.offline) return
126
151
  if (this.transport) {
127
- this.transport.send('OperationMessage', message)
152
+ this.transport.send('OperationMessage', { sender: this.peerId, ...message })
128
153
  }
129
154
  }
130
155
 
@@ -140,9 +165,8 @@ export class SyncEngine {
140
165
  updater.applyMessage(operation)
141
166
  }
142
167
 
143
- getNumberOfConnectedPeers() {
144
- if (this.peers) return this.peers.length
145
- return 0
168
+ getPeers() {
169
+ return this.peers || {}
146
170
  }
147
171
 
148
172
  /**
@@ -163,6 +187,8 @@ export class SyncEngine {
163
187
  } else if (payload.message.verb === 'ListOperationsResponse') {
164
188
  this.onListOperationsResponse(payload)
165
189
  }
190
+ } else if (kind === 'SavedMessage') {
191
+ this.onSavedMessage(payload)
166
192
  } else {
167
193
  throw new Error(`Received unknown message from the websocket server: ${kind}`)
168
194
  }
@@ -176,6 +202,8 @@ export class SyncEngine {
176
202
  * @param {Object} payload
177
203
  */
178
204
  onOperationMessage(payload) {
205
+ if (payload.sender === this.peerId) return
206
+ debug('received operation', payload)
179
207
  this._operations.storeRemoteOperations([payload])
180
208
  this._applyOperation(payload)
181
209
  }
@@ -187,9 +215,8 @@ export class SyncEngine {
187
215
  * @param {string} payload.uuid The server-assigned uuid for this peer
188
216
  * @param {string[]} payload.peers The list of peers uuids
189
217
  */
190
- onJoinResponse({ uuid, peers }) {
191
- debug('received join response', { uuid, peers })
192
- this.uuid = uuid
218
+ onJoinResponse({ peer, peers }) {
219
+ debug('received join response', { peer, peers })
193
220
  this.onListPeersResponse({ peers })
194
221
 
195
222
  // Get one peer at random
@@ -210,7 +237,7 @@ export class SyncEngine {
210
237
  * @param {string[]} payload.peers The list of peers uuids
211
238
  */
212
239
  onListPeersResponse({ peers }) {
213
- debug('received peerinfo', { peers })
240
+ debug('received peerinfo', peers)
214
241
  this.peers = peers
215
242
  this.updaters.map.update({ key: 'numberOfConnectedPeers' })
216
243
  }
@@ -240,7 +267,7 @@ export class SyncEngine {
240
267
  * @param {*} operations The list of (encoded operations)
241
268
  */
242
269
  onListOperationsResponse({ sender, message }) {
243
- debug(`received operations from peer ${sender}`, message.operations)
270
+ debug(`received operations list from peer ${sender}`, message.operations)
244
271
 
245
272
  if (message.operations.length === 0) return
246
273
 
@@ -275,6 +302,13 @@ export class SyncEngine {
275
302
  // Else: apply
276
303
  }
277
304
 
305
+ onSavedMessage({ sender, lastKnownHLC }) {
306
+ debug(`received saved message from peer ${sender}`, lastKnownHLC)
307
+ if (lastKnownHLC === this._operations.getLastKnownHLC() && SaveManager.isDirty) {
308
+ SaveManager.clear()
309
+ }
310
+ }
311
+
278
312
  /**
279
313
  * Send a message to another peer (via the transport layer)
280
314
  *
@@ -285,7 +319,7 @@ export class SyncEngine {
285
319
  sendToPeer(recipient, verb, payload) {
286
320
  payload.verb = verb
287
321
  this.transport.send('PeerMessage', {
288
- sender: this.uuid,
322
+ sender: this.peerId,
289
323
  recipient: recipient,
290
324
  message: payload,
291
325
  })
@@ -297,7 +331,7 @@ export class SyncEngine {
297
331
  * @returns {string|bool} the selected peer uuid, or False if none was found.
298
332
  */
299
333
  _getRandomPeer() {
300
- const otherPeers = this.peers.filter((p) => p !== this.uuid)
334
+ const otherPeers = Object.keys(this.peers).filter((p) => p !== this.peerId)
301
335
  if (otherPeers.length > 0) {
302
336
  const random = Math.floor(Math.random() * otherPeers.length)
303
337
  return otherPeers[random]
@@ -345,7 +379,7 @@ export class Operations {
345
379
  }
346
380
 
347
381
  /**
348
- * Tick the clock and add store the passed message in the operations list.
382
+ * Tick the clock and store the passed message in the operations list.
349
383
  *
350
384
  * @param {*} inputMessage
351
385
  * @returns {*} clock-aware message
@@ -474,16 +508,12 @@ export class Operations {
474
508
  * @return {bool} true if the two operations share the same context.
475
509
  */
476
510
  static haveSameContext(local, remote) {
477
- const shouldCheckKey =
478
- local.hasOwnProperty('key') &&
479
- remote.hasOwnProperty('key') &&
480
- typeof local.key !== 'undefined' &&
481
- typeof remote.key !== 'undefined'
511
+ const shouldCheckKey = local.key !== undefined && remote.key !== undefined
482
512
 
483
513
  return (
484
514
  Utils.deepEqual(local.subject, remote.subject) &&
485
515
  Utils.deepEqual(local.metadata, remote.metadata) &&
486
- (!shouldCheckKey || (shouldCheckKey && local.key == remote.key))
516
+ (!shouldCheckKey || (shouldCheckKey && local.key === remote.key))
487
517
  )
488
518
  }
489
519
  }
@@ -54,10 +54,11 @@ 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
- 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
57
+ try {
58
+ this.getDataLayerFromID(value.id)
59
+ } catch {
60
+ this._umap.createDataLayer(value, false)
61
+ }
61
62
  }
62
63
 
63
64
  update({ key, metadata, value }) {
@@ -72,6 +73,14 @@ export class DataLayerUpdater extends BaseUpdater {
72
73
  }
73
74
  datalayer.render([key])
74
75
  }
76
+
77
+ delete({ metadata }) {
78
+ const datalayer = this.getDataLayerFromID(metadata.id)
79
+ if (datalayer) {
80
+ datalayer.del(false)
81
+ datalayer.commitDelete()
82
+ }
83
+ }
75
84
  }
76
85
 
77
86
  export class FeatureUpdater extends BaseUpdater {
@@ -84,7 +93,7 @@ export class FeatureUpdater extends BaseUpdater {
84
93
  upsert({ metadata, value }) {
85
94
  const { id, layerId } = metadata
86
95
  const datalayer = this.getDataLayerFromID(layerId)
87
- const feature = this.getFeatureFromMetadata(metadata, value)
96
+ const feature = this.getFeatureFromMetadata(metadata)
88
97
 
89
98
  if (feature) {
90
99
  feature.geometry = value.geometry
@@ -101,7 +110,7 @@ export class FeatureUpdater extends BaseUpdater {
101
110
  return
102
111
  }
103
112
  if (key === 'geometry') {
104
- const feature = this.getFeatureFromMetadata(metadata, value)
113
+ const feature = this.getFeatureFromMetadata(metadata)
105
114
  feature.geometry = value
106
115
  } else {
107
116
  this.updateObjectValue(feature, key, value)
@@ -3,49 +3,57 @@ const PING_INTERVAL = 30000
3
3
  const FIRST_CONNECTION_TIMEOUT = 2000
4
4
 
5
5
  export class WebSocketTransport {
6
- constructor(webSocketURI, authToken, messagesReceiver) {
6
+ constructor(messagesReceiver) {
7
7
  this.receiver = messagesReceiver
8
- this.closeRequested = false
8
+ }
9
9
 
10
- this.websocket = new WebSocket(webSocketURI)
10
+ async connect(webSocketURI, authToken, peerId, username) {
11
+ return new Promise((resolve, reject) => {
12
+ this.websocket = new WebSocket(webSocketURI)
11
13
 
12
- this.websocket.onopen = () => {
13
- this.send('JoinRequest', { token: authToken })
14
- this.receiver.onConnection()
15
- }
16
- this.websocket.addEventListener('message', this.onMessage.bind(this))
17
- this.websocket.onclose = () => {
18
- console.log('websocket closed')
19
- if (!this.closeRequested) {
20
- console.log('Not requested, reconnecting...')
21
- this.receiver.reconnect()
14
+ this.websocket.onopen = () => {
15
+ this.send('JoinRequest', { token: authToken, peer: peerId, username })
16
+ resolve(this.websocket)
22
17
  }
23
- }
24
-
25
- this.ensureOpen = setInterval(() => {
26
- if (this.websocket.readyState !== WebSocket.OPEN) {
27
- this.websocket.close()
28
- clearInterval(this.ensureOpen)
18
+ this.websocket.addEventListener('message', this.onMessage.bind(this))
19
+ this.websocket.onclose = () => {
20
+ console.log('websocket closed')
21
+ if (!this.receiver.closeRequested) {
22
+ console.log('Not requested, reconnecting...')
23
+ this.receiver.reconnect()
24
+ }
29
25
  }
30
- }, FIRST_CONNECTION_TIMEOUT)
31
26
 
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)
27
+ this.websocket.onerror = (error) => {
28
+ console.log('WS ERROR', error)
47
29
  }
48
- }, PING_INTERVAL)
30
+
31
+ this.ensureOpen = setInterval(() => {
32
+ if (this.websocket.readyState !== WebSocket.OPEN) {
33
+ this.websocket.close()
34
+ clearInterval(this.ensureOpen)
35
+ }
36
+ }, FIRST_CONNECTION_TIMEOUT)
37
+
38
+ // To ensure the connection is still alive, we send ping and expect pong back.
39
+ // Websocket provides a `ping` method to keep the connection alive, but it's
40
+ // unfortunately not possible to access it from the WebSocket object.
41
+ // See https://making.close.com/posts/reliable-websockets/ for more details.
42
+ this.pingInterval = setInterval(() => {
43
+ if (this.websocket.readyState === WebSocket.OPEN) {
44
+ console.log('sending ping')
45
+ this.websocket.send('ping')
46
+ this.pongReceived = false
47
+ setTimeout(() => {
48
+ if (!this.pongReceived) {
49
+ console.warn('No pong received, reconnecting...')
50
+ this.websocket.close()
51
+ clearInterval(this.pingInterval)
52
+ }
53
+ }, PONG_TIMEOUT)
54
+ }
55
+ }, PING_INTERVAL)
56
+ })
49
57
  }
50
58
 
51
59
  onMessage(wsMessage) {
@@ -64,7 +72,12 @@ export class WebSocketTransport {
64
72
  }
65
73
 
66
74
  close() {
67
- this.closeRequested = true
75
+ console.log('Closing')
76
+ this.receiver.closeRequested = true
68
77
  this.websocket.close()
69
78
  }
79
+
80
+ get isOpen() {
81
+ return this.websocket?.readyState === WebSocket.OPEN
82
+ }
70
83
  }
@@ -2,6 +2,7 @@ import { DomEvent, DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
2
2
  import { translate } from './i18n.js'
3
3
  import ContextMenu from './ui/contextmenu.js'
4
4
  import { WithTemplate, loadTemplate } from './utils.js'
5
+ import { MutatingForm } from './form/builder.js'
5
6
 
6
7
  const TEMPLATE = `
7
8
  <table>
@@ -103,7 +104,7 @@ export default class TableEditor extends WithTemplate {
103
104
  }
104
105
 
105
106
  resetProperties() {
106
- this.properties = this.datalayer._propertiesIndex
107
+ this.properties = this.datalayer.allProperties()
107
108
  if (this.properties.length === 0) {
108
109
  this.properties = [U.DEFAULT_LABEL_KEY, 'description']
109
110
  }
@@ -205,7 +206,7 @@ export default class TableEditor extends WithTemplate {
205
206
  const tr = event.target.closest('tr')
206
207
  const feature = this.datalayer.getFeatureById(tr.dataset.feature)
207
208
  const handler = property === 'description' ? 'Textarea' : 'Input'
208
- const builder = new U.FormBuilder(feature, [[field, { handler }]], {
209
+ const builder = new MutatingForm(feature, [[field, { handler }]], {
209
210
  id: `umap-feature-properties_${L.stamp(feature)}`,
210
211
  })
211
212
  cell.innerHTML = ''
@@ -2,13 +2,15 @@ import { DomEvent } from '../../../vendors/leaflet/leaflet-src.esm.js'
2
2
  import { translate } from '../i18n.js'
3
3
  import { WithTemplate } from '../utils.js'
4
4
  import ContextMenu from './contextmenu.js'
5
+ import * as Utils from '../utils.js'
6
+ import { Point, LineString, Polygon } from '../data/features.js'
5
7
 
6
8
  const TOP_BAR_TEMPLATE = `
7
9
  <div class="umap-main-edit-toolbox with-transition dark">
8
10
  <div class="umap-left-edit-toolbox" data-ref="left">
9
11
  <div class="logo"><a class="" href="/" title="${translate('Go to the homepage')}">uMap</a></div>
10
- <button class="map-name" type="button" data-ref="name"></button>
11
- <button class="share-status" type="button" data-ref="share"></button>
12
+ <button class="map-name flat" type="button" data-ref="name"></button>
13
+ <button class="share-status flat" type="button" data-ref="share"></button>
12
14
  </div>
13
15
  <div class="umap-right-edit-toolbox" data-ref="right">
14
16
  <button class="connected-peers round" type="button" data-ref="peers">
@@ -19,7 +21,7 @@ const TOP_BAR_TEMPLATE = `
19
21
  <i class="icon icon-16 icon-profile"></i>
20
22
  <span class="username" data-ref="username"></span>
21
23
  </button>
22
- <button class="umap-help-link" type="button" title="${translate('Help')}" data-ref="help">${translate('Help')}</button>
24
+ <button class="umap-help-link flat" type="button" title="${translate('Help')}" data-ref="help">${translate('Help')}</button>
23
25
  <button class="edit-cancel round" type="button" data-ref="cancel">
24
26
  <i class="icon icon-16 icon-restore"></i>
25
27
  <span class="">${translate('Cancel edits')}</span>
@@ -96,17 +98,22 @@ export class TopBar extends WithTemplate {
96
98
  }
97
99
  })
98
100
 
99
- const connectedPeers = this._umap.sync.getNumberOfConnectedPeers()
100
101
  this.elements.peers.addEventListener('mouseover', () => {
101
- if (!connectedPeers) return
102
+ const connectedPeers = this._umap.sync.getPeers()
103
+ if (!Object.keys(connectedPeers).length) return
104
+ const ul = Utils.loadTemplate(
105
+ `<ul>${Object.entries(connectedPeers)
106
+ .sort((el) => el !== this._umap.user?.name)
107
+ .map(([id, name]) => `<li>${name || translate('Anonymous')}</li>`)
108
+ .join('')}</ul>`
109
+ )
102
110
  this._umap.tooltip.open({
103
- content: translate('{connectedPeers} peer(s) currently connected to this map', {
104
- connectedPeers: connectedPeers,
105
- }),
111
+ content: ul,
106
112
  anchor: this.elements.peers,
107
113
  position: 'bottom',
108
114
  delay: 500,
109
115
  duration: 5000,
116
+ accent: true,
110
117
  })
111
118
  })
112
119
 
@@ -145,7 +152,9 @@ export class TopBar extends WithTemplate {
145
152
  }
146
153
 
147
154
  redraw() {
148
- this.elements.peers.hidden = !this._umap.getProperty('syncEnabled')
155
+ const syncEnabled = this._umap.getProperty('syncEnabled')
156
+ this.elements.peers.hidden = !syncEnabled
157
+ this.elements.cancel.hidden = syncEnabled
149
158
  this.elements.saveLabel.hidden = this._umap.permissions.isDraft()
150
159
  this.elements.saveDraftLabel.hidden = !this._umap.permissions.isDraft()
151
160
  }
@@ -194,3 +203,86 @@ export class BottomBar extends WithTemplate {
194
203
  this.elements.filter.hidden = !showMenus || !this._umap.properties.facetKey
195
204
  }
196
205
  }
206
+
207
+ const EDIT_BAR_TEMPLATE = `
208
+ <ul class="umap-edit-bar dark with-transition">
209
+ <li data-ref="marker"><button type="button" data-getstarted><i class="icon icon-24 icon-marker"></i></button></li>
210
+ <li data-ref="polyline"><button type="button" data-getstarted><i class="icon icon-24 icon-polyline"></i></button></li>
211
+ <li data-ref="multiline" hidden>
212
+ <button type="button" title="${translate('Add a line to the current multi')}"><i class="icon icon-24 icon-multiline"></i></button>
213
+ </li>
214
+ <li data-ref="polygon"><button type="button" data-getstarted><i class="icon icon-24 icon-polygon"></i></button></li>
215
+ <li data-ref="multipolygon" hidden>
216
+ <button type="button" title="${translate('Add a polygon to the current multi')}"><i class="icon icon-24 icon-multipolygon"></i></button>
217
+ </li>
218
+ <hr>
219
+ <li data-ref="caption" hidden><button data-getstarted type="button" title="${translate('Edit map name and caption')}"><i class="icon icon-24 icon-caption"></i></button></li>
220
+ <li data-ref="import" hidden><button type="button"><i class="icon icon-24 icon-upload"></i></button></li>
221
+ <li data-ref="layers" hidden><button type="button" title="${translate('Manage layers')}"><i class="icon icon-24 icon-layers"></i></button></li>
222
+ <li data-ref="tilelayers" hidden><button type="button" title="${translate('Change tilelayers')}"><i class="icon icon-24 icon-tilelayer"></i></button></li>
223
+ <li data-ref="center" hidden><button type="button"><i class="icon icon-24 icon-center"></i></button></li>
224
+ <li data-ref="permissions" hidden><button type="button" title="${translate('Update permissions and editors')}"><i class="icon icon-24 icon-key"></i></button></li>
225
+ <li data-ref="settings" hidden><button data-getstarted type="button" title="${translate('Map advanced properties')}"><i class="icon icon-24 icon-settings"></i></button></li>
226
+ </ul>
227
+ `
228
+
229
+ export class EditBar extends WithTemplate {
230
+ constructor(umap, leafletMap, parent) {
231
+ super()
232
+ this._umap = umap
233
+ this._leafletMap = leafletMap
234
+ this.loadTemplate(EDIT_BAR_TEMPLATE)
235
+ this.parent = parent
236
+ }
237
+
238
+ setup() {
239
+ this.parent.appendChild(this.element)
240
+ DomEvent.disableClickPropagation(this.element)
241
+ this._onClick('marker', () => this._leafletMap.editTools.startMarker())
242
+ this._onClick('polyline', () => this._leafletMap.editTools.startPolyline())
243
+ this._onClick('multiline', () => {
244
+ console.log('click click')
245
+ this._umap.editedFeature.ui.editor.newShape()
246
+ })
247
+ this._onClick('polygon', () => this._leafletMap.editTools.startPolygon())
248
+ this._onClick('multipolygon', () => this._umap.editedFeature.ui.editor.newShape())
249
+ this._onClick('caption', () => this._umap.editCaption())
250
+ this._onClick('import', () => this._umap.importer.open())
251
+ this._onClick('layers', () => this._umap.editDatalayers())
252
+ this._onClick('tilelayers', () => this._leafletMap.editTileLayers())
253
+ this._onClick('center', () => this._umap.editCenter())
254
+ this._onClick('permissions', () => this._umap.permissions.edit())
255
+ this._onClick('settings', () => this._umap.edit())
256
+ this._addTitle('import', 'IMPORT_PANEL')
257
+ this._addTitle('marker', 'DRAW_MARKER')
258
+ this._addTitle('polyline', 'DRAW_LINE')
259
+ this._addTitle('polygon', 'DRAW_POLYGON')
260
+ this._leafletMap.on('seteditedfeature', () => this.redraw())
261
+ }
262
+
263
+ redraw() {
264
+ const editedFeature = this._umap.editedFeature
265
+ this.elements.multiline.hidden = !(editedFeature instanceof LineString)
266
+ this.elements.multipolygon.hidden = !(editedFeature instanceof Polygon)
267
+ this.elements.caption.hidden = this._umap.properties.editMode !== 'advanced'
268
+ this.elements.import.hidden = this._umap.properties.editMode !== 'advanced'
269
+ this.elements.layers.hidden = this._umap.properties.editMode !== 'advanced'
270
+ this.elements.tilelayers.hidden = this._umap.properties.editMode !== 'advanced'
271
+ this.elements.center.hidden = this._umap.properties.editMode !== 'advanced'
272
+ this.elements.permissions.hidden = this._umap.properties.editMode !== 'advanced'
273
+ this.elements.settings.hidden = this._umap.properties.editMode !== 'advanced'
274
+ }
275
+
276
+ _addTitle(ref, label) {
277
+ this.elements[ref].querySelector('button').title = this._umap.help.displayLabel(
278
+ label,
279
+ false
280
+ )
281
+ }
282
+
283
+ _onClick(ref, action) {
284
+ // Put the click on the button, not on the li, but keep the data-ref on the li
285
+ // so to hide/show it when needed.
286
+ this.elements[ref].querySelector('button').addEventListener('click', action)
287
+ }
288
+ }
@@ -2,27 +2,18 @@ export class Positioned {
2
2
  openAt({ anchor, position }) {
3
3
  if (anchor && position === 'top') {
4
4
  this.anchorTop(anchor)
5
- } else if (anchor && position === 'left') {
6
- this.anchorLeft(anchor)
7
5
  } else if (anchor && position === 'bottom') {
8
6
  this.anchorBottom(anchor)
9
- } else {
10
- this.anchorAbsolute()
11
7
  }
12
8
  }
13
9
 
14
- anchorAbsolute() {
15
- this.container.className = ''
16
- const left =
17
- this.parent.offsetLeft +
18
- this.parent.clientWidth / 2 -
19
- this.container.clientWidth / 2
20
- const top = this.parent.offsetTop + 75
21
- this.setPosition({ top: top, left: left })
10
+ toggleClassPosition(position) {
11
+ this.container.classList.toggle('tooltip-bottom', position === 'bottom')
12
+ this.container.classList.toggle('tooltip-top', position === 'top')
22
13
  }
23
14
 
24
15
  anchorTop(el) {
25
- this.container.className = 'tooltip-top'
16
+ this.toggleClassPosition('top')
26
17
  const coords = this.getPosition(el)
27
18
  this.setPosition({
28
19
  left: coords.left - 10,
@@ -31,23 +22,15 @@ export class Positioned {
31
22
  }
32
23
 
33
24
  anchorBottom(el) {
34
- this.container.className = 'tooltip-bottom'
25
+ this.toggleClassPosition('bottom')
35
26
  const coords = this.getPosition(el)
27
+ const selfCoords = this.getPosition(this.container)
36
28
  this.setPosition({
37
- left: coords.left,
29
+ left: coords.left + coords.width / 2 - selfCoords.width / 2,
38
30
  top: coords.bottom + 11,
39
31
  })
40
32
  }
41
33
 
42
- anchorLeft(el) {
43
- this.container.className = 'tooltip-left'
44
- const coords = this.getPosition(el)
45
- this.setPosition({
46
- top: coords.top,
47
- right: document.documentElement.offsetWidth - coords.left + 11,
48
- })
49
- }
50
-
51
34
  getPosition(el) {
52
35
  return el.getBoundingClientRect()
53
36
  }
@@ -10,6 +10,9 @@ export default class ContextMenu extends Positioned {
10
10
  if (options.className) {
11
11
  this.container.classList.add(options.className)
12
12
  }
13
+ if (options.orientation === 'rows') {
14
+ this.container.classList.add('umap-contextmenu-rows')
15
+ }
13
16
  this.container.addEventListener('focusout', (event) => {
14
17
  if (!this.container.contains(event.relatedTarget)) this.close()
15
18
  })
@@ -37,10 +40,14 @@ export default class ContextMenu extends Positioned {
37
40
  )
38
41
  this.container.appendChild(li)
39
42
  } else {
43
+ let content = item.label || ''
44
+ if (item.icon) {
45
+ content = `<i class="icon icon-16 ${item.icon}"></i>${content}`
46
+ }
40
47
  const li = loadTemplate(
41
- `<li class="${item.className || ''}"><button tabindex="0" class="flat">${item.label}</button></li>`
48
+ `<li class="${item.className || ''}"><button tabindex="0" class="flat" title="${item.title || ''}">${content}</button></li>`
42
49
  )
43
- li.addEventListener('click', () => {
50
+ li.firstChild.addEventListener('click', () => {
44
51
  this.close()
45
52
  item.action()
46
53
  })
@@ -25,13 +25,16 @@ export class Panel {
25
25
  return this.container.classList.contains('on')
26
26
  }
27
27
 
28
- open({ content, className, actions = [] } = {}) {
28
+ open({ content, className, highlight, actions = [] } = {}) {
29
29
  if (this.isOpen()) {
30
30
  this.onClose()
31
31
  }
32
32
  this.container.className = `with-transition panel window ${this.className} ${
33
33
  this.mode || ''
34
34
  }`
35
+ if (highlight) {
36
+ this.container.dataset.highlight = highlight
37
+ }
35
38
  document.body.classList.add(`panel-${this.className.split(' ')[0]}-on`)
36
39
  this.container.innerHTML = ''
37
40
  const actionsContainer = DomUtil.create('ul', 'buttons', this.container)
@@ -75,6 +78,7 @@ export class Panel {
75
78
 
76
79
  close() {
77
80
  document.body.classList.remove(`panel-${this.className.split(' ')[0]}-on`)
81
+ this.container.dataset.highlight = null
78
82
  this.onClose()
79
83
  }
80
84