diff --git a/package.json b/package.json index d9388928..201541fd 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,8 @@ }, "homepage": "http://wiki.openstreetmap.org/wiki/UMap", "dependencies": { + "@dwayneparton/geojson-to-gpx": "^0.2.0", + "@placemarkio/tokml": "^0.3.3", "@tmcw/togeojson": "^5.8.0", "colorbrewer": "^1.5.6", "csv2geojson": "5.1.1", @@ -62,9 +64,7 @@ "leaflet.path.drag": "0.0.6", "leaflet.photon": "0.9.1", "osmtogeojson": "^3.0.0-beta.3", - "simple-statistics": "^7.8.3", - "togpx": "^0.5.4", - "tokml": "0.4.0" + "simple-statistics": "^7.8.3" }, "browserslist": [ "> 0.5%, last 2 versions, Firefox ESR, not dead, not op_mini all" diff --git a/scripts/vendorsjs.sh b/scripts/vendorsjs.sh index 163f72f7..fbcd906d 100755 --- a/scripts/vendorsjs.sh +++ b/scripts/vendorsjs.sh @@ -19,16 +19,16 @@ mkdir -p umap/static/umap/vendors/formbuilder/ && cp -r node_modules/leaflet-for mkdir -p umap/static/umap/vendors/measurable/ && cp -r node_modules/leaflet-measurable/Leaflet.Measurable.* umap/static/umap/vendors/measurable/ mkdir -p umap/static/umap/vendors/photon/ && cp -r node_modules/leaflet.photon/leaflet.photon.js umap/static/umap/vendors/photon/ mkdir -p umap/static/umap/vendors/csv2geojson/ && cp -r node_modules/csv2geojson/csv2geojson.js umap/static/umap/vendors/csv2geojson/ -mkdir -p umap/static/umap/vendors/togeojson/ && cp -r node_modules/@tmcw/togeojson/dist/togeojson.umd.* umap/static/umap/vendors/togeojson/ +mkdir -p umap/static/umap/vendors/togeojson/ && cp node_modules/@tmcw/togeojson/dist/togeojson.es.mjs umap/static/umap/vendors/togeojson/togeojson.es.js +mkdir -p umap/static/umap/vendors/tokml/ && cp node_modules/@placemarkio/tokml/dist/tokml.es.mjs umap/static/umap/vendors/tokml/tokml.es.js mkdir -p umap/static/umap/vendors/osmtogeojson/ && cp -r node_modules/osmtogeojson/osmtogeojson.js umap/static/umap/vendors/osmtogeojson/ mkdir -p umap/static/umap/vendors/georsstogeojson/ && cp -r node_modules/georsstogeojson/GeoRSSToGeoJSON.js umap/static/umap/vendors/georsstogeojson/ -mkdir -p umap/static/umap/vendors/togpx/ && cp -r node_modules/togpx/togpx.js umap/static/umap/vendors/togpx/ -mkdir -p umap/static/umap/vendors/tokml && cp -r node_modules/tokml/tokml.js umap/static/umap/vendors/tokml mkdir -p umap/static/umap/vendors/locatecontrol/ && cp -r node_modules/leaflet.locatecontrol/dist/L.Control.Locate.min.* umap/static/umap/vendors/locatecontrol/ mkdir -p umap/static/umap/vendors/dompurify/ && cp -r node_modules/dompurify/dist/purify.es.mjs umap/static/umap/vendors/dompurify/purify.es.js mkdir -p umap/static/umap/vendors/dompurify/ && cp -r node_modules/dompurify/dist/purify.es.mjs.map umap/static/umap/vendors/dompurify/purify.es.mjs.map mkdir -p umap/static/umap/vendors/colorbrewer/ && cp node_modules/colorbrewer/index.js umap/static/umap/vendors/colorbrewer/colorbrewer.js mkdir -p umap/static/umap/vendors/simple-statistics/ && cp node_modules/simple-statistics/dist/simple-statistics.min.* umap/static/umap/vendors/simple-statistics/ mkdir -p umap/static/umap/vendors/iconlayers/ && cp node_modules/leaflet-iconlayers/dist/* umap/static/umap/vendors/iconlayers/ +mkdir -p umap/static/umap/vendors/geojson-to-gpx/ && cp node_modules/@dwayneparton/geojson-to-gpx/dist/index.js umap/static/umap/vendors/geojson-to-gpx/ echo 'Done!' diff --git a/umap/static/umap/js/modules/formatter.js b/umap/static/umap/js/modules/formatter.js new file mode 100644 index 00000000..ac7ed595 --- /dev/null +++ b/umap/static/umap/js/modules/formatter.js @@ -0,0 +1,132 @@ +/* Uses globals for: csv2geojson, osmtogeojson, GeoRSSToGeoJSON (not available as ESM) */ +import { translate } from './i18n.js' + +export default class Formatter { + async fromGPX(str) { + let togeojson + await import('../../vendors/togeojson/togeojson.es.js').then((module) => { + togeojson = module + }) + return togeojson.gpx(this.toDom(str)) + } + + async fromKML(str) { + console.log(str) + let togeojson + await import('../../vendors/togeojson/togeojson.es.js').then((module) => { + togeojson = module + }) + return togeojson.kml(this.toDom(str), { + skipNullGeometry: true, + }) + } + + async fromGeoJSON(str) { + try { + return JSON.parse(str) + } catch (err) { + U.Alert.error(`Invalid JSON file: ${err}`) + } + } + + async fromOSM(str) { + let src + try { + src = JSON.parse(str) + } catch (e) { + src = this.toDom(str) + } + return osmtogeojson(src, { flatProperties: true }) + } + + fromCSV(str, callback) { + csv2geojson.csv2geojson( + str, + { + delimiter: 'auto', + includeLatLon: false, + }, + (err, result) => { + // csv2geojson fallback to null geometries when it cannot determine + // lat or lon columns. This is valid geojson, but unwanted from a user + // point of view. + if (result?.features.length) { + if (result.features[0].geometry === null) { + err = { + type: 'Error', + message: translate('Cannot determine latitude and longitude columns.'), + } + } + } + if (err) { + let message + if (err.type === 'Error') { + message = err.message + } else { + message = translate('{count} errors during import: {message}', { + count: err.length, + message: err[0].message, + }) + } + U.Alert.error(message, 10000) + console.error(err) + } + if (result?.features.length) { + callback(result) + } + } + ) + } + + async fromGeoRSS(str) { + return GeoRSSToGeoJSON(this.toDom(c)) + } + + toDom(x) { + const doc = new DOMParser().parseFromString(x, 'text/xml') + const errorNode = doc.querySelector('parsererror') + if (errorNode) { + U.Alert.error(translate('Cannot parse data')) + } + return doc + } + + async parse(str, format) { + switch (format) { + case 'csv': + return new Promise((resolve, reject) => { + return this.fromCSV(str, (data) => resolve(data)) + }) + case 'gpx': + return await this.fromGPX(str) + case 'kml': + return await this.fromKML(str) + case 'osm': + return await this.fromOSM(str) + case 'georss': + return await this.fromGeoRSS(str) + case 'geojson': + return await this.fromGeoJSON(str) + } + } + + async toGPX(geojson) { + let togpx + await import('../../vendors/geojson-to-gpx/index.js').then((module) => { + togpx = module + }) + for (const feature of geojson.features) { + feature.properties.desc = feature.properties.description + } + const gpx = togpx.default(geojson) + return new XMLSerializer().serializeToString(gpx) + } + + async toKML(geojson) { + let tokml + await import('../../vendors/tokml/tokml.es.js').then((module) => { + tokml = module + }) + return tokml.toKML(geojson) + } +} diff --git a/umap/static/umap/js/modules/global.js b/umap/static/umap/js/modules/global.js index 9e71921c..f49786e3 100644 --- a/umap/static/umap/js/modules/global.js +++ b/umap/static/umap/js/modules/global.js @@ -7,6 +7,7 @@ import { AjaxAutocomplete, AjaxAutocompleteMultiple } from './autocomplete.js' import Browser from './browser.js' import Caption from './caption.js' import Facets from './facets.js' +import Formatter from './formatter.js' import Help from './help.js' import Importer from './importer.js' import Orderable from './orderable.js' @@ -36,6 +37,7 @@ window.U = { Dialog, EditPanel, Facets, + Formatter, FullPanel, Help, HTTPError, diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index d1ef06d2..00e14ded 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -116,6 +116,8 @@ U.Map = L.Map.extend({ // Needed for actions labels this.help = new U.Help(this) + this.formatter = new U.Formatter(this) + this.initControls() // Needs locate control and hash to exist this.initCenter() diff --git a/umap/static/umap/js/umap.layer.js b/umap/static/umap/js/umap.layer.js index 857befe0..e321db5d 100644 --- a/umap/static/umap/js/umap.layer.js +++ b/umap/static/umap/js/umap.layer.js @@ -792,11 +792,9 @@ U.DataLayer = L.Evented.extend({ const response = await this.map.request.get(url) if (response?.ok) { this.clear() - this.rawToGeoJSON( - await response.text(), - this.options.remoteData.format, - (geojson) => this.fromGeoJSON(geojson) - ) + await this.map.formatter + .parse(await response.text(), this.options.remoteData.format) + .then((geojson) => this.fromGeoJSON(geojson)) } }, @@ -930,83 +928,6 @@ U.DataLayer = L.Evented.extend({ } }, - addRawData: function (c, type) { - this.rawToGeoJSON(c, type, (geojson) => this.addData(geojson)) - }, - - rawToGeoJSON: (c, type, callback) => { - const toDom = (x) => { - const doc = new DOMParser().parseFromString(x, 'text/xml') - const errorNode = doc.querySelector('parsererror') - if (errorNode) { - U.Alert.error(L._('Cannot parse data')) - } - return doc - } - - // TODO add a duck typing guessType - if (type === 'csv') { - csv2geojson.csv2geojson( - c, - { - delimiter: 'auto', - includeLatLon: false, - }, - (err, result) => { - // csv2geojson fallback to null geometries when it cannot determine - // lat or lon columns. This is valid geojson, but unwanted from a user - // point of view. - if (result?.features.length) { - if (result.features[0].geometry === null) { - err = { - type: 'Error', - message: L._('Cannot determine latitude and longitude columns.'), - } - } - } - if (err) { - let message - if (err.type === 'Error') { - message = err.message - } else { - message = L._('{count} errors during import: {message}', { - count: err.length, - message: err[0].message, - }) - } - U.Alert.error(message, 10000) - console.error(err) - } - if (result?.features.length) { - callback(result) - } - } - ) - } else if (type === 'gpx') { - callback(toGeoJSON.gpx(toDom(c))) - } else if (type === 'georss') { - callback(GeoRSSToGeoJSON(toDom(c))) - } else if (type === 'kml') { - callback(toGeoJSON.kml(toDom(c))) - } else if (type === 'osm') { - let d - try { - d = JSON.parse(c) - } catch (e) { - d = toDom(c) - } - callback(osmtogeojson(d, { flatProperties: true })) - } else if (type === 'geojson') { - try { - const gj = JSON.parse(c) - callback(gj) - } catch (err) { - U.Alert.error(`Invalid JSON file: ${err}`) - return - } - } - }, - // The choice of the name is not ours, because it is required by Leaflet. // It is misleading, as the returned objects are uMap objects, and not // GeoJSON features. @@ -1136,10 +1057,12 @@ U.DataLayer = L.Evented.extend({ return new U.Polygon(this.map, latlngs, { geojson: geojson, datalayer: this }, id) }, - importRaw: function (raw, type) { - this.addRawData(raw, type) + importRaw: async function (raw, format) { + await this.map.formatter + .parse(raw, format) + .then((geojson) => this.addData(geojson)) + .then(() => this.zoomTo()) this.isDirty = true - this.zoomTo() }, importFromFiles: function (files, type) { diff --git a/umap/static/umap/js/umap.permissions.js b/umap/static/umap/js/umap.permissions.js index 42630301..58cdd2e5 100644 --- a/umap/static/umap/js/umap.permissions.js +++ b/umap/static/umap/js/umap.permissions.js @@ -116,7 +116,7 @@ U.MapPermissions = L.Class.extend({ L._('Advanced actions') ) const advancedButtons = L.DomUtil.create('div', 'button-bar', advancedActions) - const download = L.DomUtil.createButton( + L.DomUtil.createButton( 'button', advancedButtons, L._('Attach the map to my account'), diff --git a/umap/static/umap/js/umap.share.js b/umap/static/umap/js/umap.share.js index a2ab6106..21207255 100644 --- a/umap/static/umap/js/umap.share.js +++ b/umap/static/umap/js/umap.share.js @@ -1,22 +1,22 @@ U.Share = L.Class.extend({ EXPORT_TYPES: { geojson: { - formatter: (map) => JSON.stringify(map.toGeoJSON(), null, 2), + formatter: async (map) => JSON.stringify(map.toGeoJSON(), null, 2), ext: '.geojson', filetype: 'application/json', }, gpx: { - formatter: (map) => togpx(map.toGeoJSON()), + formatter: async (map) => await map.formatter.toGPX(map.toGeoJSON()), ext: '.gpx', filetype: 'application/gpx+xml', }, kml: { - formatter: (map) => tokml(map.toGeoJSON()), + formatter: async (map) => await map.formatter.toKML(map.toGeoJSON()), ext: '.kml', filetype: 'application/vnd.google-earth.kml+xml', }, csv: { - formatter: (map) => { + formatter: async (map) => { const table = [] map.eachFeature((feature) => { const row = feature.toGeoJSON().properties @@ -156,17 +156,17 @@ U.Share = L.Class.extend({ this.map.panel.open({ content: this.container }) }, - format: function (mode) { + format: async function (mode) { const type = this.EXPORT_TYPES[mode] - const content = type.formatter(this.map) + const content = await type.formatter(this.map) let name = this.map.options.name || 'data' name = name.replace(/[^a-z0-9]/gi, '_').toLowerCase() const filename = name + type.ext return { content, filetype: type.filetype, filename } }, - download: function (mode) { - const { content, filetype, filename } = this.format(mode) + download: async function (mode) { + const { content, filetype, filename } = await this.format(mode) const blob = new Blob([content], { type: filetype }) window.URL = window.URL || window.webkitURL const el = document.createElement('a') diff --git a/umap/templates/umap/js.html b/umap/templates/umap/js.html index 4bdaeaeb..7e2c0037 100644 --- a/umap/templates/umap/js.html +++ b/umap/templates/umap/js.html @@ -19,7 +19,6 @@ - - - diff --git a/umap/tests/integration/test_export_map.py b/umap/tests/integration/test_export_map.py index 1f65d46f..9f0d0b6a 100644 --- a/umap/tests/integration/test_export_map.py +++ b/umap/tests/integration/test_export_map.py @@ -218,8 +218,7 @@ def test_gpx_export(map, live_server, bootstrap, page): download.save_as(path) assert ( path.read_text() - == """testname=test -description=Some descriptionname polyname=name polytestname=test""" + == """testSome descriptiontest""" ) @@ -235,7 +234,7 @@ def test_kml_export(map, live_server, bootstrap, page): download.save_as(path) assert ( path.read_text() - == """name polyname poly11.25,53.585984 10.151367,52.975108 12.689209,52.167194 14.084473,53.199452 12.634277,53.618579 11.25,53.585984 11.25,53.585984testSome description[object Object]testSome description-0.274658,52.57635test[object Object]test-0.571289,54.476422 0.439453,54.610255 1.724854,53.448807 4.163818,53.988395 5.306396,53.533778 6.591797,53.709714 7.042236,53.350551""" + == """\n\nname poly\n \n\n 11.25,53.585984\n10.151367,52.975108\n12.689209,52.167194\n14.084473,53.199452\n12.634277,53.618579\n11.25,53.585984\n11.25,53.585984\n\ntestSome description\n {"color":"OliveDrab"}\n -0.274658,52.57635\n\ntest\n {"fill":false,"opacity":0.6}\n -0.571289,54.476422\n0.439453,54.610255\n1.724854,53.448807\n4.163818,53.988395\n5.306396,53.533778\n6.591797,53.709714\n7.042236,53.350551""" )