fix(sync): Import the data when syncing GeoJSON objects.

Prior to these changes, the data wasn't transmitted over WebSocket, and
even if present it wasn't taken into account.
This commit is contained in:
Alexis Métaireau 2024-05-08 14:03:15 +02:00
parent e24173eb9f
commit ce0f3c9d3e
8 changed files with 63 additions and 32 deletions

View file

@ -16,7 +16,7 @@ export class SyncEngine {
} }
_initialize() { _initialize() {
this.transport = undefined this.transport = undefined
const noop = () => undefined const noop = () => {}
// by default, all operations do nothing, until the engine is started. // by default, all operations do nothing, until the engine is started.
this.upsert = this.update = this.delete = noop this.upsert = this.update = this.delete = noop
} }

View file

@ -68,25 +68,29 @@ class FeatureUpdater extends BaseUpdater {
return datalayer.getFeatureById(id) return datalayer.getFeatureById(id)
} }
// XXX Not sure about the naming. It's returning latlng OR latlngS // Create or update an object at a specific position
getGeometry({ type, coordinates }) {
if (type == 'Point') {
return L.GeoJSON.coordsToLatLng(coordinates)
}
return L.GeoJSON.coordsToLatLngs(coordinates)
}
upsert({ metadata, value }) { upsert({ metadata, value }) {
let { id, layerId } = metadata let { id, layerId } = metadata
const datalayer = this.getDataLayerFromID(layerId) const datalayer = this.getDataLayerFromID(layerId)
let feature = this.getFeatureFromMetadata(metadata, value) let feature = this.getFeatureFromMetadata(metadata, value)
feature = datalayer.geometryToFeature({ geometry: value.geometry, id, feature }) if (feature === undefined) {
console.log(`Unable to find feature with id = ${metadata.id}. Creating a new one`)
}
feature = datalayer.geometryToFeature({
geometry: value.geometry,
geojson: value,
id,
feature,
})
feature.addTo(datalayer) feature.addTo(datalayer)
} }
// Update a property of an object
update({ key, metadata, value }) { update({ key, metadata, value }) {
let feature = this.getFeatureFromMetadata(metadata) let feature = this.getFeatureFromMetadata(metadata)
if (feature === undefined) {
console.error(`Unable to find feature with id = ${metadata.id}.`)
}
switch (key) { switch (key) {
case 'geometry': case 'geometry':
const datalayer = this.getDataLayerFromID(metadata.layerId) const datalayer = this.getDataLayerFromID(metadata.layerId)

View file

@ -1249,6 +1249,7 @@ U.Editable = L.Editable.extend({
L.Editable.prototype.initialize.call(this, map, options) L.Editable.prototype.initialize.call(this, map, options)
this.on('editable:drawing:click editable:drawing:move', this.drawingTooltip) this.on('editable:drawing:click editable:drawing:move', this.drawingTooltip)
this.on('editable:drawing:end', (e) => { this.on('editable:drawing:end', (e) => {
console.log('editable:drawing:end')
this.map.tooltip.close() this.map.tooltip.close()
// Leaflet.Editable will delete the drawn shape if invalid // Leaflet.Editable will delete the drawn shape if invalid
// (eg. line has only one drawn point) // (eg. line has only one drawn point)
@ -1258,14 +1259,21 @@ U.Editable = L.Editable.extend({
}) })
// Layer for items added by users // Layer for items added by users
this.on('editable:drawing:cancel', (e) => { this.on('editable:drawing:cancel', (e) => {
console.log('editable:drawing:cancel')
if (e.layer instanceof U.Marker) e.layer.del() if (e.layer instanceof U.Marker) e.layer.del()
else {
// the user might just exit with escape
e.layer.onCommit()
}
}) })
this.on('editable:drawing:commit', function (e) { this.on('editable:drawing:commit', function (e) {
console.log('editable:drawing:commit')
e.layer.isDirty = true e.layer.isDirty = true
if (this.map.editedFeature !== e.layer) e.layer.edit(e) if (this.map.editedFeature !== e.layer) e.layer.edit(e)
e.layer.onCommit() e.layer.onCommit()
}) })
this.on('editable:editing', (e) => { this.on('editable:editing', (e) => {
console.log('editable:editing')
const layer = e.layer const layer = e.layer
layer.isDirty = true layer.isDirty = true
if (layer._tooltip && layer.isTooltipOpen()) { if (layer._tooltip && layer.isTooltipOpen()) {
@ -1274,11 +1282,13 @@ U.Editable = L.Editable.extend({
} }
}) })
this.on('editable:vertex:ctrlclick', (e) => { this.on('editable:vertex:ctrlclick', (e) => {
console.log('editable:vertex:ctrlclick')
const index = e.vertex.getIndex() const index = e.vertex.getIndex()
if (index === 0 || (index === e.vertex.getLastIndex() && e.vertex.continue)) if (index === 0 || (index === e.vertex.getLastIndex() && e.vertex.continue))
e.vertex.continue() e.vertex.continue()
}) })
this.on('editable:vertex:altclick', (e) => { this.on('editable:vertex:altclick', (e) => {
console.log('editable:vertex:altclick')
if (e.vertex.editor.vertexCanBeDeleted(e.vertex)) e.vertex.delete() if (e.vertex.editor.vertexCanBeDeleted(e.vertex)) e.vertex.delete()
}) })
this.on('editable:vertex:rawclick', this.onVertexRawClick) this.on('editable:vertex:rawclick', this.onVertexRawClick)
@ -1366,6 +1376,7 @@ U.Editable = L.Editable.extend({
}, },
onVertexRawClick: function (e) { onVertexRawClick: function (e) {
console.log('editable:vertex:rawclick')
e.layer.onVertexRawClick(e) e.layer.onVertexRawClick(e)
L.DomEvent.stop(e) L.DomEvent.stop(e)
e.cancel() e.cancel()

View file

@ -15,9 +15,7 @@ U.FeatureMixin = {
onCommit: function () { onCommit: function () {
const { subject, metadata, engine } = this.getSyncMetadata() const { subject, metadata, engine } = this.getSyncMetadata()
engine.upsert(subject, metadata, { engine.upsert(subject, metadata, this.toGeoJSON())
geometry: this.getGeometry(),
})
}, },
getGeometry: function () { getGeometry: function () {
@ -25,6 +23,7 @@ U.FeatureMixin = {
}, },
syncUpdatedProperties: function (properties) { syncUpdatedProperties: function (properties) {
// When updating latlng, sync the whole geometry
if ('latlng'.includes(properties)) { if ('latlng'.includes(properties)) {
const { subject, metadata, engine } = this.getSyncMetadata() const { subject, metadata, engine } = this.getSyncMetadata()
engine.update(subject, metadata, 'geometry', this.getGeometry()) engine.update(subject, metadata, 'geometry', this.getGeometry())
@ -44,12 +43,16 @@ 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 (options.geojson) {
this.populate(options.geojson)
}
if (id) { if (id) {
this.id = id this.id = id
} else { } else {
let geojson_id let geojson_id
if (options.geojson) { if (options.geojson) {
this.populate(options.geojson)
geojson_id = options.geojson.id geojson_id = options.geojson.id
} }
@ -189,7 +192,7 @@ U.FeatureMixin = {
}, },
getAdvancedEditActions: function (container) { getAdvancedEditActions: function (container) {
const deleteButton = L.DomUtil.createButton( L.DomUtil.createButton(
'button umap-delete', 'button umap-delete',
container, container,
L._('Delete'), L._('Delete'),
@ -939,6 +942,7 @@ U.PathMixin = {
_onDrag: function () { _onDrag: function () {
if (this._tooltip) this._tooltip.setLatLng(this.getCenter()) if (this._tooltip) this._tooltip.setLatLng(this.getCenter())
this.syncUpdatedProperties(['latlng'])
}, },
transferShape: function (at, to) { transferShape: function (at, to) {
@ -1130,6 +1134,9 @@ U.Polyline = L.Polyline.extend({
geojson.geometry.coordinates = [ geojson.geometry.coordinates = [
U.Utils.flattenCoordinates(geojson.geometry.coordinates), U.Utils.flattenCoordinates(geojson.geometry.coordinates),
] ]
delete geojson.id // delete the copied id, a new one will be generated.
const polygon = this.datalayer.geojsonToFeatures(geojson) const polygon = this.datalayer.geojsonToFeatures(geojson)
polygon.edit() polygon.edit()
this.del() this.del()
@ -1137,7 +1144,7 @@ U.Polyline = L.Polyline.extend({
getAdvancedEditActions: function (container) { getAdvancedEditActions: function (container) {
U.FeatureMixin.getAdvancedEditActions.call(this, container) U.FeatureMixin.getAdvancedEditActions.call(this, container)
const toPolygon = L.DomUtil.createButton( L.DomUtil.createButton(
'button umap-to-polygon', 'button umap-to-polygon',
container, container,
L._('Transform to polygon'), L._('Transform to polygon'),

View file

@ -257,6 +257,9 @@ U.Map = L.Map.extend({
if (this.options.syncEnabled != true) { if (this.options.syncEnabled != true) {
this.sync.stop() this.sync.stop()
} else { } else {
// FIXME: Do this directly in the sync engine, which should check if the engine
// is already started or not.
// Get the authentication token from the server // Get the authentication token from the server
// And pass it to the sync engine. // And pass it to the sync engine.
// FIXME: use `this.urls` // FIXME: use `this.urls`

View file

@ -179,12 +179,18 @@ U.Layer.Choropleth = L.FeatureGroup.extend({
return +feature.properties[key] // TODO: should we catch values non castable to int ? return +feature.properties[key] // TODO: should we catch values non castable to int ?
}, },
computeBreaks: function () { getValues: function () {
const values = [] const values = []
this.datalayer.eachLayer((layer) => { this.datalayer.eachLayer((layer) => {
let value = this._getValue(layer) let value = this._getValue(layer)
if (!isNaN(value)) values.push(value) if (!isNaN(value)) values.push(value)
}) })
return values
},
computeBreaks: function () {
const values = this.getValues()
if (!values.length) { if (!values.length) {
this.options.breaks = [] this.options.breaks = []
this.options.colors = [] this.options.colors = []
@ -609,6 +615,7 @@ U.DataLayer = L.Evented.extend({
render: function (fields, builder) { render: function (fields, builder) {
let impacts = U.Utils.getImpactsFromSchema(fields) let impacts = U.Utils.getImpactsFromSchema(fields)
console.log('impacts', impacts)
for (let impact of impacts) { for (let impact of impacts) {
switch (impact) { switch (impact) {
@ -623,6 +630,7 @@ U.DataLayer = L.Evented.extend({
fields.forEach((field) => { fields.forEach((field) => {
this.layer.onEdit(field, builder) this.layer.onEdit(field, builder)
}) })
this.redraw()
this.show() this.show()
break break
case 'remote-data': case 'remote-data':
@ -1026,7 +1034,7 @@ U.DataLayer = L.Evented.extend({
for (i = 0, len = features.length; i < len; i++) { for (i = 0, len = features.length; i < len; i++) {
this.geojsonToFeatures(features[i]) this.geojsonToFeatures(features[i])
} }
return this return this // Why returning "this" ?
} }
const geometry = geojson.type === 'Feature' ? geojson.geometry : geojson const geometry = geojson.type === 'Feature' ? geojson.geometry : geojson
@ -1034,6 +1042,7 @@ U.DataLayer = L.Evented.extend({
let feature = this.geometryToFeature({ geometry, geojson }) let feature = this.geometryToFeature({ geometry, geojson })
if (feature) { if (feature) {
this.addLayer(feature) this.addLayer(feature)
feature.onCommit()
return feature return feature
} }
}, },
@ -1597,13 +1606,7 @@ U.DataLayer = L.Evented.extend({
// TODO Add an index // TODO Add an index
// For now, iterate on all the features. // For now, iterate on all the features.
getFeatureById: function (id) { getFeatureById: function (id) {
console.log('looking for feature with id ', id) return Object.values(this._layers).find((feature) => feature.id === id)
for (const i in this._layers) {
let feature = this._layers[i]
if (feature.id === id) {
return feature
}
}
}, },
getNextFeature: function (feature) { getNextFeature: function (feature) {

View file

@ -806,7 +806,9 @@ def get_websocket_auth_token(request, map_id, map_inst):
) )
return simple_json_response(token=signed_token) return simple_json_response(token=signed_token)
else: else:
return HttpResponseForbidden return HttpResponseForbidden(
_("You cannot edit this map with your current permissions.")
)
class MapUpdate(FormLessEditMixin, PermissionsMixin, UpdateView): class MapUpdate(FormLessEditMixin, PermissionsMixin, UpdateView):

View file

@ -33,7 +33,7 @@ class JoinMessage(BaseModel):
class Geometry(BaseModel): class Geometry(BaseModel):
type: Literal["Point", "Polygon"] type: Literal["Point", "Polygon", "LineString"]
coordinates: list coordinates: list
@ -70,11 +70,12 @@ async def join_and_listen(
# 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: # try:
OperationMessage.model_validate_json(raw_message) # OperationMessage.model_validate_json(raw_message)
except ValidationError as e: # except ValidationError as e:
print(raw_message, e) print(raw_message)
# For now, broadcast anyway
websockets.broadcast(peers, raw_message) websockets.broadcast(peers, raw_message)
finally: finally:
CONNECTIONS[map_id].remove(websocket) CONNECTIONS[map_id].remove(websocket)