mirror of
https://github.com/umap-project/umap.git
synced 2025-05-06 06:21:49 +02:00
WIP
This commit is contained in:
parent
d219ed331f
commit
74ea6d800c
16 changed files with 3188 additions and 89 deletions
7
Makefile
7
Makefile
|
@ -25,6 +25,13 @@ lint: ## Lint the code and template files
|
||||||
ruff format --check --target-version=py38 . &&\
|
ruff format --check --target-version=py38 . &&\
|
||||||
vermin --no-tips --violations -t=3.8- .
|
vermin --no-tips --violations -t=3.8- .
|
||||||
|
|
||||||
|
yjs:
|
||||||
|
npx webpack --entry ./localyjs.js && rm -fr umap/static/umap/vendors/yjs && mkdir umap/static/umap/vendors/yjs && mv dist/main.js umap/static/umap/vendors/yjs/yjs.js
|
||||||
|
yws:
|
||||||
|
npx webpack --entry ./localyws.js && rm -fr umap/static/umap/vendors/yws && mkdir umap/static/umap/vendors/yws && mv dist/main.js umap/static/umap/vendors/yws/yws.js
|
||||||
|
serve:
|
||||||
|
umap runserver
|
||||||
|
|
||||||
docs: ## Compile the docs
|
docs: ## Compile the docs
|
||||||
mkdocs build
|
mkdocs build
|
||||||
|
|
||||||
|
|
2960
package-lock.json
generated
2960
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -13,7 +13,8 @@
|
||||||
"optimist": "~0.4.0",
|
"optimist": "~0.4.0",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"sinon": "^15.1.0",
|
"sinon": "^15.1.0",
|
||||||
"uglify-js": "~3.17.4"
|
"uglify-js": "~3.17.4",
|
||||||
|
"webpack-cli": "^5.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "firefox test/index.html",
|
"test": "firefox test/index.html",
|
||||||
|
@ -38,6 +39,7 @@
|
||||||
"csv2geojson": "5.1.1",
|
"csv2geojson": "5.1.1",
|
||||||
"dompurify": "^3.0.3",
|
"dompurify": "^3.0.3",
|
||||||
"georsstogeojson": "^0.1.0",
|
"georsstogeojson": "^0.1.0",
|
||||||
|
"json-joy": "^11.28.0",
|
||||||
"leaflet": "1.9.4",
|
"leaflet": "1.9.4",
|
||||||
"leaflet-contextmenu": "^1.4.0",
|
"leaflet-contextmenu": "^1.4.0",
|
||||||
"leaflet-editable": "^1.2.0",
|
"leaflet-editable": "^1.2.0",
|
||||||
|
@ -59,6 +61,9 @@
|
||||||
"osmtogeojson": "^3.0.0-beta.3",
|
"osmtogeojson": "^3.0.0-beta.3",
|
||||||
"simple-statistics": "^7.8.3",
|
"simple-statistics": "^7.8.3",
|
||||||
"togpx": "^0.5.4",
|
"togpx": "^0.5.4",
|
||||||
"tokml": "0.4.0"
|
"tokml": "0.4.0",
|
||||||
|
"webpack": "^5.89.0",
|
||||||
|
"y-websocket": "^1.5.3",
|
||||||
|
"yjs": "^13.6.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,5 +29,6 @@ mkdir -p umap/static/umap/vendors/dompurify/ && cp -r node_modules/dompurify/dis
|
||||||
mkdir -p umap/static/umap/vendors/colorbrewer/ && cp node_modules/colorbrewer/index.js umap/static/umap/vendors/colorbrewer/colorbrewer.js
|
mkdir -p umap/static/umap/vendors/colorbrewer/ && cp node_modules/colorbrewer/index.js umap/static/umap/vendors/colorbrewer/colorbrewer.js
|
||||||
mkdir -p umap/static/umap/vendors/simple-statistics/ && cp node_modules/simple-statistics/dist/simple-statistics.min.js umap/static/umap/vendors/simple-statistics/
|
mkdir -p umap/static/umap/vendors/simple-statistics/ && cp node_modules/simple-statistics/dist/simple-statistics.min.js umap/static/umap/vendors/simple-statistics/
|
||||||
mkdir -p umap/static/umap/vendors/iconlayers/ && cp node_modules/leaflet-iconlayers/dist/* umap/static/umap/vendors/iconlayers/
|
mkdir -p umap/static/umap/vendors/iconlayers/ && cp node_modules/leaflet-iconlayers/dist/* umap/static/umap/vendors/iconlayers/
|
||||||
|
mkdir -p umap/static/umap/vendors/y-websocket/ && cp -r node_modules/y-websocket/dist/y-websocket.cjs umap/static/umap/vendors/y-websocket/y-websocket.js
|
||||||
|
|
||||||
echo 'Done!'
|
echo 'Done!'
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import * as L from '../../vendors/leaflet/leaflet-src.esm.js'
|
import * as L from '../../vendors/leaflet/leaflet-src.esm.js'
|
||||||
import * as Y from '../../vendors/yjs/yjs.js'
|
import {MessagesSender} from './sync/messages/sender.js'
|
||||||
|
import {MessagesReceiver} from "./sync/messages/receiver.js"
|
||||||
|
import {WebSocketTransport} from './sync/websocket.js'
|
||||||
|
|
||||||
import URLs from './urls.js'
|
import URLs from './urls.js'
|
||||||
|
|
||||||
// Import modules and export them to the global scope.
|
// Import modules and export them to the global scope.
|
||||||
// For the not yet module-compatible JS out there.
|
// For the not yet module-compatible JS out there.
|
||||||
|
|
||||||
// Copy the leaflet module, it's expected by leaflet plugins to be writeable.
|
// Copy the leaflet module, it's expected by leaflet plugins to be writeable.
|
||||||
window.L = { ...L }
|
window.L = { ...L }
|
||||||
|
|
||||||
window.Y = Y
|
window.umap = { URLs, WebSocketTransport, MessagesReceiver, MessagesSender}
|
||||||
window.umap = { URLs }
|
|
||||||
|
|
56
umap/static/umap/js/modules/sync/messages/messages.js
Normal file
56
umap/static/umap/js/modules/sync/messages/messages.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
export class BaseMessage {
|
||||||
|
// Possible actions:
|
||||||
|
// - set-obj-data
|
||||||
|
// - new-layer
|
||||||
|
constructor(action, subject, path, value){
|
||||||
|
this.action = action
|
||||||
|
this.subject = subject
|
||||||
|
this.path = path
|
||||||
|
this.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
encode(){
|
||||||
|
return encodeMessage({
|
||||||
|
action: this.action,
|
||||||
|
subject: this.subject,
|
||||||
|
path: this.path,
|
||||||
|
value: this.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
decode(encoded){
|
||||||
|
return decodeMessage(encoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SetDataMessage extends BaseMessage{
|
||||||
|
constructor(subject, path, value){
|
||||||
|
super("set-data", subject, path, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NewLayer extends BaseMessage{
|
||||||
|
constructor(subject, path, value){
|
||||||
|
super("new-layer", subject, path, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function encodeMessage(payload, type="message"){
|
||||||
|
return JSON.stringify({
|
||||||
|
type: type,
|
||||||
|
payload: payload
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeMessage(encodedData){
|
||||||
|
console.log("encoded message", encodedData)
|
||||||
|
// XXX Ensure the data matches what we expect here.
|
||||||
|
let parsed = JSON.parse(encodedData)
|
||||||
|
console.log(parsed)
|
||||||
|
switch(parsed.payload.action){
|
||||||
|
case 'set-data':
|
||||||
|
let {subject, path, value} = parsed.payload
|
||||||
|
return new SetDataMessage(subject, path, value)
|
||||||
|
}
|
||||||
|
}
|
24
umap/static/umap/js/modules/sync/messages/receiver.js
Normal file
24
umap/static/umap/js/modules/sync/messages/receiver.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { MapUpdater } from "../updaters/mapUpdater.js"
|
||||||
|
import { LayerGroupUpdater } from "../updaters/layerGroupUpdater.js"
|
||||||
|
|
||||||
|
|
||||||
|
// FIXME: Maybe name this MessagesDispatcher ?
|
||||||
|
export class MessagesReceiver {
|
||||||
|
|
||||||
|
constructor(map){
|
||||||
|
this.updaters = {
|
||||||
|
map: new MapUpdater(map),
|
||||||
|
layers: new LayerGroupUpdater(map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(message){
|
||||||
|
// FIXME if message.subject not in this.updaters: throw
|
||||||
|
console.log("message", message)
|
||||||
|
const updater = this.updaters[message.subject]
|
||||||
|
switch(message.action){
|
||||||
|
case "set-data":
|
||||||
|
updater.setData(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
umap/static/umap/js/modules/sync/messages/sender.js
Normal file
14
umap/static/umap/js/modules/sync/messages/sender.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { SetDataMessage } from "./messages.js"
|
||||||
|
|
||||||
|
export class MessagesSender {
|
||||||
|
constructor(subject, transport){
|
||||||
|
this._transport = transport
|
||||||
|
this._subject = subject
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key, value){
|
||||||
|
let message = new SetDataMessage(this._subject, key, value)
|
||||||
|
console.log("message obj", message, message.encode())
|
||||||
|
this._transport.send(message)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
export class LayerGroupUpdater{
|
||||||
|
constructor(map){
|
||||||
|
this.map = map
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(message){
|
||||||
|
// find the layer we're talking about
|
||||||
|
// Relay the message.
|
||||||
|
}
|
||||||
|
}
|
23
umap/static/umap/js/modules/sync/updaters/mapUpdater.js
Normal file
23
umap/static/umap/js/modules/sync/updaters/mapUpdater.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// all the updaters have the same interface
|
||||||
|
// .setData() which should rerender the object.
|
||||||
|
|
||||||
|
export class FormBuilderObjectUpdater{
|
||||||
|
constructor(obj){
|
||||||
|
this.obj = obj
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(message){
|
||||||
|
console.log(message)
|
||||||
|
// FIXME, this is the simple version
|
||||||
|
// Need to support subobjects, see https://github.com/yohanboniface/Leaflet.FormBuilder/blob/master/Leaflet.FormBuilder.js#L70-L86
|
||||||
|
this.obj.options[message.path] = message.value
|
||||||
|
|
||||||
|
// FIXME, this needs to be rendered only once if possible, accross different updates.
|
||||||
|
// (not sure how)
|
||||||
|
this.obj.renderProperties([message.path])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MapUpdater extends FormBuilderObjectUpdater {
|
||||||
|
|
||||||
|
}
|
28
umap/static/umap/js/modules/sync/websocket.js
Normal file
28
umap/static/umap/js/modules/sync/websocket.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { decodeMessage } from "./messages/messages.js";
|
||||||
|
|
||||||
|
export class WebSocketTransport {
|
||||||
|
constructor(messagesReceiver){
|
||||||
|
this.id = crypto.randomUUID()
|
||||||
|
this.websocket = new WebSocket("ws://localhost:8001/");
|
||||||
|
this.websocket.onopen = (msg) => {this.onOpen(msg)}
|
||||||
|
this.websocket.addEventListener("message", this.onMessage.bind(this));
|
||||||
|
this.receiver = messagesReceiver
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpen(msg){
|
||||||
|
var joinMsg = {type: 'join'}
|
||||||
|
console.log("joining websocket")
|
||||||
|
this.websocket.send(JSON.stringify(joinMsg))
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage(encoded){
|
||||||
|
console.log("received encoded message", encoded)
|
||||||
|
let message = decodeMessage(encoded.data)
|
||||||
|
this.receiver.dispatch(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
send(message){
|
||||||
|
let encoded = message.encode()
|
||||||
|
this.websocket.send(encoded)
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,43 +3,39 @@
|
||||||
*
|
*
|
||||||
* The mixed class needs to expose:
|
* The mixed class needs to expose:
|
||||||
*
|
*
|
||||||
* - `dataUpdaters`, an object matching each property with a list of renderers.
|
* - `propertiesRenderers`, an object matching each property with a list of renderers.
|
||||||
* - `getDataObject`, a method returning where the data is stored/retrieved.
|
* - `getDataObject`, a method returning where the data is stored/retrieved.
|
||||||
|
* - `getCRDT`, a method returning the CRDT used
|
||||||
*/
|
*/
|
||||||
L.U.DataRendererMixin = {
|
L.U.DataRendererMixin = {
|
||||||
populateCRDT: function () {
|
|
||||||
for (const [key, value] of Object.entries(this.options)) {
|
|
||||||
this.crdt.set(key, value)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
* For each passed property, find the functions to rerender the interface,
|
* Rerender the interface for the properties passed as an argument.
|
||||||
* and call them.
|
*
|
||||||
*
|
|
||||||
* @param list updatedProperties : properties that have been updated.
|
* @param list updatedProperties : properties that have been updated.
|
||||||
*/
|
*/
|
||||||
renderProperties: function (updatedProperties) {
|
renderProperties: function (updatedProperties) {
|
||||||
console.debug(updatedProperties)
|
|
||||||
let renderers = new Set()
|
let renderers = new Set()
|
||||||
for (const prop of updatedProperties) {
|
for (const prop of updatedProperties) {
|
||||||
const propRenderers = this.dataUpdaters[prop]
|
const propRenderers = this.propertiesRenderers[prop]
|
||||||
if (propRenderers) {
|
if (propRenderers) {
|
||||||
for (const renderer of propRenderers) renderers.add(renderer)
|
for (const renderer of propRenderers) renderers.add(renderer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.debug('renderers', renderers)
|
|
||||||
for (const renderer of renderers) this[renderer]()
|
for (const renderer of renderers) this[renderer]()
|
||||||
},
|
},
|
||||||
|
|
||||||
dataReceived: function () {
|
|
||||||
// Data has been received over the wire
|
|
||||||
this.updateInternalData()
|
|
||||||
this.onPropertiesUpdated(['name', 'color'])
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
L.U.FormBuilderDataRendererMixin = {
|
// L.U.LWWMapDataRendereMixin = L.U.DataRendererMixin {
|
||||||
getDataObject: function () {
|
// updateData: function(){
|
||||||
return this.options
|
// if (this.crdt) this.crdt.set(field, value)
|
||||||
},
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
// L.U.FormBuilderDataRendererMixin = L.U.DataRendererMixin.extend({
|
||||||
|
// getDataObject: function () {
|
||||||
|
// return this.options
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
|
||||||
|
|
|
@ -1260,11 +1260,14 @@ L.U.FormBuilder = L.FormBuilder.extend({
|
||||||
L.FormBuilder.prototype.setter.call(this, field, value)
|
L.FormBuilder.prototype.setter.call(this, field, value)
|
||||||
if (this.options.makeDirty !== false) this.obj.isDirty = true
|
if (this.options.makeDirty !== false) this.obj.isDirty = true
|
||||||
|
|
||||||
// FIXME: for now remove the options prefix
|
// FIXME: for now remove the options prefix if it's present.
|
||||||
field = field.replace('options.', '')
|
field = field.replace('options.', '')
|
||||||
if (this.obj.crdt) this.obj.crdt.set(field, value)
|
console.log("formbuilder object updated", this.obj)
|
||||||
|
|
||||||
this.obj.onPropertiesUpdated([field])
|
// FIXME: maybe name this "obj.updateProperty" / "obj.updateData",
|
||||||
|
// and let it trigger the rendering to expose less surface.
|
||||||
|
this.obj.broadcast.set(field, value)
|
||||||
|
this.obj.renderProperties([field])
|
||||||
},
|
},
|
||||||
|
|
||||||
finish: function () {
|
finish: function () {
|
||||||
|
|
|
@ -55,6 +55,7 @@ L.Map.mergeOptions({
|
||||||
featuresHaveOwner: false,
|
featuresHaveOwner: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
L.U.Map.include(L.U.DataRendererMixin)
|
||||||
L.U.Map.include({
|
L.U.Map.include({
|
||||||
HIDDABLE_CONTROLS: [
|
HIDDABLE_CONTROLS: [
|
||||||
'zoom',
|
'zoom',
|
||||||
|
@ -171,18 +172,40 @@ L.U.Map.include({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
broadcastChanges: function (data) {
|
initializeCRDT: function(data){
|
||||||
// Send changes over the wire
|
this.crdt = this.getCRDT()
|
||||||
console.log(data)
|
this.populateCRDT(data)
|
||||||
},
|
|
||||||
|
|
||||||
updateInternalData: function () {
|
|
||||||
this.options.name = 'CRDTS, yeah'
|
|
||||||
this.options.color = 'Fushia'
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getCRDT: function () {
|
getCRDT: function () {
|
||||||
return this._main_crdt.getMap('map')
|
return this._main_crdt.obj
|
||||||
|
},
|
||||||
|
|
||||||
|
populateCRDT: function (data) {
|
||||||
|
|
||||||
|
let object = {}
|
||||||
|
for (const [key, value] of Object.entries(data)){
|
||||||
|
// For now ignore embedded types
|
||||||
|
switch (typeof(value)){
|
||||||
|
case 'string':
|
||||||
|
case 'number':
|
||||||
|
case 'boolean':
|
||||||
|
object[key] = s.val(value)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._main_crdt.api.root({
|
||||||
|
zoom: val
|
||||||
|
})
|
||||||
|
console.log(this._main_crdt.view());
|
||||||
|
|
||||||
|
// for (const [key, value] of Object.entries(data)) {
|
||||||
|
// this.crdt.set(key, value)
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCRDT: function(field, value){
|
||||||
|
this._main_crdt.obj.set(field, value)
|
||||||
},
|
},
|
||||||
|
|
||||||
initialize: function (el, geojson) {
|
initialize: function (el, geojson) {
|
||||||
|
@ -408,7 +431,12 @@ L.U.Map.include({
|
||||||
this.backup()
|
this.backup()
|
||||||
this.initContextMenu()
|
this.initContextMenu()
|
||||||
this.on('click contextmenu.show', this.closeInplaceToolbar)
|
this.on('click contextmenu.show', this.closeInplaceToolbar)
|
||||||
this._main_crdt = new YJS.Doc()
|
|
||||||
|
// Sync with other party
|
||||||
|
// The dispatcher takes remote messages and route them to the proper handler
|
||||||
|
let receiver = new umap.MessagesReceiver(this)
|
||||||
|
let transport = new umap.WebSocketTransport(receiver)
|
||||||
|
this.broadcast = new umap.MessagesSender("map", transport)
|
||||||
},
|
},
|
||||||
|
|
||||||
initControls: function () {
|
initControls: function () {
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
{% if locale %}<script src="{{ STATIC_URL }}umap/locale/{{ locale }}.js" defer></script>{% endif %}
|
{% if locale %}<script src="{{ STATIC_URL }}umap/locale/{{ locale }}.js" defer></script>{% endif %}
|
||||||
{% compress js %}
|
{% compress js %}
|
||||||
<script src="{{ STATIC_URL }}umap/js/umap.core.js" defer></script>
|
<script src="{{ STATIC_URL }}umap/js/umap.core.js" defer></script>
|
||||||
|
<script src="{{ STATIC_URL }}umap/js/umap.data.js" defer></script>
|
||||||
<script src="{{ STATIC_URL }}umap/js/umap.autocomplete.js" defer></script>
|
<script src="{{ STATIC_URL }}umap/js/umap.autocomplete.js" defer></script>
|
||||||
<script src="{{ STATIC_URL }}umap/js/umap.popup.js" defer></script>
|
<script src="{{ STATIC_URL }}umap/js/umap.popup.js" defer></script>
|
||||||
<script src="{{ STATIC_URL }}umap/js/umap.xhr.js" defer></script>
|
<script src="{{ STATIC_URL }}umap/js/umap.xhr.js" defer></script>
|
||||||
|
|
37
ws.py
Normal file
37
ws.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import asyncio
|
||||||
|
import websockets
|
||||||
|
from websockets.server import serve
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Just relay all messages to other connected peers for now
|
||||||
|
|
||||||
|
CONNECTIONS = set()
|
||||||
|
|
||||||
|
async def join_and_listen(websocket):
|
||||||
|
print(f"Someone joined: {id(websocket)}")
|
||||||
|
CONNECTIONS.add(websocket)
|
||||||
|
try:
|
||||||
|
async for message in websocket:
|
||||||
|
# recompute the peers-list at the time of message-sending.
|
||||||
|
# doing so beforehand would miss new connections
|
||||||
|
peers = CONNECTIONS - {websocket}
|
||||||
|
print(message)
|
||||||
|
print(peers)
|
||||||
|
websockets.broadcast(peers, message)
|
||||||
|
finally:
|
||||||
|
CONNECTIONS.remove(websocket)
|
||||||
|
|
||||||
|
|
||||||
|
async def handler(websocket):
|
||||||
|
message = await websocket.recv()
|
||||||
|
event = json.loads(message)
|
||||||
|
|
||||||
|
# The first event should always be 'join'
|
||||||
|
assert event["type"] == "join"
|
||||||
|
await join_and_listen(websocket)
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with serve(handler, "localhost", 8001):
|
||||||
|
await asyncio.Future() # run forever
|
||||||
|
|
||||||
|
asyncio.run(main())
|
Loading…
Reference in a new issue