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 () {
|
onCommit: function () {
|
||||||
this.map.sync.upsert(this.getSyncSubject(), this.getSyncMetadata(), {
|
const { subject, metadata } = this.getSyncMetadata()
|
||||||
|
this.map.sync.upsert(subject, metadata, {
|
||||||
geometry: this.getGeometry(),
|
geometry: this.getGeometry(),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -39,7 +40,7 @@ U.FeatureMixin = {
|
||||||
this.map.sync.delete(subject, metadata)
|
this.map.sync.delete(subject, metadata)
|
||||||
},
|
},
|
||||||
|
|
||||||
initialize: function (map, latlng, options) {
|
initialize: function (map, latlng, options, id) {
|
||||||
this.map = map
|
this.map = map
|
||||||
if (typeof options === 'undefined') {
|
if (typeof options === 'undefined') {
|
||||||
options = {}
|
options = {}
|
||||||
|
@ -47,6 +48,9 @@ U.FeatureMixin = {
|
||||||
// DataLayer the marker belongs to
|
// DataLayer the marker belongs to
|
||||||
this.datalayer = options.datalayer || null
|
this.datalayer = options.datalayer || null
|
||||||
this.properties = { _umap_options: {} }
|
this.properties = { _umap_options: {} }
|
||||||
|
if (id) {
|
||||||
|
this.id = id
|
||||||
|
} else {
|
||||||
let geojson_id
|
let geojson_id
|
||||||
if (options.geojson) {
|
if (options.geojson) {
|
||||||
this.populate(options.geojson)
|
this.populate(options.geojson)
|
||||||
|
@ -59,6 +63,7 @@ U.FeatureMixin = {
|
||||||
} else {
|
} else {
|
||||||
this.id = U.Utils.generateId()
|
this.id = U.Utils.generateId()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let isDirty = false
|
let isDirty = false
|
||||||
const self = this
|
const self = this
|
||||||
try {
|
try {
|
||||||
|
@ -136,6 +141,7 @@ U.FeatureMixin = {
|
||||||
this.view()
|
this.view()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this._redraw()
|
||||||
},
|
},
|
||||||
|
|
||||||
openPopup: function () {
|
openPopup: function () {
|
||||||
|
|
|
@ -1010,8 +1010,6 @@ 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) {
|
||||||
U.Utils.sortFeatures(features, this.map.getOption('sortKey'), L.lang)
|
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
|
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.
|
if (!geometry) return // null geometry is valid geojson.
|
||||||
const coords = geometry.coordinates
|
const coords = geometry.coordinates
|
||||||
let layer
|
let latlng, latlngs
|
||||||
let tmp
|
|
||||||
|
// Create a default geojson if none is provided
|
||||||
|
geojson ??= { type: 'Feature', geometry: geometry }
|
||||||
|
|
||||||
switch (geometry.type) {
|
switch (geometry.type) {
|
||||||
case 'Point':
|
case 'Point':
|
||||||
|
@ -1035,8 +1065,11 @@ 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':
|
||||||
|
@ -1045,14 +1078,20 @@ 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)
|
||||||
|
|
||||||
|
@ -1064,14 +1103,10 @@ 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 U.Marker(this.map, latlng, { geojson: geojson, datalayer: this })
|
return new U.Marker(this.map, latlng, { geojson: geojson, datalayer: this }, id)
|
||||||
},
|
},
|
||||||
|
|
||||||
_lineToLayer: function (geojson, latlngs) {
|
_lineToLayer: function (geojson, latlngs) {
|
||||||
|
@ -1544,6 +1579,17 @@ U.DataLayer = L.Evented.extend({
|
||||||
return this._layers[id]
|
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) {
|
getNextFeature: function (feature) {
|
||||||
const id = this._index.indexOf(L.stamp(feature))
|
const id = this._index.indexOf(L.stamp(feature))
|
||||||
const nextId = this._index[id + 1]
|
const nextId = this._index[id + 1]
|
||||||
|
|
20
umap/ws.py
20
umap/ws.py
|
@ -8,7 +8,7 @@ import django
|
||||||
import websockets
|
import websockets
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.signing import TimestampSigner
|
from django.core.signing import TimestampSigner
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, ValidationError
|
||||||
from websockets import WebSocketClientProtocol
|
from websockets import WebSocketClientProtocol
|
||||||
from websockets.server import serve
|
from websockets.server import serve
|
||||||
|
|
||||||
|
@ -32,6 +32,15 @@ class JoinMessage(BaseModel):
|
||||||
token: str
|
token: str
|
||||||
|
|
||||||
|
|
||||||
|
class Geometry(BaseModel):
|
||||||
|
type: Literal["Point",]
|
||||||
|
coordinates: list
|
||||||
|
|
||||||
|
|
||||||
|
class GeometryValue(BaseModel):
|
||||||
|
geometry: Geometry
|
||||||
|
|
||||||
|
|
||||||
# FIXME better define the different messages
|
# FIXME better define the different messages
|
||||||
# to ensure only relying valid ones.
|
# to ensure only relying valid ones.
|
||||||
class OperationMessage(BaseModel):
|
class OperationMessage(BaseModel):
|
||||||
|
@ -39,8 +48,8 @@ class OperationMessage(BaseModel):
|
||||||
verb: str = Literal["upsert", "update", "delete"]
|
verb: str = Literal["upsert", "update", "delete"]
|
||||||
subject: str = Literal["map", "layer", "feature"]
|
subject: str = Literal["map", "layer", "feature"]
|
||||||
metadata: Optional[dict] = None
|
metadata: Optional[dict] = None
|
||||||
key: str
|
key: Optional[str] = None
|
||||||
value: Optional[str]
|
value: Optional[str | bool | int | GeometryValue]
|
||||||
|
|
||||||
|
|
||||||
async def join_and_listen(
|
async def join_and_listen(
|
||||||
|
@ -58,9 +67,12 @@ async def join_and_listen(
|
||||||
# recompute the peers-list at the time of message-sending.
|
# recompute the peers-list at the time of message-sending.
|
||||||
# as doing so beforehand would miss new connections
|
# as doing so beforehand would miss new connections
|
||||||
peers = CONNECTIONS[map_id] - {websocket}
|
peers = CONNECTIONS[map_id] - {websocket}
|
||||||
|
|
||||||
# Only relay valid "operation" messages
|
# Only relay valid "operation" messages
|
||||||
|
try:
|
||||||
OperationMessage.model_validate_json(raw_message)
|
OperationMessage.model_validate_json(raw_message)
|
||||||
|
except ValidationError as e:
|
||||||
|
print(raw_message, e)
|
||||||
|
|
||||||
websockets.broadcast(peers, raw_message)
|
websockets.broadcast(peers, raw_message)
|
||||||
finally:
|
finally:
|
||||||
CONNECTIONS[map_id].remove(websocket)
|
CONNECTIONS[map_id].remove(websocket)
|
||||||
|
|
Loading…
Reference in a new issue