From beecd8c6e425bf47a9991ff98b4e839e40c5db4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20M=C3=A9taireau?= Date: Thu, 15 Feb 2024 14:48:16 +0100 Subject: [PATCH] Update documentation --- ...ng-collaboration-on-umap-third-update.html | 20 +- archives.html | 1 + articles.html | 20 +- articles10.html | 20 +- articles11.html | 20 +- articles12.html | 20 +- articles13.html | 20 +- articles14.html | 20 +- articles15.html | 20 +- articles16.html | 20 +- articles17.html | 20 +- articles18.html | 20 +- articles19.html | 20 +- articles2.html | 20 +- articles20.html | 20 +- articles21.html | 20 +- articles22.html | 20 +- articles23.html | 20 +- articles24.html | 20 +- articles25.html | 20 +- articles26.html | 20 +- articles27.html | 20 +- articles28.html | 20 +- articles29.html | 20 +- articles3.html | 20 +- articles30.html | 20 +- articles31.html | 20 +- articles32.html | 20 +- articles33.html | 20 +- articles34.html | 20 +- articles35.html | 20 +- articles36.html | 20 +- articles37.html | 20 +- articles38.html | 20 +- articles39.html | 20 +- articles4.html | 20 +- articles40.html | 20 +- articles41.html | 20 +- articles42.html | 20 +- articles43.html | 20 +- articles44.html | 20 +- articles45.html | 20 +- articles46.html | 20 +- articles47.html | 20 +- articles48.html | 20 +- articles49.html | 20 +- articles5.html | 20 +- articles50.html | 20 +- articles51.html | 20 +- articles52.html | 20 +- articles53.html | 20 +- articles54.html | 20 +- articles55.html | 20 +- articles56.html | 20 +- articles57.html | 20 +- articles58.html | 20 +- articles59.html | 20 +- articles6.html | 20 +- articles60.html | 20 +- articles61.html | 20 +- articles62.html | 20 +- articles63.html | 20 +- articles64.html | 20 +- articles7.html | 20 +- articles8.html | 20 +- articles9.html | 20 +- author/.html | 19 +- author/.html10 | 19 +- author/.html11 | 19 +- author/.html12 | 19 +- author/.html13 | 19 +- author/.html14 | 19 +- author/.html15 | 19 +- author/.html16 | 19 +- author/.html17 | 19 +- author/.html18 | 19 +- author/.html19 | 19 +- author/.html2 | 19 +- author/.html20 | 19 +- author/.html21 | 19 +- author/.html22 | 19 +- author/.html23 | 19 +- author/.html24 | 19 +- author/.html25 | 19 +- author/.html26 | 19 +- author/.html27 | 19 +- author/.html28 | 19 +- author/.html29 | 19 +- author/.html3 | 19 +- author/.html30 | 19 +- author/.html31 | 19 +- author/.html32 | 19 +- author/.html33 | 19 +- author/.html34 | 19 +- author/.html35 | 19 +- author/.html36 | 19 +- author/.html37 | 19 +- author/.html38 | 19 +- author/.html39 | 19 +- author/.html4 | 19 +- author/.html40 | 19 +- author/.html41 | 19 +- author/.html42 | 19 +- author/.html43 | 19 +- author/.html44 | 19 +- author/.html45 | 19 +- author/.html46 | 19 +- author/.html47 | 19 +- author/.html48 | 19 +- author/.html49 | 19 +- author/.html5 | 19 +- author/.html50 | 19 +- author/.html51 | 19 +- author/.html52 | 19 +- author/.html53 | 19 +- author/.html54 | 19 +- author/.html55 | 19 +- author/.html56 | 19 +- author/.html57 | 19 +- author/.html58 | 19 +- author/.html59 | 293 ++++++++++++++++++ author/.html6 | 19 +- author/.html7 | 19 +- author/.html8 | 19 +- author/.html9 | 19 +- categories.html | 2 +- code/index.html | 22 +- code/index10.html | 22 +- code/index11.html | 22 +- code/index12.html | 22 +- code/index13.html | 22 +- code/index14.html | 22 +- code/index15.html | 22 +- code/index16.html | 22 +- code/index17.html | 22 +- code/index18.html | 22 +- code/index19.html | 22 +- code/index2.html | 22 +- code/index20.html | 22 +- code/index21.html | 22 +- code/index22.html | 22 +- code/index23.html | 22 +- code/index24.html | 22 +- code/index25.html | 22 +- code/index26.html | 22 +- code/index3.html | 22 +- code/index4.html | 22 +- code/index5.html | 22 +- code/index6.html | 22 +- code/index7.html | 22 +- code/index8.html | 22 +- code/index9.html | 22 +- feeds/.atom.xml | 238 +++++++++++++- feeds/.rss.xml | 8 +- feeds/all-en.atom.xml | 238 +++++++++++++- feeds/all.atom.xml | 238 +++++++++++++- feeds/code.atom.xml | 238 +++++++++++++- feeds/tags/geojson.atom.xml | 238 ++++++++++++++ feeds/tags/umap.atom.xml | 238 +++++++++++++- feeds/tags/websockets.atom.xml | 238 ++++++++++++++ tag/geojson.html | 68 ++++ tag/umap.html | 16 +- tag/websockets.html | 68 ++++ tags.html | 4 +- 164 files changed, 4104 insertions(+), 996 deletions(-) rename drafts/adding-collaboration-on-umap-third-update.html => adding-collaboration-on-umap-third-update.html (95%) create mode 100644 author/.html59 create mode 100644 feeds/tags/geojson.atom.xml create mode 100644 feeds/tags/websockets.atom.xml create mode 100644 tag/geojson.html create mode 100644 tag/websockets.html diff --git a/drafts/adding-collaboration-on-umap-third-update.html b/adding-collaboration-on-umap-third-update.html similarity index 95% rename from drafts/adding-collaboration-on-umap-third-update.html rename to adding-collaboration-on-umap-third-update.html index 2547044..9fcb4f1 100644 --- a/drafts/adding-collaboration-on-umap-third-update.html +++ b/adding-collaboration-on-umap-third-update.html @@ -85,7 +85,7 @@ understand the big picture, and I’m not getting lost in the details like I

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -102,14 +114,8 @@ sudo diskutil eraseDiskPython, CRDT, Sync, uMap

Adding Real-Time Collaboration to uMap, first week

A heads-up on what I've been doing this week on uMap
-
- - - Datasette, Graphs, SQL

Using Datasette for tracking my professional activity

-

I’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

-

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -

+
+ + + umap, geojson, websockets

Adding collaboration on uMap, third update

+

I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

+

JavaScript modules

+

uMap has been there since 2012, at a time +when ES6 …

+
@@ -103,13 +115,8 @@ sudo diskutil eraseDiskI’ve been following Simon Willison since quite some time, but I’ve actually never played with his main project Datasette before.

As I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …

-
- - - SQL, SQLAlchemy, Python

Using DISTINCT in Parent-Child Relationships

