mirror of
https://github.com/umap-project/umap.git
synced 2025-04-29 11:52:38 +02:00
feat(sync): Sync features over websockets
Added a new `geometryToFeature` method in `umap.layer.js` which can update a given geometry if needed. A new `id` property can also be passed to the features on creation, to make it possible to have the same features `id` on different peers.
This commit is contained in:
parent
c9abb15dd1
commit
66105127cb
3 changed files with 97 additions and 33 deletions
|
@ -18,7 +18,8 @@ U.FeatureMixin = {
|
|||
},
|
||||
|
||||
onCommit: function () {
|
||||
this.map.sync.upsert(this.getSyncSubject(), this.getSyncMetadata(), {
|
||||
const { subject, metadata } = this.getSyncMetadata()
|
||||
this.map.sync.upsert(subject, metadata, {
|
||||
geometry: this.getGeometry(),
|
||||
})
|
||||
},
|
||||
|
@ -39,7 +40,7 @@ U.FeatureMixin = {
|
|||
this.map.sync.delete(subject, metadata)
|
||||
},
|
||||
|
||||
initialize: function (map, latlng, options) {
|
||||
initialize: function (map, latlng, options, id) {
|
||||
this.map = map
|
||||
if (typeof options === 'undefined') {
|
||||
options = {}
|
||||
|
@ -47,6 +48,9 @@ U.FeatureMixin = {
|
|||
// DataLayer the marker belongs to
|
||||
this.datalayer = options.datalayer || null
|
||||
this.properties = { _umap_options: {} }
|
||||
if (id) {
|
||||
this.id = id
|
||||
} else {
|
||||
let geojson_id
|
||||
if (options.geojson) {
|
||||
this.populate(options.geojson)
|
||||
|
@ -59,6 +63,7 @@ U.FeatureMixin = {
|
|||
} else {
|
||||
this.id = U.Utils.generateId()
|
||||
}
|
||||
}
|
||||
let isDirty = false
|
||||
const self = this
|
||||
try {
|
||||
|
@ -136,6 +141,7 @@ U.FeatureMixin = {
|
|||
this.view()
|
||||
}
|
||||
}
|
||||
this._redraw()
|
||||
},
|
||||
|
||||
openPopup: function () {
|
||||
|
|
|
@ -1010,8 +1010,6 @@ U.DataLayer = L.Evented.extend({
|
|||
const features = geojson instanceof Array ? geojson : geojson.features
|
||||
let i
|
||||
let len
|
||||
let latlng
|
||||
let latlngs
|
||||
|
||||
if (features) {
|
||||
U.Utils.sortFeatures(features, this.map.getOption('sortKey'), L.lang)
|
||||
|
@ -1022,10 +1020,42 @@ U.DataLayer = L.Evented.extend({
|
|||
}
|
||||
|
||||
const geometry = geojson.type === 'Feature' ? geojson.geometry : geojson
|
||||
|
||||
let feature = this.geometryToFeature({ geometry, geojson })
|
||||
if (feature) {
|
||||
this.addLayer(feature)
|
||||
return feature
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create or update Leaflet features from GeoJSON geometries.
|
||||
*
|
||||
* If no `feature` is provided, a new feature will be created.
|
||||
* If `feature` is provided, it will be updated with the passed geometry.
|
||||
*
|
||||
* GeoJSON and Leaflet use incompatible formats to encode coordinates.
|
||||
* This method takes care of the convertion.
|
||||
*
|
||||
* @param geometry GeoJSON geometry field
|
||||
* @param geojson Enclosing GeoJSON. If none is provided, a new one will
|
||||
* be created
|
||||
* @param id Id of the feature
|
||||
* @param feature Leaflet feature that should be updated with the new geometry
|
||||
* @returns Leaflet 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 latlng, latlngs
|
||||
|
||||
// Create a default geojson if none is provided
|
||||
geojson ??= { type: 'Feature', geometry: geometry }
|
||||
|
||||
switch (geometry.type) {
|
||||
case 'Point':
|
||||
|
@ -1035,8 +1065,11 @@ 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':
|
||||
|
@ -1045,14 +1078,20 @@ 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)
|
||||
|
||||
|
@ -1064,14 +1103,10 @@ U.DataLayer = L.Evented.extend({
|
|||
level: 'error',
|
||||
})
|
||||
}
|
||||
if (layer) {
|
||||
this.addLayer(layer)
|
||||
return layer
|
||||
}
|
||||
},
|
||||
|
||||
_pointToLayer: function (geojson, latlng) {
|
||||
return new U.Marker(this.map, latlng, { geojson: geojson, datalayer: this })
|
||||
_pointToLayer: function (geojson, latlng, id) {
|
||||
return new U.Marker(this.map, latlng, { geojson: geojson, datalayer: this }, id)
|
||||
},
|
||||
|
||||
_lineToLayer: function (geojson, latlngs) {
|
||||
|
@ -1544,6 +1579,17 @@ U.DataLayer = L.Evented.extend({
|
|||
return this._layers[id]
|
||||
},
|
||||
|
||||
// TODO Add an index
|
||||
// For now, iterate on all the features.
|
||||
getFeatureById: function (id) {
|
||||
for (const i in this._layers) {
|
||||
let feature = this._layers[i]
|
||||
if (feature.id == id) {
|
||||
return feature
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getNextFeature: function (feature) {
|
||||
const id = this._index.indexOf(L.stamp(feature))
|
||||
const nextId = this._index[id + 1]
|
||||
|
|
20
umap/ws.py
20
umap/ws.py
|
@ -8,7 +8,7 @@ import django
|
|||
import websockets
|
||||
from django.conf import settings
|
||||
from django.core.signing import TimestampSigner
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from websockets import WebSocketClientProtocol
|
||||
from websockets.server import serve
|
||||
|
||||
|
@ -32,6 +32,15 @@ class JoinMessage(BaseModel):
|
|||
token: str
|
||||
|
||||
|
||||
class Geometry(BaseModel):
|
||||
type: Literal["Point",]
|
||||
coordinates: list
|
||||
|
||||
|
||||
class GeometryValue(BaseModel):
|
||||
geometry: Geometry
|
||||
|
||||
|
||||
# FIXME better define the different messages
|
||||
# to ensure only relying valid ones.
|
||||
class OperationMessage(BaseModel):
|
||||
|
@ -39,8 +48,8 @@ class OperationMessage(BaseModel):
|
|||
verb: str = Literal["upsert", "update", "delete"]
|
||||
subject: str = Literal["map", "layer", "feature"]
|
||||
metadata: Optional[dict] = None
|
||||
key: str
|
||||
value: Optional[str]
|
||||
key: Optional[str] = None
|
||||
value: Optional[str | bool | int | GeometryValue]
|
||||
|
||||
|
||||
async def join_and_listen(
|
||||
|
@ -58,9 +67,12 @@ async def join_and_listen(
|
|||
# recompute the peers-list at the time of message-sending.
|
||||
# as doing so beforehand would miss new connections
|
||||
peers = CONNECTIONS[map_id] - {websocket}
|
||||
|
||||
# Only relay valid "operation" messages
|
||||
try:
|
||||
OperationMessage.model_validate_json(raw_message)
|
||||
except ValidationError as e:
|
||||
print(raw_message, e)
|
||||
|
||||
websockets.broadcast(peers, raw_message)
|
||||
finally:
|
||||
CONNECTIONS[map_id].remove(websocket)
|
||||
|
|
Loading…
Reference in a new issue