mirror of
https://github.com/umap-project/umap.git
synced 2025-04-29 20:02:36 +02:00
feat(websockets): First SyncEngine appearance
A new SyncEngine module has been added to the JavaScript code. It aims to sync the local changes with remote ones. This first implementation relies on a websocket connection.
This commit is contained in:
parent
1128348db6
commit
e2b9b161e6
5 changed files with 276 additions and 18 deletions
|
@ -13,31 +13,34 @@ import { AjaxAutocomplete, AjaxAutocompleteMultiple } from './autocomplete.js'
|
||||||
import Orderable from './orderable.js'
|
import Orderable from './orderable.js'
|
||||||
import Importer from './importer.js'
|
import Importer from './importer.js'
|
||||||
import Help from './help.js'
|
import Help from './help.js'
|
||||||
|
import { SyncEngine } from './sync/engine.js'
|
||||||
|
|
||||||
// Import modules and export them to the global scope.
|
// Import modules and export them to the global scope.
|
||||||
// For the not yet module-compatible JS out there.
|
// For the not yet module-compatible JS out there.
|
||||||
|
|
||||||
|
// By alphabetic order
|
||||||
window.U = {
|
window.U = {
|
||||||
URLs,
|
|
||||||
Request,
|
|
||||||
ServerRequest,
|
|
||||||
RequestError,
|
|
||||||
HTTPError,
|
|
||||||
NOKError,
|
|
||||||
Browser,
|
|
||||||
Facets,
|
|
||||||
Panel,
|
|
||||||
Alert,
|
Alert,
|
||||||
Dialog,
|
|
||||||
Tooltip,
|
|
||||||
EditPanel,
|
|
||||||
FullPanel,
|
|
||||||
Utils,
|
|
||||||
SCHEMA,
|
|
||||||
Importer,
|
|
||||||
Orderable,
|
|
||||||
Caption,
|
|
||||||
AjaxAutocomplete,
|
AjaxAutocomplete,
|
||||||
AjaxAutocompleteMultiple,
|
AjaxAutocompleteMultiple,
|
||||||
|
Browser,
|
||||||
|
Caption,
|
||||||
|
Dialog,
|
||||||
|
EditPanel,
|
||||||
|
Facets,
|
||||||
|
FullPanel,
|
||||||
Help,
|
Help,
|
||||||
|
HTTPError,
|
||||||
|
Importer,
|
||||||
|
NOKError,
|
||||||
|
Orderable,
|
||||||
|
Panel,
|
||||||
|
Request,
|
||||||
|
RequestError,
|
||||||
|
SCHEMA,
|
||||||
|
ServerRequest,
|
||||||
|
SyncEngine,
|
||||||
|
Tooltip,
|
||||||
|
URLs,
|
||||||
|
Utils,
|
||||||
}
|
}
|
||||||
|
|
91
umap/static/umap/js/modules/sync/engine.js
Normal file
91
umap/static/umap/js/modules/sync/engine.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import { WebSocketTransport } from './websocket.js'
|
||||||
|
import {
|
||||||
|
MapUpdater,
|
||||||
|
MarkerUpdater,
|
||||||
|
PolygonUpdater,
|
||||||
|
PolylineUpdater,
|
||||||
|
DatalayerUpdater,
|
||||||
|
} from './updaters.js'
|
||||||
|
|
||||||
|
export class SyncEngine {
|
||||||
|
constructor(map, webSocketURI, authToken) {
|
||||||
|
this.map = map
|
||||||
|
this.receiver = new MessagesDispatcher(this.map)
|
||||||
|
this.transport = new WebSocketTransport(webSocketURI, authToken, this.receiver)
|
||||||
|
this.sender = new MessagesSender(this.transport)
|
||||||
|
|
||||||
|
this.upsert = this.sender.upsert.bind(this.sender)
|
||||||
|
this.update = this.sender.update.bind(this.sender)
|
||||||
|
this.delete = this.sender.delete.bind(this.sender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MessagesDispatcher {
|
||||||
|
constructor(map) {
|
||||||
|
this.map = map
|
||||||
|
this.updaters = {
|
||||||
|
map: new MapUpdater(this.map),
|
||||||
|
marker: new MarkerUpdater(this.map),
|
||||||
|
polyline: new PolylineUpdater(this.map),
|
||||||
|
polygon: new PolygonUpdater(this.map),
|
||||||
|
datalayer: new DatalayerUpdater(this.map),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getUpdater(subject, metadata) {
|
||||||
|
switch (subject) {
|
||||||
|
case 'feature':
|
||||||
|
const featureTypeExists = Object.keys(this.updaters).includes(
|
||||||
|
metadata.featureType
|
||||||
|
)
|
||||||
|
if (featureTypeExists) {
|
||||||
|
const updater = this.updaters[metadata.featureType]
|
||||||
|
console.log(`found updater ${metadata.featureType}, ${updater}`)
|
||||||
|
return updater
|
||||||
|
}
|
||||||
|
case 'map':
|
||||||
|
case 'datalayer':
|
||||||
|
return this.updaters[subject]
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown updater ${subject}, ${metadata}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({ kind, payload }) {
|
||||||
|
console.log(kind, payload)
|
||||||
|
if (kind == 'sync-protocol') {
|
||||||
|
let updater = this.getUpdater(payload.subject, payload.metadata)
|
||||||
|
updater.applyMessage(payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the message to the other party (using the specified transport):
|
||||||
|
*
|
||||||
|
* - `subject` is the type of object this is referering to (map, feature, layer)
|
||||||
|
* - `metadata` contains information about the object we're refering to (id, layerId for instance)
|
||||||
|
* - `key` and
|
||||||
|
* - `value` are the keys and values that are being modified.
|
||||||
|
*/
|
||||||
|
export class MessagesSender {
|
||||||
|
constructor(transport) {
|
||||||
|
this._transport = transport
|
||||||
|
}
|
||||||
|
|
||||||
|
send(message) {
|
||||||
|
this._transport.send('sync-protocol', message)
|
||||||
|
}
|
||||||
|
|
||||||
|
upsert(subject, metadata, value) {
|
||||||
|
this.send({ verb: 'upsert', subject, metadata, value })
|
||||||
|
}
|
||||||
|
|
||||||
|
update(subject, metadata, key, value) {
|
||||||
|
this.send({ verb: 'update', subject, metadata, key, value })
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(subject, metadata, key) {
|
||||||
|
this.send({ verb: 'delete', subject, metadata, key })
|
||||||
|
}
|
||||||
|
}
|
131
umap/static/umap/js/modules/sync/updaters.js
Normal file
131
umap/static/umap/js/modules/sync/updaters.js
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
/**
|
||||||
|
* This file contains the updaters: classes that are able to convert messages
|
||||||
|
* received from another party (or the server) to changes on the map.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class BaseUpdater {
|
||||||
|
constructor(map) {
|
||||||
|
this.map = map
|
||||||
|
}
|
||||||
|
|
||||||
|
updateObjectValue(obj, key, value) {
|
||||||
|
// XXX refactor so it's cleaner
|
||||||
|
let path = key.split('.')
|
||||||
|
let what
|
||||||
|
for (var i = 0, l = path.length; i < l; i++) {
|
||||||
|
what = path[i]
|
||||||
|
if (what === path[l - 1]) {
|
||||||
|
if (typeof value === 'undefined') {
|
||||||
|
delete obj[what]
|
||||||
|
} else {
|
||||||
|
obj[what] = value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
obj = obj[what]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getLayerFromID(layerId) {
|
||||||
|
if (layerId) return this.map.getDataLayerByUmapId(layerId)
|
||||||
|
return this.map.defaultEditDataLayer()
|
||||||
|
}
|
||||||
|
|
||||||
|
applyMessage(message) {
|
||||||
|
let { verb } = message
|
||||||
|
return this[verb](message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MapUpdater extends BaseUpdater {
|
||||||
|
update({ key, value }) {
|
||||||
|
console.log(key, value)
|
||||||
|
this.updateObjectValue(this.map, key, value)
|
||||||
|
this.map.renderProperties([key.replace('options.', '')])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DatalayerUpdater extends BaseUpdater {
|
||||||
|
update({ key, metadata, value }) {
|
||||||
|
const datalayer = this.getLayerFromID(metadata.id)
|
||||||
|
console.log(datalayer, key, value)
|
||||||
|
this.updateObjectValue(datalayer, key, value)
|
||||||
|
const property = key.replace('options.', '')
|
||||||
|
datalayer.renderProperties([property])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is an abstract base class
|
||||||
|
* And needs to be subclassed to be used.
|
||||||
|
*
|
||||||
|
* The child classes need to expose:
|
||||||
|
* - `featureClass`: the name of the class to create the feature
|
||||||
|
* - `featureArgument`: an object with the properties to pass to the class when bulding it.
|
||||||
|
**/
|
||||||
|
class FeatureUpdater extends BaseUpdater {
|
||||||
|
getFeatureFromMetadata({ id, layerId }) {
|
||||||
|
const datalayer = this.getLayerFromID(layerId)
|
||||||
|
return datalayer.getFeatureById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX Not sure about the naming. It's returning latlng OR latlngS
|
||||||
|
getGeometry({ type, coordinates }) {
|
||||||
|
if (type == 'Point') {
|
||||||
|
return L.GeoJSON.coordsToLatLng(coordinates)
|
||||||
|
}
|
||||||
|
return L.GeoJSON.coordsToLatLngs(coordinates)
|
||||||
|
}
|
||||||
|
|
||||||
|
upsert({ metadata, value }) {
|
||||||
|
let { id, layerId } = metadata
|
||||||
|
const datalayer = this.getLayerFromID(layerId)
|
||||||
|
let feature = this.getFeatureFromMetadata(metadata, value)
|
||||||
|
feature = datalayer.geometryToFeature({ geometry: value.geometry, id, feature })
|
||||||
|
feature.addTo(datalayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
update({ key, metadata, value }) {
|
||||||
|
let feature = this.getFeatureFromMetadata(metadata)
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case 'geometry':
|
||||||
|
const datalayer = this.getLayerFromID(metadata.layerId)
|
||||||
|
datalayer.geometryToFeature({ geometry: value, id: metadata.id, feature })
|
||||||
|
default:
|
||||||
|
this.updateObjectValue(feature, key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
feature.datalayer.indexProperties(feature)
|
||||||
|
feature.renderProperties([key])
|
||||||
|
}
|
||||||
|
|
||||||
|
delete({ metadata }) {
|
||||||
|
// XXX Distinguish between properties getting deleted
|
||||||
|
// and the wole feature getting deleted
|
||||||
|
let feature = this.getFeatureFromMetadata(metadata)
|
||||||
|
if (feature) feature.del()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PathUpdater extends FeatureUpdater {}
|
||||||
|
|
||||||
|
class MarkerUpdater extends FeatureUpdater {
|
||||||
|
featureType = 'marker'
|
||||||
|
featureClass = U.Marker
|
||||||
|
featureArgument = 'latlng'
|
||||||
|
}
|
||||||
|
|
||||||
|
class PolygonUpdater extends PathUpdater {
|
||||||
|
featureType = 'polygon'
|
||||||
|
featureClass = U.Polygon
|
||||||
|
featureArgument = 'latlngs'
|
||||||
|
}
|
||||||
|
|
||||||
|
class PolylineUpdater extends PathUpdater {
|
||||||
|
featureType = 'polyline'
|
||||||
|
featureClass = U.Polyline
|
||||||
|
featureArgument = 'latlngs'
|
||||||
|
}
|
||||||
|
|
||||||
|
export { MarkerUpdater, PolygonUpdater, PolylineUpdater }
|
22
umap/static/umap/js/modules/sync/websocket.js
Normal file
22
umap/static/umap/js/modules/sync/websocket.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
export class WebSocketTransport {
|
||||||
|
constructor(webSocketURI, authToken, messagesReceiver) {
|
||||||
|
this.websocket = new WebSocket(webSocketURI)
|
||||||
|
this.websocket.onopen = () => {
|
||||||
|
this.send('join', { token: authToken })
|
||||||
|
}
|
||||||
|
this.websocket.addEventListener('message', this.onMessage.bind(this))
|
||||||
|
this.receiver = messagesReceiver
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage(wsMessage) {
|
||||||
|
// XXX validate incoming data.
|
||||||
|
this.receiver.dispatch(JSON.parse(wsMessage.data))
|
||||||
|
}
|
||||||
|
|
||||||
|
send(kind, payload) {
|
||||||
|
const message = { ...payload }
|
||||||
|
message.kind = kind
|
||||||
|
let encoded = JSON.stringify(message)
|
||||||
|
this.websocket.send(encoded)
|
||||||
|
}
|
||||||
|
}
|
|
@ -248,6 +248,17 @@ U.Map = L.Map.extend({
|
||||||
this.backup()
|
this.backup()
|
||||||
this.initContextMenu()
|
this.initContextMenu()
|
||||||
this.on('click contextmenu.show', this.closeInplaceToolbar)
|
this.on('click contextmenu.show', this.closeInplaceToolbar)
|
||||||
|
|
||||||
|
Promise.resolve(this.getSyncEngine())
|
||||||
|
},
|
||||||
|
|
||||||
|
getSyncEngine: async function () {
|
||||||
|
// Get the authentication token from the server
|
||||||
|
// And pass it to the sync engine.
|
||||||
|
const [response, _, error] = await this.server.get('/map/2/ws-token/')
|
||||||
|
if (!error) {
|
||||||
|
this.sync = new U.SyncEngine(this, 'ws://localhost:8001', response.token)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function (fields) {
|
render: function (fields) {
|
||||||
|
|
Loading…
Reference in a new issue