- How to get parent and most-recent child in a one-to-many relationship -
    +
  • 2023, Semaine 48
  • 2023, Semaine 47
  • Using pelican to track my worked and volunteer hours
  • Adding Real-Time Collaboration to uMap, second week
  • diff --git a/categories.html b/categories.html index 178a630..0f0aec9 100644 --- a/categories.html +++ b/categories.html @@ -45,7 +45,7 @@ Alexis Métaireau

    Code, etc.

    -
    Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais). 76 articles.
    +
    Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais). 77 articles.

    Journal

    Quelques réfléxions, bien souvent autour du monde du travail ou de la technologie.. 69 articles.

    Notes de lecture

    diff --git a/code/index.html b/code/index.html index 8304d4a..631f7c1 100644 --- a/code/index.html +++ b/code/index.html @@ -48,6 +48,19 @@ Alexis Métaireau

    Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

    +
    + + +

    Adding collaboration on uMap, third update

    +

    I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

    +

    JavaScript modules

    +

    uMap has been there since 2012, at a time +when ES6 …

    +
    + umap, geojson, websockets
    @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


    Datasette, Graphs, SQL
    -
    - - -

    Using DISTINCT in Parent-Child Relationships

    - How to get parent and most-recent child in a one-to-many relationship -
    - SQL, SQLAlchemy, Python
      +
    • Using DISTINCT in Parent-Child Relationships
    • +
    • Convert string to duration
    • llm command-line tips
    • diff --git a/code/index10.html b/code/index10.html index 8304d4a..631f7c1 100644 --- a/code/index10.html +++ b/code/index10.html @@ -48,6 +48,19 @@ Alexis Métaireau

      Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

      +
      + + +

      Adding collaboration on uMap, third update

      +

      I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

      +

      JavaScript modules

      +

      uMap has been there since 2012, at a time +when ES6 …

      +
      + umap, geojson, websockets
      @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


      Datasette, Graphs, SQL
      -
      - - -

      Using DISTINCT in Parent-Child Relationships

      - How to get parent and most-recent child in a one-to-many relationship -
      - SQL, SQLAlchemy, Python
        +
      • Using DISTINCT in Parent-Child Relationships
      • +
      • Convert string to duration
      • llm command-line tips
      • diff --git a/code/index11.html b/code/index11.html index 8304d4a..631f7c1 100644 --- a/code/index11.html +++ b/code/index11.html @@ -48,6 +48,19 @@ Alexis Métaireau

        Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

        +
        + + +

        Adding collaboration on uMap, third update

        +

        I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

        +

        JavaScript modules

        +

        uMap has been there since 2012, at a time +when ES6 …

        +
        + umap, geojson, websockets
        @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


        Datasette, Graphs, SQL
        -
        - - -

        Using DISTINCT in Parent-Child Relationships

        - How to get parent and most-recent child in a one-to-many relationship -
        - SQL, SQLAlchemy, Python
          +
        • Using DISTINCT in Parent-Child Relationships
        • +
        • Convert string to duration
        • llm command-line tips
        • diff --git a/code/index12.html b/code/index12.html index 8304d4a..631f7c1 100644 --- a/code/index12.html +++ b/code/index12.html @@ -48,6 +48,19 @@ Alexis Métaireau

          Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

          +
          + + +

          Adding collaboration on uMap, third update

          +

          I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

          +

          JavaScript modules

          +

          uMap has been there since 2012, at a time +when ES6 …

          +
          + umap, geojson, websockets
          @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


          Datasette, Graphs, SQL
          -
          - - -

          Using DISTINCT in Parent-Child Relationships

          - How to get parent and most-recent child in a one-to-many relationship -
          - SQL, SQLAlchemy, Python
            +
          • Using DISTINCT in Parent-Child Relationships
          • +
          • Convert string to duration
          • llm command-line tips
          • diff --git a/code/index13.html b/code/index13.html index 8304d4a..631f7c1 100644 --- a/code/index13.html +++ b/code/index13.html @@ -48,6 +48,19 @@ Alexis Métaireau

            Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

            +
            + + +

            Adding collaboration on uMap, third update

            +

            I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

            +

            JavaScript modules

            +

            uMap has been there since 2012, at a time +when ES6 …

            +
            + umap, geojson, websockets
            @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


            Datasette, Graphs, SQL
            -
            - - -

            Using DISTINCT in Parent-Child Relationships

            - How to get parent and most-recent child in a one-to-many relationship -
            - SQL, SQLAlchemy, Python
              +
            • Using DISTINCT in Parent-Child Relationships
            • +
            • Convert string to duration
            • llm command-line tips
            • diff --git a/code/index14.html b/code/index14.html index 8304d4a..631f7c1 100644 --- a/code/index14.html +++ b/code/index14.html @@ -48,6 +48,19 @@ Alexis Métaireau

              Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

              +
              + + +

              Adding collaboration on uMap, third update

              +

              I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

              +

              JavaScript modules

              +

              uMap has been there since 2012, at a time +when ES6 …

              +
              + umap, geojson, websockets
              @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


              Datasette, Graphs, SQL
              -
              - - -

              Using DISTINCT in Parent-Child Relationships

              - How to get parent and most-recent child in a one-to-many relationship -
              - SQL, SQLAlchemy, Python
                +
              • Using DISTINCT in Parent-Child Relationships
              • +
              • Convert string to duration
              • llm command-line tips
              • diff --git a/code/index15.html b/code/index15.html index 8304d4a..631f7c1 100644 --- a/code/index15.html +++ b/code/index15.html @@ -48,6 +48,19 @@ Alexis Métaireau

                Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

                +
                + + +

                Adding collaboration on uMap, third update

                +

                I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                +

                JavaScript modules

                +

                uMap has been there since 2012, at a time +when ES6 …

                +
                + umap, geojson, websockets
                @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


                Datasette, Graphs, SQL
                -
                - - -

                Using DISTINCT in Parent-Child Relationships

                - How to get parent and most-recent child in a one-to-many relationship -
                - SQL, SQLAlchemy, Python
                  +
                • Using DISTINCT in Parent-Child Relationships
                • +
                • Convert string to duration
                • llm command-line tips
                • diff --git a/code/index16.html b/code/index16.html index 8304d4a..631f7c1 100644 --- a/code/index16.html +++ b/code/index16.html @@ -48,6 +48,19 @@ Alexis Métaireau

                  Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

                  +
                  + + +

                  Adding collaboration on uMap, third update

                  +

                  I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                  +

                  JavaScript modules

                  +

                  uMap has been there since 2012, at a time +when ES6 …

                  +
                  + umap, geojson, websockets
                  @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


                  Datasette, Graphs, SQL
                  -
                  - - -

                  Using DISTINCT in Parent-Child Relationships

                  - How to get parent and most-recent child in a one-to-many relationship -
                  - SQL, SQLAlchemy, Python
                    +
                  • Using DISTINCT in Parent-Child Relationships
                  • +
                  • Convert string to duration
                  • llm command-line tips
                  • diff --git a/code/index17.html b/code/index17.html index 8304d4a..631f7c1 100644 --- a/code/index17.html +++ b/code/index17.html @@ -48,6 +48,19 @@ Alexis Métaireau

                    Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

                    +
                    + + +

                    Adding collaboration on uMap, third update

                    +

                    I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                    +

                    JavaScript modules

                    +

                    uMap has been there since 2012, at a time +when ES6 …

                    +
                    + umap, geojson, websockets
                    @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


                    Datasette, Graphs, SQL
                    -
                    - - -

                    Using DISTINCT in Parent-Child Relationships

                    - How to get parent and most-recent child in a one-to-many relationship -
                    - SQL, SQLAlchemy, Python
                      +
                    • Using DISTINCT in Parent-Child Relationships
                    • +
                    • Convert string to duration
                    • llm command-line tips
                    • diff --git a/code/index18.html b/code/index18.html index 8304d4a..631f7c1 100644 --- a/code/index18.html +++ b/code/index18.html @@ -48,6 +48,19 @@ Alexis Métaireau

                      Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

                      +
                      + + +

                      Adding collaboration on uMap, third update

                      +

                      I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                      +

                      JavaScript modules

                      +

                      uMap has been there since 2012, at a time +when ES6 …

                      +
                      + umap, geojson, websockets
                      @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


                      Datasette, Graphs, SQL
                      -
                      - - -

                      Using DISTINCT in Parent-Child Relationships

                      - How to get parent and most-recent child in a one-to-many relationship -
                      - SQL, SQLAlchemy, Python
                        +
                      • Using DISTINCT in Parent-Child Relationships
                      • +
                      • Convert string to duration
                      • llm command-line tips
                      • diff --git a/code/index19.html b/code/index19.html index 8304d4a..631f7c1 100644 --- a/code/index19.html +++ b/code/index19.html @@ -48,6 +48,19 @@ Alexis Métaireau

                        Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

                        +
                        + + +

                        Adding collaboration on uMap, third update

                        +

                        I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                        +

                        JavaScript modules

                        +

                        uMap has been there since 2012, at a time +when ES6 …

                        +
                        + umap, geojson, websockets
                        @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


                        Datasette, Graphs, SQL
                        -
                        - - -

                        Using DISTINCT in Parent-Child Relationships

                        - How to get parent and most-recent child in a one-to-many relationship -
                        - SQL, SQLAlchemy, Python
                          +
                        • Using DISTINCT in Parent-Child Relationships
                        • +
                        • Convert string to duration
                        • llm command-line tips
                        • diff --git a/code/index2.html b/code/index2.html index 8304d4a..631f7c1 100644 --- a/code/index2.html +++ b/code/index2.html @@ -48,6 +48,19 @@ Alexis Métaireau

                          Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

                          +
                          + + +

                          Adding collaboration on uMap, third update

                          +

                          I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                          +

                          JavaScript modules

                          +

                          uMap has been there since 2012, at a time +when ES6 …

                          +
                          + umap, geojson, websockets
                          @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


                          Datasette, Graphs, SQL
                          -
                          - - -

                          Using DISTINCT in Parent-Child Relationships

                          - How to get parent and most-recent child in a one-to-many relationship -
                          - SQL, SQLAlchemy, Python
                            +
                          • Using DISTINCT in Parent-Child Relationships
                          • +
                          • Convert string to duration
                          • llm command-line tips
                          • diff --git a/code/index20.html b/code/index20.html index 8304d4a..631f7c1 100644 --- a/code/index20.html +++ b/code/index20.html @@ -48,6 +48,19 @@ Alexis Métaireau

                            Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

                            +
                            + + +

                            Adding collaboration on uMap, third update

                            +

                            I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                            +

                            JavaScript modules

                            +

                            uMap has been there since 2012, at a time +when ES6 …

                            +
                            + umap, geojson, websockets
                            @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


                            Datasette, Graphs, SQL
                            -
                            - - -

                            Using DISTINCT in Parent-Child Relationships

                            - How to get parent and most-recent child in a one-to-many relationship -
                            - SQL, SQLAlchemy, Python
                              +
                            • Using DISTINCT in Parent-Child Relationships
                            • +
                            • Convert string to duration
                            • llm command-line tips
                            • diff --git a/code/index21.html b/code/index21.html index 8304d4a..631f7c1 100644 --- a/code/index21.html +++ b/code/index21.html @@ -48,6 +48,19 @@ Alexis Métaireau

                              Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

                              +
                              + + +

                              Adding collaboration on uMap, third update

                              +

                              I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                              +

                              JavaScript modules

                              +

                              uMap has been there since 2012, at a time +when ES6 …

                              +
                              + umap, geojson, websockets
                              @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


                              Datasette, Graphs, SQL
                              -
                              - - -

                              Using DISTINCT in Parent-Child Relationships

                              - How to get parent and most-recent child in a one-to-many relationship -
                              - SQL, SQLAlchemy, Python
                                +
                              • Using DISTINCT in Parent-Child Relationships
                              • +
                              • Convert string to duration
                              • llm command-line tips
                              • diff --git a/code/index22.html b/code/index22.html index 8304d4a..631f7c1 100644 --- a/code/index22.html +++ b/code/index22.html @@ -48,6 +48,19 @@ Alexis Métaireau

                                Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

                                +
                                + + +

                                Adding collaboration on uMap, third update

                                +

                                I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                                +

                                JavaScript modules

                                +

                                uMap has been there since 2012, at a time +when ES6 …

                                +
                                + umap, geojson, websockets
                                @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


                                Datasette, Graphs, SQL
                                -
                                - - -

                                Using DISTINCT in Parent-Child Relationships

                                - How to get parent and most-recent child in a one-to-many relationship -
                                - SQL, SQLAlchemy, Python
                                  +
                                • Using DISTINCT in Parent-Child Relationships
                                • +
                                • Convert string to duration
                                • llm command-line tips
                                • diff --git a/code/index23.html b/code/index23.html index 8304d4a..631f7c1 100644 --- a/code/index23.html +++ b/code/index23.html @@ -48,6 +48,19 @@ Alexis Métaireau

                                  Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

                                  +
                                  + + +

                                  Adding collaboration on uMap, third update

                                  +

                                  I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                                  +

                                  JavaScript modules

                                  +

                                  uMap has been there since 2012, at a time +when ES6 …

                                  +
                                  + umap, geojson, websockets
                                  @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


                                  Datasette, Graphs, SQL
                                  -
                                  - - -

                                  Using DISTINCT in Parent-Child Relationships

                                  - How to get parent and most-recent child in a one-to-many relationship -
                                  - SQL, SQLAlchemy, Python
                                    +
                                  • Using DISTINCT in Parent-Child Relationships
                                  • +
                                  • Convert string to duration
                                  • llm command-line tips
                                  • diff --git a/code/index24.html b/code/index24.html index 8304d4a..631f7c1 100644 --- a/code/index24.html +++ b/code/index24.html @@ -48,6 +48,19 @@ Alexis Métaireau

                                    Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

                                    +
                                    + + +

                                    Adding collaboration on uMap, third update

                                    +

                                    I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                                    +

                                    JavaScript modules

                                    +

                                    uMap has been there since 2012, at a time +when ES6 …

                                    +
                                    + umap, geojson, websockets
                                    @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


                                    Datasette, Graphs, SQL
                                    -
                                    - - -

                                    Using DISTINCT in Parent-Child Relationships

                                    - How to get parent and most-recent child in a one-to-many relationship -
                                    - SQL, SQLAlchemy, Python
                                      +
                                    • Using DISTINCT in Parent-Child Relationships
                                    • +
                                    • Convert string to duration
                                    • llm command-line tips
                                    • diff --git a/code/index25.html b/code/index25.html index 8304d4a..631f7c1 100644 --- a/code/index25.html +++ b/code/index25.html @@ -48,6 +48,19 @@ Alexis Métaireau

                                      Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

                                      +
                                      + + +

                                      Adding collaboration on uMap, third update

                                      +

                                      I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                                      +

                                      JavaScript modules

                                      +

                                      uMap has been there since 2012, at a time +when ES6 …

                                      +
                                      + umap, geojson, websockets
                                      @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


                                      Datasette, Graphs, SQL
                                      -
                                      - - -

                                      Using DISTINCT in Parent-Child Relationships

                                      - How to get parent and most-recent child in a one-to-many relationship -
                                      - SQL, SQLAlchemy, Python
                                        +
                                      • Using DISTINCT in Parent-Child Relationships
                                      • +
                                      • Convert string to duration
                                      • llm command-line tips
                                      • diff --git a/code/index26.html b/code/index26.html index 8304d4a..631f7c1 100644 --- a/code/index26.html +++ b/code/index26.html @@ -48,6 +48,19 @@ Alexis Métaireau

                                        Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

                                        +
                                        + + +

                                        Adding collaboration on uMap, third update

                                        +

                                        I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                                        +

                                        JavaScript modules

                                        +

                                        uMap has been there since 2012, at a time +when ES6 …

                                        +
                                        + umap, geojson, websockets
                                        @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


                                        Datasette, Graphs, SQL
                                        -
                                        - - -

                                        Using DISTINCT in Parent-Child Relationships

                                        - How to get parent and most-recent child in a one-to-many relationship -
                                        - SQL, SQLAlchemy, Python
                                          +
                                        • Using DISTINCT in Parent-Child Relationships
                                        • +
                                        • Convert string to duration
                                        • llm command-line tips
                                        • diff --git a/code/index3.html b/code/index3.html index 8304d4a..631f7c1 100644 --- a/code/index3.html +++ b/code/index3.html @@ -48,6 +48,19 @@ Alexis Métaireau

                                          Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

                                          +
                                          + + +

                                          Adding collaboration on uMap, third update

                                          +

                                          I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                                          +

                                          JavaScript modules

                                          +

                                          uMap has been there since 2012, at a time +when ES6 …

                                          +
                                          + umap, geojson, websockets
                                          @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


                                          Datasette, Graphs, SQL
                                          -
                                          - - -

                                          Using DISTINCT in Parent-Child Relationships

                                          - How to get parent and most-recent child in a one-to-many relationship -
                                          - SQL, SQLAlchemy, Python
                                            +
                                          • Using DISTINCT in Parent-Child Relationships
                                          • +
                                          • Convert string to duration
                                          • llm command-line tips
                                          • diff --git a/code/index4.html b/code/index4.html index 8304d4a..631f7c1 100644 --- a/code/index4.html +++ b/code/index4.html @@ -48,6 +48,19 @@ Alexis Métaireau

                                            Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

                                            +
                                            + + +

                                            Adding collaboration on uMap, third update

                                            +

                                            I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                                            +

                                            JavaScript modules

                                            +

                                            uMap has been there since 2012, at a time +when ES6 …

                                            +
                                            + umap, geojson, websockets
                                            @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


                                            Datasette, Graphs, SQL
                                            -
                                            - - -

                                            Using DISTINCT in Parent-Child Relationships

                                            - How to get parent and most-recent child in a one-to-many relationship -
                                            - SQL, SQLAlchemy, Python
                                              +
                                            • Using DISTINCT in Parent-Child Relationships
                                            • +
                                            • Convert string to duration
                                            • llm command-line tips
                                            • diff --git a/code/index5.html b/code/index5.html index 8304d4a..631f7c1 100644 --- a/code/index5.html +++ b/code/index5.html @@ -48,6 +48,19 @@ Alexis Métaireau

                                              Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

                                              +
                                              + + +

                                              Adding collaboration on uMap, third update

                                              +

                                              I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                                              +

                                              JavaScript modules

                                              +

                                              uMap has been there since 2012, at a time +when ES6 …

                                              +
                                              + umap, geojson, websockets
                                              @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


                                              Datasette, Graphs, SQL
                                              -
                                              - - -

                                              Using DISTINCT in Parent-Child Relationships

                                              - How to get parent and most-recent child in a one-to-many relationship -
                                              - SQL, SQLAlchemy, Python
                                                +
                                              • Using DISTINCT in Parent-Child Relationships
                                              • +
                                              • Convert string to duration
                                              • llm command-line tips
                                              • diff --git a/code/index6.html b/code/index6.html index 8304d4a..631f7c1 100644 --- a/code/index6.html +++ b/code/index6.html @@ -48,6 +48,19 @@ Alexis Métaireau

                                                Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

                                                +
                                                + + +

                                                Adding collaboration on uMap, third update

                                                +

                                                I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                                                +

                                                JavaScript modules

                                                +

                                                uMap has been there since 2012, at a time +when ES6 …

                                                +
                                                + umap, geojson, websockets
                                                @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


                                                Datasette, Graphs, SQL
                                                -
                                                - - -

                                                Using DISTINCT in Parent-Child Relationships

                                                - How to get parent and most-recent child in a one-to-many relationship -
                                                - SQL, SQLAlchemy, Python
                                                  +
                                                • Using DISTINCT in Parent-Child Relationships
                                                • +
                                                • Convert string to duration
                                                • llm command-line tips
                                                • diff --git a/code/index7.html b/code/index7.html index 8304d4a..631f7c1 100644 --- a/code/index7.html +++ b/code/index7.html @@ -48,6 +48,19 @@ Alexis Métaireau

                                                  Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

                                                  +
                                                  + + +

                                                  Adding collaboration on uMap, third update

                                                  +

                                                  I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                                                  +

                                                  JavaScript modules

                                                  +

                                                  uMap has been there since 2012, at a time +when ES6 …

                                                  +
                                                  + umap, geojson, websockets
                                                  @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


                                                  Datasette, Graphs, SQL
                                                  -
                                                  - - -

                                                  Using DISTINCT in Parent-Child Relationships

                                                  - How to get parent and most-recent child in a one-to-many relationship -
                                                  - SQL, SQLAlchemy, Python
                                                    +
                                                  • Using DISTINCT in Parent-Child Relationships
                                                  • +
                                                  • Convert string to duration
                                                  • llm command-line tips
                                                  • diff --git a/code/index8.html b/code/index8.html index 8304d4a..631f7c1 100644 --- a/code/index8.html +++ b/code/index8.html @@ -48,6 +48,19 @@ Alexis Métaireau

                                                    Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

                                                    +
                                                    + + +

                                                    Adding collaboration on uMap, third update

                                                    +

                                                    I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                                                    +

                                                    JavaScript modules

                                                    +

                                                    uMap has been there since 2012, at a time +when ES6 …

                                                    +
                                                    + umap, geojson, websockets
                                                    @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


                                                    Datasette, Graphs, SQL
                                                    -
                                                    - - -

                                                    Using DISTINCT in Parent-Child Relationships

                                                    - How to get parent and most-recent child in a one-to-many relationship -
                                                    - SQL, SQLAlchemy, Python
                                                      +
                                                    • Using DISTINCT in Parent-Child Relationships
                                                    • +
                                                    • Convert string to duration
                                                    • llm command-line tips
                                                    • diff --git a/code/index9.html b/code/index9.html index 8304d4a..631f7c1 100644 --- a/code/index9.html +++ b/code/index9.html @@ -48,6 +48,19 @@ Alexis Métaireau

                                                      Des bouts de trucs liés au code, que je trouve utiles de stocker quelque part (en anglais)

                                                      +
                                                      + + +

                                                      Adding collaboration on uMap, third update

                                                      +

                                                      I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                                                      +

                                                      JavaScript modules

                                                      +

                                                      uMap has been there since 2012, at a time +when ES6 …

                                                      +
                                                      + umap, geojson, websockets
                                                      @@ -117,15 +130,10 @@ sudo diskutil eraseDiskAs I’m going back into development, I’m trying to track where my time goes, to be able to find patterns, and just remember how much time …


                                                      Datasette, Graphs, SQL
                                                      -
                                                      - - -

                                                      Using DISTINCT in Parent-Child Relationships

                                                      - How to get parent and most-recent child in a one-to-many relationship -
                                                      - SQL, SQLAlchemy, Python
                                                        +
                                                      • Using DISTINCT in Parent-Child Relationships
                                                      • +
                                                      • Convert string to duration
                                                      • llm command-line tips
                                                      • diff --git a/feeds/.atom.xml b/feeds/.atom.xml index 07f2dcf..283e0b8 100644 --- a/feeds/.atom.xml +++ b/feeds/.atom.xml @@ -44,7 +44,243 @@ <h2 id="notes">Notes</h2> <ul> <li>Un article de <span class="caps">MDN</span> sur comment écrire de la doc: https://developer.mozilla.org/en-<span class="caps">US</span>/blog/technical-writing/ </li> -</ul>Returning objects from an arrow function2024-02-08T00:00:00+01:002024-02-08T00:00:00+01:00tag:blog.notmyidea.org,2024-02-08:/returning-objects-from-an-arrow-function.html<p>When using an arrow function in JavaScript, I was expecting to be able to return objects, but ended up with returning <code>undefined</code> values.</p> +</ul>Adding collaboration on uMap, third update2024-02-12T00:00:00+01:002024-02-12T00:00:00+01:00tag:blog.notmyidea.org,2024-02-12:/adding-collaboration-on-umap-third-update.html<p>I&#8217;ve spent the last few weeks working on <a href="https://umap-project.org">uMap</a>, still +with the goal of bringing real-time collaboration to the maps. I&#8217;m not there +yet, but I&#8217;ve made some progress that I will relate&nbsp;here.</p> +<h2 id="javascript-modules">JavaScript&nbsp;modules</h2> +<p>uMap has been there <a href="https://github.com/ +umap-project/umap/commit/0cce7f9e2a19c83fa76645d7773d39d54f357c43">since 2012</a>, at a time +when <span class="caps">ES6 …</span></p><p>I&#8217;ve spent the last few weeks working on <a href="https://umap-project.org">uMap</a>, still +with the goal of bringing real-time collaboration to the maps. I&#8217;m not there +yet, but I&#8217;ve made some progress that I will relate&nbsp;here.</p> +<h2 id="javascript-modules">JavaScript&nbsp;modules</h2> +<p>uMap has been there <a href="https://github.com/ +umap-project/umap/commit/0cce7f9e2a19c83fa76645d7773d39d54f357c43">since 2012</a>, at a time +when <span class="caps">ES6</span> <a href="https://fr.wikipedia.org/wiki/ECMAScript">wasn&#8217;t out there&nbsp;yet</a></p> +<p>At the time, it wasn&#8217;t possible to use JavaScript modules, nor modern JavaScript +syntax. The project stayed with these requirements for a long time, in order to support +people with old browsers. But as time goes on, we can now have access to more&nbsp;features.</p> +<p>The team has been working hard on bringing modules to the mix, and it +wasn&#8217;t a piece of cake. But, the result is here: we&#8217;re <a href="https://github.com/umap-project/umap/pull/1463/files">now able to use modern +JavaSript modules</a> and we +are now more confident <a href="https://github.com/umap-project/umap/commit/65f1cdd6b4569657ef5e219d9b377fec85c41958">about which features of the languages we can use or&nbsp;not</a></p> +<hr> +<p>I then spent ~way too much~ some time trying to integrate existing CRDTs like +Automerge and <span class="caps">YJS</span> in our project. These two libs are unfortunately expecting us to +use a bundler, which we aren&#8217;t&nbsp;currently.</p> +<p>uMap is plain old JavaScript. It&#8217;s not using react or any other framework. The way +I see this is that it makes it possible for us to have something &#8220;close to the +metal&#8221;, if that means anything when it comes to web development. We&#8217;re not tied +to the development pace of these frameworks, and have more control on what we +do. It&#8217;s easier to&nbsp;debug.</p> +<p>So, after making tweaks and learning how &#8220;modules&#8221;, &#8220;requires&#8221; and &#8220;bundling&#8221; +was working, I ultimately decided to take a break from this path, to work on the +wiring with uMap. After all, CRDTs might not even be the way forward for&nbsp;us.</p> +<h2 id="internals">Internals</h2> +<p>I was not expecting this to be easy and was a bit afraid. Mostly because I&#8217;m out of my +comfort zone. After some time with the head under the water, I&#8217;m now able to better +understand the big picture, and I&#8217;m not getting lost in the details like I was at&nbsp;first.</p> +<p>Let me try to summarize what I&#8217;ve&nbsp;learned.</p> +<p>uMap appears to be doing a lot of different things, but in the end&nbsp;it&#8217;s:</p> +<ul> +<li>Using <a href="https://leafletjs.com/">Leaflet.js</a> to render <em>features</em> on the map&nbsp;;</li> +<li>Using <a href="https://github.com/Leaflet/Leaflet.Editable">Leaflet Editable</a> to edit + complex shapes, like polylines, polygons, and to draw markers&nbsp;;</li> +<li>Using the <a href="https://github.com/yohanboniface/Leaflet.FormBuilder">Formbuilder</a> + to expose a way for the users to edit the features, and the data of the&nbsp;map</li> +<li>Serializing the layers to and from <a href="https://geojson.org/">GeoJSON</a>. That&#8217;s + what&#8217;s being sent to and received from the&nbsp;server.</li> +<li>Providing different layer types (marker cluster, chloropleth, etc) to display + the data in different&nbsp;ways.</li> +</ul> +<h3 id="naming-matters">Naming&nbsp;matters</h3> +<p>There is some naming overlap between the different projects we&#8217;re using, and +it&#8217;s important to have these small clarifications in&nbsp;mind:</p> +<h4 id="leaflet-layers-and-umap-features">Leaflet layers and uMap&nbsp;features</h4> +<p><strong>In Leaflet, everything is a layer</strong>. What we call <em>features</em> in geoJSON are +leaflet layers, and even a (uMap) layer is a layer. We need to be extra careful +what are our inputs and outputs in this&nbsp;context.</p> +<p>We actually have different layers concepts: the <em>datalayer</em> and the different +kind of layers (chloropleth, marker cluster, etc). A datalayer, is (as you can +guess) where the data is stored. It&#8217;s what uMap serializes. It contains the +features (with their properties). But that&#8217;s the trick: these features are named +<em>layers</em> by&nbsp;Leaflet.</p> +<h4 id="geojson-and-leaflet">GeoJSON and&nbsp;Leaflet</h4> +<p>We&#8217;re using GeoJSON to share data with the server, but we&#8217;re using Leaflet +internally. And these two have different way of naming&nbsp;things.</p> +<p>The different geometries are named differently (a leaflet <code>Marker</code> is a GeoJSON +<code>Point</code>), and their coordinates are stored differently: Leaflet stores <code>lat, +long</code> where GeoJSON stores <code>long, lat</code>. Not a big deal, but it&#8217;s a good thing +to&nbsp;know.</p> +<p>Leaflet stores data in <code>options</code>, where GeoJSON stores it in <code>properties</code>.</p> +<h3 id="this-is-not-reactive-programming">This is not reactive&nbsp;programming</h3> +<p>I was expecting the frontend to be organised similarly to Elm apps (or React +apps): a global state and a data flow (<a href="https:// react-redux.js.org/ +introduction/getting-started">a la redux</a>), with events changing the data that will trigger +a rerendering of the&nbsp;interface.</p> +<p>Things work differently for us: different components can write to the map, and +get updated without being centralized. It&#8217;s just a different&nbsp;paradigm.</p> +<h2 id="a-syncing-proof-of-concept">A syncing proof of&nbsp;concept</h2> +<p>With that in mind, I started thinking about a simple way to implement&nbsp;syncing. </p> +<p>I left aside all the thinking about how this would relate with CRDTs. It can +be useful, but later. For now, I &#8220;just&#8221; want to synchronize two maps. I want a +proof of concept to do informed&nbsp;decisions.</p> +<h3 id="syncing-map-properties">Syncing map&nbsp;properties</h3> +<p>I started syncing map properties. Things like the name of the map, the default +color and type of the marker, the description, the default zoom level,&nbsp;etc.</p> +<p>All of these are handled by &#8220;the formbuilder&#8221;. You pass it an object, a list of +properties and a callback to call when an update happens, and it will build for +you form&nbsp;inputs.</p> +<p>Taken from the documentation (and&nbsp;simplified):</p> +<div class="highlight"><pre><span></span><code><span class="kd">var</span><span class="w"> </span><span class="nx">tilelayerFields</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;name&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;display name&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;maxZoom&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurIntInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;max zoom&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;minZoom&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurIntInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;min zoom&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;attribution&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;attribution&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;tms&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;CheckBox&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">helpText</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;TMS format&#39;</span><span class="p">}]</span> +<span class="p">];</span> +<span class="kd">var</span><span class="w"> </span><span class="nx">builder</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">L</span><span class="p">.</span><span class="nx">FormBuilder</span><span class="p">(</span><span class="nx">myObject</span><span class="p">,</span><span class="w"> </span><span class="nx">tilelayerFields</span><span class="p">,</span><span class="w"> </span><span class="p">{</span> +<span class="w"> </span><span class="nx">callback</span><span class="o">:</span><span class="w"> </span><span class="nx">myCallback</span><span class="p">,</span> +<span class="w"> </span><span class="nx">callbackContext</span><span class="o">:</span><span class="w"> </span><span class="k">this</span> +<span class="p">});</span> +</code></pre></div> + +<p>In uMap, the formbuilder is used for every form you see on the right panel. Map +properties are stored in the <code>map</code> object.</p> +<p>We want two different clients work together. When one changes the value of a +property, the other client needs to be updated, and update its&nbsp;interface.</p> +<p>I&#8217;ve started by creating a mapping of property names to rerender-methods, and +added a method <code>renderProperties(properties)</code> which updates the interface, +depending on the properties passed to&nbsp;it.</p> +<p>We now have two important&nbsp;things:</p> +<ol> +<li>Some code getting called each time a property is changed&nbsp;;</li> +<li>A way to refresh the right interface when a property is&nbsp;changed.</li> +</ol> +<p>In other words, from one client we can send the message to the other client, +which will be able to rerender&nbsp;itself.</p> +<p>Looks like a&nbsp;plan.</p> +<h2 id="websockets">Websockets</h2> +<p>We need a way for the data to go from one side to the other. The easiest +way is probably&nbsp;websockets.</p> +<p>Here is a simple code which will relay messages from one websocket to the other +connected clients. It&#8217;s not the final code, it&#8217;s just for demo&nbsp;puposes.</p> +<p>A basic way to do this on the server side is to use python&#8217;s +<a href="https://websockets.readthedocs.io/">websockets</a>&nbsp;library.</p> +<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">asyncio</span> +<span class="kn">import</span> <span class="nn">websockets</span> +<span class="kn">from</span> <span class="nn">websockets.server</span> <span class="kn">import</span> <span class="n">serve</span> +<span class="kn">import</span> <span class="nn">json</span> + +<span class="c1"># Just relay all messages to other connected peers for now</span> + +<span class="n">CONNECTIONS</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span> + +<span class="k">async</span> <span class="k">def</span> <span class="nf">join_and_listen</span><span class="p">(</span><span class="n">websocket</span><span class="p">):</span> + <span class="n">CONNECTIONS</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + <span class="k">try</span><span class="p">:</span> + <span class="k">async</span> <span class="k">for</span> <span class="n">message</span> <span class="ow">in</span> <span class="n">websocket</span><span class="p">:</span> + <span class="c1"># recompute the peers-list at the time of message-sending.</span> + <span class="c1"># doing so beforehand would miss new connections</span> + <span class="n">peers</span> <span class="o">=</span> <span class="n">CONNECTIONS</span> <span class="o">-</span> <span class="p">{</span><span class="n">websocket</span><span class="p">}</span> + <span class="n">websockets</span><span class="o">.</span><span class="n">broadcast</span><span class="p">(</span><span class="n">peers</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span> + <span class="k">finally</span><span class="p">:</span> + <span class="n">CONNECTIONS</span><span class="o">.</span><span class="n">remove</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + + +<span class="k">async</span> <span class="k">def</span> <span class="nf">handler</span><span class="p">(</span><span class="n">websocket</span><span class="p">):</span> + <span class="n">message</span> <span class="o">=</span> <span class="k">await</span> <span class="n">websocket</span><span class="o">.</span><span class="n">recv</span><span class="p">()</span> + <span class="n">event</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">message</span><span class="p">)</span> + + <span class="c1"># The first event should always be &#39;join&#39;</span> + <span class="k">assert</span> <span class="n">event</span><span class="p">[</span><span class="s2">&quot;kind&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="s2">&quot;join&quot;</span> + <span class="k">await</span> <span class="n">join_and_listen</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + +<span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span> + <span class="k">async</span> <span class="k">with</span> <span class="n">serve</span><span class="p">(</span><span class="n">handler</span><span class="p">,</span> <span class="s2">&quot;localhost&quot;</span><span class="p">,</span> <span class="mi">8001</span><span class="p">):</span> + <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">Future</span><span class="p">()</span> <span class="c1"># run forever</span> + +<span class="n">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">main</span><span class="p">())</span> +</code></pre></div> + +<p>On the client side, it&#8217;s fairly easy as well. I won&#8217;t even cover it&nbsp;here.</p> +<p>We now have a way to send data from one client to the other. +Let&#8217;s consider the actions we do as &#8220;verbs&#8221;. For now, we&#8217;re just updating +properties values, so we just need the <code>update</code> verb.</p> +<h2 id="code-architecture">Code&nbsp;architecture</h2> +<p>We need different&nbsp;parts:</p> +<ul> +<li>the <strong>transport</strong>, which connects to the websockets, sends and receives&nbsp;messages.</li> +<li>the <strong>message sender</strong> to relat local messages to the other&nbsp;party.</li> +<li>the <strong>message receiver</strong> that&#8217;s being called each time we receive a&nbsp;message.</li> +<li>the <strong>sync engine</strong> which glues everything&nbsp;together</li> +<li>Different <strong>updaters</strong>, which knows how to apply received messages, the goal being + to update the interface in the&nbsp;end.</li> +</ul> +<p>When receiving a message it will be routed to the correct updater, which will +know what to do with&nbsp;it.</p> +<p>In our case, its fairly simple: when updating the <code>name</code> property, we send a +message with <code>name</code> and <code>value</code>. We also need to send along some additional +info: the <code>subject</code>.</p> +<p>In our case, it&#8217;s <code>map</code> because we&#8217;re updating map&nbsp;properties.</p> +<p>When initializing the <code>map</code>, we&#8217;re initializing the <code>SyncEngine</code>, like&nbsp;this:</p> +<div class="highlight"><pre><span></span><code><span class="c1">// inside the map</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">syncEngine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">umap</span><span class="p">.</span><span class="nx">SyncEngine</span><span class="p">(</span><span class="k">this</span><span class="p">)</span> + +<span class="c1">// Then, when we need to send data to the other party</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">syncEngine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">obj</span><span class="p">.</span><span class="nx">getSyncEngine</span><span class="p">()</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">subject</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">obj</span><span class="p">.</span><span class="nx">getSyncSubject</span><span class="p">()</span> + +<span class="nx">syncEngine</span><span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">subject</span><span class="p">,</span><span class="w"> </span><span class="nx">field</span><span class="p">,</span><span class="w"> </span><span class="nx">value</span><span class="p">)</span> +</code></pre></div> + +<p>The code on the other side of the wire is simple enough: when you receive the +message, change the data and rerender the&nbsp;properties:</p> +<div class="highlight"><pre><span></span><code><span class="k">this</span><span class="p">.</span><span class="nx">updateObjectValue</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">map</span><span class="p">,</span><span class="w"> </span><span class="nx">key</span><span class="p">,</span><span class="w"> </span><span class="nx">value</span><span class="p">)</span> +<span class="k">this</span><span class="p">.</span><span class="nx">map</span><span class="p">.</span><span class="nx">renderProperties</span><span class="p">(</span><span class="nx">key</span><span class="p">)</span> +</code></pre></div> + +<h2 id="syncing-features">Syncing&nbsp;features</h2> +<p>At this stage I was able to sync the properties of the map. A +small victory, but not the end of the&nbsp;trip.</p> +<p>The next step was to add syncing for features: markers, polygon and polylines, +alongside their&nbsp;properties.</p> +<p>All of these features have a uMap class representation (which extends Leaflets +ones). All of them share some code in the <code>FeatureMixin</code> class.</p> +<p>That seems a good place to do the&nbsp;changes.</p> +<p>I did a few&nbsp;changes:</p> +<ul> +<li>Each feature now has an identifier, so clients know they&#8217;re talking about the + same thing. This identifier is also stored in the database when&nbsp;saved.</li> +<li>I&#8217;ve added an <code>upsert</code> verb, because we don&#8217;t have any way (from the + interface) to make a distinction between the creation of a new feature and + its modification. The way we intercept the creation of a feature (or its + update) is to use Leaflet Editable&#8217;s <code>editable:drawing:commit</code> event. We just + have to listen to it and then send the appropriate messages&nbsp;!</li> +</ul> +<p>After some giggling around (ah, everybody wants to create a new protocol !) I +went with reusing GeoJSON. It allowed me to have a better understanding of how +Leaflet is using latlongs. That&#8217;s a multi-dimensional array, with variable +width, depending on the type of geometry and the number of shapes in each of&nbsp;these.</p> +<p>Clearly not something I want to redo, so I&#8217;m now reusing some Leaflet code, which handles this serialization for&nbsp;me.</p> +<p>I&#8217;m now able to sync different types of features with their&nbsp;properties.</p> +<video controls width="80%"> + <source src="/images/umap/sync-features.webm" type="video/webm"> +</video> + +<p>Point properties are also editable, using the already-existing table editor. I +was expecting this to require some work but it&#8217;s just working without more&nbsp;changes.</p> +<h2 id="whats-next">What&#8217;s next&nbsp;?</h2> +<p>I&#8217;m able to sync map properties, features and their properties, but I&#8217;m not +yet syncing layers. That&#8217;s the next step! I also plan to make some pull +requests with the interesting bits I&#8217;m sure will go in the final&nbsp;implementation:</p> +<ul> +<li>Adding ids to features, so we have a way to refer to&nbsp;them.</li> +<li>Having a way to map properties with how they render the interface, the <code>renderProperties</code> bits.</li> +</ul> +<p>When this demo will be working, I&#8217;ll probably spend some time updating it with the latest changes (umap is moving a lot these weeks). +I will probably focus on how to integrate websockets in the server side, and then will see how to leverage (maybe) some magic from CRDTs, if we need&nbsp;it.</p> +<p>See you for the next&nbsp;update!</p>Returning objects from an arrow function2024-02-08T00:00:00+01:002024-02-08T00:00:00+01:00tag:blog.notmyidea.org,2024-02-08:/returning-objects-from-an-arrow-function.html<p>When using an arrow function in JavaScript, I was expecting to be able to return objects, but ended up with returning <code>undefined</code> values.</p> <p>Turns out it&#8217;s not possible to return directly objects from inside the arrow function because they&#8217;re confused as&nbsp;statements.</p> <p>This is <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#function_body">covered by <span class="caps">MDN</span></a>.</p> <p>To …</p><p>When using an arrow function in JavaScript, I was expecting to be able to return objects, but ended up with returning <code>undefined</code> values.</p> diff --git a/feeds/.rss.xml b/feeds/.rss.xml index 4ab95bb..4c80abc 100644 --- a/feeds/.rss.xml +++ b/feeds/.rss.xml @@ -4,7 +4,13 @@ <h2 id="ce-qui-sest-passe">Ce qui s&#8217;est&nbsp;passé</h2> <dl> <dt><strong>🗺️ <a href="https://umap-projet.org">uMap</a></strong></dt> -<dd>On à commencé à clarifier certains aspects économiques du projet lors d&#8217;une discussion …</dd></dl>Tue, 13 Feb 2024 00:00:00 +0100tag:blog.notmyidea.org,2024-02-13:/notes-hebdo-16.htmlweeknotesReturning objects from an arrow functionhttps://blog.notmyidea.org/returning-objects-from-an-arrow-function.html<p>When using an arrow function in JavaScript, I was expecting to be able to return objects, but ended up with returning <code>undefined</code> values.</p> +<dd>On à commencé à clarifier certains aspects économiques du projet lors d&#8217;une discussion …</dd></dl>Tue, 13 Feb 2024 00:00:00 +0100tag:blog.notmyidea.org,2024-02-13:/notes-hebdo-16.htmlweeknotesAdding collaboration on uMap, third updatehttps://blog.notmyidea.org/adding-collaboration-on-umap-third-update.html<p>I&#8217;ve spent the last few weeks working on <a href="https://umap-project.org">uMap</a>, still +with the goal of bringing real-time collaboration to the maps. I&#8217;m not there +yet, but I&#8217;ve made some progress that I will relate&nbsp;here.</p> +<h2 id="javascript-modules">JavaScript&nbsp;modules</h2> +<p>uMap has been there <a href="https://github.com/ +umap-project/umap/commit/0cce7f9e2a19c83fa76645d7773d39d54f357c43">since 2012</a>, at a time +when <span class="caps">ES6 …</span></p>Mon, 12 Feb 2024 00:00:00 +0100tag:blog.notmyidea.org,2024-02-12:/adding-collaboration-on-umap-third-update.htmlcodeumapgeojsonwebsocketsReturning objects from an arrow functionhttps://blog.notmyidea.org/returning-objects-from-an-arrow-function.html<p>When using an arrow function in JavaScript, I was expecting to be able to return objects, but ended up with returning <code>undefined</code> values.</p> <p>Turns out it&#8217;s not possible to return directly objects from inside the arrow function because they&#8217;re confused as&nbsp;statements.</p> <p>This is <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#function_body">covered by <span class="caps">MDN</span></a>.</p> <p>To …</p>Thu, 08 Feb 2024 00:00:00 +0100tag:blog.notmyidea.org,2024-02-08:/returning-objects-from-an-arrow-function.htmlcodeJavascriptNotes hebdo #15https://blog.notmyidea.org/notes-hebdo-15.html diff --git a/feeds/all-en.atom.xml b/feeds/all-en.atom.xml index 28593e7..6ce518d 100644 --- a/feeds/all-en.atom.xml +++ b/feeds/all-en.atom.xml @@ -44,7 +44,243 @@ <h2 id="notes">Notes</h2> <ul> <li>Un article de <span class="caps">MDN</span> sur comment écrire de la doc: https://developer.mozilla.org/en-<span class="caps">US</span>/blog/technical-writing/ </li> -</ul>Returning objects from an arrow function2024-02-08T00:00:00+01:002024-02-08T00:00:00+01:00tag:blog.notmyidea.org,2024-02-08:/returning-objects-from-an-arrow-function.html<p>When using an arrow function in JavaScript, I was expecting to be able to return objects, but ended up with returning <code>undefined</code> values.</p> +</ul>Adding collaboration on uMap, third update2024-02-12T00:00:00+01:002024-02-12T00:00:00+01:00tag:blog.notmyidea.org,2024-02-12:/adding-collaboration-on-umap-third-update.html<p>I&#8217;ve spent the last few weeks working on <a href="https://umap-project.org">uMap</a>, still +with the goal of bringing real-time collaboration to the maps. I&#8217;m not there +yet, but I&#8217;ve made some progress that I will relate&nbsp;here.</p> +<h2 id="javascript-modules">JavaScript&nbsp;modules</h2> +<p>uMap has been there <a href="https://github.com/ +umap-project/umap/commit/0cce7f9e2a19c83fa76645d7773d39d54f357c43">since 2012</a>, at a time +when <span class="caps">ES6 …</span></p><p>I&#8217;ve spent the last few weeks working on <a href="https://umap-project.org">uMap</a>, still +with the goal of bringing real-time collaboration to the maps. I&#8217;m not there +yet, but I&#8217;ve made some progress that I will relate&nbsp;here.</p> +<h2 id="javascript-modules">JavaScript&nbsp;modules</h2> +<p>uMap has been there <a href="https://github.com/ +umap-project/umap/commit/0cce7f9e2a19c83fa76645d7773d39d54f357c43">since 2012</a>, at a time +when <span class="caps">ES6</span> <a href="https://fr.wikipedia.org/wiki/ECMAScript">wasn&#8217;t out there&nbsp;yet</a></p> +<p>At the time, it wasn&#8217;t possible to use JavaScript modules, nor modern JavaScript +syntax. The project stayed with these requirements for a long time, in order to support +people with old browsers. But as time goes on, we can now have access to more&nbsp;features.</p> +<p>The team has been working hard on bringing modules to the mix, and it +wasn&#8217;t a piece of cake. But, the result is here: we&#8217;re <a href="https://github.com/umap-project/umap/pull/1463/files">now able to use modern +JavaSript modules</a> and we +are now more confident <a href="https://github.com/umap-project/umap/commit/65f1cdd6b4569657ef5e219d9b377fec85c41958">about which features of the languages we can use or&nbsp;not</a></p> +<hr> +<p>I then spent ~way too much~ some time trying to integrate existing CRDTs like +Automerge and <span class="caps">YJS</span> in our project. These two libs are unfortunately expecting us to +use a bundler, which we aren&#8217;t&nbsp;currently.</p> +<p>uMap is plain old JavaScript. It&#8217;s not using react or any other framework. The way +I see this is that it makes it possible for us to have something &#8220;close to the +metal&#8221;, if that means anything when it comes to web development. We&#8217;re not tied +to the development pace of these frameworks, and have more control on what we +do. It&#8217;s easier to&nbsp;debug.</p> +<p>So, after making tweaks and learning how &#8220;modules&#8221;, &#8220;requires&#8221; and &#8220;bundling&#8221; +was working, I ultimately decided to take a break from this path, to work on the +wiring with uMap. After all, CRDTs might not even be the way forward for&nbsp;us.</p> +<h2 id="internals">Internals</h2> +<p>I was not expecting this to be easy and was a bit afraid. Mostly because I&#8217;m out of my +comfort zone. After some time with the head under the water, I&#8217;m now able to better +understand the big picture, and I&#8217;m not getting lost in the details like I was at&nbsp;first.</p> +<p>Let me try to summarize what I&#8217;ve&nbsp;learned.</p> +<p>uMap appears to be doing a lot of different things, but in the end&nbsp;it&#8217;s:</p> +<ul> +<li>Using <a href="https://leafletjs.com/">Leaflet.js</a> to render <em>features</em> on the map&nbsp;;</li> +<li>Using <a href="https://github.com/Leaflet/Leaflet.Editable">Leaflet Editable</a> to edit + complex shapes, like polylines, polygons, and to draw markers&nbsp;;</li> +<li>Using the <a href="https://github.com/yohanboniface/Leaflet.FormBuilder">Formbuilder</a> + to expose a way for the users to edit the features, and the data of the&nbsp;map</li> +<li>Serializing the layers to and from <a href="https://geojson.org/">GeoJSON</a>. That&#8217;s + what&#8217;s being sent to and received from the&nbsp;server.</li> +<li>Providing different layer types (marker cluster, chloropleth, etc) to display + the data in different&nbsp;ways.</li> +</ul> +<h3 id="naming-matters">Naming&nbsp;matters</h3> +<p>There is some naming overlap between the different projects we&#8217;re using, and +it&#8217;s important to have these small clarifications in&nbsp;mind:</p> +<h4 id="leaflet-layers-and-umap-features">Leaflet layers and uMap&nbsp;features</h4> +<p><strong>In Leaflet, everything is a layer</strong>. What we call <em>features</em> in geoJSON are +leaflet layers, and even a (uMap) layer is a layer. We need to be extra careful +what are our inputs and outputs in this&nbsp;context.</p> +<p>We actually have different layers concepts: the <em>datalayer</em> and the different +kind of layers (chloropleth, marker cluster, etc). A datalayer, is (as you can +guess) where the data is stored. It&#8217;s what uMap serializes. It contains the +features (with their properties). But that&#8217;s the trick: these features are named +<em>layers</em> by&nbsp;Leaflet.</p> +<h4 id="geojson-and-leaflet">GeoJSON and&nbsp;Leaflet</h4> +<p>We&#8217;re using GeoJSON to share data with the server, but we&#8217;re using Leaflet +internally. And these two have different way of naming&nbsp;things.</p> +<p>The different geometries are named differently (a leaflet <code>Marker</code> is a GeoJSON +<code>Point</code>), and their coordinates are stored differently: Leaflet stores <code>lat, +long</code> where GeoJSON stores <code>long, lat</code>. Not a big deal, but it&#8217;s a good thing +to&nbsp;know.</p> +<p>Leaflet stores data in <code>options</code>, where GeoJSON stores it in <code>properties</code>.</p> +<h3 id="this-is-not-reactive-programming">This is not reactive&nbsp;programming</h3> +<p>I was expecting the frontend to be organised similarly to Elm apps (or React +apps): a global state and a data flow (<a href="https:// react-redux.js.org/ +introduction/getting-started">a la redux</a>), with events changing the data that will trigger +a rerendering of the&nbsp;interface.</p> +<p>Things work differently for us: different components can write to the map, and +get updated without being centralized. It&#8217;s just a different&nbsp;paradigm.</p> +<h2 id="a-syncing-proof-of-concept">A syncing proof of&nbsp;concept</h2> +<p>With that in mind, I started thinking about a simple way to implement&nbsp;syncing. </p> +<p>I left aside all the thinking about how this would relate with CRDTs. It can +be useful, but later. For now, I &#8220;just&#8221; want to synchronize two maps. I want a +proof of concept to do informed&nbsp;decisions.</p> +<h3 id="syncing-map-properties">Syncing map&nbsp;properties</h3> +<p>I started syncing map properties. Things like the name of the map, the default +color and type of the marker, the description, the default zoom level,&nbsp;etc.</p> +<p>All of these are handled by &#8220;the formbuilder&#8221;. You pass it an object, a list of +properties and a callback to call when an update happens, and it will build for +you form&nbsp;inputs.</p> +<p>Taken from the documentation (and&nbsp;simplified):</p> +<div class="highlight"><pre><span></span><code><span class="kd">var</span><span class="w"> </span><span class="nx">tilelayerFields</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;name&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;display name&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;maxZoom&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurIntInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;max zoom&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;minZoom&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurIntInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;min zoom&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;attribution&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;attribution&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;tms&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;CheckBox&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">helpText</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;TMS format&#39;</span><span class="p">}]</span> +<span class="p">];</span> +<span class="kd">var</span><span class="w"> </span><span class="nx">builder</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">L</span><span class="p">.</span><span class="nx">FormBuilder</span><span class="p">(</span><span class="nx">myObject</span><span class="p">,</span><span class="w"> </span><span class="nx">tilelayerFields</span><span class="p">,</span><span class="w"> </span><span class="p">{</span> +<span class="w"> </span><span class="nx">callback</span><span class="o">:</span><span class="w"> </span><span class="nx">myCallback</span><span class="p">,</span> +<span class="w"> </span><span class="nx">callbackContext</span><span class="o">:</span><span class="w"> </span><span class="k">this</span> +<span class="p">});</span> +</code></pre></div> + +<p>In uMap, the formbuilder is used for every form you see on the right panel. Map +properties are stored in the <code>map</code> object.</p> +<p>We want two different clients work together. When one changes the value of a +property, the other client needs to be updated, and update its&nbsp;interface.</p> +<p>I&#8217;ve started by creating a mapping of property names to rerender-methods, and +added a method <code>renderProperties(properties)</code> which updates the interface, +depending on the properties passed to&nbsp;it.</p> +<p>We now have two important&nbsp;things:</p> +<ol> +<li>Some code getting called each time a property is changed&nbsp;;</li> +<li>A way to refresh the right interface when a property is&nbsp;changed.</li> +</ol> +<p>In other words, from one client we can send the message to the other client, +which will be able to rerender&nbsp;itself.</p> +<p>Looks like a&nbsp;plan.</p> +<h2 id="websockets">Websockets</h2> +<p>We need a way for the data to go from one side to the other. The easiest +way is probably&nbsp;websockets.</p> +<p>Here is a simple code which will relay messages from one websocket to the other +connected clients. It&#8217;s not the final code, it&#8217;s just for demo&nbsp;puposes.</p> +<p>A basic way to do this on the server side is to use python&#8217;s +<a href="https://websockets.readthedocs.io/">websockets</a>&nbsp;library.</p> +<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">asyncio</span> +<span class="kn">import</span> <span class="nn">websockets</span> +<span class="kn">from</span> <span class="nn">websockets.server</span> <span class="kn">import</span> <span class="n">serve</span> +<span class="kn">import</span> <span class="nn">json</span> + +<span class="c1"># Just relay all messages to other connected peers for now</span> + +<span class="n">CONNECTIONS</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span> + +<span class="k">async</span> <span class="k">def</span> <span class="nf">join_and_listen</span><span class="p">(</span><span class="n">websocket</span><span class="p">):</span> + <span class="n">CONNECTIONS</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + <span class="k">try</span><span class="p">:</span> + <span class="k">async</span> <span class="k">for</span> <span class="n">message</span> <span class="ow">in</span> <span class="n">websocket</span><span class="p">:</span> + <span class="c1"># recompute the peers-list at the time of message-sending.</span> + <span class="c1"># doing so beforehand would miss new connections</span> + <span class="n">peers</span> <span class="o">=</span> <span class="n">CONNECTIONS</span> <span class="o">-</span> <span class="p">{</span><span class="n">websocket</span><span class="p">}</span> + <span class="n">websockets</span><span class="o">.</span><span class="n">broadcast</span><span class="p">(</span><span class="n">peers</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span> + <span class="k">finally</span><span class="p">:</span> + <span class="n">CONNECTIONS</span><span class="o">.</span><span class="n">remove</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + + +<span class="k">async</span> <span class="k">def</span> <span class="nf">handler</span><span class="p">(</span><span class="n">websocket</span><span class="p">):</span> + <span class="n">message</span> <span class="o">=</span> <span class="k">await</span> <span class="n">websocket</span><span class="o">.</span><span class="n">recv</span><span class="p">()</span> + <span class="n">event</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">message</span><span class="p">)</span> + + <span class="c1"># The first event should always be &#39;join&#39;</span> + <span class="k">assert</span> <span class="n">event</span><span class="p">[</span><span class="s2">&quot;kind&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="s2">&quot;join&quot;</span> + <span class="k">await</span> <span class="n">join_and_listen</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + +<span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span> + <span class="k">async</span> <span class="k">with</span> <span class="n">serve</span><span class="p">(</span><span class="n">handler</span><span class="p">,</span> <span class="s2">&quot;localhost&quot;</span><span class="p">,</span> <span class="mi">8001</span><span class="p">):</span> + <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">Future</span><span class="p">()</span> <span class="c1"># run forever</span> + +<span class="n">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">main</span><span class="p">())</span> +</code></pre></div> + +<p>On the client side, it&#8217;s fairly easy as well. I won&#8217;t even cover it&nbsp;here.</p> +<p>We now have a way to send data from one client to the other. +Let&#8217;s consider the actions we do as &#8220;verbs&#8221;. For now, we&#8217;re just updating +properties values, so we just need the <code>update</code> verb.</p> +<h2 id="code-architecture">Code&nbsp;architecture</h2> +<p>We need different&nbsp;parts:</p> +<ul> +<li>the <strong>transport</strong>, which connects to the websockets, sends and receives&nbsp;messages.</li> +<li>the <strong>message sender</strong> to relat local messages to the other&nbsp;party.</li> +<li>the <strong>message receiver</strong> that&#8217;s being called each time we receive a&nbsp;message.</li> +<li>the <strong>sync engine</strong> which glues everything&nbsp;together</li> +<li>Different <strong>updaters</strong>, which knows how to apply received messages, the goal being + to update the interface in the&nbsp;end.</li> +</ul> +<p>When receiving a message it will be routed to the correct updater, which will +know what to do with&nbsp;it.</p> +<p>In our case, its fairly simple: when updating the <code>name</code> property, we send a +message with <code>name</code> and <code>value</code>. We also need to send along some additional +info: the <code>subject</code>.</p> +<p>In our case, it&#8217;s <code>map</code> because we&#8217;re updating map&nbsp;properties.</p> +<p>When initializing the <code>map</code>, we&#8217;re initializing the <code>SyncEngine</code>, like&nbsp;this:</p> +<div class="highlight"><pre><span></span><code><span class="c1">// inside the map</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">syncEngine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">umap</span><span class="p">.</span><span class="nx">SyncEngine</span><span class="p">(</span><span class="k">this</span><span class="p">)</span> + +<span class="c1">// Then, when we need to send data to the other party</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">syncEngine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">obj</span><span class="p">.</span><span class="nx">getSyncEngine</span><span class="p">()</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">subject</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">obj</span><span class="p">.</span><span class="nx">getSyncSubject</span><span class="p">()</span> + +<span class="nx">syncEngine</span><span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">subject</span><span class="p">,</span><span class="w"> </span><span class="nx">field</span><span class="p">,</span><span class="w"> </span><span class="nx">value</span><span class="p">)</span> +</code></pre></div> + +<p>The code on the other side of the wire is simple enough: when you receive the +message, change the data and rerender the&nbsp;properties:</p> +<div class="highlight"><pre><span></span><code><span class="k">this</span><span class="p">.</span><span class="nx">updateObjectValue</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">map</span><span class="p">,</span><span class="w"> </span><span class="nx">key</span><span class="p">,</span><span class="w"> </span><span class="nx">value</span><span class="p">)</span> +<span class="k">this</span><span class="p">.</span><span class="nx">map</span><span class="p">.</span><span class="nx">renderProperties</span><span class="p">(</span><span class="nx">key</span><span class="p">)</span> +</code></pre></div> + +<h2 id="syncing-features">Syncing&nbsp;features</h2> +<p>At this stage I was able to sync the properties of the map. A +small victory, but not the end of the&nbsp;trip.</p> +<p>The next step was to add syncing for features: markers, polygon and polylines, +alongside their&nbsp;properties.</p> +<p>All of these features have a uMap class representation (which extends Leaflets +ones). All of them share some code in the <code>FeatureMixin</code> class.</p> +<p>That seems a good place to do the&nbsp;changes.</p> +<p>I did a few&nbsp;changes:</p> +<ul> +<li>Each feature now has an identifier, so clients know they&#8217;re talking about the + same thing. This identifier is also stored in the database when&nbsp;saved.</li> +<li>I&#8217;ve added an <code>upsert</code> verb, because we don&#8217;t have any way (from the + interface) to make a distinction between the creation of a new feature and + its modification. The way we intercept the creation of a feature (or its + update) is to use Leaflet Editable&#8217;s <code>editable:drawing:commit</code> event. We just + have to listen to it and then send the appropriate messages&nbsp;!</li> +</ul> +<p>After some giggling around (ah, everybody wants to create a new protocol !) I +went with reusing GeoJSON. It allowed me to have a better understanding of how +Leaflet is using latlongs. That&#8217;s a multi-dimensional array, with variable +width, depending on the type of geometry and the number of shapes in each of&nbsp;these.</p> +<p>Clearly not something I want to redo, so I&#8217;m now reusing some Leaflet code, which handles this serialization for&nbsp;me.</p> +<p>I&#8217;m now able to sync different types of features with their&nbsp;properties.</p> +<video controls width="80%"> + <source src="/images/umap/sync-features.webm" type="video/webm"> +</video> + +<p>Point properties are also editable, using the already-existing table editor. I +was expecting this to require some work but it&#8217;s just working without more&nbsp;changes.</p> +<h2 id="whats-next">What&#8217;s next&nbsp;?</h2> +<p>I&#8217;m able to sync map properties, features and their properties, but I&#8217;m not +yet syncing layers. That&#8217;s the next step! I also plan to make some pull +requests with the interesting bits I&#8217;m sure will go in the final&nbsp;implementation:</p> +<ul> +<li>Adding ids to features, so we have a way to refer to&nbsp;them.</li> +<li>Having a way to map properties with how they render the interface, the <code>renderProperties</code> bits.</li> +</ul> +<p>When this demo will be working, I&#8217;ll probably spend some time updating it with the latest changes (umap is moving a lot these weeks). +I will probably focus on how to integrate websockets in the server side, and then will see how to leverage (maybe) some magic from CRDTs, if we need&nbsp;it.</p> +<p>See you for the next&nbsp;update!</p>Returning objects from an arrow function2024-02-08T00:00:00+01:002024-02-08T00:00:00+01:00tag:blog.notmyidea.org,2024-02-08:/returning-objects-from-an-arrow-function.html<p>When using an arrow function in JavaScript, I was expecting to be able to return objects, but ended up with returning <code>undefined</code> values.</p> <p>Turns out it&#8217;s not possible to return directly objects from inside the arrow function because they&#8217;re confused as&nbsp;statements.</p> <p>This is <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#function_body">covered by <span class="caps">MDN</span></a>.</p> <p>To …</p><p>When using an arrow function in JavaScript, I was expecting to be able to return objects, but ended up with returning <code>undefined</code> values.</p> diff --git a/feeds/all.atom.xml b/feeds/all.atom.xml index e383ae7..524d3bb 100644 --- a/feeds/all.atom.xml +++ b/feeds/all.atom.xml @@ -44,7 +44,243 @@ <h2 id="notes">Notes</h2> <ul> <li>Un article de <span class="caps">MDN</span> sur comment écrire de la doc: https://developer.mozilla.org/en-<span class="caps">US</span>/blog/technical-writing/ </li> -</ul>Returning objects from an arrow function2024-02-08T00:00:00+01:002024-02-08T00:00:00+01:00tag:blog.notmyidea.org,2024-02-08:/returning-objects-from-an-arrow-function.html<p>When using an arrow function in JavaScript, I was expecting to be able to return objects, but ended up with returning <code>undefined</code> values.</p> +</ul>Adding collaboration on uMap, third update2024-02-12T00:00:00+01:002024-02-12T00:00:00+01:00tag:blog.notmyidea.org,2024-02-12:/adding-collaboration-on-umap-third-update.html<p>I&#8217;ve spent the last few weeks working on <a href="https://umap-project.org">uMap</a>, still +with the goal of bringing real-time collaboration to the maps. I&#8217;m not there +yet, but I&#8217;ve made some progress that I will relate&nbsp;here.</p> +<h2 id="javascript-modules">JavaScript&nbsp;modules</h2> +<p>uMap has been there <a href="https://github.com/ +umap-project/umap/commit/0cce7f9e2a19c83fa76645d7773d39d54f357c43">since 2012</a>, at a time +when <span class="caps">ES6 …</span></p><p>I&#8217;ve spent the last few weeks working on <a href="https://umap-project.org">uMap</a>, still +with the goal of bringing real-time collaboration to the maps. I&#8217;m not there +yet, but I&#8217;ve made some progress that I will relate&nbsp;here.</p> +<h2 id="javascript-modules">JavaScript&nbsp;modules</h2> +<p>uMap has been there <a href="https://github.com/ +umap-project/umap/commit/0cce7f9e2a19c83fa76645d7773d39d54f357c43">since 2012</a>, at a time +when <span class="caps">ES6</span> <a href="https://fr.wikipedia.org/wiki/ECMAScript">wasn&#8217;t out there&nbsp;yet</a></p> +<p>At the time, it wasn&#8217;t possible to use JavaScript modules, nor modern JavaScript +syntax. The project stayed with these requirements for a long time, in order to support +people with old browsers. But as time goes on, we can now have access to more&nbsp;features.</p> +<p>The team has been working hard on bringing modules to the mix, and it +wasn&#8217;t a piece of cake. But, the result is here: we&#8217;re <a href="https://github.com/umap-project/umap/pull/1463/files">now able to use modern +JavaSript modules</a> and we +are now more confident <a href="https://github.com/umap-project/umap/commit/65f1cdd6b4569657ef5e219d9b377fec85c41958">about which features of the languages we can use or&nbsp;not</a></p> +<hr> +<p>I then spent ~way too much~ some time trying to integrate existing CRDTs like +Automerge and <span class="caps">YJS</span> in our project. These two libs are unfortunately expecting us to +use a bundler, which we aren&#8217;t&nbsp;currently.</p> +<p>uMap is plain old JavaScript. It&#8217;s not using react or any other framework. The way +I see this is that it makes it possible for us to have something &#8220;close to the +metal&#8221;, if that means anything when it comes to web development. We&#8217;re not tied +to the development pace of these frameworks, and have more control on what we +do. It&#8217;s easier to&nbsp;debug.</p> +<p>So, after making tweaks and learning how &#8220;modules&#8221;, &#8220;requires&#8221; and &#8220;bundling&#8221; +was working, I ultimately decided to take a break from this path, to work on the +wiring with uMap. After all, CRDTs might not even be the way forward for&nbsp;us.</p> +<h2 id="internals">Internals</h2> +<p>I was not expecting this to be easy and was a bit afraid. Mostly because I&#8217;m out of my +comfort zone. After some time with the head under the water, I&#8217;m now able to better +understand the big picture, and I&#8217;m not getting lost in the details like I was at&nbsp;first.</p> +<p>Let me try to summarize what I&#8217;ve&nbsp;learned.</p> +<p>uMap appears to be doing a lot of different things, but in the end&nbsp;it&#8217;s:</p> +<ul> +<li>Using <a href="https://leafletjs.com/">Leaflet.js</a> to render <em>features</em> on the map&nbsp;;</li> +<li>Using <a href="https://github.com/Leaflet/Leaflet.Editable">Leaflet Editable</a> to edit + complex shapes, like polylines, polygons, and to draw markers&nbsp;;</li> +<li>Using the <a href="https://github.com/yohanboniface/Leaflet.FormBuilder">Formbuilder</a> + to expose a way for the users to edit the features, and the data of the&nbsp;map</li> +<li>Serializing the layers to and from <a href="https://geojson.org/">GeoJSON</a>. That&#8217;s + what&#8217;s being sent to and received from the&nbsp;server.</li> +<li>Providing different layer types (marker cluster, chloropleth, etc) to display + the data in different&nbsp;ways.</li> +</ul> +<h3 id="naming-matters">Naming&nbsp;matters</h3> +<p>There is some naming overlap between the different projects we&#8217;re using, and +it&#8217;s important to have these small clarifications in&nbsp;mind:</p> +<h4 id="leaflet-layers-and-umap-features">Leaflet layers and uMap&nbsp;features</h4> +<p><strong>In Leaflet, everything is a layer</strong>. What we call <em>features</em> in geoJSON are +leaflet layers, and even a (uMap) layer is a layer. We need to be extra careful +what are our inputs and outputs in this&nbsp;context.</p> +<p>We actually have different layers concepts: the <em>datalayer</em> and the different +kind of layers (chloropleth, marker cluster, etc). A datalayer, is (as you can +guess) where the data is stored. It&#8217;s what uMap serializes. It contains the +features (with their properties). But that&#8217;s the trick: these features are named +<em>layers</em> by&nbsp;Leaflet.</p> +<h4 id="geojson-and-leaflet">GeoJSON and&nbsp;Leaflet</h4> +<p>We&#8217;re using GeoJSON to share data with the server, but we&#8217;re using Leaflet +internally. And these two have different way of naming&nbsp;things.</p> +<p>The different geometries are named differently (a leaflet <code>Marker</code> is a GeoJSON +<code>Point</code>), and their coordinates are stored differently: Leaflet stores <code>lat, +long</code> where GeoJSON stores <code>long, lat</code>. Not a big deal, but it&#8217;s a good thing +to&nbsp;know.</p> +<p>Leaflet stores data in <code>options</code>, where GeoJSON stores it in <code>properties</code>.</p> +<h3 id="this-is-not-reactive-programming">This is not reactive&nbsp;programming</h3> +<p>I was expecting the frontend to be organised similarly to Elm apps (or React +apps): a global state and a data flow (<a href="https:// react-redux.js.org/ +introduction/getting-started">a la redux</a>), with events changing the data that will trigger +a rerendering of the&nbsp;interface.</p> +<p>Things work differently for us: different components can write to the map, and +get updated without being centralized. It&#8217;s just a different&nbsp;paradigm.</p> +<h2 id="a-syncing-proof-of-concept">A syncing proof of&nbsp;concept</h2> +<p>With that in mind, I started thinking about a simple way to implement&nbsp;syncing. </p> +<p>I left aside all the thinking about how this would relate with CRDTs. It can +be useful, but later. For now, I &#8220;just&#8221; want to synchronize two maps. I want a +proof of concept to do informed&nbsp;decisions.</p> +<h3 id="syncing-map-properties">Syncing map&nbsp;properties</h3> +<p>I started syncing map properties. Things like the name of the map, the default +color and type of the marker, the description, the default zoom level,&nbsp;etc.</p> +<p>All of these are handled by &#8220;the formbuilder&#8221;. You pass it an object, a list of +properties and a callback to call when an update happens, and it will build for +you form&nbsp;inputs.</p> +<p>Taken from the documentation (and&nbsp;simplified):</p> +<div class="highlight"><pre><span></span><code><span class="kd">var</span><span class="w"> </span><span class="nx">tilelayerFields</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;name&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;display name&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;maxZoom&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurIntInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;max zoom&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;minZoom&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurIntInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;min zoom&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;attribution&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;attribution&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;tms&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;CheckBox&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">helpText</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;TMS format&#39;</span><span class="p">}]</span> +<span class="p">];</span> +<span class="kd">var</span><span class="w"> </span><span class="nx">builder</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">L</span><span class="p">.</span><span class="nx">FormBuilder</span><span class="p">(</span><span class="nx">myObject</span><span class="p">,</span><span class="w"> </span><span class="nx">tilelayerFields</span><span class="p">,</span><span class="w"> </span><span class="p">{</span> +<span class="w"> </span><span class="nx">callback</span><span class="o">:</span><span class="w"> </span><span class="nx">myCallback</span><span class="p">,</span> +<span class="w"> </span><span class="nx">callbackContext</span><span class="o">:</span><span class="w"> </span><span class="k">this</span> +<span class="p">});</span> +</code></pre></div> + +<p>In uMap, the formbuilder is used for every form you see on the right panel. Map +properties are stored in the <code>map</code> object.</p> +<p>We want two different clients work together. When one changes the value of a +property, the other client needs to be updated, and update its&nbsp;interface.</p> +<p>I&#8217;ve started by creating a mapping of property names to rerender-methods, and +added a method <code>renderProperties(properties)</code> which updates the interface, +depending on the properties passed to&nbsp;it.</p> +<p>We now have two important&nbsp;things:</p> +<ol> +<li>Some code getting called each time a property is changed&nbsp;;</li> +<li>A way to refresh the right interface when a property is&nbsp;changed.</li> +</ol> +<p>In other words, from one client we can send the message to the other client, +which will be able to rerender&nbsp;itself.</p> +<p>Looks like a&nbsp;plan.</p> +<h2 id="websockets">Websockets</h2> +<p>We need a way for the data to go from one side to the other. The easiest +way is probably&nbsp;websockets.</p> +<p>Here is a simple code which will relay messages from one websocket to the other +connected clients. It&#8217;s not the final code, it&#8217;s just for demo&nbsp;puposes.</p> +<p>A basic way to do this on the server side is to use python&#8217;s +<a href="https://websockets.readthedocs.io/">websockets</a>&nbsp;library.</p> +<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">asyncio</span> +<span class="kn">import</span> <span class="nn">websockets</span> +<span class="kn">from</span> <span class="nn">websockets.server</span> <span class="kn">import</span> <span class="n">serve</span> +<span class="kn">import</span> <span class="nn">json</span> + +<span class="c1"># Just relay all messages to other connected peers for now</span> + +<span class="n">CONNECTIONS</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span> + +<span class="k">async</span> <span class="k">def</span> <span class="nf">join_and_listen</span><span class="p">(</span><span class="n">websocket</span><span class="p">):</span> + <span class="n">CONNECTIONS</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + <span class="k">try</span><span class="p">:</span> + <span class="k">async</span> <span class="k">for</span> <span class="n">message</span> <span class="ow">in</span> <span class="n">websocket</span><span class="p">:</span> + <span class="c1"># recompute the peers-list at the time of message-sending.</span> + <span class="c1"># doing so beforehand would miss new connections</span> + <span class="n">peers</span> <span class="o">=</span> <span class="n">CONNECTIONS</span> <span class="o">-</span> <span class="p">{</span><span class="n">websocket</span><span class="p">}</span> + <span class="n">websockets</span><span class="o">.</span><span class="n">broadcast</span><span class="p">(</span><span class="n">peers</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span> + <span class="k">finally</span><span class="p">:</span> + <span class="n">CONNECTIONS</span><span class="o">.</span><span class="n">remove</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + + +<span class="k">async</span> <span class="k">def</span> <span class="nf">handler</span><span class="p">(</span><span class="n">websocket</span><span class="p">):</span> + <span class="n">message</span> <span class="o">=</span> <span class="k">await</span> <span class="n">websocket</span><span class="o">.</span><span class="n">recv</span><span class="p">()</span> + <span class="n">event</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">message</span><span class="p">)</span> + + <span class="c1"># The first event should always be &#39;join&#39;</span> + <span class="k">assert</span> <span class="n">event</span><span class="p">[</span><span class="s2">&quot;kind&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="s2">&quot;join&quot;</span> + <span class="k">await</span> <span class="n">join_and_listen</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + +<span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span> + <span class="k">async</span> <span class="k">with</span> <span class="n">serve</span><span class="p">(</span><span class="n">handler</span><span class="p">,</span> <span class="s2">&quot;localhost&quot;</span><span class="p">,</span> <span class="mi">8001</span><span class="p">):</span> + <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">Future</span><span class="p">()</span> <span class="c1"># run forever</span> + +<span class="n">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">main</span><span class="p">())</span> +</code></pre></div> + +<p>On the client side, it&#8217;s fairly easy as well. I won&#8217;t even cover it&nbsp;here.</p> +<p>We now have a way to send data from one client to the other. +Let&#8217;s consider the actions we do as &#8220;verbs&#8221;. For now, we&#8217;re just updating +properties values, so we just need the <code>update</code> verb.</p> +<h2 id="code-architecture">Code&nbsp;architecture</h2> +<p>We need different&nbsp;parts:</p> +<ul> +<li>the <strong>transport</strong>, which connects to the websockets, sends and receives&nbsp;messages.</li> +<li>the <strong>message sender</strong> to relat local messages to the other&nbsp;party.</li> +<li>the <strong>message receiver</strong> that&#8217;s being called each time we receive a&nbsp;message.</li> +<li>the <strong>sync engine</strong> which glues everything&nbsp;together</li> +<li>Different <strong>updaters</strong>, which knows how to apply received messages, the goal being + to update the interface in the&nbsp;end.</li> +</ul> +<p>When receiving a message it will be routed to the correct updater, which will +know what to do with&nbsp;it.</p> +<p>In our case, its fairly simple: when updating the <code>name</code> property, we send a +message with <code>name</code> and <code>value</code>. We also need to send along some additional +info: the <code>subject</code>.</p> +<p>In our case, it&#8217;s <code>map</code> because we&#8217;re updating map&nbsp;properties.</p> +<p>When initializing the <code>map</code>, we&#8217;re initializing the <code>SyncEngine</code>, like&nbsp;this:</p> +<div class="highlight"><pre><span></span><code><span class="c1">// inside the map</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">syncEngine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">umap</span><span class="p">.</span><span class="nx">SyncEngine</span><span class="p">(</span><span class="k">this</span><span class="p">)</span> + +<span class="c1">// Then, when we need to send data to the other party</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">syncEngine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">obj</span><span class="p">.</span><span class="nx">getSyncEngine</span><span class="p">()</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">subject</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">obj</span><span class="p">.</span><span class="nx">getSyncSubject</span><span class="p">()</span> + +<span class="nx">syncEngine</span><span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">subject</span><span class="p">,</span><span class="w"> </span><span class="nx">field</span><span class="p">,</span><span class="w"> </span><span class="nx">value</span><span class="p">)</span> +</code></pre></div> + +<p>The code on the other side of the wire is simple enough: when you receive the +message, change the data and rerender the&nbsp;properties:</p> +<div class="highlight"><pre><span></span><code><span class="k">this</span><span class="p">.</span><span class="nx">updateObjectValue</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">map</span><span class="p">,</span><span class="w"> </span><span class="nx">key</span><span class="p">,</span><span class="w"> </span><span class="nx">value</span><span class="p">)</span> +<span class="k">this</span><span class="p">.</span><span class="nx">map</span><span class="p">.</span><span class="nx">renderProperties</span><span class="p">(</span><span class="nx">key</span><span class="p">)</span> +</code></pre></div> + +<h2 id="syncing-features">Syncing&nbsp;features</h2> +<p>At this stage I was able to sync the properties of the map. A +small victory, but not the end of the&nbsp;trip.</p> +<p>The next step was to add syncing for features: markers, polygon and polylines, +alongside their&nbsp;properties.</p> +<p>All of these features have a uMap class representation (which extends Leaflets +ones). All of them share some code in the <code>FeatureMixin</code> class.</p> +<p>That seems a good place to do the&nbsp;changes.</p> +<p>I did a few&nbsp;changes:</p> +<ul> +<li>Each feature now has an identifier, so clients know they&#8217;re talking about the + same thing. This identifier is also stored in the database when&nbsp;saved.</li> +<li>I&#8217;ve added an <code>upsert</code> verb, because we don&#8217;t have any way (from the + interface) to make a distinction between the creation of a new feature and + its modification. The way we intercept the creation of a feature (or its + update) is to use Leaflet Editable&#8217;s <code>editable:drawing:commit</code> event. We just + have to listen to it and then send the appropriate messages&nbsp;!</li> +</ul> +<p>After some giggling around (ah, everybody wants to create a new protocol !) I +went with reusing GeoJSON. It allowed me to have a better understanding of how +Leaflet is using latlongs. That&#8217;s a multi-dimensional array, with variable +width, depending on the type of geometry and the number of shapes in each of&nbsp;these.</p> +<p>Clearly not something I want to redo, so I&#8217;m now reusing some Leaflet code, which handles this serialization for&nbsp;me.</p> +<p>I&#8217;m now able to sync different types of features with their&nbsp;properties.</p> +<video controls width="80%"> + <source src="/images/umap/sync-features.webm" type="video/webm"> +</video> + +<p>Point properties are also editable, using the already-existing table editor. I +was expecting this to require some work but it&#8217;s just working without more&nbsp;changes.</p> +<h2 id="whats-next">What&#8217;s next&nbsp;?</h2> +<p>I&#8217;m able to sync map properties, features and their properties, but I&#8217;m not +yet syncing layers. That&#8217;s the next step! I also plan to make some pull +requests with the interesting bits I&#8217;m sure will go in the final&nbsp;implementation:</p> +<ul> +<li>Adding ids to features, so we have a way to refer to&nbsp;them.</li> +<li>Having a way to map properties with how they render the interface, the <code>renderProperties</code> bits.</li> +</ul> +<p>When this demo will be working, I&#8217;ll probably spend some time updating it with the latest changes (umap is moving a lot these weeks). +I will probably focus on how to integrate websockets in the server side, and then will see how to leverage (maybe) some magic from CRDTs, if we need&nbsp;it.</p> +<p>See you for the next&nbsp;update!</p>Returning objects from an arrow function2024-02-08T00:00:00+01:002024-02-08T00:00:00+01:00tag:blog.notmyidea.org,2024-02-08:/returning-objects-from-an-arrow-function.html<p>When using an arrow function in JavaScript, I was expecting to be able to return objects, but ended up with returning <code>undefined</code> values.</p> <p>Turns out it&#8217;s not possible to return directly objects from inside the arrow function because they&#8217;re confused as&nbsp;statements.</p> <p>This is <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#function_body">covered by <span class="caps">MDN</span></a>.</p> <p>To …</p><p>When using an arrow function in JavaScript, I was expecting to be able to return objects, but ended up with returning <code>undefined</code> values.</p> diff --git a/feeds/code.atom.xml b/feeds/code.atom.xml index 28da959..a3ec887 100644 --- a/feeds/code.atom.xml +++ b/feeds/code.atom.xml @@ -1,5 +1,241 @@ -Alexis Métaireau - codehttps://blog.notmyidea.org/2024-02-08T00:00:00+01:00Returning objects from an arrow function2024-02-08T00:00:00+01:002024-02-08T00:00:00+01:00tag:blog.notmyidea.org,2024-02-08:/returning-objects-from-an-arrow-function.html<p>When using an arrow function in JavaScript, I was expecting to be able to return objects, but ended up with returning <code>undefined</code> values.</p> +Alexis Métaireau - codehttps://blog.notmyidea.org/2024-02-12T00:00:00+01:00Adding collaboration on uMap, third update2024-02-12T00:00:00+01:002024-02-12T00:00:00+01:00tag:blog.notmyidea.org,2024-02-12:/adding-collaboration-on-umap-third-update.html<p>I&#8217;ve spent the last few weeks working on <a href="https://umap-project.org">uMap</a>, still +with the goal of bringing real-time collaboration to the maps. I&#8217;m not there +yet, but I&#8217;ve made some progress that I will relate&nbsp;here.</p> +<h2 id="javascript-modules">JavaScript&nbsp;modules</h2> +<p>uMap has been there <a href="https://github.com/ +umap-project/umap/commit/0cce7f9e2a19c83fa76645d7773d39d54f357c43">since 2012</a>, at a time +when <span class="caps">ES6 …</span></p><p>I&#8217;ve spent the last few weeks working on <a href="https://umap-project.org">uMap</a>, still +with the goal of bringing real-time collaboration to the maps. I&#8217;m not there +yet, but I&#8217;ve made some progress that I will relate&nbsp;here.</p> +<h2 id="javascript-modules">JavaScript&nbsp;modules</h2> +<p>uMap has been there <a href="https://github.com/ +umap-project/umap/commit/0cce7f9e2a19c83fa76645d7773d39d54f357c43">since 2012</a>, at a time +when <span class="caps">ES6</span> <a href="https://fr.wikipedia.org/wiki/ECMAScript">wasn&#8217;t out there&nbsp;yet</a></p> +<p>At the time, it wasn&#8217;t possible to use JavaScript modules, nor modern JavaScript +syntax. The project stayed with these requirements for a long time, in order to support +people with old browsers. But as time goes on, we can now have access to more&nbsp;features.</p> +<p>The team has been working hard on bringing modules to the mix, and it +wasn&#8217;t a piece of cake. But, the result is here: we&#8217;re <a href="https://github.com/umap-project/umap/pull/1463/files">now able to use modern +JavaSript modules</a> and we +are now more confident <a href="https://github.com/umap-project/umap/commit/65f1cdd6b4569657ef5e219d9b377fec85c41958">about which features of the languages we can use or&nbsp;not</a></p> +<hr> +<p>I then spent ~way too much~ some time trying to integrate existing CRDTs like +Automerge and <span class="caps">YJS</span> in our project. These two libs are unfortunately expecting us to +use a bundler, which we aren&#8217;t&nbsp;currently.</p> +<p>uMap is plain old JavaScript. It&#8217;s not using react or any other framework. The way +I see this is that it makes it possible for us to have something &#8220;close to the +metal&#8221;, if that means anything when it comes to web development. We&#8217;re not tied +to the development pace of these frameworks, and have more control on what we +do. It&#8217;s easier to&nbsp;debug.</p> +<p>So, after making tweaks and learning how &#8220;modules&#8221;, &#8220;requires&#8221; and &#8220;bundling&#8221; +was working, I ultimately decided to take a break from this path, to work on the +wiring with uMap. After all, CRDTs might not even be the way forward for&nbsp;us.</p> +<h2 id="internals">Internals</h2> +<p>I was not expecting this to be easy and was a bit afraid. Mostly because I&#8217;m out of my +comfort zone. After some time with the head under the water, I&#8217;m now able to better +understand the big picture, and I&#8217;m not getting lost in the details like I was at&nbsp;first.</p> +<p>Let me try to summarize what I&#8217;ve&nbsp;learned.</p> +<p>uMap appears to be doing a lot of different things, but in the end&nbsp;it&#8217;s:</p> +<ul> +<li>Using <a href="https://leafletjs.com/">Leaflet.js</a> to render <em>features</em> on the map&nbsp;;</li> +<li>Using <a href="https://github.com/Leaflet/Leaflet.Editable">Leaflet Editable</a> to edit + complex shapes, like polylines, polygons, and to draw markers&nbsp;;</li> +<li>Using the <a href="https://github.com/yohanboniface/Leaflet.FormBuilder">Formbuilder</a> + to expose a way for the users to edit the features, and the data of the&nbsp;map</li> +<li>Serializing the layers to and from <a href="https://geojson.org/">GeoJSON</a>. That&#8217;s + what&#8217;s being sent to and received from the&nbsp;server.</li> +<li>Providing different layer types (marker cluster, chloropleth, etc) to display + the data in different&nbsp;ways.</li> +</ul> +<h3 id="naming-matters">Naming&nbsp;matters</h3> +<p>There is some naming overlap between the different projects we&#8217;re using, and +it&#8217;s important to have these small clarifications in&nbsp;mind:</p> +<h4 id="leaflet-layers-and-umap-features">Leaflet layers and uMap&nbsp;features</h4> +<p><strong>In Leaflet, everything is a layer</strong>. What we call <em>features</em> in geoJSON are +leaflet layers, and even a (uMap) layer is a layer. We need to be extra careful +what are our inputs and outputs in this&nbsp;context.</p> +<p>We actually have different layers concepts: the <em>datalayer</em> and the different +kind of layers (chloropleth, marker cluster, etc). A datalayer, is (as you can +guess) where the data is stored. It&#8217;s what uMap serializes. It contains the +features (with their properties). But that&#8217;s the trick: these features are named +<em>layers</em> by&nbsp;Leaflet.</p> +<h4 id="geojson-and-leaflet">GeoJSON and&nbsp;Leaflet</h4> +<p>We&#8217;re using GeoJSON to share data with the server, but we&#8217;re using Leaflet +internally. And these two have different way of naming&nbsp;things.</p> +<p>The different geometries are named differently (a leaflet <code>Marker</code> is a GeoJSON +<code>Point</code>), and their coordinates are stored differently: Leaflet stores <code>lat, +long</code> where GeoJSON stores <code>long, lat</code>. Not a big deal, but it&#8217;s a good thing +to&nbsp;know.</p> +<p>Leaflet stores data in <code>options</code>, where GeoJSON stores it in <code>properties</code>.</p> +<h3 id="this-is-not-reactive-programming">This is not reactive&nbsp;programming</h3> +<p>I was expecting the frontend to be organised similarly to Elm apps (or React +apps): a global state and a data flow (<a href="https:// react-redux.js.org/ +introduction/getting-started">a la redux</a>), with events changing the data that will trigger +a rerendering of the&nbsp;interface.</p> +<p>Things work differently for us: different components can write to the map, and +get updated without being centralized. It&#8217;s just a different&nbsp;paradigm.</p> +<h2 id="a-syncing-proof-of-concept">A syncing proof of&nbsp;concept</h2> +<p>With that in mind, I started thinking about a simple way to implement&nbsp;syncing. </p> +<p>I left aside all the thinking about how this would relate with CRDTs. It can +be useful, but later. For now, I &#8220;just&#8221; want to synchronize two maps. I want a +proof of concept to do informed&nbsp;decisions.</p> +<h3 id="syncing-map-properties">Syncing map&nbsp;properties</h3> +<p>I started syncing map properties. Things like the name of the map, the default +color and type of the marker, the description, the default zoom level,&nbsp;etc.</p> +<p>All of these are handled by &#8220;the formbuilder&#8221;. You pass it an object, a list of +properties and a callback to call when an update happens, and it will build for +you form&nbsp;inputs.</p> +<p>Taken from the documentation (and&nbsp;simplified):</p> +<div class="highlight"><pre><span></span><code><span class="kd">var</span><span class="w"> </span><span class="nx">tilelayerFields</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;name&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;display name&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;maxZoom&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurIntInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;max zoom&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;minZoom&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurIntInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;min zoom&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;attribution&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;attribution&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;tms&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;CheckBox&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">helpText</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;TMS format&#39;</span><span class="p">}]</span> +<span class="p">];</span> +<span class="kd">var</span><span class="w"> </span><span class="nx">builder</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">L</span><span class="p">.</span><span class="nx">FormBuilder</span><span class="p">(</span><span class="nx">myObject</span><span class="p">,</span><span class="w"> </span><span class="nx">tilelayerFields</span><span class="p">,</span><span class="w"> </span><span class="p">{</span> +<span class="w"> </span><span class="nx">callback</span><span class="o">:</span><span class="w"> </span><span class="nx">myCallback</span><span class="p">,</span> +<span class="w"> </span><span class="nx">callbackContext</span><span class="o">:</span><span class="w"> </span><span class="k">this</span> +<span class="p">});</span> +</code></pre></div> + +<p>In uMap, the formbuilder is used for every form you see on the right panel. Map +properties are stored in the <code>map</code> object.</p> +<p>We want two different clients work together. When one changes the value of a +property, the other client needs to be updated, and update its&nbsp;interface.</p> +<p>I&#8217;ve started by creating a mapping of property names to rerender-methods, and +added a method <code>renderProperties(properties)</code> which updates the interface, +depending on the properties passed to&nbsp;it.</p> +<p>We now have two important&nbsp;things:</p> +<ol> +<li>Some code getting called each time a property is changed&nbsp;;</li> +<li>A way to refresh the right interface when a property is&nbsp;changed.</li> +</ol> +<p>In other words, from one client we can send the message to the other client, +which will be able to rerender&nbsp;itself.</p> +<p>Looks like a&nbsp;plan.</p> +<h2 id="websockets">Websockets</h2> +<p>We need a way for the data to go from one side to the other. The easiest +way is probably&nbsp;websockets.</p> +<p>Here is a simple code which will relay messages from one websocket to the other +connected clients. It&#8217;s not the final code, it&#8217;s just for demo&nbsp;puposes.</p> +<p>A basic way to do this on the server side is to use python&#8217;s +<a href="https://websockets.readthedocs.io/">websockets</a>&nbsp;library.</p> +<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">asyncio</span> +<span class="kn">import</span> <span class="nn">websockets</span> +<span class="kn">from</span> <span class="nn">websockets.server</span> <span class="kn">import</span> <span class="n">serve</span> +<span class="kn">import</span> <span class="nn">json</span> + +<span class="c1"># Just relay all messages to other connected peers for now</span> + +<span class="n">CONNECTIONS</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span> + +<span class="k">async</span> <span class="k">def</span> <span class="nf">join_and_listen</span><span class="p">(</span><span class="n">websocket</span><span class="p">):</span> + <span class="n">CONNECTIONS</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + <span class="k">try</span><span class="p">:</span> + <span class="k">async</span> <span class="k">for</span> <span class="n">message</span> <span class="ow">in</span> <span class="n">websocket</span><span class="p">:</span> + <span class="c1"># recompute the peers-list at the time of message-sending.</span> + <span class="c1"># doing so beforehand would miss new connections</span> + <span class="n">peers</span> <span class="o">=</span> <span class="n">CONNECTIONS</span> <span class="o">-</span> <span class="p">{</span><span class="n">websocket</span><span class="p">}</span> + <span class="n">websockets</span><span class="o">.</span><span class="n">broadcast</span><span class="p">(</span><span class="n">peers</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span> + <span class="k">finally</span><span class="p">:</span> + <span class="n">CONNECTIONS</span><span class="o">.</span><span class="n">remove</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + + +<span class="k">async</span> <span class="k">def</span> <span class="nf">handler</span><span class="p">(</span><span class="n">websocket</span><span class="p">):</span> + <span class="n">message</span> <span class="o">=</span> <span class="k">await</span> <span class="n">websocket</span><span class="o">.</span><span class="n">recv</span><span class="p">()</span> + <span class="n">event</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">message</span><span class="p">)</span> + + <span class="c1"># The first event should always be &#39;join&#39;</span> + <span class="k">assert</span> <span class="n">event</span><span class="p">[</span><span class="s2">&quot;kind&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="s2">&quot;join&quot;</span> + <span class="k">await</span> <span class="n">join_and_listen</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + +<span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span> + <span class="k">async</span> <span class="k">with</span> <span class="n">serve</span><span class="p">(</span><span class="n">handler</span><span class="p">,</span> <span class="s2">&quot;localhost&quot;</span><span class="p">,</span> <span class="mi">8001</span><span class="p">):</span> + <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">Future</span><span class="p">()</span> <span class="c1"># run forever</span> + +<span class="n">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">main</span><span class="p">())</span> +</code></pre></div> + +<p>On the client side, it&#8217;s fairly easy as well. I won&#8217;t even cover it&nbsp;here.</p> +<p>We now have a way to send data from one client to the other. +Let&#8217;s consider the actions we do as &#8220;verbs&#8221;. For now, we&#8217;re just updating +properties values, so we just need the <code>update</code> verb.</p> +<h2 id="code-architecture">Code&nbsp;architecture</h2> +<p>We need different&nbsp;parts:</p> +<ul> +<li>the <strong>transport</strong>, which connects to the websockets, sends and receives&nbsp;messages.</li> +<li>the <strong>message sender</strong> to relat local messages to the other&nbsp;party.</li> +<li>the <strong>message receiver</strong> that&#8217;s being called each time we receive a&nbsp;message.</li> +<li>the <strong>sync engine</strong> which glues everything&nbsp;together</li> +<li>Different <strong>updaters</strong>, which knows how to apply received messages, the goal being + to update the interface in the&nbsp;end.</li> +</ul> +<p>When receiving a message it will be routed to the correct updater, which will +know what to do with&nbsp;it.</p> +<p>In our case, its fairly simple: when updating the <code>name</code> property, we send a +message with <code>name</code> and <code>value</code>. We also need to send along some additional +info: the <code>subject</code>.</p> +<p>In our case, it&#8217;s <code>map</code> because we&#8217;re updating map&nbsp;properties.</p> +<p>When initializing the <code>map</code>, we&#8217;re initializing the <code>SyncEngine</code>, like&nbsp;this:</p> +<div class="highlight"><pre><span></span><code><span class="c1">// inside the map</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">syncEngine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">umap</span><span class="p">.</span><span class="nx">SyncEngine</span><span class="p">(</span><span class="k">this</span><span class="p">)</span> + +<span class="c1">// Then, when we need to send data to the other party</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">syncEngine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">obj</span><span class="p">.</span><span class="nx">getSyncEngine</span><span class="p">()</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">subject</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">obj</span><span class="p">.</span><span class="nx">getSyncSubject</span><span class="p">()</span> + +<span class="nx">syncEngine</span><span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">subject</span><span class="p">,</span><span class="w"> </span><span class="nx">field</span><span class="p">,</span><span class="w"> </span><span class="nx">value</span><span class="p">)</span> +</code></pre></div> + +<p>The code on the other side of the wire is simple enough: when you receive the +message, change the data and rerender the&nbsp;properties:</p> +<div class="highlight"><pre><span></span><code><span class="k">this</span><span class="p">.</span><span class="nx">updateObjectValue</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">map</span><span class="p">,</span><span class="w"> </span><span class="nx">key</span><span class="p">,</span><span class="w"> </span><span class="nx">value</span><span class="p">)</span> +<span class="k">this</span><span class="p">.</span><span class="nx">map</span><span class="p">.</span><span class="nx">renderProperties</span><span class="p">(</span><span class="nx">key</span><span class="p">)</span> +</code></pre></div> + +<h2 id="syncing-features">Syncing&nbsp;features</h2> +<p>At this stage I was able to sync the properties of the map. A +small victory, but not the end of the&nbsp;trip.</p> +<p>The next step was to add syncing for features: markers, polygon and polylines, +alongside their&nbsp;properties.</p> +<p>All of these features have a uMap class representation (which extends Leaflets +ones). All of them share some code in the <code>FeatureMixin</code> class.</p> +<p>That seems a good place to do the&nbsp;changes.</p> +<p>I did a few&nbsp;changes:</p> +<ul> +<li>Each feature now has an identifier, so clients know they&#8217;re talking about the + same thing. This identifier is also stored in the database when&nbsp;saved.</li> +<li>I&#8217;ve added an <code>upsert</code> verb, because we don&#8217;t have any way (from the + interface) to make a distinction between the creation of a new feature and + its modification. The way we intercept the creation of a feature (or its + update) is to use Leaflet Editable&#8217;s <code>editable:drawing:commit</code> event. We just + have to listen to it and then send the appropriate messages&nbsp;!</li> +</ul> +<p>After some giggling around (ah, everybody wants to create a new protocol !) I +went with reusing GeoJSON. It allowed me to have a better understanding of how +Leaflet is using latlongs. That&#8217;s a multi-dimensional array, with variable +width, depending on the type of geometry and the number of shapes in each of&nbsp;these.</p> +<p>Clearly not something I want to redo, so I&#8217;m now reusing some Leaflet code, which handles this serialization for&nbsp;me.</p> +<p>I&#8217;m now able to sync different types of features with their&nbsp;properties.</p> +<video controls width="80%"> + <source src="/images/umap/sync-features.webm" type="video/webm"> +</video> + +<p>Point properties are also editable, using the already-existing table editor. I +was expecting this to require some work but it&#8217;s just working without more&nbsp;changes.</p> +<h2 id="whats-next">What&#8217;s next&nbsp;?</h2> +<p>I&#8217;m able to sync map properties, features and their properties, but I&#8217;m not +yet syncing layers. That&#8217;s the next step! I also plan to make some pull +requests with the interesting bits I&#8217;m sure will go in the final&nbsp;implementation:</p> +<ul> +<li>Adding ids to features, so we have a way to refer to&nbsp;them.</li> +<li>Having a way to map properties with how they render the interface, the <code>renderProperties</code> bits.</li> +</ul> +<p>When this demo will be working, I&#8217;ll probably spend some time updating it with the latest changes (umap is moving a lot these weeks). +I will probably focus on how to integrate websockets in the server side, and then will see how to leverage (maybe) some magic from CRDTs, if we need&nbsp;it.</p> +<p>See you for the next&nbsp;update!</p>Returning objects from an arrow function2024-02-08T00:00:00+01:002024-02-08T00:00:00+01:00tag:blog.notmyidea.org,2024-02-08:/returning-objects-from-an-arrow-function.html<p>When using an arrow function in JavaScript, I was expecting to be able to return objects, but ended up with returning <code>undefined</code> values.</p> <p>Turns out it&#8217;s not possible to return directly objects from inside the arrow function because they&#8217;re confused as&nbsp;statements.</p> <p>This is <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#function_body">covered by <span class="caps">MDN</span></a>.</p> <p>To …</p><p>When using an arrow function in JavaScript, I was expecting to be able to return objects, but ended up with returning <code>undefined</code> values.</p> diff --git a/feeds/tags/geojson.atom.xml b/feeds/tags/geojson.atom.xml new file mode 100644 index 0000000..ccbf9dd --- /dev/null +++ b/feeds/tags/geojson.atom.xml @@ -0,0 +1,238 @@ + +Alexis Métaireau - geojsonhttps://blog.notmyidea.org/2024-02-12T00:00:00+01:00Adding collaboration on uMap, third update2024-02-12T00:00:00+01:002024-02-12T00:00:00+01:00tag:blog.notmyidea.org,2024-02-12:/adding-collaboration-on-umap-third-update.html<p>I&#8217;ve spent the last few weeks working on <a href="https://umap-project.org">uMap</a>, still +with the goal of bringing real-time collaboration to the maps. I&#8217;m not there +yet, but I&#8217;ve made some progress that I will relate&nbsp;here.</p> +<h2 id="javascript-modules">JavaScript&nbsp;modules</h2> +<p>uMap has been there <a href="https://github.com/ +umap-project/umap/commit/0cce7f9e2a19c83fa76645d7773d39d54f357c43">since 2012</a>, at a time +when <span class="caps">ES6 …</span></p><p>I&#8217;ve spent the last few weeks working on <a href="https://umap-project.org">uMap</a>, still +with the goal of bringing real-time collaboration to the maps. I&#8217;m not there +yet, but I&#8217;ve made some progress that I will relate&nbsp;here.</p> +<h2 id="javascript-modules">JavaScript&nbsp;modules</h2> +<p>uMap has been there <a href="https://github.com/ +umap-project/umap/commit/0cce7f9e2a19c83fa76645d7773d39d54f357c43">since 2012</a>, at a time +when <span class="caps">ES6</span> <a href="https://fr.wikipedia.org/wiki/ECMAScript">wasn&#8217;t out there&nbsp;yet</a></p> +<p>At the time, it wasn&#8217;t possible to use JavaScript modules, nor modern JavaScript +syntax. The project stayed with these requirements for a long time, in order to support +people with old browsers. But as time goes on, we can now have access to more&nbsp;features.</p> +<p>The team has been working hard on bringing modules to the mix, and it +wasn&#8217;t a piece of cake. But, the result is here: we&#8217;re <a href="https://github.com/umap-project/umap/pull/1463/files">now able to use modern +JavaSript modules</a> and we +are now more confident <a href="https://github.com/umap-project/umap/commit/65f1cdd6b4569657ef5e219d9b377fec85c41958">about which features of the languages we can use or&nbsp;not</a></p> +<hr> +<p>I then spent ~way too much~ some time trying to integrate existing CRDTs like +Automerge and <span class="caps">YJS</span> in our project. These two libs are unfortunately expecting us to +use a bundler, which we aren&#8217;t&nbsp;currently.</p> +<p>uMap is plain old JavaScript. It&#8217;s not using react or any other framework. The way +I see this is that it makes it possible for us to have something &#8220;close to the +metal&#8221;, if that means anything when it comes to web development. We&#8217;re not tied +to the development pace of these frameworks, and have more control on what we +do. It&#8217;s easier to&nbsp;debug.</p> +<p>So, after making tweaks and learning how &#8220;modules&#8221;, &#8220;requires&#8221; and &#8220;bundling&#8221; +was working, I ultimately decided to take a break from this path, to work on the +wiring with uMap. After all, CRDTs might not even be the way forward for&nbsp;us.</p> +<h2 id="internals">Internals</h2> +<p>I was not expecting this to be easy and was a bit afraid. Mostly because I&#8217;m out of my +comfort zone. After some time with the head under the water, I&#8217;m now able to better +understand the big picture, and I&#8217;m not getting lost in the details like I was at&nbsp;first.</p> +<p>Let me try to summarize what I&#8217;ve&nbsp;learned.</p> +<p>uMap appears to be doing a lot of different things, but in the end&nbsp;it&#8217;s:</p> +<ul> +<li>Using <a href="https://leafletjs.com/">Leaflet.js</a> to render <em>features</em> on the map&nbsp;;</li> +<li>Using <a href="https://github.com/Leaflet/Leaflet.Editable">Leaflet Editable</a> to edit + complex shapes, like polylines, polygons, and to draw markers&nbsp;;</li> +<li>Using the <a href="https://github.com/yohanboniface/Leaflet.FormBuilder">Formbuilder</a> + to expose a way for the users to edit the features, and the data of the&nbsp;map</li> +<li>Serializing the layers to and from <a href="https://geojson.org/">GeoJSON</a>. That&#8217;s + what&#8217;s being sent to and received from the&nbsp;server.</li> +<li>Providing different layer types (marker cluster, chloropleth, etc) to display + the data in different&nbsp;ways.</li> +</ul> +<h3 id="naming-matters">Naming&nbsp;matters</h3> +<p>There is some naming overlap between the different projects we&#8217;re using, and +it&#8217;s important to have these small clarifications in&nbsp;mind:</p> +<h4 id="leaflet-layers-and-umap-features">Leaflet layers and uMap&nbsp;features</h4> +<p><strong>In Leaflet, everything is a layer</strong>. What we call <em>features</em> in geoJSON are +leaflet layers, and even a (uMap) layer is a layer. We need to be extra careful +what are our inputs and outputs in this&nbsp;context.</p> +<p>We actually have different layers concepts: the <em>datalayer</em> and the different +kind of layers (chloropleth, marker cluster, etc). A datalayer, is (as you can +guess) where the data is stored. It&#8217;s what uMap serializes. It contains the +features (with their properties). But that&#8217;s the trick: these features are named +<em>layers</em> by&nbsp;Leaflet.</p> +<h4 id="geojson-and-leaflet">GeoJSON and&nbsp;Leaflet</h4> +<p>We&#8217;re using GeoJSON to share data with the server, but we&#8217;re using Leaflet +internally. And these two have different way of naming&nbsp;things.</p> +<p>The different geometries are named differently (a leaflet <code>Marker</code> is a GeoJSON +<code>Point</code>), and their coordinates are stored differently: Leaflet stores <code>lat, +long</code> where GeoJSON stores <code>long, lat</code>. Not a big deal, but it&#8217;s a good thing +to&nbsp;know.</p> +<p>Leaflet stores data in <code>options</code>, where GeoJSON stores it in <code>properties</code>.</p> +<h3 id="this-is-not-reactive-programming">This is not reactive&nbsp;programming</h3> +<p>I was expecting the frontend to be organised similarly to Elm apps (or React +apps): a global state and a data flow (<a href="https:// react-redux.js.org/ +introduction/getting-started">a la redux</a>), with events changing the data that will trigger +a rerendering of the&nbsp;interface.</p> +<p>Things work differently for us: different components can write to the map, and +get updated without being centralized. It&#8217;s just a different&nbsp;paradigm.</p> +<h2 id="a-syncing-proof-of-concept">A syncing proof of&nbsp;concept</h2> +<p>With that in mind, I started thinking about a simple way to implement&nbsp;syncing. </p> +<p>I left aside all the thinking about how this would relate with CRDTs. It can +be useful, but later. For now, I &#8220;just&#8221; want to synchronize two maps. I want a +proof of concept to do informed&nbsp;decisions.</p> +<h3 id="syncing-map-properties">Syncing map&nbsp;properties</h3> +<p>I started syncing map properties. Things like the name of the map, the default +color and type of the marker, the description, the default zoom level,&nbsp;etc.</p> +<p>All of these are handled by &#8220;the formbuilder&#8221;. You pass it an object, a list of +properties and a callback to call when an update happens, and it will build for +you form&nbsp;inputs.</p> +<p>Taken from the documentation (and&nbsp;simplified):</p> +<div class="highlight"><pre><span></span><code><span class="kd">var</span><span class="w"> </span><span class="nx">tilelayerFields</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;name&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;display name&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;maxZoom&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurIntInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;max zoom&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;minZoom&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurIntInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;min zoom&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;attribution&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;attribution&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;tms&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;CheckBox&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">helpText</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;TMS format&#39;</span><span class="p">}]</span> +<span class="p">];</span> +<span class="kd">var</span><span class="w"> </span><span class="nx">builder</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">L</span><span class="p">.</span><span class="nx">FormBuilder</span><span class="p">(</span><span class="nx">myObject</span><span class="p">,</span><span class="w"> </span><span class="nx">tilelayerFields</span><span class="p">,</span><span class="w"> </span><span class="p">{</span> +<span class="w"> </span><span class="nx">callback</span><span class="o">:</span><span class="w"> </span><span class="nx">myCallback</span><span class="p">,</span> +<span class="w"> </span><span class="nx">callbackContext</span><span class="o">:</span><span class="w"> </span><span class="k">this</span> +<span class="p">});</span> +</code></pre></div> + +<p>In uMap, the formbuilder is used for every form you see on the right panel. Map +properties are stored in the <code>map</code> object.</p> +<p>We want two different clients work together. When one changes the value of a +property, the other client needs to be updated, and update its&nbsp;interface.</p> +<p>I&#8217;ve started by creating a mapping of property names to rerender-methods, and +added a method <code>renderProperties(properties)</code> which updates the interface, +depending on the properties passed to&nbsp;it.</p> +<p>We now have two important&nbsp;things:</p> +<ol> +<li>Some code getting called each time a property is changed&nbsp;;</li> +<li>A way to refresh the right interface when a property is&nbsp;changed.</li> +</ol> +<p>In other words, from one client we can send the message to the other client, +which will be able to rerender&nbsp;itself.</p> +<p>Looks like a&nbsp;plan.</p> +<h2 id="websockets">Websockets</h2> +<p>We need a way for the data to go from one side to the other. The easiest +way is probably&nbsp;websockets.</p> +<p>Here is a simple code which will relay messages from one websocket to the other +connected clients. It&#8217;s not the final code, it&#8217;s just for demo&nbsp;puposes.</p> +<p>A basic way to do this on the server side is to use python&#8217;s +<a href="https://websockets.readthedocs.io/">websockets</a>&nbsp;library.</p> +<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">asyncio</span> +<span class="kn">import</span> <span class="nn">websockets</span> +<span class="kn">from</span> <span class="nn">websockets.server</span> <span class="kn">import</span> <span class="n">serve</span> +<span class="kn">import</span> <span class="nn">json</span> + +<span class="c1"># Just relay all messages to other connected peers for now</span> + +<span class="n">CONNECTIONS</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span> + +<span class="k">async</span> <span class="k">def</span> <span class="nf">join_and_listen</span><span class="p">(</span><span class="n">websocket</span><span class="p">):</span> + <span class="n">CONNECTIONS</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + <span class="k">try</span><span class="p">:</span> + <span class="k">async</span> <span class="k">for</span> <span class="n">message</span> <span class="ow">in</span> <span class="n">websocket</span><span class="p">:</span> + <span class="c1"># recompute the peers-list at the time of message-sending.</span> + <span class="c1"># doing so beforehand would miss new connections</span> + <span class="n">peers</span> <span class="o">=</span> <span class="n">CONNECTIONS</span> <span class="o">-</span> <span class="p">{</span><span class="n">websocket</span><span class="p">}</span> + <span class="n">websockets</span><span class="o">.</span><span class="n">broadcast</span><span class="p">(</span><span class="n">peers</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span> + <span class="k">finally</span><span class="p">:</span> + <span class="n">CONNECTIONS</span><span class="o">.</span><span class="n">remove</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + + +<span class="k">async</span> <span class="k">def</span> <span class="nf">handler</span><span class="p">(</span><span class="n">websocket</span><span class="p">):</span> + <span class="n">message</span> <span class="o">=</span> <span class="k">await</span> <span class="n">websocket</span><span class="o">.</span><span class="n">recv</span><span class="p">()</span> + <span class="n">event</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">message</span><span class="p">)</span> + + <span class="c1"># The first event should always be &#39;join&#39;</span> + <span class="k">assert</span> <span class="n">event</span><span class="p">[</span><span class="s2">&quot;kind&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="s2">&quot;join&quot;</span> + <span class="k">await</span> <span class="n">join_and_listen</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + +<span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span> + <span class="k">async</span> <span class="k">with</span> <span class="n">serve</span><span class="p">(</span><span class="n">handler</span><span class="p">,</span> <span class="s2">&quot;localhost&quot;</span><span class="p">,</span> <span class="mi">8001</span><span class="p">):</span> + <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">Future</span><span class="p">()</span> <span class="c1"># run forever</span> + +<span class="n">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">main</span><span class="p">())</span> +</code></pre></div> + +<p>On the client side, it&#8217;s fairly easy as well. I won&#8217;t even cover it&nbsp;here.</p> +<p>We now have a way to send data from one client to the other. +Let&#8217;s consider the actions we do as &#8220;verbs&#8221;. For now, we&#8217;re just updating +properties values, so we just need the <code>update</code> verb.</p> +<h2 id="code-architecture">Code&nbsp;architecture</h2> +<p>We need different&nbsp;parts:</p> +<ul> +<li>the <strong>transport</strong>, which connects to the websockets, sends and receives&nbsp;messages.</li> +<li>the <strong>message sender</strong> to relat local messages to the other&nbsp;party.</li> +<li>the <strong>message receiver</strong> that&#8217;s being called each time we receive a&nbsp;message.</li> +<li>the <strong>sync engine</strong> which glues everything&nbsp;together</li> +<li>Different <strong>updaters</strong>, which knows how to apply received messages, the goal being + to update the interface in the&nbsp;end.</li> +</ul> +<p>When receiving a message it will be routed to the correct updater, which will +know what to do with&nbsp;it.</p> +<p>In our case, its fairly simple: when updating the <code>name</code> property, we send a +message with <code>name</code> and <code>value</code>. We also need to send along some additional +info: the <code>subject</code>.</p> +<p>In our case, it&#8217;s <code>map</code> because we&#8217;re updating map&nbsp;properties.</p> +<p>When initializing the <code>map</code>, we&#8217;re initializing the <code>SyncEngine</code>, like&nbsp;this:</p> +<div class="highlight"><pre><span></span><code><span class="c1">// inside the map</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">syncEngine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">umap</span><span class="p">.</span><span class="nx">SyncEngine</span><span class="p">(</span><span class="k">this</span><span class="p">)</span> + +<span class="c1">// Then, when we need to send data to the other party</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">syncEngine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">obj</span><span class="p">.</span><span class="nx">getSyncEngine</span><span class="p">()</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">subject</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">obj</span><span class="p">.</span><span class="nx">getSyncSubject</span><span class="p">()</span> + +<span class="nx">syncEngine</span><span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">subject</span><span class="p">,</span><span class="w"> </span><span class="nx">field</span><span class="p">,</span><span class="w"> </span><span class="nx">value</span><span class="p">)</span> +</code></pre></div> + +<p>The code on the other side of the wire is simple enough: when you receive the +message, change the data and rerender the&nbsp;properties:</p> +<div class="highlight"><pre><span></span><code><span class="k">this</span><span class="p">.</span><span class="nx">updateObjectValue</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">map</span><span class="p">,</span><span class="w"> </span><span class="nx">key</span><span class="p">,</span><span class="w"> </span><span class="nx">value</span><span class="p">)</span> +<span class="k">this</span><span class="p">.</span><span class="nx">map</span><span class="p">.</span><span class="nx">renderProperties</span><span class="p">(</span><span class="nx">key</span><span class="p">)</span> +</code></pre></div> + +<h2 id="syncing-features">Syncing&nbsp;features</h2> +<p>At this stage I was able to sync the properties of the map. A +small victory, but not the end of the&nbsp;trip.</p> +<p>The next step was to add syncing for features: markers, polygon and polylines, +alongside their&nbsp;properties.</p> +<p>All of these features have a uMap class representation (which extends Leaflets +ones). All of them share some code in the <code>FeatureMixin</code> class.</p> +<p>That seems a good place to do the&nbsp;changes.</p> +<p>I did a few&nbsp;changes:</p> +<ul> +<li>Each feature now has an identifier, so clients know they&#8217;re talking about the + same thing. This identifier is also stored in the database when&nbsp;saved.</li> +<li>I&#8217;ve added an <code>upsert</code> verb, because we don&#8217;t have any way (from the + interface) to make a distinction between the creation of a new feature and + its modification. The way we intercept the creation of a feature (or its + update) is to use Leaflet Editable&#8217;s <code>editable:drawing:commit</code> event. We just + have to listen to it and then send the appropriate messages&nbsp;!</li> +</ul> +<p>After some giggling around (ah, everybody wants to create a new protocol !) I +went with reusing GeoJSON. It allowed me to have a better understanding of how +Leaflet is using latlongs. That&#8217;s a multi-dimensional array, with variable +width, depending on the type of geometry and the number of shapes in each of&nbsp;these.</p> +<p>Clearly not something I want to redo, so I&#8217;m now reusing some Leaflet code, which handles this serialization for&nbsp;me.</p> +<p>I&#8217;m now able to sync different types of features with their&nbsp;properties.</p> +<video controls width="80%"> + <source src="/images/umap/sync-features.webm" type="video/webm"> +</video> + +<p>Point properties are also editable, using the already-existing table editor. I +was expecting this to require some work but it&#8217;s just working without more&nbsp;changes.</p> +<h2 id="whats-next">What&#8217;s next&nbsp;?</h2> +<p>I&#8217;m able to sync map properties, features and their properties, but I&#8217;m not +yet syncing layers. That&#8217;s the next step! I also plan to make some pull +requests with the interesting bits I&#8217;m sure will go in the final&nbsp;implementation:</p> +<ul> +<li>Adding ids to features, so we have a way to refer to&nbsp;them.</li> +<li>Having a way to map properties with how they render the interface, the <code>renderProperties</code> bits.</li> +</ul> +<p>When this demo will be working, I&#8217;ll probably spend some time updating it with the latest changes (umap is moving a lot these weeks). +I will probably focus on how to integrate websockets in the server side, and then will see how to leverage (maybe) some magic from CRDTs, if we need&nbsp;it.</p> +<p>See you for the next&nbsp;update!</p> \ No newline at end of file diff --git a/feeds/tags/umap.atom.xml b/feeds/tags/umap.atom.xml index d755112..0211ea4 100644 --- a/feeds/tags/umap.atom.xml +++ b/feeds/tags/umap.atom.xml @@ -1,5 +1,241 @@ -Alexis Métaireau - uMaphttps://blog.notmyidea.org/2023-11-21T00:00:00+01:00Adding Real-Time Collaboration to uMap, second week2023-11-21T00:00:00+01:002023-11-21T00:00:00+01:00tag:blog.notmyidea.org,2023-11-21:/adding-real-time-collaboration-to-umap-second-week.html<p>I continued working on <a href="https://github.com/umap-project/umap/">uMap</a>, an open-source map-making tool to create and share customizable maps, based on Open Street Map&nbsp;data.</p> +Alexis Métaireau - umaphttps://blog.notmyidea.org/2024-02-12T00:00:00+01:00Adding collaboration on uMap, third update2024-02-12T00:00:00+01:002024-02-12T00:00:00+01:00tag:blog.notmyidea.org,2024-02-12:/adding-collaboration-on-umap-third-update.html<p>I&#8217;ve spent the last few weeks working on <a href="https://umap-project.org">uMap</a>, still +with the goal of bringing real-time collaboration to the maps. I&#8217;m not there +yet, but I&#8217;ve made some progress that I will relate&nbsp;here.</p> +<h2 id="javascript-modules">JavaScript&nbsp;modules</h2> +<p>uMap has been there <a href="https://github.com/ +umap-project/umap/commit/0cce7f9e2a19c83fa76645d7773d39d54f357c43">since 2012</a>, at a time +when <span class="caps">ES6 …</span></p><p>I&#8217;ve spent the last few weeks working on <a href="https://umap-project.org">uMap</a>, still +with the goal of bringing real-time collaboration to the maps. I&#8217;m not there +yet, but I&#8217;ve made some progress that I will relate&nbsp;here.</p> +<h2 id="javascript-modules">JavaScript&nbsp;modules</h2> +<p>uMap has been there <a href="https://github.com/ +umap-project/umap/commit/0cce7f9e2a19c83fa76645d7773d39d54f357c43">since 2012</a>, at a time +when <span class="caps">ES6</span> <a href="https://fr.wikipedia.org/wiki/ECMAScript">wasn&#8217;t out there&nbsp;yet</a></p> +<p>At the time, it wasn&#8217;t possible to use JavaScript modules, nor modern JavaScript +syntax. The project stayed with these requirements for a long time, in order to support +people with old browsers. But as time goes on, we can now have access to more&nbsp;features.</p> +<p>The team has been working hard on bringing modules to the mix, and it +wasn&#8217;t a piece of cake. But, the result is here: we&#8217;re <a href="https://github.com/umap-project/umap/pull/1463/files">now able to use modern +JavaSript modules</a> and we +are now more confident <a href="https://github.com/umap-project/umap/commit/65f1cdd6b4569657ef5e219d9b377fec85c41958">about which features of the languages we can use or&nbsp;not</a></p> +<hr> +<p>I then spent ~way too much~ some time trying to integrate existing CRDTs like +Automerge and <span class="caps">YJS</span> in our project. These two libs are unfortunately expecting us to +use a bundler, which we aren&#8217;t&nbsp;currently.</p> +<p>uMap is plain old JavaScript. It&#8217;s not using react or any other framework. The way +I see this is that it makes it possible for us to have something &#8220;close to the +metal&#8221;, if that means anything when it comes to web development. We&#8217;re not tied +to the development pace of these frameworks, and have more control on what we +do. It&#8217;s easier to&nbsp;debug.</p> +<p>So, after making tweaks and learning how &#8220;modules&#8221;, &#8220;requires&#8221; and &#8220;bundling&#8221; +was working, I ultimately decided to take a break from this path, to work on the +wiring with uMap. After all, CRDTs might not even be the way forward for&nbsp;us.</p> +<h2 id="internals">Internals</h2> +<p>I was not expecting this to be easy and was a bit afraid. Mostly because I&#8217;m out of my +comfort zone. After some time with the head under the water, I&#8217;m now able to better +understand the big picture, and I&#8217;m not getting lost in the details like I was at&nbsp;first.</p> +<p>Let me try to summarize what I&#8217;ve&nbsp;learned.</p> +<p>uMap appears to be doing a lot of different things, but in the end&nbsp;it&#8217;s:</p> +<ul> +<li>Using <a href="https://leafletjs.com/">Leaflet.js</a> to render <em>features</em> on the map&nbsp;;</li> +<li>Using <a href="https://github.com/Leaflet/Leaflet.Editable">Leaflet Editable</a> to edit + complex shapes, like polylines, polygons, and to draw markers&nbsp;;</li> +<li>Using the <a href="https://github.com/yohanboniface/Leaflet.FormBuilder">Formbuilder</a> + to expose a way for the users to edit the features, and the data of the&nbsp;map</li> +<li>Serializing the layers to and from <a href="https://geojson.org/">GeoJSON</a>. That&#8217;s + what&#8217;s being sent to and received from the&nbsp;server.</li> +<li>Providing different layer types (marker cluster, chloropleth, etc) to display + the data in different&nbsp;ways.</li> +</ul> +<h3 id="naming-matters">Naming&nbsp;matters</h3> +<p>There is some naming overlap between the different projects we&#8217;re using, and +it&#8217;s important to have these small clarifications in&nbsp;mind:</p> +<h4 id="leaflet-layers-and-umap-features">Leaflet layers and uMap&nbsp;features</h4> +<p><strong>In Leaflet, everything is a layer</strong>. What we call <em>features</em> in geoJSON are +leaflet layers, and even a (uMap) layer is a layer. We need to be extra careful +what are our inputs and outputs in this&nbsp;context.</p> +<p>We actually have different layers concepts: the <em>datalayer</em> and the different +kind of layers (chloropleth, marker cluster, etc). A datalayer, is (as you can +guess) where the data is stored. It&#8217;s what uMap serializes. It contains the +features (with their properties). But that&#8217;s the trick: these features are named +<em>layers</em> by&nbsp;Leaflet.</p> +<h4 id="geojson-and-leaflet">GeoJSON and&nbsp;Leaflet</h4> +<p>We&#8217;re using GeoJSON to share data with the server, but we&#8217;re using Leaflet +internally. And these two have different way of naming&nbsp;things.</p> +<p>The different geometries are named differently (a leaflet <code>Marker</code> is a GeoJSON +<code>Point</code>), and their coordinates are stored differently: Leaflet stores <code>lat, +long</code> where GeoJSON stores <code>long, lat</code>. Not a big deal, but it&#8217;s a good thing +to&nbsp;know.</p> +<p>Leaflet stores data in <code>options</code>, where GeoJSON stores it in <code>properties</code>.</p> +<h3 id="this-is-not-reactive-programming">This is not reactive&nbsp;programming</h3> +<p>I was expecting the frontend to be organised similarly to Elm apps (or React +apps): a global state and a data flow (<a href="https:// react-redux.js.org/ +introduction/getting-started">a la redux</a>), with events changing the data that will trigger +a rerendering of the&nbsp;interface.</p> +<p>Things work differently for us: different components can write to the map, and +get updated without being centralized. It&#8217;s just a different&nbsp;paradigm.</p> +<h2 id="a-syncing-proof-of-concept">A syncing proof of&nbsp;concept</h2> +<p>With that in mind, I started thinking about a simple way to implement&nbsp;syncing. </p> +<p>I left aside all the thinking about how this would relate with CRDTs. It can +be useful, but later. For now, I &#8220;just&#8221; want to synchronize two maps. I want a +proof of concept to do informed&nbsp;decisions.</p> +<h3 id="syncing-map-properties">Syncing map&nbsp;properties</h3> +<p>I started syncing map properties. Things like the name of the map, the default +color and type of the marker, the description, the default zoom level,&nbsp;etc.</p> +<p>All of these are handled by &#8220;the formbuilder&#8221;. You pass it an object, a list of +properties and a callback to call when an update happens, and it will build for +you form&nbsp;inputs.</p> +<p>Taken from the documentation (and&nbsp;simplified):</p> +<div class="highlight"><pre><span></span><code><span class="kd">var</span><span class="w"> </span><span class="nx">tilelayerFields</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;name&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;display name&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;maxZoom&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurIntInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;max zoom&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;minZoom&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurIntInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;min zoom&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;attribution&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;attribution&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;tms&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;CheckBox&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">helpText</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;TMS format&#39;</span><span class="p">}]</span> +<span class="p">];</span> +<span class="kd">var</span><span class="w"> </span><span class="nx">builder</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">L</span><span class="p">.</span><span class="nx">FormBuilder</span><span class="p">(</span><span class="nx">myObject</span><span class="p">,</span><span class="w"> </span><span class="nx">tilelayerFields</span><span class="p">,</span><span class="w"> </span><span class="p">{</span> +<span class="w"> </span><span class="nx">callback</span><span class="o">:</span><span class="w"> </span><span class="nx">myCallback</span><span class="p">,</span> +<span class="w"> </span><span class="nx">callbackContext</span><span class="o">:</span><span class="w"> </span><span class="k">this</span> +<span class="p">});</span> +</code></pre></div> + +<p>In uMap, the formbuilder is used for every form you see on the right panel. Map +properties are stored in the <code>map</code> object.</p> +<p>We want two different clients work together. When one changes the value of a +property, the other client needs to be updated, and update its&nbsp;interface.</p> +<p>I&#8217;ve started by creating a mapping of property names to rerender-methods, and +added a method <code>renderProperties(properties)</code> which updates the interface, +depending on the properties passed to&nbsp;it.</p> +<p>We now have two important&nbsp;things:</p> +<ol> +<li>Some code getting called each time a property is changed&nbsp;;</li> +<li>A way to refresh the right interface when a property is&nbsp;changed.</li> +</ol> +<p>In other words, from one client we can send the message to the other client, +which will be able to rerender&nbsp;itself.</p> +<p>Looks like a&nbsp;plan.</p> +<h2 id="websockets">Websockets</h2> +<p>We need a way for the data to go from one side to the other. The easiest +way is probably&nbsp;websockets.</p> +<p>Here is a simple code which will relay messages from one websocket to the other +connected clients. It&#8217;s not the final code, it&#8217;s just for demo&nbsp;puposes.</p> +<p>A basic way to do this on the server side is to use python&#8217;s +<a href="https://websockets.readthedocs.io/">websockets</a>&nbsp;library.</p> +<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">asyncio</span> +<span class="kn">import</span> <span class="nn">websockets</span> +<span class="kn">from</span> <span class="nn">websockets.server</span> <span class="kn">import</span> <span class="n">serve</span> +<span class="kn">import</span> <span class="nn">json</span> + +<span class="c1"># Just relay all messages to other connected peers for now</span> + +<span class="n">CONNECTIONS</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span> + +<span class="k">async</span> <span class="k">def</span> <span class="nf">join_and_listen</span><span class="p">(</span><span class="n">websocket</span><span class="p">):</span> + <span class="n">CONNECTIONS</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + <span class="k">try</span><span class="p">:</span> + <span class="k">async</span> <span class="k">for</span> <span class="n">message</span> <span class="ow">in</span> <span class="n">websocket</span><span class="p">:</span> + <span class="c1"># recompute the peers-list at the time of message-sending.</span> + <span class="c1"># doing so beforehand would miss new connections</span> + <span class="n">peers</span> <span class="o">=</span> <span class="n">CONNECTIONS</span> <span class="o">-</span> <span class="p">{</span><span class="n">websocket</span><span class="p">}</span> + <span class="n">websockets</span><span class="o">.</span><span class="n">broadcast</span><span class="p">(</span><span class="n">peers</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span> + <span class="k">finally</span><span class="p">:</span> + <span class="n">CONNECTIONS</span><span class="o">.</span><span class="n">remove</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + + +<span class="k">async</span> <span class="k">def</span> <span class="nf">handler</span><span class="p">(</span><span class="n">websocket</span><span class="p">):</span> + <span class="n">message</span> <span class="o">=</span> <span class="k">await</span> <span class="n">websocket</span><span class="o">.</span><span class="n">recv</span><span class="p">()</span> + <span class="n">event</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">message</span><span class="p">)</span> + + <span class="c1"># The first event should always be &#39;join&#39;</span> + <span class="k">assert</span> <span class="n">event</span><span class="p">[</span><span class="s2">&quot;kind&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="s2">&quot;join&quot;</span> + <span class="k">await</span> <span class="n">join_and_listen</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + +<span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span> + <span class="k">async</span> <span class="k">with</span> <span class="n">serve</span><span class="p">(</span><span class="n">handler</span><span class="p">,</span> <span class="s2">&quot;localhost&quot;</span><span class="p">,</span> <span class="mi">8001</span><span class="p">):</span> + <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">Future</span><span class="p">()</span> <span class="c1"># run forever</span> + +<span class="n">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">main</span><span class="p">())</span> +</code></pre></div> + +<p>On the client side, it&#8217;s fairly easy as well. I won&#8217;t even cover it&nbsp;here.</p> +<p>We now have a way to send data from one client to the other. +Let&#8217;s consider the actions we do as &#8220;verbs&#8221;. For now, we&#8217;re just updating +properties values, so we just need the <code>update</code> verb.</p> +<h2 id="code-architecture">Code&nbsp;architecture</h2> +<p>We need different&nbsp;parts:</p> +<ul> +<li>the <strong>transport</strong>, which connects to the websockets, sends and receives&nbsp;messages.</li> +<li>the <strong>message sender</strong> to relat local messages to the other&nbsp;party.</li> +<li>the <strong>message receiver</strong> that&#8217;s being called each time we receive a&nbsp;message.</li> +<li>the <strong>sync engine</strong> which glues everything&nbsp;together</li> +<li>Different <strong>updaters</strong>, which knows how to apply received messages, the goal being + to update the interface in the&nbsp;end.</li> +</ul> +<p>When receiving a message it will be routed to the correct updater, which will +know what to do with&nbsp;it.</p> +<p>In our case, its fairly simple: when updating the <code>name</code> property, we send a +message with <code>name</code> and <code>value</code>. We also need to send along some additional +info: the <code>subject</code>.</p> +<p>In our case, it&#8217;s <code>map</code> because we&#8217;re updating map&nbsp;properties.</p> +<p>When initializing the <code>map</code>, we&#8217;re initializing the <code>SyncEngine</code>, like&nbsp;this:</p> +<div class="highlight"><pre><span></span><code><span class="c1">// inside the map</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">syncEngine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">umap</span><span class="p">.</span><span class="nx">SyncEngine</span><span class="p">(</span><span class="k">this</span><span class="p">)</span> + +<span class="c1">// Then, when we need to send data to the other party</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">syncEngine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">obj</span><span class="p">.</span><span class="nx">getSyncEngine</span><span class="p">()</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">subject</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">obj</span><span class="p">.</span><span class="nx">getSyncSubject</span><span class="p">()</span> + +<span class="nx">syncEngine</span><span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">subject</span><span class="p">,</span><span class="w"> </span><span class="nx">field</span><span class="p">,</span><span class="w"> </span><span class="nx">value</span><span class="p">)</span> +</code></pre></div> + +<p>The code on the other side of the wire is simple enough: when you receive the +message, change the data and rerender the&nbsp;properties:</p> +<div class="highlight"><pre><span></span><code><span class="k">this</span><span class="p">.</span><span class="nx">updateObjectValue</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">map</span><span class="p">,</span><span class="w"> </span><span class="nx">key</span><span class="p">,</span><span class="w"> </span><span class="nx">value</span><span class="p">)</span> +<span class="k">this</span><span class="p">.</span><span class="nx">map</span><span class="p">.</span><span class="nx">renderProperties</span><span class="p">(</span><span class="nx">key</span><span class="p">)</span> +</code></pre></div> + +<h2 id="syncing-features">Syncing&nbsp;features</h2> +<p>At this stage I was able to sync the properties of the map. A +small victory, but not the end of the&nbsp;trip.</p> +<p>The next step was to add syncing for features: markers, polygon and polylines, +alongside their&nbsp;properties.</p> +<p>All of these features have a uMap class representation (which extends Leaflets +ones). All of them share some code in the <code>FeatureMixin</code> class.</p> +<p>That seems a good place to do the&nbsp;changes.</p> +<p>I did a few&nbsp;changes:</p> +<ul> +<li>Each feature now has an identifier, so clients know they&#8217;re talking about the + same thing. This identifier is also stored in the database when&nbsp;saved.</li> +<li>I&#8217;ve added an <code>upsert</code> verb, because we don&#8217;t have any way (from the + interface) to make a distinction between the creation of a new feature and + its modification. The way we intercept the creation of a feature (or its + update) is to use Leaflet Editable&#8217;s <code>editable:drawing:commit</code> event. We just + have to listen to it and then send the appropriate messages&nbsp;!</li> +</ul> +<p>After some giggling around (ah, everybody wants to create a new protocol !) I +went with reusing GeoJSON. It allowed me to have a better understanding of how +Leaflet is using latlongs. That&#8217;s a multi-dimensional array, with variable +width, depending on the type of geometry and the number of shapes in each of&nbsp;these.</p> +<p>Clearly not something I want to redo, so I&#8217;m now reusing some Leaflet code, which handles this serialization for&nbsp;me.</p> +<p>I&#8217;m now able to sync different types of features with their&nbsp;properties.</p> +<video controls width="80%"> + <source src="/images/umap/sync-features.webm" type="video/webm"> +</video> + +<p>Point properties are also editable, using the already-existing table editor. I +was expecting this to require some work but it&#8217;s just working without more&nbsp;changes.</p> +<h2 id="whats-next">What&#8217;s next&nbsp;?</h2> +<p>I&#8217;m able to sync map properties, features and their properties, but I&#8217;m not +yet syncing layers. That&#8217;s the next step! I also plan to make some pull +requests with the interesting bits I&#8217;m sure will go in the final&nbsp;implementation:</p> +<ul> +<li>Adding ids to features, so we have a way to refer to&nbsp;them.</li> +<li>Having a way to map properties with how they render the interface, the <code>renderProperties</code> bits.</li> +</ul> +<p>When this demo will be working, I&#8217;ll probably spend some time updating it with the latest changes (umap is moving a lot these weeks). +I will probably focus on how to integrate websockets in the server side, and then will see how to leverage (maybe) some magic from CRDTs, if we need&nbsp;it.</p> +<p>See you for the next&nbsp;update!</p>Adding Real-Time Collaboration to uMap, second week2023-11-21T00:00:00+01:002023-11-21T00:00:00+01:00tag:blog.notmyidea.org,2023-11-21:/adding-real-time-collaboration-to-umap-second-week.html<p>I continued working on <a href="https://github.com/umap-project/umap/">uMap</a>, an open-source map-making tool to create and share customizable maps, based on Open Street Map&nbsp;data.</p> <p>Here is a summary of what I&nbsp;did:</p> <ul> <li>I reviewed, rebased and made some minor changes to <a href="https://github.com/umap-project/umap/pull/772">a pull request which makes it possible to merge geojson features together …</a></li></ul><p>I continued working on <a href="https://github.com/umap-project/umap/">uMap</a>, an open-source map-making tool to create and share customizable maps, based on Open Street Map&nbsp;data.</p> diff --git a/feeds/tags/websockets.atom.xml b/feeds/tags/websockets.atom.xml new file mode 100644 index 0000000..63b0b1c --- /dev/null +++ b/feeds/tags/websockets.atom.xml @@ -0,0 +1,238 @@ + +Alexis Métaireau - websocketshttps://blog.notmyidea.org/2024-02-12T00:00:00+01:00Adding collaboration on uMap, third update2024-02-12T00:00:00+01:002024-02-12T00:00:00+01:00tag:blog.notmyidea.org,2024-02-12:/adding-collaboration-on-umap-third-update.html<p>I&#8217;ve spent the last few weeks working on <a href="https://umap-project.org">uMap</a>, still +with the goal of bringing real-time collaboration to the maps. I&#8217;m not there +yet, but I&#8217;ve made some progress that I will relate&nbsp;here.</p> +<h2 id="javascript-modules">JavaScript&nbsp;modules</h2> +<p>uMap has been there <a href="https://github.com/ +umap-project/umap/commit/0cce7f9e2a19c83fa76645d7773d39d54f357c43">since 2012</a>, at a time +when <span class="caps">ES6 …</span></p><p>I&#8217;ve spent the last few weeks working on <a href="https://umap-project.org">uMap</a>, still +with the goal of bringing real-time collaboration to the maps. I&#8217;m not there +yet, but I&#8217;ve made some progress that I will relate&nbsp;here.</p> +<h2 id="javascript-modules">JavaScript&nbsp;modules</h2> +<p>uMap has been there <a href="https://github.com/ +umap-project/umap/commit/0cce7f9e2a19c83fa76645d7773d39d54f357c43">since 2012</a>, at a time +when <span class="caps">ES6</span> <a href="https://fr.wikipedia.org/wiki/ECMAScript">wasn&#8217;t out there&nbsp;yet</a></p> +<p>At the time, it wasn&#8217;t possible to use JavaScript modules, nor modern JavaScript +syntax. The project stayed with these requirements for a long time, in order to support +people with old browsers. But as time goes on, we can now have access to more&nbsp;features.</p> +<p>The team has been working hard on bringing modules to the mix, and it +wasn&#8217;t a piece of cake. But, the result is here: we&#8217;re <a href="https://github.com/umap-project/umap/pull/1463/files">now able to use modern +JavaSript modules</a> and we +are now more confident <a href="https://github.com/umap-project/umap/commit/65f1cdd6b4569657ef5e219d9b377fec85c41958">about which features of the languages we can use or&nbsp;not</a></p> +<hr> +<p>I then spent ~way too much~ some time trying to integrate existing CRDTs like +Automerge and <span class="caps">YJS</span> in our project. These two libs are unfortunately expecting us to +use a bundler, which we aren&#8217;t&nbsp;currently.</p> +<p>uMap is plain old JavaScript. It&#8217;s not using react or any other framework. The way +I see this is that it makes it possible for us to have something &#8220;close to the +metal&#8221;, if that means anything when it comes to web development. We&#8217;re not tied +to the development pace of these frameworks, and have more control on what we +do. It&#8217;s easier to&nbsp;debug.</p> +<p>So, after making tweaks and learning how &#8220;modules&#8221;, &#8220;requires&#8221; and &#8220;bundling&#8221; +was working, I ultimately decided to take a break from this path, to work on the +wiring with uMap. After all, CRDTs might not even be the way forward for&nbsp;us.</p> +<h2 id="internals">Internals</h2> +<p>I was not expecting this to be easy and was a bit afraid. Mostly because I&#8217;m out of my +comfort zone. After some time with the head under the water, I&#8217;m now able to better +understand the big picture, and I&#8217;m not getting lost in the details like I was at&nbsp;first.</p> +<p>Let me try to summarize what I&#8217;ve&nbsp;learned.</p> +<p>uMap appears to be doing a lot of different things, but in the end&nbsp;it&#8217;s:</p> +<ul> +<li>Using <a href="https://leafletjs.com/">Leaflet.js</a> to render <em>features</em> on the map&nbsp;;</li> +<li>Using <a href="https://github.com/Leaflet/Leaflet.Editable">Leaflet Editable</a> to edit + complex shapes, like polylines, polygons, and to draw markers&nbsp;;</li> +<li>Using the <a href="https://github.com/yohanboniface/Leaflet.FormBuilder">Formbuilder</a> + to expose a way for the users to edit the features, and the data of the&nbsp;map</li> +<li>Serializing the layers to and from <a href="https://geojson.org/">GeoJSON</a>. That&#8217;s + what&#8217;s being sent to and received from the&nbsp;server.</li> +<li>Providing different layer types (marker cluster, chloropleth, etc) to display + the data in different&nbsp;ways.</li> +</ul> +<h3 id="naming-matters">Naming&nbsp;matters</h3> +<p>There is some naming overlap between the different projects we&#8217;re using, and +it&#8217;s important to have these small clarifications in&nbsp;mind:</p> +<h4 id="leaflet-layers-and-umap-features">Leaflet layers and uMap&nbsp;features</h4> +<p><strong>In Leaflet, everything is a layer</strong>. What we call <em>features</em> in geoJSON are +leaflet layers, and even a (uMap) layer is a layer. We need to be extra careful +what are our inputs and outputs in this&nbsp;context.</p> +<p>We actually have different layers concepts: the <em>datalayer</em> and the different +kind of layers (chloropleth, marker cluster, etc). A datalayer, is (as you can +guess) where the data is stored. It&#8217;s what uMap serializes. It contains the +features (with their properties). But that&#8217;s the trick: these features are named +<em>layers</em> by&nbsp;Leaflet.</p> +<h4 id="geojson-and-leaflet">GeoJSON and&nbsp;Leaflet</h4> +<p>We&#8217;re using GeoJSON to share data with the server, but we&#8217;re using Leaflet +internally. And these two have different way of naming&nbsp;things.</p> +<p>The different geometries are named differently (a leaflet <code>Marker</code> is a GeoJSON +<code>Point</code>), and their coordinates are stored differently: Leaflet stores <code>lat, +long</code> where GeoJSON stores <code>long, lat</code>. Not a big deal, but it&#8217;s a good thing +to&nbsp;know.</p> +<p>Leaflet stores data in <code>options</code>, where GeoJSON stores it in <code>properties</code>.</p> +<h3 id="this-is-not-reactive-programming">This is not reactive&nbsp;programming</h3> +<p>I was expecting the frontend to be organised similarly to Elm apps (or React +apps): a global state and a data flow (<a href="https:// react-redux.js.org/ +introduction/getting-started">a la redux</a>), with events changing the data that will trigger +a rerendering of the&nbsp;interface.</p> +<p>Things work differently for us: different components can write to the map, and +get updated without being centralized. It&#8217;s just a different&nbsp;paradigm.</p> +<h2 id="a-syncing-proof-of-concept">A syncing proof of&nbsp;concept</h2> +<p>With that in mind, I started thinking about a simple way to implement&nbsp;syncing. </p> +<p>I left aside all the thinking about how this would relate with CRDTs. It can +be useful, but later. For now, I &#8220;just&#8221; want to synchronize two maps. I want a +proof of concept to do informed&nbsp;decisions.</p> +<h3 id="syncing-map-properties">Syncing map&nbsp;properties</h3> +<p>I started syncing map properties. Things like the name of the map, the default +color and type of the marker, the description, the default zoom level,&nbsp;etc.</p> +<p>All of these are handled by &#8220;the formbuilder&#8221;. You pass it an object, a list of +properties and a callback to call when an update happens, and it will build for +you form&nbsp;inputs.</p> +<p>Taken from the documentation (and&nbsp;simplified):</p> +<div class="highlight"><pre><span></span><code><span class="kd">var</span><span class="w"> </span><span class="nx">tilelayerFields</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;name&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;display name&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;maxZoom&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurIntInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;max zoom&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;minZoom&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurIntInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;min zoom&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;attribution&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;BlurInput&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">placeholder</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;attribution&#39;</span><span class="p">}],</span> +<span class="w"> </span><span class="p">[</span><span class="s1">&#39;tms&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="nx">handler</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;CheckBox&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">helpText</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;TMS format&#39;</span><span class="p">}]</span> +<span class="p">];</span> +<span class="kd">var</span><span class="w"> </span><span class="nx">builder</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">L</span><span class="p">.</span><span class="nx">FormBuilder</span><span class="p">(</span><span class="nx">myObject</span><span class="p">,</span><span class="w"> </span><span class="nx">tilelayerFields</span><span class="p">,</span><span class="w"> </span><span class="p">{</span> +<span class="w"> </span><span class="nx">callback</span><span class="o">:</span><span class="w"> </span><span class="nx">myCallback</span><span class="p">,</span> +<span class="w"> </span><span class="nx">callbackContext</span><span class="o">:</span><span class="w"> </span><span class="k">this</span> +<span class="p">});</span> +</code></pre></div> + +<p>In uMap, the formbuilder is used for every form you see on the right panel. Map +properties are stored in the <code>map</code> object.</p> +<p>We want two different clients work together. When one changes the value of a +property, the other client needs to be updated, and update its&nbsp;interface.</p> +<p>I&#8217;ve started by creating a mapping of property names to rerender-methods, and +added a method <code>renderProperties(properties)</code> which updates the interface, +depending on the properties passed to&nbsp;it.</p> +<p>We now have two important&nbsp;things:</p> +<ol> +<li>Some code getting called each time a property is changed&nbsp;;</li> +<li>A way to refresh the right interface when a property is&nbsp;changed.</li> +</ol> +<p>In other words, from one client we can send the message to the other client, +which will be able to rerender&nbsp;itself.</p> +<p>Looks like a&nbsp;plan.</p> +<h2 id="websockets">Websockets</h2> +<p>We need a way for the data to go from one side to the other. The easiest +way is probably&nbsp;websockets.</p> +<p>Here is a simple code which will relay messages from one websocket to the other +connected clients. It&#8217;s not the final code, it&#8217;s just for demo&nbsp;puposes.</p> +<p>A basic way to do this on the server side is to use python&#8217;s +<a href="https://websockets.readthedocs.io/">websockets</a>&nbsp;library.</p> +<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">asyncio</span> +<span class="kn">import</span> <span class="nn">websockets</span> +<span class="kn">from</span> <span class="nn">websockets.server</span> <span class="kn">import</span> <span class="n">serve</span> +<span class="kn">import</span> <span class="nn">json</span> + +<span class="c1"># Just relay all messages to other connected peers for now</span> + +<span class="n">CONNECTIONS</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span> + +<span class="k">async</span> <span class="k">def</span> <span class="nf">join_and_listen</span><span class="p">(</span><span class="n">websocket</span><span class="p">):</span> + <span class="n">CONNECTIONS</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + <span class="k">try</span><span class="p">:</span> + <span class="k">async</span> <span class="k">for</span> <span class="n">message</span> <span class="ow">in</span> <span class="n">websocket</span><span class="p">:</span> + <span class="c1"># recompute the peers-list at the time of message-sending.</span> + <span class="c1"># doing so beforehand would miss new connections</span> + <span class="n">peers</span> <span class="o">=</span> <span class="n">CONNECTIONS</span> <span class="o">-</span> <span class="p">{</span><span class="n">websocket</span><span class="p">}</span> + <span class="n">websockets</span><span class="o">.</span><span class="n">broadcast</span><span class="p">(</span><span class="n">peers</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span> + <span class="k">finally</span><span class="p">:</span> + <span class="n">CONNECTIONS</span><span class="o">.</span><span class="n">remove</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + + +<span class="k">async</span> <span class="k">def</span> <span class="nf">handler</span><span class="p">(</span><span class="n">websocket</span><span class="p">):</span> + <span class="n">message</span> <span class="o">=</span> <span class="k">await</span> <span class="n">websocket</span><span class="o">.</span><span class="n">recv</span><span class="p">()</span> + <span class="n">event</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">message</span><span class="p">)</span> + + <span class="c1"># The first event should always be &#39;join&#39;</span> + <span class="k">assert</span> <span class="n">event</span><span class="p">[</span><span class="s2">&quot;kind&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="s2">&quot;join&quot;</span> + <span class="k">await</span> <span class="n">join_and_listen</span><span class="p">(</span><span class="n">websocket</span><span class="p">)</span> + +<span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span> + <span class="k">async</span> <span class="k">with</span> <span class="n">serve</span><span class="p">(</span><span class="n">handler</span><span class="p">,</span> <span class="s2">&quot;localhost&quot;</span><span class="p">,</span> <span class="mi">8001</span><span class="p">):</span> + <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">Future</span><span class="p">()</span> <span class="c1"># run forever</span> + +<span class="n">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">main</span><span class="p">())</span> +</code></pre></div> + +<p>On the client side, it&#8217;s fairly easy as well. I won&#8217;t even cover it&nbsp;here.</p> +<p>We now have a way to send data from one client to the other. +Let&#8217;s consider the actions we do as &#8220;verbs&#8221;. For now, we&#8217;re just updating +properties values, so we just need the <code>update</code> verb.</p> +<h2 id="code-architecture">Code&nbsp;architecture</h2> +<p>We need different&nbsp;parts:</p> +<ul> +<li>the <strong>transport</strong>, which connects to the websockets, sends and receives&nbsp;messages.</li> +<li>the <strong>message sender</strong> to relat local messages to the other&nbsp;party.</li> +<li>the <strong>message receiver</strong> that&#8217;s being called each time we receive a&nbsp;message.</li> +<li>the <strong>sync engine</strong> which glues everything&nbsp;together</li> +<li>Different <strong>updaters</strong>, which knows how to apply received messages, the goal being + to update the interface in the&nbsp;end.</li> +</ul> +<p>When receiving a message it will be routed to the correct updater, which will +know what to do with&nbsp;it.</p> +<p>In our case, its fairly simple: when updating the <code>name</code> property, we send a +message with <code>name</code> and <code>value</code>. We also need to send along some additional +info: the <code>subject</code>.</p> +<p>In our case, it&#8217;s <code>map</code> because we&#8217;re updating map&nbsp;properties.</p> +<p>When initializing the <code>map</code>, we&#8217;re initializing the <code>SyncEngine</code>, like&nbsp;this:</p> +<div class="highlight"><pre><span></span><code><span class="c1">// inside the map</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">syncEngine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">umap</span><span class="p">.</span><span class="nx">SyncEngine</span><span class="p">(</span><span class="k">this</span><span class="p">)</span> + +<span class="c1">// Then, when we need to send data to the other party</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">syncEngine</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">obj</span><span class="p">.</span><span class="nx">getSyncEngine</span><span class="p">()</span> +<span class="kd">let</span><span class="w"> </span><span class="nx">subject</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">obj</span><span class="p">.</span><span class="nx">getSyncSubject</span><span class="p">()</span> + +<span class="nx">syncEngine</span><span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">subject</span><span class="p">,</span><span class="w"> </span><span class="nx">field</span><span class="p">,</span><span class="w"> </span><span class="nx">value</span><span class="p">)</span> +</code></pre></div> + +<p>The code on the other side of the wire is simple enough: when you receive the +message, change the data and rerender the&nbsp;properties:</p> +<div class="highlight"><pre><span></span><code><span class="k">this</span><span class="p">.</span><span class="nx">updateObjectValue</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">map</span><span class="p">,</span><span class="w"> </span><span class="nx">key</span><span class="p">,</span><span class="w"> </span><span class="nx">value</span><span class="p">)</span> +<span class="k">this</span><span class="p">.</span><span class="nx">map</span><span class="p">.</span><span class="nx">renderProperties</span><span class="p">(</span><span class="nx">key</span><span class="p">)</span> +</code></pre></div> + +<h2 id="syncing-features">Syncing&nbsp;features</h2> +<p>At this stage I was able to sync the properties of the map. A +small victory, but not the end of the&nbsp;trip.</p> +<p>The next step was to add syncing for features: markers, polygon and polylines, +alongside their&nbsp;properties.</p> +<p>All of these features have a uMap class representation (which extends Leaflets +ones). All of them share some code in the <code>FeatureMixin</code> class.</p> +<p>That seems a good place to do the&nbsp;changes.</p> +<p>I did a few&nbsp;changes:</p> +<ul> +<li>Each feature now has an identifier, so clients know they&#8217;re talking about the + same thing. This identifier is also stored in the database when&nbsp;saved.</li> +<li>I&#8217;ve added an <code>upsert</code> verb, because we don&#8217;t have any way (from the + interface) to make a distinction between the creation of a new feature and + its modification. The way we intercept the creation of a feature (or its + update) is to use Leaflet Editable&#8217;s <code>editable:drawing:commit</code> event. We just + have to listen to it and then send the appropriate messages&nbsp;!</li> +</ul> +<p>After some giggling around (ah, everybody wants to create a new protocol !) I +went with reusing GeoJSON. It allowed me to have a better understanding of how +Leaflet is using latlongs. That&#8217;s a multi-dimensional array, with variable +width, depending on the type of geometry and the number of shapes in each of&nbsp;these.</p> +<p>Clearly not something I want to redo, so I&#8217;m now reusing some Leaflet code, which handles this serialization for&nbsp;me.</p> +<p>I&#8217;m now able to sync different types of features with their&nbsp;properties.</p> +<video controls width="80%"> + <source src="/images/umap/sync-features.webm" type="video/webm"> +</video> + +<p>Point properties are also editable, using the already-existing table editor. I +was expecting this to require some work but it&#8217;s just working without more&nbsp;changes.</p> +<h2 id="whats-next">What&#8217;s next&nbsp;?</h2> +<p>I&#8217;m able to sync map properties, features and their properties, but I&#8217;m not +yet syncing layers. That&#8217;s the next step! I also plan to make some pull +requests with the interesting bits I&#8217;m sure will go in the final&nbsp;implementation:</p> +<ul> +<li>Adding ids to features, so we have a way to refer to&nbsp;them.</li> +<li>Having a way to map properties with how they render the interface, the <code>renderProperties</code> bits.</li> +</ul> +<p>When this demo will be working, I&#8217;ll probably spend some time updating it with the latest changes (umap is moving a lot these weeks). +I will probably focus on how to integrate websockets in the server side, and then will see how to leverage (maybe) some magic from CRDTs, if we need&nbsp;it.</p> +<p>See you for the next&nbsp;update!</p> \ No newline at end of file diff --git a/tag/geojson.html b/tag/geojson.html new file mode 100644 index 0000000..e190ad3 --- /dev/null +++ b/tag/geojson.html @@ -0,0 +1,68 @@ + + + + +geojson - Alexis Métaireau + + + + + + +
                                                        + +

                                                        Tag « geojson »

                                                        +
                                                        + + + umap, geojson, websockets

                                                        Adding collaboration on uMap, third update

                                                        +

                                                        I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                                                        +

                                                        JavaScript modules

                                                        +

                                                        uMap has been there since 2012, at a time +when ES6 …

                                                        +
                                                        +
                                                          +
                                                        +
                                                      + + + + \ No newline at end of file diff --git a/tag/umap.html b/tag/umap.html index 6f2742e..eaf8a84 100644 --- a/tag/umap.html +++ b/tag/umap.html @@ -2,7 +2,7 @@ -uMap - Alexis Métaireau +umap - Alexis Métaireau
                                                    -

                                                    Tag « uMap »

                                                    +

                                                    Tag « umap »

                                                    +
                                                    + + + umap, geojson, websockets

                                                    Adding collaboration on uMap, third update

                                                    +

                                                    I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                                                    +

                                                    JavaScript modules

                                                    +

                                                    uMap has been there since 2012, at a time +when ES6 …

                                                    +
                                                    diff --git a/tag/websockets.html b/tag/websockets.html new file mode 100644 index 0000000..034e05b --- /dev/null +++ b/tag/websockets.html @@ -0,0 +1,68 @@ + + + + +websockets - Alexis Métaireau + + + + + + +
                                                    + +

                                                    Tag « websockets »

                                                    +
                                                    + + + umap, geojson, websockets

                                                    Adding collaboration on uMap, third update

                                                    +

                                                    I’ve spent the last few weeks working on uMap, still +with the goal of bringing real-time collaboration to the maps. I’m not there +yet, but I’ve made some progress that I will relate here.

                                                    +

                                                    JavaScript modules

                                                    +

                                                    uMap has been there since 2012, at a time +when ES6 …

                                                    +
                                                    +
                                                      +
                                                    +
                                                    + + + + \ No newline at end of file diff --git a/tags.html b/tags.html index 74d159b..722bdab 100644 --- a/tags.html +++ b/tags.html @@ -45,6 +45,9 @@ Alexis Métaireau

                                                    Voici une liste de tous les tags utilisés sur ce site :