diff --git a/umap/static/umap/js/modules/global.js b/umap/static/umap/js/modules/global.js index 51ba37d0..43cf5afe 100644 --- a/umap/static/umap/js/modules/global.js +++ b/umap/static/umap/js/modules/global.js @@ -1,8 +1,9 @@ import * as L from '../../vendors/leaflet/leaflet-src.esm.js' import URLs from './urls.js' +import * as utils from './utils.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.umap = { URLs } +window.umap = { URLs, utils } diff --git a/umap/static/umap/js/modules/utils.js b/umap/static/umap/js/modules/utils.js new file mode 100644 index 00000000..1bfeff86 --- /dev/null +++ b/umap/static/umap/js/modules/utils.js @@ -0,0 +1,8 @@ +export function generateId() { + // A real implementation would benefit from another id type. + // Using uppercase + lowercase + digits, here's the collision risk + // For 6 chars, 1 in 100 000 + // For 5 chars, 5 in 100 000 + // for 4 chars, 500 in 100 000 + return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1) +} diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index f4bdead3..624cb312 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -1065,7 +1065,10 @@ L.U.TileLayerControl = L.Control.IconLayers.extend({ // when the tilelayer is actually added to the map (needs this._tileZoom // to be defined) // Fixme when https://github.com/Leaflet/Leaflet/pull/9201 is released - const icon = L.Util.template(layer.options.url_template, this.map.demoTileInfos) + const icon = L.Util.template( + layer.options.url_template, + this.map.demoTileInfos + ) layers.push({ title: layer.options.name, layer: layer, @@ -1431,10 +1434,12 @@ L.U.Editable = L.Editable.extend({ if (e.layer instanceof L.U.Marker) e.layer.del() }) this.on('editable:drawing:commit', function (e) { + console.log('sync: polyline ready', e.layer) e.layer.isDirty = true if (this.map.editedFeature !== e.layer) e.layer.edit(e) }) this.on('editable:editing', (e) => { + console.log('sync: polyline moved', e.layer) const layer = e.layer layer.isDirty = true if (layer._tooltip && layer.isTooltipOpen()) { @@ -1462,6 +1467,7 @@ L.U.Editable = L.Editable.extend({ }, createMarker: function (latlng) { + console.log('sync: create marker', latlng) return new L.U.Marker(this.map, latlng, this._getDefaultProperties()) }, diff --git a/umap/static/umap/js/umap.features.js b/umap/static/umap/js/umap.features.js index 8e826131..b0211866 100644 --- a/umap/static/umap/js/umap.features.js +++ b/umap/static/umap/js/umap.features.js @@ -1,3 +1,5 @@ +const generateId = window.umap.utils.generateId + L.U.FeatureMixin = { staticOptions: { mainColor: 'color' }, @@ -9,9 +11,21 @@ L.U.FeatureMixin = { // DataLayer the marker belongs to this.datalayer = options.datalayer || null this.properties = { _umap_options: {} } + let geojson_id if (options.geojson) { this.populate(options.geojson) + geojson_id = options.geojson.id } + + // Each feature needs an unique ID + if (this._checkId(geojson_id)) { + this.id = geojson_id + } else { + this.id = generateId() + } + + console.log('id', this.id) + let isDirty = false const self = this try { @@ -222,6 +236,7 @@ L.U.FeatureMixin = { }, del: function () { + console.log('sync: feature deleted', this) this.isDirty = true this.map.closePopup() if (this.datalayer) { @@ -248,6 +263,11 @@ L.U.FeatureMixin = { return [key, value] }, + // Ensures the id meets our requirements + _checkId: function (string) { + return typeof string !== 'undefined' + }, + populate: function (feature) { this.properties = Object.fromEntries( Object.entries(feature.properties || {}).map(this.cleanProperty) @@ -344,7 +364,9 @@ L.U.FeatureMixin = { toGeoJSON: function () { const geojson = this.parentClass.prototype.toGeoJSON.call(this) geojson.properties = this.cloneProperties() + geojson.id = this.id delete geojson.properties._storage_options + console.log(geojson) return geojson }, @@ -510,6 +532,7 @@ L.U.FeatureMixin = { }, clone: function () { + console.log('sync: clone feature') const layer = this.datalayer.geojsonToFeatures(this.toGeoJSON()) layer.isDirty = true layer.edit() @@ -560,6 +583,7 @@ L.U.Marker = L.Marker.extend({ this.on( 'dragend', function (e) { + console.log('sync: marker latlng updated', this._latlng) this.isDirty = true this.edit(e) }, diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 88d328e7..deead71f 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, @@ -830,7 +830,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 () { diff --git a/umap/static/umap/js/umap.layer.js b/umap/static/umap/js/umap.layer.js index 81c364c5..b5d8f05b 100644 --- a/umap/static/umap/js/umap.layer.js +++ b/umap/static/umap/js/umap.layer.js @@ -1,3 +1,5 @@ +// Rendering only + L.U.Layer = { canBrowse: true, diff --git a/umap/tests/integration/test_ids_generation.py b/umap/tests/integration/test_ids_generation.py new file mode 100644 index 00000000..1999d56c --- /dev/null +++ b/umap/tests/integration/test_ids_generation.py @@ -0,0 +1,53 @@ +import json +from pathlib import Path + +from playwright.sync_api import expect + + +def test_ids_generation(page, live_server, tilelayer): + page.goto(f"{live_server.url}/en/map/new/") + + # Click on the Draw a line button on a new map. + create_polyline = page.locator(".leaflet-control-toolbar ").get_by_title( + "Draw a polyline" + ) + create_polyline.click() + + map = page.locator("#map") + map.click(position={"x": 200, "y": 200}) + map.click(position={"x": 100, "y": 100}) + # Click again to finish + map.click(position={"x": 100, "y": 100}) + + # Click on the Draw a polygon button on a new map. + create_polygon = page.locator(".leaflet-control-toolbar ").get_by_title( + "Draw a polygon" + ) + create_polygon.click() + + map = page.locator("#map") + map.click(position={"x": 300, "y": 300}) + map.click(position={"x": 300, "y": 400}) + map.click(position={"x": 350, "y": 450}) + # Click again to finish + map.click(position={"x": 350, "y": 450}) + + download_panel = page.get_by_title("Share and download") + download_panel.click() + + button = page.get_by_role("button", name="geojson") + + with page.expect_download() as download_info: + button.click() + + download = download_info.value + + path = Path("/tmp/") / download.suggested_filename + download.save_as(path) + downloaded = json.loads(path.read_text()) + + assert "features" in downloaded + features = downloaded["features"] + assert len(features) == 2 + assert "id" in features[0] + assert "id" in features[1]