mirror of
https://github.com/umap-project/umap.git
synced 2025-05-11 00:21:49 +02:00
Compare commits
15 commits
24511d796d
...
d4fb92ec56
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d4fb92ec56 | ||
![]() |
650110fe8a | ||
![]() |
36fdb8190c | ||
![]() |
56f2d3d2f9 | ||
![]() |
80152bf4fb | ||
![]() |
9ddca50d21 | ||
![]() |
1996e315e4 | ||
![]() |
dd7641c92e | ||
![]() |
6caf4c3ed1 | ||
![]() |
471abe1f1b | ||
![]() |
4ea5a28f04 | ||
![]() |
d24f05907c | ||
![]() |
0bc4900b16 | ||
![]() |
0e78d58c03 | ||
![]() |
f93d0b5ca7 |
13 changed files with 302 additions and 122 deletions
|
@ -32,6 +32,10 @@
|
|||
background-color: var(--color-lightCyan);
|
||||
color: var(--color-dark);
|
||||
}
|
||||
.dark .off.connected-peers {
|
||||
background-color: var(--color-lightGray);
|
||||
color: var(--color-darkGray);
|
||||
}
|
||||
|
||||
.leaflet-container .edit-cancel,
|
||||
.leaflet-container .edit-disable,
|
||||
|
|
|
@ -1,11 +1,39 @@
|
|||
import { DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
|
||||
import { translate } from './i18n.js'
|
||||
import * as Utils from './utils.js'
|
||||
|
||||
export default class Caption {
|
||||
const TEMPLATE = `
|
||||
<div class="umap-caption">
|
||||
<hgroup>
|
||||
<h3>
|
||||
<i class="icon icon-16 icon-caption icon-block"></i>
|
||||
<span class="map-name" data-ref="name"></span>
|
||||
</h3>
|
||||
<h4 data-ref="author"></h4>
|
||||
</hgroup>
|
||||
<div class="umap-map-description text" data-ref="description"></div>
|
||||
<div class="datalayer-container" data-ref="datalayersContainer"></div>
|
||||
<div class="credits-container">
|
||||
<details>
|
||||
<summary>${translate('Credits')}</summary>
|
||||
<fieldset>
|
||||
<h5>${translate('User content credits')}</h5>
|
||||
<p data-ref="userCredits"></p>
|
||||
<p data-ref="licence">${translate('Map user content has been published under licence')} <a href="#" data-ref="licenceLink"></a></p>
|
||||
<p data-ref="noLicence">${translate('No licence has been set')}</p>
|
||||
<h5>${translate('Map background credits')}</h5>
|
||||
<p><strong data-ref="bgName"></strong> <span data-ref="bgAttribution"></span></p>
|
||||
<p data-ref="poweredBy"></p>
|
||||
</fieldset>
|
||||
</details>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
export default class Caption extends Utils.WithTemplate {
|
||||
constructor(umap, leafletMap) {
|
||||
super()
|
||||
this._umap = umap
|
||||
this._leafletMap = leafletMap
|
||||
this.loadTemplate(TEMPLATE)
|
||||
}
|
||||
|
||||
isOpen() {
|
||||
|
@ -18,94 +46,67 @@ export default class Caption {
|
|||
}
|
||||
|
||||
open() {
|
||||
const container = DomUtil.create('div', 'umap-caption')
|
||||
const hgroup = DomUtil.element({ tagName: 'hgroup', parent: container })
|
||||
DomUtil.createTitle(
|
||||
hgroup,
|
||||
this._umap.getDisplayName(),
|
||||
'icon-caption icon-block',
|
||||
'map-name'
|
||||
)
|
||||
const title = Utils.loadTemplate('<h4></h4>')
|
||||
hgroup.appendChild(title)
|
||||
this._umap.addAuthorLink(title)
|
||||
this.elements.name.textContent = this._umap.getDisplayName()
|
||||
this.elements.author.innerHTML = ''
|
||||
this._umap.addAuthorLink(this.elements.author)
|
||||
if (this._umap.properties.description) {
|
||||
const description = DomUtil.element({
|
||||
tagName: 'div',
|
||||
className: 'umap-map-description text',
|
||||
safeHTML: Utils.toHTML(this._umap.properties.description),
|
||||
parent: container,
|
||||
})
|
||||
this.elements.description.innerHTML = Utils.toHTML(
|
||||
this._umap.properties.description
|
||||
)
|
||||
} else {
|
||||
this.elements.description.hidden = true
|
||||
}
|
||||
const datalayerContainer = DomUtil.create('div', 'datalayer-container', container)
|
||||
this.elements.datalayersContainer.innerHTML = ''
|
||||
this._umap.eachDataLayerReverse((datalayer) =>
|
||||
this.addDataLayer(datalayer, datalayerContainer)
|
||||
this.addDataLayer(datalayer, this.elements.datalayersContainer)
|
||||
)
|
||||
const creditsContainer = DomUtil.create('div', 'credits-container', container)
|
||||
this.addCredits(creditsContainer)
|
||||
this._umap.panel.open({ content: container }).then(() => {
|
||||
this.addCredits()
|
||||
this._umap.panel.open({ content: this.element }).then(() => {
|
||||
// Create the legend when the panel is actually on the DOM
|
||||
this._umap.eachDataLayerReverse((datalayer) => datalayer.renderLegend())
|
||||
})
|
||||
}
|
||||
|
||||
addDataLayer(datalayer, container) {
|
||||
addDataLayer(datalayer, parent) {
|
||||
if (!datalayer.options.inCaption) return
|
||||
const p = DomUtil.create('p', `caption-item ${datalayer.cssId}`, container)
|
||||
const legend = DomUtil.create('span', 'datalayer-legend', p)
|
||||
const headline = DomUtil.create('strong', '', p)
|
||||
const template = `
|
||||
<p class="caption-item ${datalayer.cssId}">
|
||||
<span class="datalayer-legend"></span>
|
||||
<strong data-ref="toolbox"></strong>
|
||||
<span class="text" data-ref="description"></span>
|
||||
</p>`
|
||||
const [element, { toolbox, description }] = Utils.loadTemplateWithRefs(template)
|
||||
if (datalayer.options.description) {
|
||||
DomUtil.element({
|
||||
tagName: 'span',
|
||||
parent: p,
|
||||
safeHTML: Utils.toHTML(datalayer.options.description),
|
||||
})
|
||||
description.innerHTML = Utils.toHTML(datalayer.options.description)
|
||||
} else {
|
||||
description.hidden = true
|
||||
}
|
||||
datalayer.renderToolbox(headline)
|
||||
DomUtil.add('span', '', headline, `${datalayer.options.name} `)
|
||||
datalayer.renderToolbox(toolbox)
|
||||
parent.appendChild(element)
|
||||
// Use textContent for security
|
||||
const name = Utils.loadTemplate('<span></span>')
|
||||
name.textContent = datalayer.options.name
|
||||
toolbox.appendChild(name)
|
||||
}
|
||||
|
||||
addCredits(container) {
|
||||
const credits = DomUtil.createFieldset(container, translate('Credits'))
|
||||
let title = DomUtil.add('h5', '', credits, translate('User content credits'))
|
||||
addCredits() {
|
||||
if (this._umap.properties.shortCredit || this._umap.properties.longCredit) {
|
||||
DomUtil.element({
|
||||
tagName: 'p',
|
||||
parent: credits,
|
||||
safeHTML: Utils.toHTML(
|
||||
this._umap.properties.longCredit || this._umap.properties.shortCredit
|
||||
),
|
||||
})
|
||||
}
|
||||
if (this._umap.properties.licence) {
|
||||
const licence = DomUtil.add(
|
||||
'p',
|
||||
'',
|
||||
credits,
|
||||
`${translate('Map user content has been published under licence')} `
|
||||
)
|
||||
DomUtil.createLink(
|
||||
'',
|
||||
licence,
|
||||
this._umap.properties.licence.name,
|
||||
this._umap.properties.licence.url
|
||||
this.elements.userCredits.innerHTML = Utils.toHTML(
|
||||
this._umap.properties.longCredit || this._umap.properties.shortCredit
|
||||
)
|
||||
} else {
|
||||
DomUtil.add('p', '', credits, translate('No licence has been set'))
|
||||
this.elements.userCredits.hidden = true
|
||||
}
|
||||
title = DomUtil.create('h5', '', credits)
|
||||
title.textContent = translate('Map background credits')
|
||||
const tilelayerCredit = DomUtil.create('p', '', credits)
|
||||
DomUtil.element({
|
||||
tagName: 'strong',
|
||||
parent: tilelayerCredit,
|
||||
textContent: `${this._leafletMap.selectedTilelayer.options.name} `,
|
||||
})
|
||||
DomUtil.element({
|
||||
tagName: 'span',
|
||||
parent: tilelayerCredit,
|
||||
safeHTML: this._leafletMap.selectedTilelayer.getAttribution(),
|
||||
})
|
||||
if (this._umap.properties.licence) {
|
||||
this.elements.licenceLink.href = this._umap.properties.licence.url
|
||||
this.elements.licenceLink.textContent = this._umap.properties.licence.name
|
||||
this.elements.noLicence.hidden = true
|
||||
} else {
|
||||
this.elements.licence.hidden = true
|
||||
}
|
||||
this.elements.bgName.textContent = this._leafletMap.selectedTilelayer.options.name
|
||||
this.elements.bgAttribution.innerHTML =
|
||||
this._leafletMap.selectedTilelayer.getAttribution()
|
||||
const urls = {
|
||||
leaflet: 'http://leafletjs.com',
|
||||
django: 'https://www.djangoproject.com',
|
||||
|
@ -113,7 +114,7 @@ export default class Caption {
|
|||
changelog: 'https://docs.umap-project.org/en/master/changelog/',
|
||||
version: this._umap.properties.umap_version,
|
||||
}
|
||||
const creditHTML = translate(
|
||||
this.elements.poweredBy.innerHTML = translate(
|
||||
`
|
||||
Powered by <a href="{leaflet}">Leaflet</a> and
|
||||
<a href="{django}">Django</a>,
|
||||
|
@ -122,6 +123,5 @@ export default class Caption {
|
|||
`,
|
||||
urls
|
||||
)
|
||||
DomUtil.element({ tagName: 'p', innerHTML: creditHTML, parent: credits })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import loadPopup from '../rendering/popup.js'
|
|||
class Feature {
|
||||
constructor(umap, datalayer, geojson = {}, id = null) {
|
||||
this._umap = umap
|
||||
this.sync = umap.sync_engine.proxy(this)
|
||||
this.sync = umap.syncEngine.proxy(this)
|
||||
this._marked_for_deletion = false
|
||||
this._isDirty = false
|
||||
this._ui = null
|
||||
|
|
|
@ -41,7 +41,7 @@ export class DataLayer extends ServerStored {
|
|||
constructor(umap, leafletMap, data = {}) {
|
||||
super()
|
||||
this._umap = umap
|
||||
this.sync = umap.sync_engine.proxy(this)
|
||||
this.sync = umap.syncEngine.proxy(this)
|
||||
this._index = Array()
|
||||
this._features = {}
|
||||
this._geojson = null
|
||||
|
@ -88,7 +88,6 @@ export class DataLayer extends ServerStored {
|
|||
|
||||
if (!this.createdOnServer) {
|
||||
if (this.showAtLoad()) this.show()
|
||||
this.isDirty = true
|
||||
}
|
||||
|
||||
// Only layers that are displayed on load must be hidden/shown
|
||||
|
@ -151,7 +150,6 @@ export class DataLayer extends ServerStored {
|
|||
for (const field of fields) {
|
||||
this.layer.onEdit(field, builder)
|
||||
}
|
||||
this.redraw()
|
||||
this.show()
|
||||
break
|
||||
case 'remote-data':
|
||||
|
@ -592,7 +590,7 @@ export class DataLayer extends ServerStored {
|
|||
options.name = translate('Clone of {name}', { name: this.options.name })
|
||||
delete options.id
|
||||
const geojson = Utils.CopyJSON(this._geojson)
|
||||
const datalayer = this._umap.createDataLayer(options)
|
||||
const datalayer = this._umap.createDirtyDataLayer(options)
|
||||
datalayer.fromGeoJSON(geojson)
|
||||
return datalayer
|
||||
}
|
||||
|
@ -1066,7 +1064,7 @@ export class DataLayer extends ServerStored {
|
|||
|
||||
setReferenceVersion({ response, sync }) {
|
||||
this._referenceVersion = response.headers.get('X-Datalayer-Version')
|
||||
this.sync.update('_referenceVersion', this._referenceVersion)
|
||||
if (sync) this.sync.update('_referenceVersion', this._referenceVersion)
|
||||
}
|
||||
|
||||
async save() {
|
||||
|
|
|
@ -161,7 +161,7 @@ export default class Importer extends Utils.WithTemplate {
|
|||
get layer() {
|
||||
return (
|
||||
this._umap.datalayers[this.layerId] ||
|
||||
this._umap.createDataLayer({ name: this.layerName })
|
||||
this._umap.createDirtyDataLayer({ name: this.layerName })
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,12 @@ import { HybridLogicalClock } from './hlc.js'
|
|||
import { DataLayerUpdater, FeatureUpdater, MapUpdater } from './updaters.js'
|
||||
import { WebSocketTransport } from './websocket.js'
|
||||
|
||||
// Start reconnecting after 2 seconds, then double the delay each time
|
||||
// maxing out at 32 seconds.
|
||||
const RECONNECT_DELAY = 2000
|
||||
const RECONNECT_DELAY_FACTOR = 2
|
||||
const MAX_RECONNECT_DELAY = 32000
|
||||
|
||||
/**
|
||||
* The syncEngine exposes an API to sync messages between peers over the network.
|
||||
*
|
||||
|
@ -42,32 +48,65 @@ import { WebSocketTransport } from './websocket.js'
|
|||
* ```
|
||||
*/
|
||||
export class SyncEngine {
|
||||
constructor(map) {
|
||||
constructor(umap) {
|
||||
this._umap = umap
|
||||
this.updaters = {
|
||||
map: new MapUpdater(map),
|
||||
feature: new FeatureUpdater(map),
|
||||
datalayer: new DataLayerUpdater(map),
|
||||
map: new MapUpdater(umap),
|
||||
feature: new FeatureUpdater(umap),
|
||||
datalayer: new DataLayerUpdater(umap),
|
||||
}
|
||||
this.transport = undefined
|
||||
this._operations = new Operations()
|
||||
|
||||
this._reconnectTimeout = null
|
||||
this._reconnectDelay = RECONNECT_DELAY
|
||||
this.websocketConnected = false
|
||||
}
|
||||
|
||||
async authenticate(tokenURI, webSocketURI, server) {
|
||||
const [response, _, error] = await server.get(tokenURI)
|
||||
async authenticate() {
|
||||
const websocketTokenURI = this._umap.urls.get('map_websocket_auth_token', {
|
||||
map_id: this._umap.id,
|
||||
})
|
||||
|
||||
const [response, _, error] = await this._umap.server.get(websocketTokenURI)
|
||||
if (!error) {
|
||||
this.start(webSocketURI, response.token)
|
||||
this.start(response.token)
|
||||
}
|
||||
}
|
||||
|
||||
start(webSocketURI, authToken) {
|
||||
this.transport = new WebSocketTransport(webSocketURI, authToken, this)
|
||||
start(authToken) {
|
||||
this.transport = new WebSocketTransport(
|
||||
this._umap.properties.websocketURI,
|
||||
authToken,
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.transport) this.transport.close()
|
||||
if (this.transport) {
|
||||
this.transport.close()
|
||||
}
|
||||
this.transport = undefined
|
||||
}
|
||||
|
||||
onConnection() {
|
||||
this._reconnectTimeout = null
|
||||
this._reconnectDelay = RECONNECT_DELAY
|
||||
this.websocketConnected = true
|
||||
this.updaters.map.update({ key: 'numberOfConnectedPeers' })
|
||||
}
|
||||
|
||||
reconnect() {
|
||||
this.websocketConnected = false
|
||||
this.updaters.map.update({ key: 'numberOfConnectedPeers' })
|
||||
|
||||
this._reconnectTimeout = setTimeout(() => {
|
||||
if (this._reconnectDelay < MAX_RECONNECT_DELAY) {
|
||||
this._reconnectDelay = this._reconnectDelay * RECONNECT_DELAY_FACTOR
|
||||
}
|
||||
this.authenticate()
|
||||
}, this._reconnectDelay)
|
||||
}
|
||||
upsert(subject, metadata, value) {
|
||||
this._send({ verb: 'upsert', subject, metadata, value })
|
||||
}
|
||||
|
@ -183,9 +222,13 @@ export class SyncEngine {
|
|||
* @param {string} payload.sender the uuid of the requesting peer
|
||||
* @param {string} payload.latestKnownHLC the latest known HLC of the requesting peer
|
||||
*/
|
||||
onListOperationsRequest({ sender, lastKnownHLC }) {
|
||||
onListOperationsRequest({ sender, message }) {
|
||||
debug(
|
||||
`received operations request from peer ${sender} (since ${message.lastKnownHLC})`
|
||||
)
|
||||
|
||||
this.sendToPeer(sender, 'ListOperationsResponse', {
|
||||
operations: this._operations.getOperationsSince(lastKnownHLC),
|
||||
operations: this._operations.getOperationsSince(message.lastKnownHLC),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -446,5 +489,5 @@ export class Operations {
|
|||
}
|
||||
|
||||
function debug(...args) {
|
||||
console.debug('SYNC ⇆', ...args)
|
||||
console.debug('SYNC ⇆', ...args.map((x) => JSON.stringify(x)))
|
||||
}
|
||||
|
|
|
@ -54,7 +54,10 @@ export class MapUpdater extends BaseUpdater {
|
|||
export class DataLayerUpdater extends BaseUpdater {
|
||||
upsert({ value }) {
|
||||
// Upsert only happens when a new datalayer is created.
|
||||
this._umap.createDataLayer(value, false)
|
||||
const datalayer = this._umap.createDataLayer(value, false)
|
||||
// Prevent the layer to get data from the server, as it will get it
|
||||
// from the sync.
|
||||
datalayer._loaded = true
|
||||
}
|
||||
|
||||
update({ key, metadata, value }) {
|
||||
|
|
|
@ -1,15 +1,59 @@
|
|||
const PONG_TIMEOUT = 5000
|
||||
const PING_INTERVAL = 30000
|
||||
const FIRST_CONNECTION_TIMEOUT = 2000
|
||||
|
||||
export class WebSocketTransport {
|
||||
constructor(webSocketURI, authToken, messagesReceiver) {
|
||||
this.receiver = messagesReceiver
|
||||
this.closeRequested = false
|
||||
|
||||
this.websocket = new WebSocket(webSocketURI)
|
||||
|
||||
this.websocket.onopen = () => {
|
||||
this.send('JoinRequest', { token: authToken })
|
||||
this.receiver.onConnection()
|
||||
}
|
||||
this.websocket.addEventListener('message', this.onMessage.bind(this))
|
||||
this.receiver = messagesReceiver
|
||||
this.websocket.onclose = () => {
|
||||
console.log('websocket closed')
|
||||
if (!this.closeRequested) {
|
||||
console.log('Not requested, reconnecting...')
|
||||
this.receiver.reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
this.ensureOpen = setInterval(() => {
|
||||
if (this.websocket.readyState !== WebSocket.OPEN) {
|
||||
this.websocket.close()
|
||||
clearInterval(this.ensureOpen)
|
||||
}
|
||||
}, FIRST_CONNECTION_TIMEOUT)
|
||||
|
||||
// To ensure the connection is still alive, we send ping and expect pong back.
|
||||
// Websocket provides a `ping` method to keep the connection alive, but it's
|
||||
// unfortunately not possible to access it from the WebSocket object.
|
||||
// See https://making.close.com/posts/reliable-websockets/ for more details.
|
||||
this.pingInterval = setInterval(() => {
|
||||
if (this.websocket.readyState === WebSocket.OPEN) {
|
||||
this.websocket.send('ping')
|
||||
this.pongReceived = false
|
||||
setTimeout(() => {
|
||||
if (!this.pongReceived) {
|
||||
console.warn('No pong received, reconnecting...')
|
||||
this.websocket.close()
|
||||
clearInterval(this.pingInterval)
|
||||
}
|
||||
}, PONG_TIMEOUT)
|
||||
}
|
||||
}, PING_INTERVAL)
|
||||
}
|
||||
|
||||
onMessage(wsMessage) {
|
||||
this.receiver.receive(JSON.parse(wsMessage.data))
|
||||
if (wsMessage.data === 'pong') {
|
||||
this.pongReceived = true
|
||||
} else {
|
||||
this.receiver.receive(JSON.parse(wsMessage.data))
|
||||
}
|
||||
}
|
||||
|
||||
send(kind, payload) {
|
||||
|
@ -20,6 +64,7 @@ export class WebSocketTransport {
|
|||
}
|
||||
|
||||
close() {
|
||||
this.closeRequested = true
|
||||
this.websocket.close()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,8 +61,6 @@ export default class Umap extends ServerStored {
|
|||
)
|
||||
this.searchParams = new URLSearchParams(window.location.search)
|
||||
|
||||
this.sync_engine = new SyncEngine(this)
|
||||
this.sync = this.sync_engine.proxy(this)
|
||||
// Locale name (pt_PT, en_US…)
|
||||
// To be used for Django localization
|
||||
if (geojson.properties.locale) setLocale(geojson.properties.locale)
|
||||
|
@ -124,6 +122,9 @@ export default class Umap extends ServerStored {
|
|||
this.share = new Share(this)
|
||||
this.rules = new Rules(this)
|
||||
|
||||
this.syncEngine = new SyncEngine(this)
|
||||
this.sync = this.syncEngine.proxy(this)
|
||||
|
||||
if (this.hasEditMode()) {
|
||||
this.editPanel = new EditPanel(this, this._leafletMap)
|
||||
this.fullPanel = new FullPanel(this, this._leafletMap)
|
||||
|
@ -323,14 +324,14 @@ export default class Umap extends ServerStored {
|
|||
dataUrl = decodeURIComponent(dataUrl)
|
||||
dataUrl = this.renderUrl(dataUrl)
|
||||
dataUrl = this.proxyUrl(dataUrl)
|
||||
const datalayer = this.createDataLayer()
|
||||
const datalayer = this.createDirtyDataLayer()
|
||||
await datalayer
|
||||
.importFromUrl(dataUrl, dataFormat)
|
||||
.then(() => datalayer.zoomTo())
|
||||
}
|
||||
} else if (data) {
|
||||
data = decodeURIComponent(data)
|
||||
const datalayer = this.createDataLayer()
|
||||
const datalayer = this.createDirtyDataLayer()
|
||||
await datalayer.importRaw(data, dataFormat).then(() => datalayer.zoomTo())
|
||||
}
|
||||
}
|
||||
|
@ -598,8 +599,14 @@ export default class Umap extends ServerStored {
|
|||
return datalayer
|
||||
}
|
||||
|
||||
createDirtyDataLayer(options) {
|
||||
const datalayer = this.createDataLayer(options, true)
|
||||
datalayer.isDirty = true
|
||||
return datalayer
|
||||
}
|
||||
|
||||
newDataLayer() {
|
||||
const datalayer = this.createDataLayer({})
|
||||
const datalayer = this.createDirtyDataLayer({})
|
||||
datalayer.edit()
|
||||
}
|
||||
|
||||
|
@ -1257,18 +1264,13 @@ export default class Umap extends ServerStored {
|
|||
}
|
||||
|
||||
async initSyncEngine() {
|
||||
// this.properties.websocketEnabled is set by the server admin
|
||||
if (this.properties.websocketEnabled === false) return
|
||||
// this.properties.syncEnabled is set by the user in the map settings
|
||||
if (this.properties.syncEnabled !== true) {
|
||||
this.sync.stop()
|
||||
} else {
|
||||
const ws_token_uri = this.urls.get('map_websocket_auth_token', {
|
||||
map_id: this.id,
|
||||
})
|
||||
await this.sync.authenticate(
|
||||
ws_token_uri,
|
||||
this.properties.websocketURI,
|
||||
this.server
|
||||
)
|
||||
await this.sync.authenticate()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1343,7 +1345,17 @@ export default class Umap extends ServerStored {
|
|||
},
|
||||
numberOfConnectedPeers: () => {
|
||||
Utils.eachElement('.connected-peers span', (el) => {
|
||||
el.textContent = this.sync.getNumberOfConnectedPeers()
|
||||
if (this.sync.websocketConnected) {
|
||||
el.textContent = this.sync.getNumberOfConnectedPeers()
|
||||
} else {
|
||||
el.textContent = translate('Disconnected')
|
||||
}
|
||||
el.parentElement.classList.toggle('off', !this.sync.websocketConnected)
|
||||
})
|
||||
},
|
||||
'properties.starred': () => {
|
||||
Utils.eachElement('.map-star', (el) => {
|
||||
el.classList.toggle('starred', this.properties.starred)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
@ -1383,7 +1395,7 @@ export default class Umap extends ServerStored {
|
|||
fallback.show()
|
||||
return fallback
|
||||
}
|
||||
return this.createDataLayer()
|
||||
return this.createDirtyDataLayer()
|
||||
}
|
||||
|
||||
findDataLayer(method, context) {
|
||||
|
@ -1531,7 +1543,7 @@ export default class Umap extends ServerStored {
|
|||
? translate('Map has been starred')
|
||||
: translate('Map has been unstarred')
|
||||
)
|
||||
this.render(['starred'])
|
||||
this.render(['properties.starred'])
|
||||
}
|
||||
|
||||
processFileToImport(file, layer, type) {
|
||||
|
@ -1547,7 +1559,7 @@ export default class Umap extends ServerStored {
|
|||
if (type === 'umap') {
|
||||
this.importUmapFile(file, 'umap')
|
||||
} else {
|
||||
if (!layer) layer = this.createDataLayer({ name: file.name })
|
||||
if (!layer) layer = this.createDirtyDataLayer({ name: file.name })
|
||||
layer.importFromFile(file, type)
|
||||
}
|
||||
}
|
||||
|
@ -1573,7 +1585,7 @@ export default class Umap extends ServerStored {
|
|||
delete geojson._storage
|
||||
}
|
||||
delete geojson._umap_options?.id // Never trust an id at this stage
|
||||
const dataLayer = this.createDataLayer(geojson._umap_options)
|
||||
const dataLayer = this.createDirtyDataLayer(geojson._umap_options)
|
||||
dataLayer.fromUmapGeoJSON(geojson)
|
||||
}
|
||||
|
||||
|
|
|
@ -541,11 +541,7 @@ U.StarControl = L.Control.Button.extend({
|
|||
options: {
|
||||
position: 'topleft',
|
||||
title: L._('Star this map'),
|
||||
},
|
||||
|
||||
getClassName: function () {
|
||||
const status = this._umap.properties.starred ? ' starred' : ''
|
||||
return `leaflet-control-star umap-control${status}`
|
||||
className: 'leaflet-control-star map-star umap-control',
|
||||
},
|
||||
|
||||
onClick: function () {
|
||||
|
|
|
@ -8,8 +8,11 @@ import { MapUpdater } from '../js/modules/sync/updaters.js'
|
|||
import { SyncEngine, Operations } from '../js/modules/sync/engine.js'
|
||||
|
||||
describe('SyncEngine', () => {
|
||||
const websocketTokenURI = 'http://localhost:8000/api/v1/maps/1/websocket_auth_token/'
|
||||
const websocketURI = 'ws://localhost:8000/ws/maps/1/'
|
||||
|
||||
it('should initialize methods even before start', () => {
|
||||
const engine = new SyncEngine({})
|
||||
const engine = new SyncEngine({}, websocketTokenURI, websocketURI)
|
||||
engine.upsert()
|
||||
engine.update()
|
||||
engine.delete()
|
||||
|
|
|
@ -39,6 +39,9 @@ def test_websocket_connection_can_sync_markers(
|
|||
a_map_el.click(position={"x": 220, "y": 220})
|
||||
expect(a_marker_pane).to_have_count(1)
|
||||
expect(b_marker_pane).to_have_count(1)
|
||||
# Peer B should not be in state dirty
|
||||
expect(peerB.get_by_role("button", name="View")).to_be_visible()
|
||||
expect(peerB.get_by_role("button", name="Cancel edits")).to_be_hidden()
|
||||
peerA.locator("body").type("Synced name")
|
||||
peerA.locator("body").press("Escape")
|
||||
|
||||
|
@ -415,3 +418,69 @@ def test_should_sync_datalayers(new_page, live_server, websocket_server, tilelay
|
|||
peerA.get_by_role("button", name="Save").click()
|
||||
|
||||
assert DataLayer.objects.count() == 2
|
||||
|
||||
|
||||
@pytest.mark.xdist_group(name="websockets")
|
||||
def test_create_and_sync_map(
|
||||
new_page, live_server, websocket_server, tilelayer, login, user
|
||||
):
|
||||
# Create a syncable map with peerA
|
||||
peerA = login(user, prefix="Page A")
|
||||
peerA.goto(f"{live_server.url}/en/map/new/")
|
||||
with peerA.expect_response(re.compile("./map/create/.*")):
|
||||
peerA.get_by_role("button", name="Save Draft").click()
|
||||
peerA.get_by_role("link", name="Map advanced properties").click()
|
||||
peerA.get_by_text("Real-time collaboration", exact=True).click()
|
||||
peerA.get_by_text("Enable real-time").click()
|
||||
peerA.get_by_role("link", name="Update permissions and editors").click()
|
||||
peerA.locator('select[name="share_status"]').select_option(str(Map.PUBLIC))
|
||||
with peerA.expect_response(re.compile("./update/settings/.*")):
|
||||
peerA.get_by_role("button", name="Save").click()
|
||||
expect(peerA.get_by_role("button", name="Cancel edits")).to_be_hidden()
|
||||
# Quit edit mode
|
||||
peerA.get_by_role("button", name="View").click()
|
||||
|
||||
# Open map and go to edit mode with peer B
|
||||
peerB = new_page("Page B")
|
||||
peerB.goto(peerA.url)
|
||||
peerB.get_by_role("button", name="Edit").click()
|
||||
|
||||
# Create a marker from peerA
|
||||
markersA = peerA.locator(".leaflet-marker-pane > div")
|
||||
markersB = peerB.locator(".leaflet-marker-pane > div")
|
||||
expect(markersA).to_have_count(0)
|
||||
expect(markersB).to_have_count(0)
|
||||
|
||||
# Add a marker from peer A
|
||||
peerA.get_by_role("button", name="Edit").click()
|
||||
peerA.get_by_title("Draw a marker").click()
|
||||
peerA.locator("#map").click(position={"x": 220, "y": 220})
|
||||
expect(markersA).to_have_count(1)
|
||||
expect(markersB).to_have_count(1)
|
||||
|
||||
# Save and quit edit mode again
|
||||
with peerA.expect_response(re.compile("./datalayer/create/.*")):
|
||||
peerA.get_by_role("button", name="Save").click()
|
||||
peerA.get_by_role("button", name="View").click()
|
||||
expect(markersA).to_have_count(1)
|
||||
expect(markersB).to_have_count(1)
|
||||
peerA.wait_for_timeout(500)
|
||||
expect(markersA).to_have_count(1)
|
||||
expect(markersB).to_have_count(1)
|
||||
|
||||
# Peer B should not be in state dirty
|
||||
expect(peerB.get_by_role("button", name="View")).to_be_visible()
|
||||
expect(peerB.get_by_role("button", name="Cancel edits")).to_be_hidden()
|
||||
|
||||
# Add a marker from peer B
|
||||
peerB.get_by_title("Draw a marker").click()
|
||||
peerB.locator("#map").click(position={"x": 200, "y": 200})
|
||||
expect(markersB).to_have_count(2)
|
||||
expect(markersA).to_have_count(1)
|
||||
with peerB.expect_response(re.compile("./datalayer/update/.*")):
|
||||
peerB.get_by_role("button", name="Save").click()
|
||||
expect(markersB).to_have_count(2)
|
||||
expect(markersA).to_have_count(1)
|
||||
peerA.get_by_role("button", name="Edit").click()
|
||||
expect(markersA).to_have_count(2)
|
||||
expect(markersB).to_have_count(2)
|
||||
|
|
|
@ -126,6 +126,10 @@ async def join_and_listen(
|
|||
|
||||
try:
|
||||
async for raw_message in websocket:
|
||||
if raw_message == "ping":
|
||||
await websocket.send("pong")
|
||||
continue
|
||||
|
||||
# recompute the peers list at the time of message-sending.
|
||||
# as doing so beforehand would miss new connections
|
||||
other_peers = connections.get_other_peers(websocket)
|
||||
|
@ -192,4 +196,7 @@ def run(host: str, port: int):
|
|||
logging.debug(f"Waiting for connections on {host}:{port}")
|
||||
await asyncio.Future() # run forever
|
||||
|
||||
asyncio.run(_serve())
|
||||
try:
|
||||
asyncio.run(_serve())
|
||||
except KeyboardInterrupt:
|
||||
print("Closing WebSocket server")
|
||||
|
|
Loading…
Reference in a new issue