diff --git a/umap/static/umap/js/modules/global.js b/umap/static/umap/js/modules/global.js index 51ba37d0..d11f52f6 100644 --- a/umap/static/umap/js/modules/global.js +++ b/umap/static/umap/js/modules/global.js @@ -1,8 +1,11 @@ import * as L from '../../vendors/leaflet/leaflet-src.esm.js' +import * as Y from '../../vendors/yjs/yjs.js' import URLs from './urls.js' // Import modules and export them to the global scope. // For the not yet module-compatible JS out there. // Copy the leaflet module, it's expected by leaflet plugins to be writeable. window.L = { ...L } + +window.Y = Y window.umap = { URLs } diff --git a/umap/static/umap/js/umap.browser.js b/umap/static/umap/js/umap.browser.js index 77c3049d..189807b2 100644 --- a/umap/static/umap/js/umap.browser.js +++ b/umap/static/umap/js/umap.browser.js @@ -62,8 +62,13 @@ L.U.Browser = L.Class.extend({ container.id = `browse_data_datalayer_${datalayer.umap_id}` datalayer.renderToolbox(headline) L.DomUtil.add('span', '', headline, datalayer.options.name) - const counter = L.DomUtil.add('span', 'datalayer-counter', headline, datalayer.count()) - counter.title = L._('{count} features in this layer', {count: datalayer.count()}) + const counter = L.DomUtil.add( + 'span', + 'datalayer-counter', + headline, + datalayer.count() + ) + counter.title = L._('{count} features in this layer', { count: datalayer.count() }) const ul = L.DomUtil.create('ul', '', container) L.DomUtil.classIf(container, 'off', !datalayer.isVisible()) diff --git a/umap/static/umap/js/umap.data.js b/umap/static/umap/js/umap.data.js new file mode 100644 index 00000000..b6953316 --- /dev/null +++ b/umap/static/umap/js/umap.data.js @@ -0,0 +1,45 @@ +/** + * A mixin to ease the rendering of the data, and updating of a local CRDT. + * + * The mixed class needs to expose: + * + * - `dataUpdaters`, an object matching each property with a list of renderers. + * - `getDataObject`, a method returning where the data is stored/retrieved. + */ +L.U.DataRendererMixin = { + populateCRDT: function () { + for (const [key, value] of Object.entries(this.options)) { + this.crdt.set(key, value) + } + }, + /** + * For each passed property, find the functions to rerender the interface, + * and call them. + * + * @param list updatedProperties : properties that have been updated. + */ + renderProperties: function (updatedProperties) { + console.debug(updatedProperties) + let renderers = new Set() + for (const prop of updatedProperties) { + const propRenderers = this.dataUpdaters[prop] + if (propRenderers) { + for (const renderer of propRenderers) renderers.add(renderer) + } + } + console.debug('renderers', renderers) + for (const renderer of renderers) this[renderer]() + }, + + dataReceived: function () { + // Data has been received over the wire + this.updateInternalData() + this.onPropertiesUpdated(['name', 'color']) + }, +} + +L.U.FormBuilderDataRendererMixin = { + getDataObject: function () { + return this.options + }, +} diff --git a/umap/static/umap/js/umap.forms.js b/umap/static/umap/js/umap.forms.js index fabf2424..d7d6dd49 100644 --- a/umap/static/umap/js/umap.forms.js +++ b/umap/static/umap/js/umap.forms.js @@ -1259,6 +1259,12 @@ L.U.FormBuilder = L.FormBuilder.extend({ setter: function (field, value) { L.FormBuilder.prototype.setter.call(this, field, value) if (this.options.makeDirty !== false) this.obj.isDirty = true + + // FIXME: for now remove the options prefix + field = field.replace('options.', '') + if (this.obj.crdt) this.obj.crdt.set(field, value) + + this.obj.onPropertiesUpdated([field]) }, finish: function () { diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 88d328e7..e4b30e50 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -34,7 +34,7 @@ L.Map.mergeOptions({ // we cannot rely on this because of the y is overriden by Leaflet // See https://github.com/Leaflet/Leaflet/pull/9201 // And let's remove this -y when this PR is merged and released. - demoTileInfos: { s: 'a', z: 9, x: 265, y: 181, '-y': 181, r: '' }, + demoTileInfos: { 's': 'a', 'z': 9, 'x': 265, 'y': 181, '-y': 181, 'r': '' }, licences: [], licence: '', enableMarkerDraw: true, @@ -69,6 +69,122 @@ L.U.Map.include({ 'tilelayers', ], + //Used by the L.U.DataRendererMixin + propertiesRenderers: { + // Controls + 'name': ['renderEditToolbar', 'renderControls'], + 'color': ['renderVisibleDataLayers'], + 'moreControl': ['renderControls', 'initCaptionBar'], + 'scrollWheelZoom': ['renderControls', 'initCaptionBar'], + 'miniMap': ['renderControls', 'initCaptionBar'], + 'scaleControl': ['renderControls', 'initCaptionBar'], + 'onLoadPanel': ['renderControls', 'initCaptionBar'], + 'defaultView': ['renderControls', 'initCaptionBar'], + 'displayPopupFooter': ['renderControls', 'initCaptionBar'], + 'captionBar': ['renderControls', 'initCaptionBar'], + 'captionMenus': ['renderControls', 'initCaptionBar'], + 'zoomControl': ['renderControls', 'initCaptionBar'], + 'searchControl': ['renderControls', 'initCaptionBar'], + 'fullscreenControl': ['renderControls', 'initCaptionBar'], + 'embedControl': ['renderControls', 'initCaptionBar'], + 'locateControl': ['renderControls', 'initCaptionBar'], + 'measureControl': ['renderControls', 'initCaptionBar'], + 'editinosmControl': ['renderControls', 'initCaptionBar'], + 'datalayersControl': ['renderControls', 'initCaptionBar'], + 'starControl': ['renderControls', 'initCaptionBar'], + 'tilelayersControl': ['renderControls', 'initCaptionBar'], + + // Shape properties + 'color': ['renderVisibleDataLayers', 'renderControls'], + 'iconClass': ['renderVisibleDataLayers', 'renderControls'], + 'iconUrl': ['renderVisibleDataLayers', 'renderControls'], + 'iconOpacity': ['renderVisibleDataLayers', 'renderControls'], + 'opacity': ['renderVisibleDataLayers', 'renderControls'], + 'weight': ['renderVisibleDataLayers', 'renderControls'], + 'fill': ['renderVisibleDataLayers', 'renderControls'], + 'fillColor': ['renderVisibleDataLayers', 'renderControls'], + 'fillOpacity': ['renderVisibleDataLayers', 'renderControls'], + 'smoothFactor': ['renderVisibleDataLayers', 'renderControls'], + 'dashArray': ['renderVisibleDataLayers', 'renderControls'], + + // Default properties + 'zoomTo': ['initCaptionBar'], + 'easing': ['initCaptionBar'], + 'labelKey': ['initCaptionBar'], + 'sortKey': ['initCaptionBar', 'reindexEachDataLayer'], + 'filterKey': ['initCaptionBar'], + 'facetKey': ['initCaptionBar'], + 'slugKey': ['initCaptionBar'], + + // Interaction properties + 'popupShape': [], + 'popupTemplate': [], + 'popupContentTemplate': [], + 'showLabel': ['renderVisibleDataLayers'], + 'labelDirection': ['renderVisibleDataLayers'], + 'labelInteractive': ['renderVisibleDataLayers'], + 'outlinkTarget': [], + + // Tile layer + 'tilelayer.name': ['initTileLayer'], + 'tilelayer.url_template': ['initTileLayer'], + 'tilelayer.maxZoom': ['initTileLayer'], + 'tilelayer.minZoom': ['initTileLayer'], + 'tilelayer.attribution': ['initTileLayer'], + 'tilelayer.tms': ['initTileLayer'], + + // Overlay + 'overlay.url_template': ['initTileLayer'], + 'overlay.maxZoom': ['initTileLayer'], + 'overlay.minZoom': ['initTileLayer'], + 'overlay.attribution': ['initTileLayer'], + 'overlay.opacity': ['initTileLayer'], + 'overlay.tms': ['initTileLayer'], + + // Bounds + 'limitBounds.south': ['handleLimitBounds'], + 'limitBounds.west': ['handleLimitBounds'], + 'limitBounds.north': ['handleLimitBounds'], + 'limitBounds.east': ['handleLimitBounds'], + + // Slideshow + 'slideshow.active': ['renderControls'], + 'slideshow.delay': ['renderControls'], + 'slideshow.easing': ['renderControls'], + 'slideshow.autoplay': ['renderControls'], + + // Credits + 'licence': ['renderControls'], + 'shortCredit': ['renderControls'], + 'longCredit': ['renderControls'], + 'permanentCredit': ['renderControls'], + 'permanentCreditBackground': ['renderControls'], + }, + + reindexEachDataLayer: function () { + this.eachDataLayer((datalayer) => datalayer.reindex()) + }, + + renderVisibleDataLayers: function () { + this.eachVisibleDataLayer((datalayer) => { + datalayer.redraw() + }) + }, + + broadcastChanges: function (data) { + // Send changes over the wire + console.log(data) + }, + + updateInternalData: function () { + this.options.name = 'CRDTS, yeah' + this.options.color = 'Fushia' + }, + + getCRDT: function () { + return this._main_crdt.getMap('map') + }, + initialize: function (el, geojson) { // Locale name (pt_PT, en_US…) // To be used for Django localization @@ -292,6 +408,7 @@ L.U.Map.include({ this.backup() this.initContextMenu() this.on('click contextmenu.show', this.closeInplaceToolbar) + this._main_crdt = new YJS.Doc() }, initControls: function () { @@ -830,7 +947,10 @@ L.U.Map.include({ self.isDirty = true } if (this._controls.tilelayersChooser) - this._controls.tilelayersChooser.openSwitcher({ callback: callback, className: 'dark' }) + this._controls.tilelayersChooser.openSwitcher({ + callback: callback, + className: 'dark', + }) }, manageDatalayers: function () { @@ -1260,13 +1380,7 @@ L.U.Map.include({ 'options.captionBar', 'options.captionMenus', ]) - builder = new L.U.FormBuilder(this, UIFields, { - callback: function () { - this.renderControls() - this.initCaptionBar() - }, - callbackContext: this, - }) + builder = new L.U.FormBuilder(this, UIFields) const controlsOptions = L.DomUtil.createFieldset( container, L._('User interface options') @@ -1289,14 +1403,7 @@ L.U.Map.include({ 'options.dashArray', ] - builder = new L.U.FormBuilder(this, shapeOptions, { - callback: function (e) { - if (this._controls.miniMap) this.renderControls() - this.eachVisibleDataLayer((datalayer) => { - datalayer.redraw() - }) - }, - }) + builder = new L.U.FormBuilder(this, shapeOptions) const defaultShapeProperties = L.DomUtil.createFieldset( container, L._('Default shape properties') @@ -1349,14 +1456,7 @@ L.U.Map.include({ ], ] - builder = new L.U.FormBuilder(this, optionsFields, { - callback: function (e) { - this.initCaptionBar() - if (e.helper.field === 'options.sortKey') { - this.eachDataLayer((datalayer) => datalayer.reindex()) - } - }, - }) + builder = new L.U.FormBuilder(this, optionsFields) const defaultProperties = L.DomUtil.createFieldset( container, L._('Default properties') @@ -1374,20 +1474,7 @@ L.U.Map.include({ 'options.labelInteractive', 'options.outlinkTarget', ] - builder = new L.U.FormBuilder(this, popupFields, { - callback: function (e) { - if ( - e.helper.field === 'options.popupTemplate' || - e.helper.field === 'options.popupContentTemplate' || - e.helper.field === 'options.popupShape' || - e.helper.field === 'options.outlinkTarget' - ) - return - this.eachVisibleDataLayer((datalayer) => { - datalayer.redraw() - }) - }, - }) + builder = new L.U.FormBuilder(this, popupFields) const popupFieldset = L.DomUtil.createFieldset( container, L._('Default interaction options') @@ -1441,10 +1528,7 @@ L.U.Map.include({ container, L._('Custom background') ) - builder = new L.U.FormBuilder(this, tilelayerFields, { - callback: this.initTileLayers, - callbackContext: this, - }) + builder = new L.U.FormBuilder(this, tilelayerFields) customTilelayer.appendChild(builder.build()) }, @@ -1492,10 +1576,7 @@ L.U.Map.include({ ['options.overlay.tms', { handler: 'Switch', label: L._('TMS format') }], ] const overlay = L.DomUtil.createFieldset(container, L._('Custom overlay')) - builder = new L.U.FormBuilder(this, overlayFields, { - callback: this.initTileLayers, - callbackContext: this, - }) + builder = new L.U.FormBuilder(this, overlayFields) overlay.appendChild(builder.build()) }, @@ -1522,10 +1603,7 @@ L.U.Map.include({ { handler: 'BlurFloatInput', placeholder: L._('max East') }, ], ] - const boundsBuilder = new L.U.FormBuilder(this, boundsFields, { - callback: this.handleLimitBounds, - callbackContext: this, - }) + const boundsBuilder = new L.U.FormBuilder(this, boundsFields) limitBounds.appendChild(boundsBuilder.build()) const boundsButtons = L.DomUtil.create('div', 'button-bar half', limitBounds) L.DomUtil.createButton( @@ -1584,13 +1662,9 @@ L.U.Map.include({ { handler: 'Switch', label: L._('Autostart when map is loaded') }, ], ] - const slideshowHandler = function () { + + const slideshowBuilder = new L.U.FormBuilder(this, slideshowFields, function () { this.slideshow.setOptions(this.options.slideshow) - this.renderControls() - } - const slideshowBuilder = new L.U.FormBuilder(this, slideshowFields, { - callback: slideshowHandler, - callbackContext: this, }) slideshow.appendChild(slideshowBuilder.build()) }, @@ -1628,10 +1702,7 @@ L.U.Map.include({ { handler: 'Switch', label: L._('Permanent credits background') }, ], ] - const creditsBuilder = new L.U.FormBuilder(this, creditsFields, { - callback: this.renderControls, - callbackContext: this, - }) + const creditsBuilder = new L.U.FormBuilder(this, creditsFields) credits.appendChild(creditsBuilder.build()) }, diff --git a/umap/templates/umap/map_init.html b/umap/templates/umap/map_init.html index 61b8a6bc..0cce7aff 100644 --- a/umap/templates/umap/map_init.html +++ b/umap/templates/umap/map_init.html @@ -2,8 +2,9 @@