Use GeoJSON in the WS protocol

This commit is contained in:
Alexis Métaireau 2024-02-12 15:29:54 +01:00
parent 5b0bc265a6
commit 88c0dac6d3
4 changed files with 155 additions and 49 deletions

View file

@ -1,5 +1,5 @@
import { WebSocketTransport } from "./websocket.js" import { WebSocketTransport } from "./websocket.js"
import { MapUpdater, FeatureUpdater } from "./updaters.js" import { MapUpdater, MarkerUpdater, PolygonUpdater, PolylineUpdater } from "./updaters.js"
export class SyncEngine { export class SyncEngine {
constructor(map) { constructor(map) {
@ -7,7 +7,7 @@ export class SyncEngine {
this.transport = new WebSocketTransport(this.receiver) this.transport = new WebSocketTransport(this.receiver)
this.sender = new MessagesSender(this.transport) 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.update = this.sender.update.bind(this.sender)
this.delete = this.sender.delete.bind(this.sender) this.delete = this.sender.delete.bind(this.sender)
} }
@ -18,21 +18,33 @@ export class MessagesDispatcher {
this.map = map this.map = map
this.updaters = { this.updaters = {
map: new MapUpdater(this.map), 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) { getUpdater(subject, metadata) {
if (["map", "feature"].includes(subject)) { switch (subject) {
return this.updaters[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
} }
throw new Error(`Unknown updater ${subject}`) case 'map':
return this.updaters[subject]
default:
throw new Error(`Unknown updater ${subject}, ${metadata}`)
}
} }
dispatch({ kind, payload }) { dispatch({ kind, payload }) {
console.log(kind, payload) console.log(kind, payload)
if (kind == "sync-protocol") { if (kind == "sync-protocol") {
let updater = this.getUpdater(payload.subject) let updater = this.getUpdater(payload.subject, payload.metadata)
updater.applyMessage(payload) updater.applyMessage(payload)
} }
} }
@ -55,8 +67,8 @@ export class MessagesSender {
this._transport.send("sync-protocol", message) this._transport.send("sync-protocol", message)
} }
create(subject, metadata, value) { upsert(subject, metadata, value) {
this.send({ verb: "create", subject, metadata, value }) this.send({ verb: "upsert", subject, metadata, value })
} }
update(subject, metadata, key, value) { update(subject, metadata, key, value) {

View file

@ -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 { class BaseUpdater {
updateObjectValue(obj, key, value) { updateObjectValue(obj, key, value) {
// XXX refactor so it's cleaner // 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) { constructor(map) {
super() super()
this.map = map this.map = map
@ -54,29 +67,65 @@ export class FeatureUpdater extends BaseUpdater {
return datalayer.getFeatureById(id) 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 let { id, layerId } = metadata
const datalayer = this.getLayerFromID(layerId) const datalayer = this.getLayerFromID(layerId)
let marker = new L.U.Marker(this.map, value.latlng, { datalayer }, id) let feature = this.getFeatureFromMetadata(metadata, value)
marker.addTo(datalayer) feature = datalayer.geometryToFeature({ geometry: value.geometry, id, feature })
feature.addTo(datalayer)
} }
update({ key, metadata, value }) { update({ key, metadata, value }) {
let feature = this.getFeatureFromMetadata(metadata) let feature = this.getFeatureFromMetadata(metadata)
if (key == "latlng") { switch (key) {
feature.setLatLng(value) case "geometry":
} else { const datalayer = this.getLayerFromID(metadata.layerId)
datalayer.geometryToFeature({ geometry: value, id: metadata.id, feature })
default:
this.updateObjectValue(feature, key, value) this.updateObjectValue(feature, key, value)
} }
feature.datalayer.indexProperties(feature)
feature.renderProperties([key]) feature.renderProperties([key])
} }
delete({ metadata }) { delete({ metadata }) {
// XXX // XXX Distinguish between properties getting deleted
// We need to distinguish between properties getting deleted
// and the wole feature getting deleted // and the wole feature getting deleted
let feature = this.getFeatureFromMetadata(metadata) let feature = this.getFeatureFromMetadata(metadata)
feature.del() if (feature) feature.del()
} }
} }
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 }

View file

@ -25,12 +25,22 @@ L.U.FeatureMixin = {
onCommit: function () { onCommit: function () {
console.log("onCommit") 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.getSyncSubject(),
this.getSyncMetadata(), this.getSyncMetadata(),
{ { 'geometry': this.getGeometry() })
'latlng': this._latlng
}) },
// Sync-related methods
getGeometry: function () {
return this.toGeoJSON().geometry
}, },
updateProperties: function (properties) { updateProperties: function (properties) {
@ -38,7 +48,7 @@ L.U.FeatureMixin = {
let metadata = this.getSyncMetadata() let metadata = this.getSyncMetadata()
if ('latlng'.includes(properties)) { 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 = { 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 () { hasGeom: function () {
return !this.isEmpty() return !this.isEmpty()
}, },

View file

@ -961,8 +961,6 @@ L.U.DataLayer = L.Evented.extend({
const features = geojson instanceof Array ? geojson : geojson.features const features = geojson instanceof Array ? geojson : geojson.features
let i let i
let len let len
let latlng
let latlngs
if (features) { if (features) {
L.Util.sortFeatures(features, this.map.getOption('sortKey')) L.Util.sortFeatures(features, this.map.getOption('sortKey'))
@ -971,12 +969,23 @@ L.U.DataLayer = L.Evented.extend({
} }
return this return this
} }
const geometry = geojson.type === 'Feature' ? geojson.geometry : geojson 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. if (!geometry) return // null geometry is valid geojson.
const coords = geometry.coordinates const coords = geometry.coordinates
let layer let latlngs
let tmp let latlng
// Create a default geojson if none is provided
geojson ??= { type: "Feature", geometry: geometry }
switch (geometry.type) { switch (geometry.type) {
case 'Point': case 'Point':
@ -986,8 +995,11 @@ L.U.DataLayer = L.Evented.extend({
console.error('Invalid latlng object from', coords) console.error('Invalid latlng object from', coords)
break break
} }
layer = this._pointToLayer(geojson, latlng) if (feature) {
break feature.setLatLng(latlng)
return feature
}
return this._pointToLayer(geojson, latlng, id)
case 'MultiLineString': case 'MultiLineString':
case 'LineString': case 'LineString':
@ -996,14 +1008,21 @@ L.U.DataLayer = L.Evented.extend({
geometry.type === 'LineString' ? 0 : 1 geometry.type === 'LineString' ? 0 : 1
) )
if (!latlngs.length) break if (!latlngs.length) break
layer = this._lineToLayer(geojson, latlngs) if (feature) {
break feature.setLatLngs(latlngs)
return feature
}
return this._lineToLayer(geojson, latlngs, id)
case 'MultiPolygon': case 'MultiPolygon':
case 'Polygon': case 'Polygon':
latlngs = L.GeoJSON.coordsToLatLngs(coords, geometry.type === 'Polygon' ? 1 : 2) latlngs = L.GeoJSON.coordsToLatLngs(coords, geometry.type === 'Polygon' ? 1 : 2)
layer = this._polygonToLayer(geojson, latlngs) if (feature) {
break feature.setLatLngs(latlngs)
return feature
}
return this._polygonToLayer(geojson, latlngs, id)
case 'GeometryCollection': case 'GeometryCollection':
return this.geojsonToFeatures(geometry.geometries) return this.geojsonToFeatures(geometry.geometries)
@ -1015,30 +1034,26 @@ L.U.DataLayer = L.Evented.extend({
level: 'error', level: 'error',
}) })
} }
if (layer) {
this.addLayer(layer)
return layer
}
}, },
_pointToLayer: function (geojson, latlng) { _pointToLayer: function (geojson, latlng, id) {
return new L.U.Marker(this.map, latlng, { geojson: geojson, datalayer: this }) 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, { return new L.U.Polyline(this.map, latlngs, {
geojson: geojson, geojson: geojson,
datalayer: this, datalayer: this,
color: null, color: null,
}) }, id)
}, },
_polygonToLayer: function (geojson, latlngs) { _polygonToLayer: function (geojson, latlngs, id) {
// Ensure no empty hole // Ensure no empty hole
// for (let i = latlngs.length - 1; i > 0; i--) { // for (let i = latlngs.length - 1; i > 0; i--) {
// if (!latlngs.slice()[i].length) latlngs.splice(i, 1); // 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) { importRaw: function (raw, type) {