mirror of
https://github.com/umap-project/umap.git
synced 2025-04-29 03:42:37 +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 Importer from './importer.js'
|
||||
import Help from './help.js'
|
||||
import { SyncEngine } from './sync/engine.js'
|
||||
|
||||
// Import modules and export them to the global scope.
|
||||
// For the not yet module-compatible JS out there.
|
||||
|
||||
// By alphabetic order
|
||||
window.U = {
|
||||
URLs,
|
||||
Request,
|
||||
ServerRequest,
|
||||
RequestError,
|
||||
HTTPError,
|
||||
NOKError,
|
||||
Browser,
|
||||
Facets,
|
||||
Panel,
|
||||
Alert,
|
||||
Dialog,
|
||||
Tooltip,
|
||||
EditPanel,
|
||||
FullPanel,
|
||||
Utils,
|
||||
SCHEMA,
|
||||
Importer,
|
||||
Orderable,
|
||||
Caption,
|
||||
AjaxAutocomplete,
|
||||
AjaxAutocompleteMultiple,
|
||||
Browser,
|
||||
Caption,
|
||||
Dialog,
|
||||
EditPanel,
|
||||
Facets,
|
||||
FullPanel,
|
||||
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.initContextMenu()
|
||||
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) {
|
||||
|
|
Loading…
Reference in a new issue