From 88c0dac6d374ba6d7fdf4fc1c68e9e4c43d4ac56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20M=C3=A9taireau?= Date: Mon, 12 Feb 2024 15:29:54 +0100 Subject: [PATCH] Use GeoJSON in the WS protocol --- umap/static/umap/js/modules/sync/engine.js | 32 ++++++--- umap/static/umap/js/modules/sync/updaters.js | 75 ++++++++++++++++---- umap/static/umap/js/umap.features.js | 40 +++++++++-- umap/static/umap/js/umap.layer.js | 57 +++++++++------ 4 files changed, 155 insertions(+), 49 deletions(-) diff --git a/umap/static/umap/js/modules/sync/engine.js b/umap/static/umap/js/modules/sync/engine.js index e692c6ec..da33d136 100644 --- a/umap/static/umap/js/modules/sync/engine.js +++ b/umap/static/umap/js/modules/sync/engine.js @@ -1,5 +1,5 @@ import { WebSocketTransport } from "./websocket.js" -import { MapUpdater, FeatureUpdater } from "./updaters.js" +import { MapUpdater, MarkerUpdater, PolygonUpdater, PolylineUpdater } from "./updaters.js" export class SyncEngine { constructor(map) { @@ -7,7 +7,7 @@ export class SyncEngine { this.transport = new WebSocketTransport(this.receiver) this.sender = new MessagesSender(this.transport) - this.create = this.sender.create.bind(this.sender) + this.upsert = this.sender.upsert.bind(this.sender) this.update = this.sender.update.bind(this.sender) this.delete = this.sender.delete.bind(this.sender) } @@ -18,21 +18,33 @@ export class MessagesDispatcher { this.map = map this.updaters = { map: new MapUpdater(this.map), - feature: new FeatureUpdater(this.map) + marker: new MarkerUpdater(this.map), + polyline: new PolylineUpdater(this.map), + polygon: new PolygonUpdater(this.map), } } - getUpdater(subject) { - if (["map", "feature"].includes(subject)) { - return this.updaters[subject] + 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': + return this.updaters[subject] + default: + throw new Error(`Unknown updater ${subject}, ${metadata}`) } - throw new Error(`Unknown updater ${subject}`) + } dispatch({ kind, payload }) { console.log(kind, payload) if (kind == "sync-protocol") { - let updater = this.getUpdater(payload.subject) + let updater = this.getUpdater(payload.subject, payload.metadata) updater.applyMessage(payload) } } @@ -55,8 +67,8 @@ export class MessagesSender { this._transport.send("sync-protocol", message) } - create(subject, metadata, value) { - this.send({ verb: "create", subject, metadata, value }) + upsert(subject, metadata, value) { + this.send({ verb: "upsert", subject, metadata, value }) } update(subject, metadata, key, value) { diff --git a/umap/static/umap/js/modules/sync/updaters.js b/umap/static/umap/js/modules/sync/updaters.js index 0dae608e..f7e69b85 100644 --- a/umap/static/umap/js/modules/sync/updaters.js +++ b/umap/static/umap/js/modules/sync/updaters.js @@ -1,3 +1,8 @@ +/** + * 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 { updateObjectValue(obj, key, value) { // XXX refactor so it's cleaner @@ -36,8 +41,16 @@ export class MapUpdater extends BaseUpdater { } } -// Maybe have an updater per type of feature (marker, polyline, etc) -export class FeatureUpdater extends BaseUpdater { +/** + * 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 { + constructor(map) { super() this.map = map @@ -54,29 +67,65 @@ export class FeatureUpdater extends BaseUpdater { return datalayer.getFeatureById(id) } - create({ metadata, value }) { + // 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 marker = new L.U.Marker(this.map, value.latlng, { datalayer }, id) - marker.addTo(datalayer) + 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) - if (key == "latlng") { - feature.setLatLng(value) - } else { - this.updateObjectValue(feature, key, value) + 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 - // We need to distinguish between properties getting deleted + // XXX Distinguish between properties getting deleted // and the wole feature getting deleted let feature = this.getFeatureFromMetadata(metadata) - feature.del() + if (feature) feature.del() } -} \ No newline at end of file +} + +class PathUpdater extends FeatureUpdater { +} + +class MarkerUpdater extends FeatureUpdater { + featureType = 'marker' + featureClass = L.U.Marker + featureArgument = 'latlng' +} + + +class PolygonUpdater extends PathUpdater { + featureType = 'polygon' + featureClass = L.U.Polygon + featureArgument = 'latlngs' +} + +class PolylineUpdater extends PathUpdater { + featureType = 'polyline' + featureClass = L.U.Polyline + featureArgument = 'latlngs' +} + +export { MarkerUpdater, PolygonUpdater, PolylineUpdater } \ No newline at end of file diff --git a/umap/static/umap/js/umap.features.js b/umap/static/umap/js/umap.features.js index 00b1152e..0c66797d 100644 --- a/umap/static/umap/js/umap.features.js +++ b/umap/static/umap/js/umap.features.js @@ -25,12 +25,22 @@ L.U.FeatureMixin = { onCommit: function () { console.log("onCommit") - this.map.syncEngine.create( + // We cannot make a difference between a creation + // and an edition, so we use the "upsert" verb. + + // The updater will lookup for already existing objects with the given ID + // before creating a new one. + + this.map.syncEngine.upsert( this.getSyncSubject(), this.getSyncMetadata(), - { - 'latlng': this._latlng - }) + { 'geometry': this.getGeometry() }) + + }, + + // Sync-related methods + getGeometry: function () { + return this.toGeoJSON().geometry }, updateProperties: function (properties) { @@ -38,7 +48,7 @@ L.U.FeatureMixin = { let metadata = this.getSyncMetadata() if ('latlng'.includes(properties)) { - this.map.syncEngine.update(subject, metadata, 'latlng', this._latlng) + this.map.syncEngine.update(subject, metadata, 'geometry', this.getGeometry()) } }, @@ -798,6 +808,26 @@ L.U.Marker = L.Marker.extend({ }) L.U.PathMixin = { + + /* getGeometry: function () { + // Clean the latlngs to avoid private properties to be carried away + console.log("getLatlngs", this.getLatLngs()) + // latlngs can be an array of latlng, but also arrays of arrays. + // Remove the extra object properties + let _cleanLatLngs = function (item) { + if ('lat' in item) { + let { lat, lng } = item + return { lat, lng } + } + return item.map(_cleanLatLngs) + } + const cleaned_latlngs = this.getLatLngs()//.map(_cleanLatLngs) + console.log("cleaned latlngs", cleaned_latlngs) + return { + 'latlngs': cleaned_latlngs + } + }, */ + hasGeom: function () { return !this.isEmpty() }, diff --git a/umap/static/umap/js/umap.layer.js b/umap/static/umap/js/umap.layer.js index ba496c0f..df481482 100644 --- a/umap/static/umap/js/umap.layer.js +++ b/umap/static/umap/js/umap.layer.js @@ -961,8 +961,6 @@ L.U.DataLayer = L.Evented.extend({ const features = geojson instanceof Array ? geojson : geojson.features let i let len - let latlng - let latlngs if (features) { L.Util.sortFeatures(features, this.map.getOption('sortKey')) @@ -971,12 +969,23 @@ L.U.DataLayer = L.Evented.extend({ } return this } - const geometry = geojson.type === 'Feature' ? geojson.geometry : geojson + + let feature = this.geometryToFeature({ geometry, geojson }) + if (feature) { + this.addLayer(feature) + return feature + } + }, + + geometryToFeature: function ({ geometry, geojson = null, id = null, feature = null } = {}) { if (!geometry) return // null geometry is valid geojson. const coords = geometry.coordinates - let layer - let tmp + let latlngs + let latlng + + // Create a default geojson if none is provided + geojson ??= { type: "Feature", geometry: geometry } switch (geometry.type) { case 'Point': @@ -986,8 +995,11 @@ L.U.DataLayer = L.Evented.extend({ console.error('Invalid latlng object from', coords) break } - layer = this._pointToLayer(geojson, latlng) - break + if (feature) { + feature.setLatLng(latlng) + return feature + } + return this._pointToLayer(geojson, latlng, id) case 'MultiLineString': case 'LineString': @@ -996,14 +1008,21 @@ L.U.DataLayer = L.Evented.extend({ geometry.type === 'LineString' ? 0 : 1 ) if (!latlngs.length) break - layer = this._lineToLayer(geojson, latlngs) - break + if (feature) { + feature.setLatLngs(latlngs) + return feature + } + return this._lineToLayer(geojson, latlngs, id) case 'MultiPolygon': case 'Polygon': latlngs = L.GeoJSON.coordsToLatLngs(coords, geometry.type === 'Polygon' ? 1 : 2) - layer = this._polygonToLayer(geojson, latlngs) - break + if (feature) { + feature.setLatLngs(latlngs) + return feature + } + return this._polygonToLayer(geojson, latlngs, id) + case 'GeometryCollection': return this.geojsonToFeatures(geometry.geometries) @@ -1015,30 +1034,26 @@ L.U.DataLayer = L.Evented.extend({ level: 'error', }) } - if (layer) { - this.addLayer(layer) - return layer - } }, - _pointToLayer: function (geojson, latlng) { - return new L.U.Marker(this.map, latlng, { geojson: geojson, datalayer: this }) + _pointToLayer: function (geojson, latlng, id) { + return new L.U.Marker(this.map, latlng, { geojson: geojson, datalayer: this }, id) }, - _lineToLayer: function (geojson, latlngs) { + _lineToLayer: function (geojson, latlngs, id) { return new L.U.Polyline(this.map, latlngs, { geojson: geojson, datalayer: this, color: null, - }) + }, id) }, - _polygonToLayer: function (geojson, latlngs) { + _polygonToLayer: function (geojson, latlngs, id) { // Ensure no empty hole // for (let i = latlngs.length - 1; i > 0; i--) { // if (!latlngs.slice()[i].length) latlngs.splice(i, 1); // } - return new L.U.Polygon(this.map, latlngs, { geojson: geojson, datalayer: this }) + return new L.U.Polygon(this.map, latlngs, { geojson: geojson, datalayer: this }, id) }, importRaw: function (raw, type) {