diff --git a/src/androidTest/java/de/blau/android/gpx/GpxTest.java b/src/androidTest/java/de/blau/android/gpx/GpxTest.java index 51ac63d146..bf41305c9f 100644 --- a/src/androidTest/java/de/blau/android/gpx/GpxTest.java +++ b/src/androidTest/java/de/blau/android/gpx/GpxTest.java @@ -26,6 +26,7 @@ import android.location.Location; import android.location.LocationManager; import androidx.annotation.NonNull; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; import androidx.test.platform.app.InstrumentationRegistry; @@ -343,6 +344,55 @@ public void importWayPoints() { fail(ex.getMessage()); } } + + /** + * Import a track file with waypoints with links + */ + // @SdkSuppress(minSdkVersion = 26) + @Test + public void importWayPointsWithLinks() { + try { + File zippedGpxFile = JavaResources.copyFileFromResources(ApplicationProvider.getApplicationContext(), "2011-06-08_13-21-55 OT.zip", null, "/"); + assertTrue(FileUtil.unpackZip(FileUtil.getPublicDirectory(FileUtil.getPublicDirectory(), "/").getAbsolutePath() + "/", zippedGpxFile.getName())); + assertTrue(TestUtils.clickResource(device, true, device.getCurrentPackageName() + ":id/layers", true)); + assertTrue(TestUtils.clickButton(device, device.getCurrentPackageName() + ":id/add", true)); + assertTrue(TestUtils.clickText(device, false, main.getString(R.string.layer_add_gpx), true, false)); + TestUtils.selectFile(device, main, "2011-06-08_13-21-55 OT", "2011-06-08_13-21-55.gpx", true); + TestUtils.textGone(device, "Imported", 10000); + assertTrue(TestUtils.clickText(device, false, main.getString(R.string.okay), true, false)); + assertTrue(TestUtils.clickText(device, false, main.getString(R.string.Done), true, false)); + MapViewLayer foundLayer = null; + for (MapViewLayer layer : main.getMap().getLayers()) { + if (layer instanceof de.blau.android.layer.gpx.MapOverlay && "2011-06-08_13-21-55.gpx".equals(layer.getName())) { + foundLayer = layer; + break; + } + } + assertNotNull(foundLayer); + + Track track = ((de.blau.android.layer.gpx.MapOverlay)foundLayer).getTrack(); + assertEquals(3, track.getWayPoints().size()); + WayPoint wp = track.getWayPoints().get(0); + + Map map = main.getMap(); + ViewBox viewBox = map.getViewBox(); + App.getLogic().setZoom(map, 19); + viewBox.moveTo(map, wp.getLon(), wp.getLat()); // NOSONAR + map.invalidate(); + + TestUtils.unlock(device); + + TestUtils.clickAtCoordinates(device, map, wp.getLon(), wp.getLat(), true); + assertTrue(TestUtils.findText(device, false, "2011-06-08_13-22-47.3gpp", 1000, true)); + assertTrue(TestUtils.clickText(device, false, "2011-06-08_13-22-47.3gpp", true, false)); + // unblear what an assertion should look like here + TestUtils.sleep(10000); + device.pressBack(); + } catch (Exception ex) { + fail(ex.getMessage()); + } + } + /** * Replay a track and pretend the output are network generated locations diff --git a/src/main/java/de/blau/android/dialogs/TableLayoutUtils.java b/src/main/java/de/blau/android/dialogs/TableLayoutUtils.java index 7d793f9b48..f43315e7f8 100644 --- a/src/main/java/de/blau/android/dialogs/TableLayoutUtils.java +++ b/src/main/java/de/blau/android/dialogs/TableLayoutUtils.java @@ -333,6 +333,23 @@ public static TableRow createRow(@NonNull Context context, int cell1, @Nullable @SuppressLint("NewApi") @NonNull public static TableRow createRow(@NonNull Context context, int cell1, @Nullable CharSequence cell2, boolean isUrl, @NonNull TableLayout.LayoutParams tp) { + return createRow(context, context.getString(cell1), cell2, isUrl, null, tp); + } + + /** + * Get a new TableRow with the provided contents - two columns + * + * @param context an Android Context + * @param cell1 a string for the first cell + * @param cell2 text for the second cell + * @param isUrl if true don't allow C&P on the values so that they can be clicked on + * @param tp LayoutParams for the row + * @return a TableRow + */ + @SuppressLint("NewApi") + @NonNull + public static TableRow createRow(@NonNull Context context, @NonNull CharSequence cell1, @Nullable CharSequence cell2, boolean isUrl, + @Nullable View.OnClickListener listener, @NonNull TableLayout.LayoutParams tp) { TableRow tr = new TableRow(context); TextView cell = new TextView(context); cell.setMinEms(FIRST_CELL_WIDTH); @@ -343,13 +360,20 @@ public static TableRow createRow(@NonNull Context context, int cell1, @Nullable cell.setTypeface(null, Typeface.BOLD); } cell.setEllipsize(TruncateAt.MARQUEE); - cell.setTextIsSelectable(true); tr.addView(cell); TableRow.LayoutParams trp = new TableRow.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); if (cell2 == null) { trp.span = 2; } - addCell(context, cell2, isUrl, tr, trp); + + if (listener != null) { + SpannableString span = new SpannableString(cell2); + ThemeUtils.setSpanColor(context, span, android.R.attr.textColorLink, R.color.ccc_blue); + addCell(context, span, isUrl, tr, trp).setOnClickListener(listener); + tr.setOnClickListener(listener); + } else { + addCell(context, cell2, isUrl, tr, trp); + } tr.setLayoutParams(tp); return tr; } @@ -365,7 +389,8 @@ public static TableRow createRow(@NonNull Context context, int cell1, @Nullable * @return the TextView added */ @NonNull - private static TextView addCell(@NonNull Context context, @Nullable CharSequence cellText, boolean isUrl, TableRow tr, @Nullable TableRow.LayoutParams tp) { + private static TextView addCell(@NonNull Context context, @Nullable CharSequence cellText, boolean isUrl, @NonNull TableRow tr, + @Nullable TableRow.LayoutParams tp) { TextView cell = new TextView(context); if (cellText != null) { cell.setText(cellText); diff --git a/src/main/java/de/blau/android/dialogs/ViewWayPoint.java b/src/main/java/de/blau/android/dialogs/ViewWayPoint.java index a335ba5a20..d53f2b40b3 100644 --- a/src/main/java/de/blau/android/dialogs/ViewWayPoint.java +++ b/src/main/java/de/blau/android/dialogs/ViewWayPoint.java @@ -1,17 +1,25 @@ package de.blau.android.dialogs; +import static de.blau.android.contract.Constants.LOG_TAG_LEN; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.media.MediaScannerConnection; +import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; import android.widget.ScrollView; import android.widget.TableLayout; +import android.widget.TableRow; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; @@ -33,6 +41,7 @@ import de.blau.android.presets.PresetElement; import de.blau.android.presets.PresetElementPath; import de.blau.android.presets.PresetGroup; +import de.blau.android.util.ContentResolverUtil; import de.blau.android.util.DateFormatter; import de.blau.android.util.ImmersiveDialogFragment; import de.blau.android.util.ScreenMessage; @@ -47,25 +56,29 @@ */ public class ViewWayPoint extends ImmersiveDialogFragment { - private static final String WAYPOINT = "waypoint"; - - private static final String DEBUG_TAG = ViewWayPoint.class.getSimpleName().substring(0, Math.min(23, ViewWayPoint.class.getSimpleName().length())); + private static final int TAG_LEN = Math.min(LOG_TAG_LEN, ViewWayPoint.class.getSimpleName().length()); + private static final String DEBUG_TAG = ViewWayPoint.class.getSimpleName().substring(0, TAG_LEN); private static final String TAG = "fragment_view_waypoint"; + private static final String URI_KEY = "uri"; + private static final String WAYPOINT_KEY = "waypoint"; + private WayPoint wp = null; + private String uriString; /** * Show dialog for a WayPoint * * @param activity the calling activity + * @param uriString String version of uri of the enclosing file * @param wp the WayPoint */ - public static void showDialog(FragmentActivity activity, WayPoint wp) { + public static void showDialog(@NonNull FragmentActivity activity, @NonNull String uriString, @NonNull WayPoint wp) { dismissDialog(activity); try { FragmentManager fm = activity.getSupportFragmentManager(); - ViewWayPoint elementInfoFragment = newInstance(wp); + ViewWayPoint elementInfoFragment = newInstance(uriString, wp); elementInfoFragment.show(fm, TAG); } catch (IllegalStateException isex) { Log.e(DEBUG_TAG, "showDialog", isex); @@ -77,21 +90,24 @@ public static void showDialog(FragmentActivity activity, WayPoint wp) { * * @param activity the calling activity */ - private static void dismissDialog(FragmentActivity activity) { + private static void dismissDialog(@NonNull FragmentActivity activity) { de.blau.android.dialogs.Util.dismissDialog(activity, TAG); } /** * Create a new instance of this dialog * + * @param uriString String version of uri of the enclosing file * @param wp the WayPoint + * * @return the FragmentDialog instance */ - private static ViewWayPoint newInstance(WayPoint wp) { + private static ViewWayPoint newInstance(@NonNull String uriString, @NonNull WayPoint wp) { ViewWayPoint f = new ViewWayPoint(); Bundle args = new Bundle(); - args.putSerializable(WAYPOINT, wp); + args.putString(URI_KEY, uriString); + args.putSerializable(WAYPOINT_KEY, wp); f.setArguments(args); f.setShowsDialog(true); @@ -111,9 +127,11 @@ public void onCreate(@Nullable Bundle savedInstanceState) { public AppCompatDialog onCreateDialog(Bundle savedInstanceState) { if (savedInstanceState != null) { Log.d(DEBUG_TAG, "restoring from saved state"); - wp = de.blau.android.util.Util.getSerializeable(savedInstanceState, WAYPOINT, WayPoint.class); + uriString = savedInstanceState.getString(URI_KEY); + wp = de.blau.android.util.Util.getSerializeable(savedInstanceState, WAYPOINT_KEY, WayPoint.class); } else { - wp = de.blau.android.util.Util.getSerializeable(getArguments(), WAYPOINT, WayPoint.class); + uriString = getArguments().getString(URI_KEY); + wp = de.blau.android.util.Util.getSerializeable(getArguments(), WAYPOINT_KEY, WayPoint.class); } FragmentActivity activity = getActivity(); @@ -123,35 +141,50 @@ public AppCompatDialog onCreateDialog(Bundle savedInstanceState) { final LayoutInflater inflater = ThemeUtils.getLayoutInflater(activity); ScrollView sv = (ScrollView) inflater.inflate(R.layout.element_info_view, null, false); + if (wp == null) { + Log.e(DEBUG_TAG, "Null WayPoint"); + return builder.create(); + } TableLayout tl = (TableLayout) sv.findViewById(R.id.element_info_vertical_layout); TableLayout.LayoutParams tp = new TableLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); tp.setMargins(10, 2, 10, 2); - if (wp != null) { - tl.setColumnShrinkable(1, true); - if (wp.getName() != null) { - tl.addView(TableLayoutUtils.createRow(activity, R.string.name, wp.getName(), tp)); - } - if (wp.getDescription() != null) { - tl.addView(TableLayoutUtils.createRow(activity, R.string.description, wp.getDescription(), tp)); - } - if (wp.getType() != null) { - tl.addView(TableLayoutUtils.createRow(activity, R.string.type, wp.getType(), tp)); - } - long timestamp = wp.getTime(); - if (timestamp > 0) { - tl.addView( - TableLayoutUtils.createRow(activity, R.string.created, DateFormatter.getUtcFormat(OsmParser.TIMESTAMP_FORMAT).format(timestamp), tp)); - } + tl.setColumnShrinkable(1, true); + if (wp.getName() != null) { + tl.addView(TableLayoutUtils.createRow(activity, R.string.name, wp.getName(), tp)); + } + if (wp.getDescription() != null) { + tl.addView(TableLayoutUtils.createRow(activity, R.string.description, wp.getDescription(), tp)); + } + if (wp.getType() != null) { + tl.addView(TableLayoutUtils.createRow(activity, R.string.type, wp.getType(), tp)); + } + long timestamp = wp.getTime(); + if (timestamp > 0) { + tl.addView(TableLayoutUtils.createRow(activity, R.string.created, DateFormatter.getUtcFormat(OsmParser.TIMESTAMP_FORMAT).format(timestamp), tp)); + } - tl.addView(TableLayoutUtils.createRow(activity, R.string.location_lon_label, String.format(Locale.US, "%.7f", wp.getLongitude()) + "°", tp)); - tl.addView(TableLayoutUtils.createRow(activity, R.string.location_lat_label, String.format(Locale.US, "%.7f", wp.getLatitude()) + "°", tp)); + tl.addView(TableLayoutUtils.createRow(activity, R.string.location_lon_label, String.format(Locale.US, "%.7f", wp.getLongitude()) + "°", tp)); + tl.addView(TableLayoutUtils.createRow(activity, R.string.location_lat_label, String.format(Locale.US, "%.7f", wp.getLatitude()) + "°", tp)); - if (wp.hasAltitude()) { - tl.addView(TableLayoutUtils.createRow(activity, R.string.altitude, String.format(Locale.US, "%.0f", wp.getAltitude()) + "m", tp)); + if (wp.hasAltitude()) { + tl.addView(TableLayoutUtils.createRow(activity, R.string.altitude, String.format(Locale.US, "%.0f", wp.getAltitude()) + "m", tp)); + } + Uri gpxUri = Uri.parse(uriString); + List links = wp.getLinks(); + if (de.blau.android.util.Util.notEmpty(links)) { + for (WayPoint.Link link : links) { + final String description = link.getDescription(); + TableRow row = TableLayoutUtils.createRow(activity, getString(R.string.waypoint_link), + de.blau.android.util.Util + .notEmpty(description) ? description : link.getUrl(), false, + (View v) -> playLinkUri(activity, gpxUri, link), tp); + tl.addView(row); + row.requestFocus(); } } + builder.setView(sv); builder.setTitle(R.string.waypoint_title); builder.setPositiveButton(R.string.create_osm_object, (dialog, which) -> createObjectFromWayPoint(wp, false)); @@ -161,6 +194,42 @@ public AppCompatDialog onCreateDialog(Bundle savedInstanceState) { return builder.create(); } + /** + * Attempt to play/view whatever is linked to in the Link + * + * This uses a hack to find the content Uri for the file which is dubious + * + * @param context an Android Context + * @param gpxUri the URI for the GPX file + * @param link the Link Element + */ + private void playLinkUri(@NonNull Context context, @NonNull Uri gpxUri, @NonNull WayPoint.Link link) { + Uri uri = Uri.parse(link.getUrl()); + if (uri.getScheme() != null) { + context.startActivity(new Intent(Intent.ACTION_VIEW).setData(uri)); + return; + } + Uri actualUri = Uri.parse(ContentResolverUtil.getPath(context, gpxUri)); + Uri.Builder uriBuilder = new Uri.Builder(); + List pathSegments = actualUri.getPathSegments(); + for (String segment : pathSegments.subList(0, pathSegments.size() - 1)) { + uriBuilder.appendPath(segment); + } + for (String segment : uri.getPathSegments()) { + uriBuilder.appendPath(segment); + } + uri = uriBuilder.build(); + // the following is a hack suggested in + // https://stackoverflow.com/questions/7305504/convert-file-uri-to-content-uri + MediaScannerConnection.scanFile(getContext(), new String[] { uri.getPath() }, null, (String s, Uri scanUri) -> { + if (scanUri == null) { + ScreenMessage.barError(getActivity(), getString(R.string.toast_file_not_found, s)); + return; + } + context.startActivity(new Intent(Intent.ACTION_VIEW).setData(scanUri)); + }); + } + /** * Create a Node from information in and the position of a way point * @@ -199,6 +268,7 @@ private void createObjectFromWayPoint(final WayPoint wp, final boolean useSearch @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - outState.putSerializable(WAYPOINT, wp); + outState.putString(URI_KEY, uriString); + outState.putSerializable(WAYPOINT_KEY, wp); } } diff --git a/src/main/java/de/blau/android/gpx/Track.java b/src/main/java/de/blau/android/gpx/Track.java index b8d97ec9be..709d519227 100755 --- a/src/main/java/de/blau/android/gpx/Track.java +++ b/src/main/java/de/blau/android/gpx/Track.java @@ -1,5 +1,7 @@ package de.blau.android.gpx; +import static de.blau.android.contract.Constants.LOG_TAG_LEN; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; @@ -39,6 +41,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import de.blau.android.contract.FileExtensions; +import de.blau.android.gpx.WayPoint.Link; import de.blau.android.osm.OsmXml; import de.blau.android.util.ExecutorTask; import de.blau.android.util.SavingHelper; @@ -51,7 +54,8 @@ */ public class Track extends DefaultHandler implements GpxTimeFormater, Exportable { - private static final String DEBUG_TAG = Track.class.getSimpleName().substring(0, Math.min(23, Track.class.getSimpleName().length())); + private static final int TAG_LEN = Math.min(LOG_TAG_LEN, Track.class.getSimpleName().length()); + private static final String DEBUG_TAG = Track.class.getSimpleName().substring(0, TAG_LEN); private static final String TRKSEG_ELEMENT = "trkseg"; private static final String TRK_ELEMENT = "trk"; @@ -528,6 +532,7 @@ public void importFromGPX(InputStream is) { */ private void start(final InputStream in) throws SAXException, IOException, ParserConfigurationException { SAXParserFactory factory = SAXParserFactory.newInstance(); // NOSONAR + factory.setNamespaceAware(true); SAXParser saxParser = factory.newSAXParser(); saxParser.parse(in, this); } @@ -535,18 +540,20 @@ private void start(final InputStream in) throws SAXException, IOException, Parse /** * minimalistic GPX file parser */ - private boolean newSegment = false; - private double parsedLat; - private double parsedLon; - private double parsedEle = Double.NaN; - private long parsedTime = 0L; - private String parsedName = null; - private String parsedDescription = null; - private String parsedType = null; - private String parsedSymbol = null; + private boolean newSegment = false; + private double parsedLat; + private double parsedLon; + private double parsedEle = Double.NaN; + private long parsedTime = 0L; + private String parsedName = null; + private String parsedDescription = null; + private String parsedType = null; + private String parsedSymbol = null; + private Link parsedLink = null; + private List parsedLinks = new ArrayList<>(); private enum State { - NONE, TIME, ELE, NAME, DESC, TYPE, SYM + NONE, TIME, ELE, NAME, DESC, TYPE, SYM, LINK, TRACK, WAYPOINT, WAYPOINT_LINK, LINK_TEXT } private State state = State.NONE; @@ -561,6 +568,7 @@ public void startElement(final String uri, final String element, final String qN break; case TRK_ELEMENT: Log.d(DEBUG_TAG, "parsing trk"); + state = State.TRACK; break; case TRKSEG_ELEMENT: Log.d(DEBUG_TAG, "parsing trkseg"); @@ -570,6 +578,10 @@ public void startElement(final String uri, final String element, final String qN case WayPoint.WPT_ELEMENT: parsedLat = Double.parseDouble(atts.getValue(TrackPoint.LAT_ATTR)); parsedLon = Double.parseDouble(atts.getValue(TrackPoint.LON_ATTR)); + if (WayPoint.WPT_ELEMENT.equals(element)) { + state = State.WAYPOINT; + parsedLinks.clear(); + } break; case TrackPoint.TIME_ELEMENT: state = State.TIME; @@ -589,6 +601,20 @@ public void startElement(final String uri, final String element, final String qN case WayPoint.SYM_ELEMENT: state = State.SYM; break; + case WayPoint.Link.TEXT_ELEMENT: + if (state != State.WAYPOINT_LINK) { + break; + } + state = State.LINK_TEXT; + break; + case WayPoint.LINK_ELEMENT: + if (state != State.WAYPOINT) { + break; + } + state = State.WAYPOINT_LINK; + parsedLink = new WayPoint.Link(); + parsedLink.setUrl(atts.getValue(WayPoint.Link.HREF_ATTR)); + break; default: } } catch (Exception e) { @@ -598,27 +624,33 @@ public void startElement(final String uri, final String element, final String qN @Override public void characters(char[] ch, int start, int length) { + final String string = new String(ch, start, length); switch (state) { case NONE: return; case ELE: - parsedEle = Double.parseDouble(new String(ch, start, length)); + parsedEle = Double.parseDouble(string); return; case TIME: try { - parsedTime = parseTime(new String(ch, start, length)); + parsedTime = parseTime(string); } catch (ParseException e) { parsedTime = 0L; } return; case NAME: - parsedName = new String(ch, start, length); + parsedName = string; return; case DESC: - parsedDescription = new String(ch, start, length); + parsedDescription = string; return; case TYPE: - parsedType = new String(ch, start, length); + parsedType = string; + return; + case LINK_TEXT: + if (parsedLink != null) { + parsedLink.setDescription(string); + } return; default: break; @@ -644,6 +676,7 @@ public void endElement(final String uri, final String element, final String qNam case GPX_ELEMENT: break; case TRK_ELEMENT: + state = State.NONE; break; case TRKSEG_ELEMENT: break; @@ -652,25 +685,42 @@ public void endElement(final String uri, final String element, final String qNam newSegment = false; parsedEle = Double.NaN; parsedTime = 0L; - state = State.NONE; + state = State.TRACK; break; case WayPoint.WPT_ELEMENT: - currentWayPoints.add(new WayPoint(parsedLat, parsedLon, parsedEle, parsedTime, parsedName, parsedDescription, parsedType, parsedSymbol)); + WayPoint wpt = new WayPoint(parsedLat, parsedLon, parsedEle, parsedTime); + wpt.setName(parsedName); + wpt.setDescription(parsedDescription); + wpt.setType(parsedType); + wpt.setSymbol(parsedSymbol); + if (!parsedLinks.isEmpty()) { + wpt.setLinks(new ArrayList<>(parsedLinks)); + } + currentWayPoints.add(wpt); + parsedEle = Double.NaN; parsedTime = 0L; parsedName = null; parsedDescription = null; parsedType = null; parsedSymbol = null; + parsedLinks.clear(); state = State.NONE; break; + case WayPoint.Link.TEXT_ELEMENT: + state = State.WAYPOINT_LINK; + break; + case WayPoint.LINK_ELEMENT: + parsedLinks.add(parsedLink); + state = State.WAYPOINT; + break; case TrackPoint.TIME_ELEMENT: case TrackPoint.ELE_ELEMENT: case WayPoint.NAME_ELEMENT: case WayPoint.DESC_ELEMENT: case WayPoint.TYPE_ELEMENT: case WayPoint.SYM_ELEMENT: - state = State.NONE; + state = State.WAYPOINT; break; default: state = State.NONE; diff --git a/src/main/java/de/blau/android/gpx/WayPoint.java b/src/main/java/de/blau/android/gpx/WayPoint.java index 48fa62bcc8..db60b8fc65 100644 --- a/src/main/java/de/blau/android/gpx/WayPoint.java +++ b/src/main/java/de/blau/android/gpx/WayPoint.java @@ -1,6 +1,8 @@ package de.blau.android.gpx; import java.io.IOException; +import java.io.Serializable; +import java.util.List; import java.util.Locale; import org.xmlpull.v1.XmlSerializer; @@ -14,18 +16,59 @@ public class WayPoint extends TrackPoint { /** * */ - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 2L; static final String TYPE_ELEMENT = "type"; static final String DESC_ELEMENT = "desc"; static final String NAME_ELEMENT = "name"; static final String WPT_ELEMENT = "wpt"; static final String SYM_ELEMENT = "sym"; + static final String LINK_ELEMENT = "link"; - private String name; - private String description; - private String type; - private String symbol; + private String name; + private String description; + private String type; + private String symbol; + private List links; + + public static class Link implements Serializable { + + private static final long serialVersionUID = 1L; + + static final String TEXT_ELEMENT = "text"; + static final String HREF_ATTR = "href"; + + private String url; + private String description; + + /** + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * @param description the description to set + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * @return the url + */ + public String getUrl() { + return url; + } + + /** + * @param url the url to set + */ + public void setUrl(String url) { + this.url = url; + } + } /** * Construct a new WayPoint @@ -34,18 +77,9 @@ public class WayPoint extends TrackPoint { * @param longitude the longitude (WSG84) * @param altitude altitude in meters * @param time time (ms since the epoch) - * @param name optional name value - * @param description optional description - * @param type optional type value - * @param symbol optional symbol */ - public WayPoint(double latitude, double longitude, double altitude, long time, @Nullable String name, @Nullable String description, @Nullable String type, - @Nullable String symbol) { + public WayPoint(double latitude, double longitude, double altitude, long time) { super((byte) 0, latitude, longitude, altitude, time); - this.name = name; - this.description = description; - this.type = type; - this.symbol = symbol; } @Override @@ -69,6 +103,18 @@ public synchronized void toXml(XmlSerializer serializer, GpxTimeFormater gtf) th if (symbol != null) { serializer.startTag(null, SYM_ELEMENT).text(symbol).endTag(null, SYM_ELEMENT); } + if (links != null) { + for (Link link : links) { + serializer.startTag(null, LINK_ELEMENT); + if (link.getUrl() != null) { + serializer.attribute(null, Link.HREF_ATTR, link.getUrl()); + } + if (link.getDescription() != null) { + serializer.startTag(null, Link.TEXT_ELEMENT).text(link.getDescription()).endTag(null, Link.TEXT_ELEMENT); + } + serializer.endTag(null, LINK_ELEMENT); + } + } serializer.endTag(null, WPT_ELEMENT); } @@ -92,6 +138,15 @@ public String getSymbol() { return symbol; } + /** + * Set an optional symbol value + * + * @param symbol the symbol value + */ + public void setSymbol(@Nullable String symbol) { + this.symbol = symbol; + } + /** * Get the name if any * @@ -102,6 +157,15 @@ public String getName() { return name; } + /** + * Set optional name value + * + * @param name the name + */ + public void setName(@Nullable String name) { + this.name = name; + } + /** * Get a description if any * @@ -112,6 +176,15 @@ public String getDescription() { return description; } + /** + * Set an optional description + * + * @param description the description + */ + public void setDescription(@Nullable String description) { + this.description = description; + } + /** * Get a String suitable for labeling the point * @@ -130,6 +203,29 @@ public String getType() { return type; } + /** + * Set an optional type + * + * @param type the type + */ + public void setType(@Nullable String type) { + this.type = type; + } + + /** + * @return the links + */ + public List getLinks() { + return links; + } + + /** + * @param links the links to set + */ + public void setLinks(@Nullable List links) { + this.links = links; + } + /** * Generate a short description suitable for a menu * diff --git a/src/main/java/de/blau/android/layer/gpx/MapOverlay.java b/src/main/java/de/blau/android/layer/gpx/MapOverlay.java index 05e30c1af5..3db4dcdcdc 100644 --- a/src/main/java/de/blau/android/layer/gpx/MapOverlay.java +++ b/src/main/java/de/blau/android/layer/gpx/MapOverlay.java @@ -357,7 +357,7 @@ public BoundingBox getExtent() { @Override public void onSelected(FragmentActivity activity, WayPoint wp) { - ViewWayPoint.showDialog(activity, wp); + ViewWayPoint.showDialog(activity, contentId, wp); } @Override diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index c655f609e4..03408dab72 100755 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -439,6 +439,7 @@ Altitude Create OSM object Create OSM object from preset search + Link Custom imagery OAM imagery diff --git a/src/test/java/de/blau/android/gpx/GpxTest.java b/src/test/java/de/blau/android/gpx/GpxTest.java new file mode 100644 index 0000000000..ffbc9419da --- /dev/null +++ b/src/test/java/de/blau/android/gpx/GpxTest.java @@ -0,0 +1,53 @@ +package de.blau.android.gpx; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.filters.LargeTest; +import de.blau.android.JavaResources; +import de.blau.android.util.FileUtil; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 33) +@LargeTest +public class GpxTest { + + @Test + public void parsingTest() { + try { + File zippedGpxFile = JavaResources.copyFileFromResources(ApplicationProvider.getApplicationContext(), "2011-06-08_13-21-55 OT.zip", null, "/"); + assertTrue(FileUtil.unpackZip(FileUtil.getPublicDirectory(FileUtil.getPublicDirectory(), "/").getAbsolutePath(), zippedGpxFile.getName())); + Track track = new Track(ApplicationProvider.getApplicationContext(), false); + try (InputStream is = new FileInputStream( + new File(FileUtil.getPublicDirectory().getAbsolutePath() + "/2011-06-08_13-21-55 OT/2011-06-08_13-21-55.gpx")); + BufferedInputStream in = new BufferedInputStream(is)) { + track.importFromGPX(in); + assertEquals(301, track.getTrackPoints().size()); + TrackPoint tp = track.getFirstTrackPoint(); + assertEquals(47.437398, tp.getLatitude(), 0.000001); + assertEquals(8.211872, tp.getLongitude(), 0.000001); + assertEquals(3, track.getWayPoints().size()); + WayPoint wp = track.getFirstWayPoint(); + assertNotNull(wp.getLinks()); + assertEquals("2011-06-08_13-22-47.3gpp", wp.getLinks().get(0).getUrl()); + assertEquals("2011-06-08_13-22-47.3gpp", wp.getLinks().get(0).getDescription()); + } + } catch (IOException e) { + fail(e.getMessage()); + } + } +} diff --git a/src/testCommon/resources/2011-06-08_13-21-55 OT.zip b/src/testCommon/resources/2011-06-08_13-21-55 OT.zip new file mode 100644 index 0000000000..f4575296d6 Binary files /dev/null and b/src/testCommon/resources/2011-06-08_13-21-55 OT.zip differ