diff --git a/umap/static/umap/js/modules/global.js b/umap/static/umap/js/modules/global.js index c7fce53f..2d579187 100644 --- a/umap/static/umap/js/modules/global.js +++ b/umap/static/umap/js/modules/global.js @@ -1,10 +1,21 @@ import * as L from '../../vendors/leaflet/leaflet-src.esm.js' import URLs from './urls.js' import Browser from './browser.js' +import * as Utils from './utils.js' import { Request, ServerRequest, RequestError, HTTPError, NOKError } from './request.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.U = { URLs, Request, ServerRequest, RequestError, HTTPError, NOKError, Browser } +window.U = { + URLs, + Request, + ServerRequest, + RequestError, + HTTPError, + NOKError, + Browser, + 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..a5116616 --- /dev/null +++ b/umap/static/umap/js/modules/utils.js @@ -0,0 +1,24 @@ +/** + * Generate a pseudo-unique identifier (5 chars long, mixed-case alphanumeric) + * + * 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 + * + * @returns string + */ +export function generateId() { + return btoa(Math.random().toString()).substring(10, 15) +} + +/** + * Ensure the ID matches the expected format. + * + * @param {string} string + * @returns {boolean} + */ +export function checkId(string) { + if (typeof string !== 'string') return false + return /^[A-Za-z0-9]{5}$/.test(string) +} diff --git a/umap/static/umap/js/umap.features.js b/umap/static/umap/js/umap.features.js index fecbba57..57caecce 100644 --- a/umap/static/umap/js/umap.features.js +++ b/umap/static/umap/js/umap.features.js @@ -9,8 +9,17 @@ 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 identifier + if (U.Utils.checkId(geojson_id)) { + this.id = geojson_id + } else { + this.id = U.Utils.generateId() } let isDirty = false const self = this @@ -344,6 +353,7 @@ 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 return geojson }, diff --git a/umap/tests/integration/test_features_id_generation.py b/umap/tests/integration/test_features_id_generation.py new file mode 100644 index 00000000..ca4558a5 --- /dev/null +++ b/umap/tests/integration/test_features_id_generation.py @@ -0,0 +1,51 @@ +import json +from pathlib import Path + + +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]