From 9f096e294c124f41498537ab27a249cc5503ed78 Mon Sep 17 00:00:00 2001 From: simonpoole Date: Thu, 9 Jan 2025 13:59:43 +0100 Subject: [PATCH] Support geo Uris as coordinate input format Resolves https://github.com/MarcusWolschon/osmeditor4android/issues/2710 --- .../java/de/blau/android/GeoUrlActivity.java | 4 +- src/main/java/de/blau/android/Main.java | 12 +- .../de/blau/android/ShareOnOpenStreetMap.java | 4 +- ...oordinatesOrOLC.java => GeocodeInput.java} | 84 +++++--- .../java/de/blau/android/util/GeoUriData.java | 180 ++++++++++++++++++ .../java/de/blau/android/util/GeoUrlData.java | 160 ---------------- .../java/de/blau/android/util/GeoUriTest.java | 36 ++++ 7 files changed, 281 insertions(+), 199 deletions(-) rename src/main/java/de/blau/android/geocode/{CoordinatesOrOLC.java => GeocodeInput.java} (68%) create mode 100644 src/main/java/de/blau/android/util/GeoUriData.java delete mode 100644 src/main/java/de/blau/android/util/GeoUrlData.java create mode 100644 src/test/java/de/blau/android/util/GeoUriTest.java diff --git a/src/main/java/de/blau/android/GeoUrlActivity.java b/src/main/java/de/blau/android/GeoUrlActivity.java index 7f457a51b9..17e9662b95 100644 --- a/src/main/java/de/blau/android/GeoUrlActivity.java +++ b/src/main/java/de/blau/android/GeoUrlActivity.java @@ -2,7 +2,7 @@ import android.content.Intent; import android.net.Uri; -import de.blau.android.util.GeoUrlData; +import de.blau.android.util.GeoUriData; /** * Start vespucci with geo: URLs. see http://www.ietf.org/rfc/rfc5870.txt @@ -13,7 +13,7 @@ public class GeoUrlActivity extends UrlActivity { @Override boolean setIntentExtras(Intent intent, Uri data) { - GeoUrlData geoUrlData = GeoUrlData.parse(data.getSchemeSpecificPart()); + GeoUriData geoUrlData = GeoUriData.parse(data.getSchemeSpecificPart()); if (geoUrlData != null) { intent.putExtra(GEODATA, geoUrlData); return true; diff --git a/src/main/java/de/blau/android/Main.java b/src/main/java/de/blau/android/Main.java index 0ed5f431ce..5026bcbe61 100644 --- a/src/main/java/de/blau/android/Main.java +++ b/src/main/java/de/blau/android/Main.java @@ -127,7 +127,7 @@ import de.blau.android.filter.Filter; import de.blau.android.filter.PresetFilter; import de.blau.android.filter.TagFilter; -import de.blau.android.geocode.CoordinatesOrOLC; +import de.blau.android.geocode.GeocodeInput; import de.blau.android.geocode.Search.SearchResult; import de.blau.android.gpx.TrackPoint; import de.blau.android.imageryoffset.ImageryAlignmentActionModeCallback; @@ -184,7 +184,7 @@ import de.blau.android.util.FileUtil; import de.blau.android.util.FullScreenAppCompatActivity; import de.blau.android.util.GeoMath; -import de.blau.android.util.GeoUrlData; +import de.blau.android.util.GeoUriData; import de.blau.android.util.Geometry; import de.blau.android.util.LatLon; import de.blau.android.util.MenuUtil; @@ -345,7 +345,7 @@ public void onReceive(Context context, Intent intent) { private Queue newIntents = new LinkedList<>(); private final Object newIntentsLock = new Object(); - private GeoUrlData geoData = null; + private GeoUriData geoData = null; private RemoteControlUrlData rcData = null; private Uri contentUri = null; private String contentUriType = null; @@ -936,7 +936,7 @@ private void checkPermission(@NonNull final String permission, @NonNull final Li */ private void getIntentData() { synchronized (newIntentsLock) { - geoData = Util.getSerializableExtra(getIntent(), GeoUrlActivity.GEODATA, GeoUrlData.class); + geoData = Util.getSerializableExtra(getIntent(), GeoUrlActivity.GEODATA, GeoUriData.class); rcData = Util.getSerializableExtra(getIntent(), RemoteControlUrlActivity.RCDATA, RemoteControlUrlData.class); shortcutExtras = getIntent().getBundleExtra(Splash.SHORTCUT_EXTRAS_KEY); Uri uri = getIntent().getData(); @@ -1227,7 +1227,7 @@ protected void onPostExecute(Note result) { * * @param geoData the data from the intent */ - void processGeoIntent(@NonNull final GeoUrlData geoData) { + void processGeoIntent(@NonNull final GeoUriData geoData) { final Logic logic = App.getLogic(); final ViewBox viewBox = logic.getViewBox(); final double lon = geoData.getLon(); @@ -2139,7 +2139,7 @@ public void onError(Context context) { return true; case R.id.menu_gps_goto_coordinates: descheduleAutoLock(); - CoordinatesOrOLC.get(this, new CoordinatesOrOLC.HandleResult() { + GeocodeInput.get(this, new GeocodeInput.HandleResult() { @Override public void onSuccess(LatLon ll) { runOnUiThread(() -> { diff --git a/src/main/java/de/blau/android/ShareOnOpenStreetMap.java b/src/main/java/de/blau/android/ShareOnOpenStreetMap.java index 0884f681e1..c151eb3ed5 100644 --- a/src/main/java/de/blau/android/ShareOnOpenStreetMap.java +++ b/src/main/java/de/blau/android/ShareOnOpenStreetMap.java @@ -4,7 +4,7 @@ import android.net.Uri; import androidx.annotation.NonNull; import de.blau.android.contract.Urls; -import de.blau.android.util.GeoUrlData; +import de.blau.android.util.GeoUriData; /** * Take a geo intent and open the location on OSM @@ -13,7 +13,7 @@ public class ShareOnOpenStreetMap extends IntentDataActivity { @Override protected void process(@NonNull Uri data) { - GeoUrlData geoUrlData = GeoUrlData.parse(data.getSchemeSpecificPart()); + GeoUriData geoUrlData = GeoUriData.parse(data.getSchemeSpecificPart()); if (geoUrlData != null) { double lat = geoUrlData.getLat(); double lon = geoUrlData.getLon(); diff --git a/src/main/java/de/blau/android/geocode/CoordinatesOrOLC.java b/src/main/java/de/blau/android/geocode/GeocodeInput.java similarity index 68% rename from src/main/java/de/blau/android/geocode/CoordinatesOrOLC.java rename to src/main/java/de/blau/android/geocode/GeocodeInput.java index 5391837264..eaaa695b5f 100644 --- a/src/main/java/de/blau/android/geocode/CoordinatesOrOLC.java +++ b/src/main/java/de/blau/android/geocode/GeocodeInput.java @@ -1,5 +1,7 @@ package de.blau.android.geocode; +import static de.blau.android.contract.Constants.LOG_TAG_LEN; + import java.io.IOException; import java.text.ParseException; import java.util.List; @@ -13,28 +15,32 @@ import com.google.openlocationcode.OpenLocationCode.CodeArea; import android.content.Context; +import android.net.Uri; import android.util.Log; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatDialog; import de.blau.android.App; import de.blau.android.R; +import de.blau.android.contract.Schemes; import de.blau.android.dialogs.TextLineDialog; import de.blau.android.geocode.Search.SearchResult; import de.blau.android.osm.ViewBox; import de.blau.android.util.CoordinateParser; import de.blau.android.util.ExecutorTask; +import de.blau.android.util.GeoUriData; import de.blau.android.util.LatLon; import de.blau.android.util.NetworkStatus; /** - * Ask the user for coordinates or an OLC for example WF8Q+WF Praia, Cabo Verde + * Ask the user for input Supported are coordinates or an OLC for example WF8Q+WF Praia, Cabo Verde, or an geo: Uri * * @author simon * */ -public class CoordinatesOrOLC { +public class GeocodeInput { - protected static final String DEBUG_TAG = CoordinatesOrOLC.class.getSimpleName().substring(0, Math.min(23, CoordinatesOrOLC.class.getSimpleName().length())); + private static final int TAG_LEN = Math.min(LOG_TAG_LEN, GeocodeInput.class.getSimpleName().length()); + protected static final String DEBUG_TAG = GeocodeInput.class.getSimpleName().substring(0, TAG_LEN); private static final Pattern OLC_SHORT = Pattern.compile("^([23456789CFGHJMPQRVWX]{4,6}\\+[23456789CFGHJMPQRVWX]{2,3})\\s*(.*)$", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); @@ -92,34 +98,17 @@ protected LatLon doInBackground(String param) { return CoordinateParser.parseVerbatimCoordinates(text); } catch (ParseException pex) { try { - OpenLocationCode olc = null; - Matcher m = OLC_FULL.matcher(text); - if (m.find()) { - olc = new OpenLocationCode(m.group(1)); - } else { - m = OLC_SHORT.matcher(text); - if (m.find()) { - olc = new OpenLocationCode(m.group(1)); - final String loc = m.group(2); - if (!"".equals(loc)) { // user has supplied a location - olc = recoverLocation(context, handler, olc, loc); - } else { // relative to screen center - ViewBox box = App.getLogic().getViewBox(); - if (box != null) { - double[] c = box.getCenter(); - olc = olc.recover(c[1], c[0]); - } - } + return parseOLC(context, handler, text); + } catch (Exception e) { + try { + Uri uri = Uri.parse(text); + if (Schemes.GEO.equals(uri.getScheme())) { + return GeoUriData.parse(uri.getSchemeSpecificPart()).getLatLon(); } + } catch (Exception e2) { + Log.e(DEBUG_TAG, e.getMessage()); + handler.onError(context.getString(R.string.unparseable_coordinates)); } - if (olc == null) { - throw new IOException("Unparseable OLC " + text); - } - CodeArea ca = olc.decode(); - return new LatLon(ca.getCenterLatitude(), ca.getCenterLongitude()); - } catch (Exception e) { - Log.e(DEBUG_TAG, e.getMessage()); - handler.onError(context.getString(R.string.unparseable_coordinates)); } } return null; @@ -144,6 +133,43 @@ private static void dismiss() { } } + /** + * Parse an OLC (code) + * + * @param context an android context + * @param handler handler for errors + * @param text the input text + * @return a LatLon object + * @throws IOException on any kind of errror + */ + private static LatLon parseOLC(@NonNull final Context context, @NonNull final HandleResult handler, @NonNull String text) throws IOException { + OpenLocationCode olc = null; + Matcher m = OLC_FULL.matcher(text); + if (m.find()) { + olc = new OpenLocationCode(m.group(1)); + } else { + m = OLC_SHORT.matcher(text); + if (m.find()) { + olc = new OpenLocationCode(m.group(1)); + final String loc = m.group(2); + if (!"".equals(loc)) { // user has supplied a location + olc = recoverLocation(context, handler, olc, loc); + } else { // relative to screen center + ViewBox box = App.getLogic().getViewBox(); + if (box != null) { + double[] c = box.getCenter(); + olc = olc.recover(c[1], c[0]); + } + } + } + } + if (olc == null) { + throw new IOException("Unparseable OLC " + text); + } + CodeArea ca = olc.decode(); + return new LatLon(ca.getCenterLatitude(), ca.getCenterLongitude()); + } + /** * Recover coordinates for a location from Nominatim and update the provided OLC * diff --git a/src/main/java/de/blau/android/util/GeoUriData.java b/src/main/java/de/blau/android/util/GeoUriData.java new file mode 100644 index 0000000000..50ad411c9e --- /dev/null +++ b/src/main/java/de/blau/android/util/GeoUriData.java @@ -0,0 +1,180 @@ +package de.blau.android.util; + +import static de.blau.android.contract.Constants.LOG_TAG_LEN; + +import java.io.Serializable; +import java.util.Locale; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import de.blau.android.contract.Schemes; + +/** + * Container for data from a geo url + * + * @author simon + * + */ +public class GeoUriData implements Serializable { + + private static final long serialVersionUID = 3L; + + private static final int TAG_LEN = Math.min(LOG_TAG_LEN, GeoUriData.class.getSimpleName().length()); + private static final String DEBUG_TAG = GeoUriData.class.getSimpleName().substring(0, TAG_LEN); + + private static final String ZOOM_PARAMETER = "z"; + + private static final int MAX_LAT = 90; + + private double lat = -Double.MAX_VALUE; + private double lon = -Double.MAX_VALUE; + private int zoom = -1; + + /** + * @return the lat + */ + public double getLat() { + return lat; + } + + /** + * @return latitude in WGS*1E7 coords + */ + public int getLatE7() { + return (int) (lat * 1E7D); + } + + /** + * @param lat the lat to set + */ + public void setLat(double lat) { + this.lat = lat; + } + + /** + * @return the lon + */ + public double getLon() { + return lon; + } + + /** + * @return longitude in WGS*1E7 coords + */ + public int getLonE7() { + return (int) (lon * 1E7D); + } + + /** + * @param lon the lon to set + */ + public void setLon(double lon) { + this.lon = lon; + } + + /** + * Get the coordinates as a LatLon object + * + * @return a LatLon object + */ + public LatLon getLatLon() { + return new LatLon(lat, lon); + } + + /** + * @return the zoom + */ + public int getZoom() { + return zoom; + } + + /** + * @param zoom the zoom to set + */ + public void setZoom(int zoom) { + this.zoom = zoom; + } + + /** + * Check if we have a valid zoom value + * + * @return true if zoom is present + */ + public boolean hasZoom() { + return zoom >= 0; + } + + @Override + public String toString() { + return Schemes.GEO + ":" + lat + "," + lon + (hasZoom() ? "?z=" + zoom : ""); + } + + /** + * Get a GeoUrlData object from an Intent + * + * @param schemeSpecificPart the scheme specific part of the url + * @return a GeoUrlData object or null + */ + @Nullable + public static GeoUriData parse(@NonNull String schemeSpecificPart) { + GeoUriData geoData = new GeoUriData(); + String[] query = schemeSpecificPart.split("[\\?\\&]"); // used by osmand likely not standard conform + if (query == null || query.length == 0) { + Log.e(DEBUG_TAG, "no query found in " + schemeSpecificPart); + return null; + } + String[] params = query[0].split(";"); + if (params != null && params.length >= 1) { + String[] coords = params[0].split(","); + boolean wgs84 = true; // for now the only supported datum + if (params.length > 1) { + for (String p : params) { + if (p.toLowerCase(Locale.US).matches("crs=.*")) { + wgs84 = p.toLowerCase(Locale.US).matches("crs=wgs84"); + Log.i(DEBUG_TAG, "crs found " + p + ", is wgs84 is " + wgs84); + } + } + } + if (coords != null && coords.length >= 2 && wgs84) { + try { + double lat = Double.parseDouble(coords[0]); + double lon = Double.parseDouble(coords[1]); + if (GeoMath.coordinatesInCompatibleRange(lon, lat)) { + geoData.setLat(lat); + geoData.setLon(lon); + } + } catch (NumberFormatException e) { + Log.e(DEBUG_TAG, "Coordinates " + coords[0] + "/" + coords[1] + " not parseable"); + } + } + } + if (query.length > 1) { + for (int i = 1; i < query.length; i++) { + params = query[i].split("=", 2); + if (params.length == 2 && ZOOM_PARAMETER.equals(params[0])) { + try { + geoData.setZoom(Integer.parseInt(params[1])); + } catch (NumberFormatException e) { + Log.e(DEBUG_TAG, "Illegal zoom value " + params[1] + " trying to recover"); + try { + geoData.setZoom((int) Math.round(Double.parseDouble(params[1]))); + } catch (NumberFormatException e1) { + Log.e(DEBUG_TAG, "Illegal zoom value parsing as double failed"); + } + } + } + } + } + return geoData.isValid() ? geoData : null; + } + + /** + * Check if received both a latitude and a longitude + * + * @return true if the object contains both a latitude and a longitude + */ + boolean isValid() { + return lat >= -MAX_LAT && lat <= MAX_LAT && lon >= -GeoMath.MAX_LON && lon <= GeoMath.MAX_LON; + } +} diff --git a/src/main/java/de/blau/android/util/GeoUrlData.java b/src/main/java/de/blau/android/util/GeoUrlData.java deleted file mode 100644 index 28c33e2a4b..0000000000 --- a/src/main/java/de/blau/android/util/GeoUrlData.java +++ /dev/null @@ -1,160 +0,0 @@ -package de.blau.android.util; - -import java.io.Serializable; -import java.util.Locale; - -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** - * Container for data from a geo url - * - * @author simon - * - */ -public class GeoUrlData implements Serializable { - private static final long serialVersionUID = 3L; - - private static final String DEBUG_TAG = GeoUrlData.class.getSimpleName().substring(0, Math.min(23, GeoUrlData.class.getSimpleName().length())); - - private double lat = -Double.MAX_VALUE; - private double lon = -Double.MAX_VALUE; - private int zoom = -1; - - /** - * @return the lat - */ - public double getLat() { - return lat; - } - - /** - * @return latitude in WGS*1E7 coords - */ - public int getLatE7() { - return (int) (lat * 1E7D); - } - - /** - * @param lat the lat to set - */ - public void setLat(double lat) { - this.lat = lat; - } - - /** - * @return the lon - */ - public double getLon() { - return lon; - } - - /** - * @return longitude in WGS*1E7 coords - */ - public int getLonE7() { - return (int) (lon * 1E7D); - } - - /** - * @param lon the lon to set - */ - public void setLon(double lon) { - this.lon = lon; - } - - /** - * @return the zoom - */ - public int getZoom() { - return zoom; - } - - /** - * @param zoom the zoom to set - */ - public void setZoom(int zoom) { - this.zoom = zoom; - } - - /** - * Check if we have a valid zoom value - * - * @return true if zoom is present - */ - public boolean hasZoom() { - return zoom >= 0; - } - - @Override - public String toString() { - return "geo:" + lat + "," + lon + (hasZoom() ? "?z=" + zoom : ""); - } - - /** - * Get a GeoUrlData object from an Intent - * - * @param schemeSpecificPart the scheme specific part of the url - * @return a GeoUrlData object or null - */ - @Nullable - public static GeoUrlData parse(@NonNull String schemeSpecificPart) { - GeoUrlData geoData = new GeoUrlData(); - String[] query = schemeSpecificPart.split("[\\?\\&]"); // used by osmand likely not standard conform - if (query != null && query.length >= 1) { - String[] params = query[0].split(";"); - if (params != null && params.length >= 1) { - String[] coords = params[0].split(","); - boolean wgs84 = true; // for now the only supported datum - if (params.length > 1) { - for (String p : params) { - if (p.toLowerCase(Locale.US).matches("crs=.*")) { - wgs84 = p.toLowerCase(Locale.US).matches("crs=wgs84"); - Log.e(DEBUG_TAG, "crs found " + p + ", is wgs84 is " + wgs84); - } - } - } - if (coords != null && coords.length >= 2 && wgs84) { - try { - double lat = Double.parseDouble(coords[0]); - double lon = Double.parseDouble(coords[1]); - if (GeoMath.coordinatesInCompatibleRange(lon, lat)) { - geoData.setLat(lat); - geoData.setLon(lon); - } - } catch (NumberFormatException e) { - Log.e(DEBUG_TAG, "Coordinates " + coords[0] + "/" + coords[1] + " not parseable"); - } - } - } - if (query.length > 1) { - for (int i = 1; i < query.length; i++) { - params = query[i].split("=", 2); - if (params.length == 2 && "z".equals(params[0])) { - try { - geoData.setZoom(Integer.parseInt(params[1])); - } catch (NumberFormatException e) { - Log.e(DEBUG_TAG, "Illegal zoom value " + params[1] + " trying to recover"); - try { - geoData.setZoom((int) Math.round(Double.parseDouble(params[1]))); - } catch (NumberFormatException e1) { - Log.e(DEBUG_TAG, "Illegal zoom value parsing as double failed"); - } - } - } - } - } - } - return geoData.isValid() ? geoData : null; - } - - /** - * Check if received both a latitude and a longitude - * - * @return true if the object contains both a latitude and a longitude - */ - boolean isValid() { - return lat > -Double.MAX_VALUE && lon > -Double.MAX_VALUE; - } -} diff --git a/src/test/java/de/blau/android/util/GeoUriTest.java b/src/test/java/de/blau/android/util/GeoUriTest.java new file mode 100644 index 0000000000..d68cfba021 --- /dev/null +++ b/src/test/java/de/blau/android/util/GeoUriTest.java @@ -0,0 +1,36 @@ +package de.blau.android.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import android.net.Uri; +import androidx.test.filters.LargeTest; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 33) +@LargeTest +public class GeoUriTest { + + @Test + public void parse() { + Uri geo = Uri.parse("geo:47.220104,7.715071?z=18"); + GeoUriData data = GeoUriData.parse(geo.getSchemeSpecificPart()); + assertNotNull(data); + assertEquals(47.220104, data.getLat(), 0.000001); + assertEquals(7.715071, data.getLon(), 0.000001); + assertEquals(18, data.getZoom()); + } + + @Test + public void parseFail() { + Uri geo = Uri.parse("geo:47.220104?z=18"); + GeoUriData data = GeoUriData.parse(geo.getSchemeSpecificPart()); + assertNull(data); + } +}