diff --git a/package.json b/package.json index e9c953104..049d989ab 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@placemarkio/tokml": "0.3.4", "@tmcw/togeojson": "^5.8.0", "colorbrewer": "1.5.7", - "csv2geojson": "5.1.2", + "csv2geojson": "github:umap-project/csv2geojson#patched", "dompurify": "3.1.7", "georsstogeojson": "^0.2.0", "jsdom": "^24.0.0", diff --git a/umap/static/umap/js/modules/formatter.js b/umap/static/umap/js/modules/formatter.js index c641da2f2..6a03b6b5e 100644 --- a/umap/static/umap/js/modules/formatter.js +++ b/umap/static/umap/js/modules/formatter.js @@ -81,6 +81,8 @@ export class Formatter { { delimiter: 'auto', includeLatLon: false, + sexagesimal: false, + parseLatLon: (raw) => Number.parseFloat(raw.toString().replace(',', '.')), }, (err, result) => { // csv2geojson fallback to null geometries when it cannot determine @@ -115,7 +117,9 @@ export class Formatter { } async fromGeoRSS(str) { - const GeoRSSToGeoJSON = await import('../../vendors/georsstogeojson/GeoRSSToGeoJSON.js') + const GeoRSSToGeoJSON = await import( + '../../vendors/georsstogeojson/GeoRSSToGeoJSON.js' + ) return GeoRSSToGeoJSON.parse(this.toDom(str)) } diff --git a/umap/static/umap/vendors/csv2geojson/csv2geojson.js b/umap/static/umap/vendors/csv2geojson/csv2geojson.js index 09a73e5ff..260312f52 100644 --- a/umap/static/umap/vendors/csv2geojson/csv2geojson.js +++ b/umap/static/umap/vendors/csv2geojson/csv2geojson.js @@ -81,6 +81,8 @@ function csv2geojson(x, options, callback) { } options.delimiter = options.delimiter || ','; + options.parseLatLon = options.parseLatLon || parseFloat; + options.sexagesimal = options.sexagesimal !== false; var latfield = options.latfield || '', lonfield = options.lonfield || '', @@ -129,6 +131,7 @@ function csv2geojson(x, options, callback) { if (!latfield) latfield = guessLatHeader(parsed[0]); if (!lonfield) lonfield = guessLonHeader(parsed[0]); + var noGeometry = (!latfield || !lonfield); if (noGeometry) { @@ -152,13 +155,15 @@ function csv2geojson(x, options, callback) { lonf, latf, a; - a = sexagesimal(lonk, 'EW'); - if (a) lonk = a; - a = sexagesimal(latk, 'NS'); - if (a) latk = a; + if (options.sexagesimal) { + a = sexagesimal(lonk, 'EW'); + if (a) lonk = a; + a = sexagesimal(latk, 'NS'); + if (a) latk = a; + } - lonf = parseFloat(lonk); - latf = parseFloat(latk); + lonf = options.parseLatLon(lonk); + latf = options.parseLatLon(latk); if (isNaN(lonf) || isNaN(latf)) { @@ -179,8 +184,8 @@ function csv2geojson(x, options, callback) { geometry: { type: 'Point', coordinates: [ - parseFloat(lonf), - parseFloat(latf) + lonf, + latf ] } }); diff --git a/umap/tests/integration/test_import.py b/umap/tests/integration/test_import.py index 4752eee72..67460f0a0 100644 --- a/umap/tests/integration/test_import.py +++ b/umap/tests/integration/test_import.py @@ -494,6 +494,39 @@ def test_import_csv_without_valid_latlon_headers(tilelayer, live_server, page): expect(page.locator('umap-alert div[data-level="error"]')).to_be_visible() +def test_import_csv_with_commas_in_latlon(tilelayer, live_server, page, settings): + settings.UMAP_ALLOW_ANONYMOUS = True + page.goto(f"{live_server.url}/map/new/") + page.get_by_title("Open browser").click() + layers = page.locator(".umap-browser .datalayer") + markers = page.locator(".leaflet-marker-icon") + page.get_by_title("Import data").click() + textarea = page.locator(".umap-upload textarea") + textarea.fill("lat;lon;foobar\n12,24;48,34;mypoint\n12,23;48,35;mypoint2") + page.locator('select[name="format"]').select_option("csv") + page.get_by_role("button", name="Import data", exact=True).click() + expect(layers).to_have_count(1) + expect(markers).to_have_count(2) + with page.expect_response(re.compile(r".*/datalayer/create/.*")): + page.get_by_role("button", name="Save").click() + datalayer = DataLayer.objects.last() + saved_data = json.loads(Path(datalayer.geojson.path).read_text()) + assert saved_data["features"][0]["geometry"] == { + "coordinates": [ + 48.35, + 12.23, + ], + "type": "Point", + } + assert saved_data["features"][1]["geometry"] == { + "coordinates": [ + 48.34, + 12.24, + ], + "type": "Point", + } + + def test_create_remote_data(page, live_server, tilelayer): def handle(route): route.fulfill(