diff --git a/src/org.gnome.Maps.src.gresource.xml b/src/org.gnome.Maps.src.gresource.xml index 8a6dede53c27b967a2d3d7abd111701fd37dbdb2..d9eea458fd2d1d47b755d6928bd467691070a74b 100644 --- a/src/org.gnome.Maps.src.gresource.xml +++ b/src/org.gnome.Maps.src.gresource.xml @@ -104,6 +104,7 @@ userLocationBubble.js userLocationMarker.js utils.js + urls.js wikipedia.js xmldom/dom.js xmldom/domparser.js diff --git a/src/place.js b/src/place.js index 073be2f5e5deca36dc24235f9ec30ec1ccb80c41..b0e50e9e91aa57aa4be39e522b52e532bb2452fa 100644 --- a/src/place.js +++ b/src/place.js @@ -27,6 +27,7 @@ const GObject = imports.gi.GObject; const Location = imports.location; const Overpass = imports.overpass; +const URLS = imports.urls; const Utils = imports.utils; // Matches coordinates string in 'Decimal Degrees' format @@ -41,14 +42,6 @@ const DMS_COORDINATES_REGEX = new RegExp( "i" ); -// Matches a URL pointing to an object in OpenStreetMap -const OSM_OBJECT_URL_REGEX = - new RegExp(/https?:\/\/(www\.)?openstreetmap\.org\/(node|way|relation)\/(\d+)\/?$/); - -// Matches a URL with a specifies coordinate in OpenStreetMap -const OSM_COORD_URL_REGEX = - new RegExp(/https?:\/\/(www\.)?openstreetmap\.org\/?\?(\&?mlat=(\d+(?:\.\d+)?))?(\&mlon=(\d+(?:\.\d+)?))?(\&zoom=(\d+))?(\#map=(\d+)\/(\d+(?:\.\d+)?)\/(\d+(?:\.\d+)?))?/); - var Place = GObject.registerClass( class Place extends Geocode.Place { @@ -401,71 +394,44 @@ let overpass = null; */ const Application = imports.application; -function _parseOSMObjectURL(match, callback) { - let [,, type, id] = match; - let storedPlace = Application.placeStore.existsWithOsmTypeAndId(type, id); - - if (storedPlace) { - callback(storedPlace, null); - return; - } - - if (overpass === null) - overpass = new Overpass.Overpass(); - - Application.application.mark_busy(); - overpass.fetchPlace(type, id, (place) => { - Application.application.unmark_busy(); - if (place) - callback(place, null); - else - callback(null, _("Place not found in OpenStreetMap")); - }); -} - -function _parseOSMCoordURL(match, callback) { - let [,,,mlat,,mlon,,zoom,,mapZoom,mapLat, mapLon] = match; - let lat; - let lon; - let z; - - if (mapZoom && mapLat && mapLon) { - lat = mapLat; - lon = mapLon; - z = mapZoom; - } else if (mlat && mlon) { - lat = mlat; - lon = mlon; - if (zoom) - z = zoom; - } else { - callback(null, _("OpenStreetMap URL is not valid")); - return; - } - - if (!Place.validateCoordinates(lat, lon)) { - callback(null, _("Coordinates in URL are not valid")); - return; - } - - let location = new Location.Location({ latitude: lat, longitude: lon }); - let place = z ? new Place({ location: location, initialZoom: z }) : - new Place({ location: location }); +function parseHttpURL(text, callback) { + let [type, id] = URLS.parseAsObjectURL(text); - callback(place, null); -} + if (type && id) { + let storedPlace = Application.placeStore.existsWithOsmTypeAndId(type, id); -function parseHttpURL(text, callback) { - let match = text.match(OSM_OBJECT_URL_REGEX); + if (storedPlace) { + callback(storedPlace, null); + return; + } - if (match) { - _parseOSMObjectURL(match, callback); + if (overpass === null) + overpass = new Overpass.Overpass(); + + Application.application.mark_busy(); + overpass.fetchPlace(type, id, (place) => { + Application.application.unmark_busy(); + if (place) + callback(place, null); + else + callback(null, _("Place not found in OpenStreetMap")); + }); } else { - match = text.match(OSM_COORD_URL_REGEX); - if (match) - _parseOSMCoordURL(match, callback); - else + let [lat, lon, zoom] = URLS.parseAsCoordinateURL(text); + + if (lat && lon) { + if (!Place.validateCoordinates(lat, lon)) { + callback(null, _("Coordinates in URL are not valid")); + } else { + let location = new Location.Location({ latitude: lat, longitude: lon }); + let place = zoom ? new Place({ location: location, initialZoom: zoom }) : + new Place({ location: location }); + + callback(place, null); + } + } else { callback(null, _("URL is not supported")); + } } } diff --git a/src/placeEntry.js b/src/placeEntry.js index 7be0aa046d2a2cbb868054c8f15012243bc9f1e6..732d3f0f9ed613fd1aa735fb1cd1851effba1a01 100644 --- a/src/placeEntry.js +++ b/src/placeEntry.js @@ -199,6 +199,15 @@ var PlaceEntry = GObject.registerClass({ return false; } + /** + * Returns true if two locations are equal when rounded to displayes + * coordinate precision + */ + _roundedLocEquals(locA, locB) { + return '%.5f, %.5f'.format(locA.latitude, locA.longitude) === + '%.5f, %.5f'.format(locB.latitude, locB.longitude) + } + _parse() { let parsed = false; @@ -218,7 +227,14 @@ var PlaceEntry = GObject.registerClass({ let parsedLocation = Place.Place.parseCoordinates(this.text); if (parsedLocation) { - this.place = new Place.Place({ location: parsedLocation }); + /* if the place was a parsed OSM coordinate URL, it will have + * gotten re-written as bare coordinates and trigger a search-changed, + * in this case don't re-set the place, as it will loose the zoom + * level from the URL if set + */ + if (!this.place || + !this._roundedLocEquals(parsedLocation, this.place.location)) + this.place = new Place.Place({ location: parsedLocation }); parsed = true; } diff --git a/src/urls.js b/src/urls.js new file mode 100644 index 0000000000000000000000000000000000000000..96b698f22d3c577107e760dc0eb7b7534f073b7b --- /dev/null +++ b/src/urls.js @@ -0,0 +1,104 @@ +/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */ +/* vim: set et ts=4 sw=4: */ +/* + * Copyright (c) 2020 Marcus Lundblad + * + * GNOME Maps is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * GNOME Maps is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with GNOME Maps; if not, see . + * + * Author: Marcus Lundblad + */ + +const _ = imports.gettext.gettext; + +const GLib = imports.gi.GLib; + +// Matches URLs for OpenStreetMap (for addressing objects or coordinates) +const OSM_URL_REGEX = new RegExp(/https?:\/\/(www\.)?openstreetmap\.org./); + +/** + * For URLs identifiable as pointing to a coordinate + * e.g. an openstreetmap.org URL with lat and lon query parameters, + * returns [lat, lon, optional zoom], otherwise []. + */ +function parseAsCoordinateURL(url) { + if (url.match(OSM_URL_REGEX)) { + /* it seems #map= is not handle well by parse_params(), so just remove + * the # as a work-around + */ + let uri = GLib.Uri.parse(url.replace('#map=', 'map='), GLib.UriFlags.NONE); + let params = GLib.Uri.parse_params(uri.get_query(), -1, '&', + GLib.UriParamsFlags.NONE); + + let lat = params.lat; + let lon = params.lon; + let mlat = params.mlat; + let mlon = params.mlon; + let zoom; + let map = params.map; + + if (map) { + let parts = map.split('/'); + + if (parts.length !== 3) { + return []; + } else { + zoom = parseInt(parts[0]); + lat = parseFloat(parts[1]); + lon = parseFloat(parts[2]); + } + } else if (mlat && mlon) { + lat = parseFloat(mlat); + lon = parseFloat(mlon); + + if (params.zoom) + zoom = parseInt(params.zoom); + } else if (lat && lon) { + lat = parseFloat(lat); + lon = parseFloat(lon); + + if (params.zoom) + zoom = parseInt(params.zoom); + } else { + return []; + } + + return [lat, lon, zoom]; + } else { + return []; + } +} + +/** + * For URLs addressing a specific OSM object (node, way, or relation), + * returns [type,id], otherwise []. + */ +function parseAsObjectURL(url) { + if (url.match(OSM_URL_REGEX)) { + let uri = GLib.Uri.parse(url, GLib.UriFlags.NONE); + let path = uri.get_path(); + let parts = path.split('/'); + + if (parts.length === 3 && parts[0] === '' && + (parts[1] === 'node' || + parts[1] === 'way' || + parts[1] === 'relation')) { + let id = parseInt(parts[2]); + + if (id > 0) + return [parts[1], id]; + } + } + + return []; +} diff --git a/tests/meson.build b/tests/meson.build index 86475d0989d9f5726ecec8095f68052a4acde729..551ade73ef34b901a160dc1ac96bbc0a919ee689 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -1,4 +1,4 @@ -tests = ['addressTest', 'colorTest', 'osmNamesTest', 'utilsTest'] +tests = ['addressTest', 'colorTest', 'osmNamesTest', 'utilsTest', 'urlsTest'] foreach test : tests script_conf = configuration_data() diff --git a/tests/urlsTest.js b/tests/urlsTest.js new file mode 100644 index 0000000000000000000000000000000000000000..b276df98c8696eb94af078174ba5ec0108a8045e --- /dev/null +++ b/tests/urlsTest.js @@ -0,0 +1,68 @@ +/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */ +/* vim: set et ts=4 sw=4: */ +/* + * Copyright (c) 2020 Marcus Lundblad + * + * GNOME Maps is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * GNOME Maps is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with GNOME Maps; if not, see . + * + * Author: Marcus Lundblad + */ + +const JsUnit = imports.jsUnit; + +const URLS = imports.urls; + +const OSM_COORD_URL1 = + 'https://www.openstreetmap.org/?lat=39.9882&lon=-78.2409&zoom=14&layers=B000FTF'; + +function main() { + parseAsObjectURLTest(); + parseAsCoordinateURLTest(); +} + +function parseAsObjectURLTest() { + _assertArrayEquals([], URLS.parseAsObjectURL('https://www.example.com')); + _assertArrayEquals([], URLS.parseAsObjectURL('https://www.openstreet.org/')); + _assertArrayEquals(['node', 1], + URLS.parseAsObjectURL('https://www.openstreetmap.org/node/1')); + _assertArrayEquals(['way', 2], + URLS.parseAsObjectURL('https://www.openstreetmap.org/way/2')); + _assertArrayEquals(['relation', 3], + URLS.parseAsObjectURL('https://www.openstreetmap.org/relation/3')); + _assertArrayEquals([], + URLS.parseAsObjectURL('https://www.openstreetmap.org/foo/1')); + _assertArrayEquals(['node', 4], + URLS.parseAsObjectURL('https://openstreetmap.org/node/4')); + _assertArrayEquals(['node', 5], + URLS.parseAsObjectURL('http://www.openstreetmap.org/node/5')); +} + +function parseAsCoordinateURLTest() { + _assertArrayEquals([], + URLS.parseAsCoordinateURL('https://www.example.com')); + _assertArrayEquals([], + URLS.parseAsCoordinateURL('https://www.openstreet.org/')); + _assertArrayEquals([39.9882, -78.2409, 14], + URLS.parseAsCoordinateURL('https://www.openstreetmap.org/?lat=39.9882&lon=-78.2409&zoom=14&layers=B000FTF')); + _assertArrayEquals([59.40538, 17.34894, 12], + URLS.parseAsCoordinateURL('https://www.openstreetmap.org/?#map=12/59.40538/17.34894')); +} + +function _assertArrayEquals(arr1, arr2) { + JsUnit.assertEquals(arr1.length, arr2.length); + for (let i = 0; i < arr1.length; i++) { + JsUnit.assertEquals(arr1[i], arr2[i]); + } +} +