diff --git a/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java b/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java index c0e8f3d64..f3d4f66bf 100644 --- a/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java +++ b/app/src/main/java/net/osmtracker/activity/DisplayTrackMap.java @@ -8,6 +8,7 @@ import net.osmtracker.R; import net.osmtracker.db.TrackContentProvider; import net.osmtracker.overlay.WayPointsOverlay; +import net.osmtracker.overlay.PathOverlays; import org.osmdroid.api.IMapController; import org.osmdroid.config.Configuration; @@ -15,7 +16,6 @@ import org.osmdroid.tileprovider.tilesource.TileSourceFactory; import org.osmdroid.util.GeoPoint; import org.osmdroid.views.MapView; -import org.osmdroid.views.overlay.PathOverlay; import org.osmdroid.views.overlay.mylocation.SimpleLocationOverlay; import org.osmdroid.views.overlay.ScaleBarOverlay; @@ -25,7 +25,6 @@ import android.content.SharedPreferences; import android.database.ContentObserver; import android.database.Cursor; -import android.graphics.Color; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; @@ -109,7 +108,7 @@ public class DisplayTrackMap extends Activity { /** * OSM view overlay that displays current path */ - private PathOverlay pathOverlay; + private PathOverlays pathOverlay; /** * OSM view overlay that displays waypoints @@ -148,7 +147,7 @@ public class DisplayTrackMap extends Activity { * Initially null, to indicate that no data has yet been read. */ private Integer lastTrackPointIdProcessed = null; - + /** * Observes changes on trackpoints */ @@ -379,9 +378,7 @@ private void createOverlays() { this.getWindowManager().getDefaultDisplay().getMetrics(metrics); // set with to hopefully DPI independent 0.5mm - pathOverlay = new PathOverlay(Color.BLUE, (float)(metrics.densityDpi / 25.4 / 2),this); - - osmView.getOverlays().add(pathOverlay); + pathOverlay = new PathOverlays((float)(metrics.densityDpi / 25.4 / 2),this, osmView); myLocationOverlay = new SimpleLocationOverlay(this); osmView.getOverlays().add(myLocationOverlay); @@ -426,7 +423,7 @@ private void pathChanged() { // Projection: The columns to retrieve. Here, we want the latitude, // longitude and primary key only - String[] projection = {TrackContentProvider.Schema.COL_LATITUDE, TrackContentProvider.Schema.COL_LONGITUDE, TrackContentProvider.Schema.COL_ID}; + String[] projection = {TrackContentProvider.Schema.COL_LATITUDE, TrackContentProvider.Schema.COL_LONGITUDE, TrackContentProvider.Schema.COL_ID, TrackContentProvider.Schema.COL_NEW_SEGMENT, TrackContentProvider.Schema.COL_IS_ROUTE}; // Selection: The where clause to use String selection = null; // SelectionArgs: The parameter replacements to use for the '?' in the selection @@ -454,16 +451,26 @@ private void pathChanged() { c.moveToFirst(); double lastLat = 0; double lastLon = 0; + boolean newSegment; + boolean isRoute; int primaryKeyColumnIndex = c.getColumnIndex(TrackContentProvider.Schema.COL_ID); int latitudeColumnIndex = c.getColumnIndex(TrackContentProvider.Schema.COL_LATITUDE); int longitudeColumnIndex = c.getColumnIndex(TrackContentProvider.Schema.COL_LONGITUDE); - + int newSegmentColumnIndex = c.getColumnIndex(TrackContentProvider.Schema.COL_NEW_SEGMENT); + int isRouteColumnIndex = c.getColumnIndex(TrackContentProvider.Schema.COL_IS_ROUTE); + // Add each new point to the track while(!c.isAfterLast()) { lastLat = c.getDouble(latitudeColumnIndex); lastLon = c.getDouble(longitudeColumnIndex); lastTrackPointIdProcessed = c.getInt(primaryKeyColumnIndex); - pathOverlay.addPoint((int)(lastLat * 1e6), (int)(lastLon * 1e6)); + newSegment = c.getShort(newSegmentColumnIndex) > 0; + isRoute = c.getShort(isRouteColumnIndex) > 0; + if(newSegment) { + pathOverlay.nextSegment(isRoute); + } + + pathOverlay.addPoint((int)(lastLat * 1e6), (int)(lastLon * 1e6), isRoute); if (doInitialBoundsCalc) { if (lastLat < minLat) minLat = lastLat; if (lastLon < minLon) minLon = lastLon; diff --git a/app/src/main/java/net/osmtracker/activity/TrackManager.java b/app/src/main/java/net/osmtracker/activity/TrackManager.java index 19d7e2fe0..6d46e7682 100644 --- a/app/src/main/java/net/osmtracker/activity/TrackManager.java +++ b/app/src/main/java/net/osmtracker/activity/TrackManager.java @@ -10,6 +10,7 @@ import androidx.recyclerview.widget.RecyclerView; import android.Manifest; +import android.app.Activity; import android.app.AlertDialog; import android.content.ContentUris; import android.content.ContentValues; @@ -17,6 +18,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; @@ -39,6 +41,7 @@ import net.osmtracker.exception.CreateTrackException; import net.osmtracker.gpx.ExportToStorageTask; import net.osmtracker.gpx.ExportToTempFileTask; +import net.osmtracker.gpx.ImportRoute; import net.osmtracker.util.FileSystemUtils; import java.io.File; @@ -61,6 +64,11 @@ public class TrackManager extends AppCompatActivity final private int RC_GPS_PERMISSION = 5; final private int RC_WRITE_PERMISSIONS_SHARE = 6; + /** + * Request code for callback after user has selected an import file + */ + private static final int REQCODE_IMPORT_OPEN = 0; + /** Bundle key for {@link #prevItemVisible} */ private static final String PREV_VISIBLE = "prev_visible"; @@ -367,6 +375,53 @@ public void onClick(DialogInterface dialog, int which) { }.execute(); } + /* Import route + */ + private void importRoute() { + + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("*/*"); // GPX application type not known to Android... + intent.addCategory(Intent.CATEGORY_OPENABLE); + startActivityForResult(intent, REQCODE_IMPORT_OPEN); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQCODE_IMPORT_OPEN: + if(resultCode == Activity.RESULT_CANCELED) { + // cancelled by user + return; + } + if(resultCode != Activity.RESULT_OK) { + // something unexpected + Toast.makeText(this, + "Result code="+resultCode, + Toast.LENGTH_LONG).show(); + return; + } + + Uri uri = data.getData(); + try { + AssetFileDescriptor afd = getContentResolver() + .openAssetFileDescriptor(uri, "r"); + new ImportRoute(this, + contextMenuSelectedTrackid) + .doImport(afd,()->updateTrackItemsInRecyclerView()); + } catch(Exception e) { + new AlertDialog.Builder(this) + .setTitle("Exception received") + .setMessage(Log.getStackTraceString(e)) + .setNeutralButton("Ok", + (dlg,id)->dlg.dismiss()) + .create() + .show(); + } + return; + } + super.onActivityResult(requestCode, resultCode, data); + } + @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo, long trackId) { super.onCreateContextMenu(menu, v, menuInfo); @@ -474,6 +529,11 @@ public void onClick(DialogInterface dialog, int which) { i.putExtra(TrackContentProvider.Schema.COL_TRACK_ID, contextMenuSelectedTrackid); startActivity(i); break; + + case R.id.trackmgr_contextmenu_import: + importRoute(); + break; + } return super.onContextItemSelected(item); } diff --git a/app/src/main/java/net/osmtracker/db/DataHelper.java b/app/src/main/java/net/osmtracker/db/DataHelper.java index 8c4934020..bfebeb2b8 100644 --- a/app/src/main/java/net/osmtracker/db/DataHelper.java +++ b/app/src/main/java/net/osmtracker/db/DataHelper.java @@ -111,8 +111,15 @@ public DataHelper(Context c) { * ignored if azimuth is invalid. * @param pressure * atmospheric pressure + * @param newSeg + * whether this point is to start a new track segment + * @param isRoute + * whether this point is a point of a route, rather + * than a track (routes are imported paths that we + * want to follow, and shown in green rather than + * blue) */ - public void track(long trackId, Location location, float azimuth, int accuracy, float pressure) { + public void track(long trackId, Location location, float azimuth, int accuracy, float pressure, boolean newSeg, boolean isRoute) { Log.v(TAG, "Tracking (trackId=" + trackId + ") location: " + location + " azimuth: " + azimuth + ", accuracy: " + accuracy); ContentValues values = new ContentValues(); values.put(TrackContentProvider.Schema.COL_TRACK_ID, trackId); @@ -146,6 +153,12 @@ public void track(long trackId, Location location, float azimuth, int accuracy, values.put(TrackContentProvider.Schema.COL_ATMOSPHERIC_PRESSURE, pressure); } + values.put(TrackContentProvider.Schema.COL_NEW_SEGMENT, + newSeg ? 1 : 0); + + values.put(TrackContentProvider.Schema.COL_IS_ROUTE, + isRoute ? 1 : 0); + Uri trackUri = ContentUris.withAppendedId(TrackContentProvider.CONTENT_URI_TRACK, trackId); contentResolver.insert(Uri.withAppendedPath(trackUri, TrackContentProvider.Schema.TBL_TRACKPOINT + "s"), values); } diff --git a/app/src/main/java/net/osmtracker/db/DatabaseHelper.java b/app/src/main/java/net/osmtracker/db/DatabaseHelper.java index 75b3f9e9d..89cb1b34b 100644 --- a/app/src/main/java/net/osmtracker/db/DatabaseHelper.java +++ b/app/src/main/java/net/osmtracker/db/DatabaseHelper.java @@ -39,7 +39,10 @@ public class DatabaseHelper extends SQLiteOpenHelper { + TrackContentProvider.Schema.COL_TIMESTAMP + " long not null," + TrackContentProvider.Schema.COL_COMPASS + " double null," + TrackContentProvider.Schema.COL_COMPASS_ACCURACY + " integer null," - + TrackContentProvider.Schema.COL_ATMOSPHERIC_PRESSURE + " double null" + ")"; + + TrackContentProvider.Schema.COL_ATMOSPHERIC_PRESSURE + " double null," + + TrackContentProvider.Schema.COL_NEW_SEGMENT + " integer default 0," + + TrackContentProvider.Schema.COL_IS_ROUTE + " integer default 0" + + ")"; /** * SQL for creating index TRACKPOINT_idx (track id) @@ -125,7 +128,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { * v17: add TBL_TRACKPOINT.COL_ATMOSPHERIC_PRESSURE and TBL_WAYPOINT.COL_ATMOSPHERIC_PRESSURE * */ - private static final int DB_VERSION = 17; + private static final int DB_VERSION = 19; public DatabaseHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); @@ -178,6 +181,10 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { case 16: db.execSQL("alter table " + TrackContentProvider.Schema.TBL_TRACKPOINT + " add column " + TrackContentProvider.Schema.COL_ATMOSPHERIC_PRESSURE + " double null"); db.execSQL("alter table " + TrackContentProvider.Schema.TBL_WAYPOINT + " add column " + TrackContentProvider.Schema.COL_ATMOSPHERIC_PRESSURE + " double null"); + case 17: + db.execSQL("alter table "+TrackContentProvider.Schema.TBL_TRACKPOINT + " add column " + TrackContentProvider.Schema.COL_NEW_SEGMENT + " integer default 0"); + case 18: + db.execSQL("alter table "+TrackContentProvider.Schema.TBL_TRACKPOINT + " add column " + TrackContentProvider.Schema.COL_IS_ROUTE + " integer default 0"); } } diff --git a/app/src/main/java/net/osmtracker/db/TrackContentProvider.java b/app/src/main/java/net/osmtracker/db/TrackContentProvider.java index ffb551ed0..9662f867d 100644 --- a/app/src/main/java/net/osmtracker/db/TrackContentProvider.java +++ b/app/src/main/java/net/osmtracker/db/TrackContentProvider.java @@ -496,7 +496,10 @@ public static final class Schema { public static final String COL_COMPASS = "compass_heading"; public static final String COL_COMPASS_ACCURACY = "compass_accuracy"; public static final String COL_ATMOSPHERIC_PRESSURE = "atmospheric_pressure"; - + + public static final String COL_NEW_SEGMENT = "new_segment"; + public static final String COL_IS_ROUTE = "is_route"; + // virtual colums that are used in some sqls but dont exist in database public static final String COL_TRACKPOINT_COUNT = "tp_count"; public static final String COL_WAYPOINT_COUNT = "wp_count"; diff --git a/app/src/main/java/net/osmtracker/gpx/ExportTrackTask.java b/app/src/main/java/net/osmtracker/gpx/ExportTrackTask.java index c9cc62751..6340a0789 100644 --- a/app/src/main/java/net/osmtracker/gpx/ExportTrackTask.java +++ b/app/src/main/java/net/osmtracker/gpx/ExportTrackTask.java @@ -358,8 +358,18 @@ private void writeTrackPoints(String trackName, Writer fw, Cursor c, boolean fil fw.write("\t\t" + "" + "\n"); int i=0; + boolean havePoint=false; for(c.moveToFirst(); !c.isAfterLast(); c.moveToNext(),i++) { StringBuffer out = new StringBuffer(); + if(c.getShort(c.getColumnIndex(TrackContentProvider.Schema.COL_IS_ROUTE)) > 0) + // do not re-export route points + continue; + if(havePoint && c.getShort(c.getColumnIndex(TrackContentProvider.Schema.COL_NEW_SEGMENT)) > 0) { + fw.write("\t\t" + "" + "\n"); + fw.write("\t\t" + "" + "\n"); + } + havePoint=true; + out.append("\t\t\t" + "" + "\n"); @@ -612,4 +622,4 @@ public String sanitizeTrackName(String trackName){ public String getErrorMsg() { return errorMsg; } -} \ No newline at end of file +} diff --git a/app/src/main/java/net/osmtracker/gpx/ImportRoute.java b/app/src/main/java/net/osmtracker/gpx/ImportRoute.java new file mode 100644 index 000000000..907117ca9 --- /dev/null +++ b/app/src/main/java/net/osmtracker/gpx/ImportRoute.java @@ -0,0 +1,566 @@ +package net.osmtracker.gpx; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.res.AssetFileDescriptor; +import android.location.Location; +import android.os.Bundle; +import android.os.Looper; +import android.util.Log; + +import net.osmtracker.db.DataHelper; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.FilterInputStream; +import java.io.InputStream; +import java.io.IOException; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +interface LongConsumer { + void accept(long l); +} + +interface DoubleConsumer { + void accept(double d); +} + +class InputStreamWithPosition extends FilterInputStream { + private long position=0; + private final LongConsumer report; + + public InputStreamWithPosition(InputStream in, + LongConsumer report) { + super(in); + this.report = report; + } + + public boolean markSupported() { + return false; + } + + private void advancePosition(long delta) { + if(delta == -1) + return; + position += delta; + report.accept(position); + } + + public int read() throws IOException { + int ret = super.read(); + if(ret != -1) + advancePosition(1); + return ret; + } + + public int read(byte[] b, int off, int len) throws IOException { + int ret = super.read(b, off, len); + advancePosition(ret); + return ret; + } + + public long skip(long n) throws IOException { + long ret = super.skip(n); + advancePosition(ret); + return ret; + } + + public long getPosition() { + return position; + } +} + +/** + * Class to import a route + */ +public class ImportRoute { + private static final String TAG = ImportRoute.class.getSimpleName(); + + private final long trackId; + private final DataHelper dataHelper; + private final Activity context; + + public ImportRoute(Activity context, long trackId) { + this.context = context; + this.trackId = trackId; + dataHelper = new DataHelper(context); + } + + private void skip(XmlPullParser parser) + throws XmlPullParserException, IOException { + if (parser.getEventType() != XmlPullParser.START_TAG) { + throw new IllegalStateException(); + } + int depth = 1; + while (depth != 0) { + switch (parser.next()) { + case XmlPullParser.END_TAG: + depth--; + break; + case XmlPullParser.START_TAG: + depth++; + break; + } + } + } + + // For getting the text of a tag. + private String readText(XmlPullParser parser) + throws IOException, XmlPullParserException { + String result = ""; + if (parser.next() == XmlPullParser.TEXT) { + result = parser.getText(); + parser.nextTag(); + } + return result; + } + + private String readString(XmlPullParser parser, String tag) + throws IOException, XmlPullParserException { + parser.require(XmlPullParser.START_TAG, null, tag); + String result = readText(parser); + parser.require(XmlPullParser.END_TAG, null, tag); + return result; + } + + + private void readDouble(XmlPullParser parser, String tag, + DoubleConsumer setter) + throws XmlPullParserException, IOException { + String str = readString(parser,tag); + storeDouble(str, setter); + } + + private void storeDouble(String str, DoubleConsumer setter) { + if(str == null || "".equals(str)) + return; + try { + setter.accept(Double.parseDouble(str)); + } catch(NumberFormatException e) { + Log.v(TAG, "Bad double :\""+str+"\""); + } + } + + private interface FloatConsumer { + void accept(float f); + } + + private void readFloat(XmlPullParser parser, String tag, + FloatConsumer setter) + throws XmlPullParserException, IOException { + String str = readString(parser,tag); + if(str == null || "".equals(str)) + return; + try { + setter.accept(Float.parseFloat(str)); + } catch(NumberFormatException e) { + Log.v(TAG, "Bad float "+tag+" :\""+str+"\""); + } + } + + private float readFloat(XmlPullParser parser, String tag, + float dflt) + throws XmlPullParserException, IOException { + String str = readString(parser,tag); + if(str == null || "".equals(str)) + return dflt; + try { + return Float.parseFloat(str); + } catch(NumberFormatException e) { + Log.v(TAG, "Bad float "+tag+" :\""+str+"\""); + return dflt; + } + } + + private int readInt(XmlPullParser parser, String tag) + throws XmlPullParserException, IOException { + String str = readString(parser,tag); + if(str == null || "".equals(str)) + return 0; + try { + return Integer.parseInt(str); + } catch(NumberFormatException e) { + Log.v(TAG, "Bad integer "+tag+" :\""+str+"\""); + return 0; + } + } + + private static final DateFormat dfz = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSz"); + private static final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); + + private static final Pattern p = + Pattern.compile("^(\\d+-\\d+-\\d+T\\d+:\\d+:\\d+)(\\.\\d+)?(Z|(\\+\\d{2}):?(\\d{2})?)?"); + // + + private static String or(String first, String second) { + return (first != null) ? first : second; + } + + private void readTime(XmlPullParser parser, String tag, + LongConsumer setter) + throws XmlPullParserException, IOException { + String str = readString(parser,tag); + if(str == null || "".equals(str)) + return; + + Matcher m = p.matcher(str); + if(!m.find()) { + Log.v(TAG, "Bad date string \""+str+"\""); + } + + StringBuilder sb = new StringBuilder(); + + sb.append(m.group(1)); // date and time + sb.append(or(m.group(2),".000")); // milliseconds + + String tz = m.group(3); + if("Z".equals(tz)) + sb.append("+0000"); + else if(tz != null) { + sb.append(m.group(4)); + sb.append(or(m.group(5),"00")); + } + String dstr = sb.toString(); + try { + Date d = (tz != null) ? dfz.parse(dstr) : df.parse(dstr); + if(d!=null) + setter.accept(d.getTime()); + } catch(ParseException e) { + Log.v(TAG, "Bad date string \""+str+"\" => \""+ + dstr+"\""); + } + } + + private void readDoubleFromAttribute(XmlPullParser parser, + String attribute, + DoubleConsumer setter) { + String str = parser.getAttributeValue(null, attribute); + if(str == null || "".equals(str)) + return; + try { + setter.accept(Double.parseDouble(str)); + } catch(NumberFormatException e) { + Log.v(TAG, "Bad double attribute "+attribute+ + " :\""+str+"\""); + } + } + + // GPX + + /** + * Reads a track point + * @param parser the parser + * @param tag the tag + * @param newSegment true if this point starts a new segment + */ + private void readPoint(XmlPullParser parser, + String tag, + boolean newSegment, + boolean isWaypoint) + throws XmlPullParserException, IOException { + int depth=1; + + float azimuth=-1.0f; + int compassAccuracy=0; + float pressure=0.0f; + + String name=null; + + /* Ensure we have correct tag */ + parser.require(XmlPullParser.START_TAG, null, tag); + + Location location = new Location("import"); + if(isWaypoint) + location.setExtras(new Bundle()); + readDoubleFromAttribute(parser, "lat", location::setLatitude); + readDoubleFromAttribute(parser, "lon", location::setLongitude); + + /* Process attributes */ + while (depth > 0) { + switch(parser.next()) { + case XmlPullParser.END_TAG: + depth--; + continue; + case XmlPullParser.START_TAG: + break; + default: + continue; /* All other tags => ignore */ + } + + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + String subTag = parser.getName(); + switch(subTag) { + case "ele": + readDouble(parser, subTag, location::setAltitude); + break; + case "time": + readTime(parser, subTag, location::setTime); + break; + case "accuracy": + readFloat(parser, subTag, location::setAccuracy); + break; + case "speed": + readFloat(parser, subTag, location::setSpeed); + break; + case "baro": + pressure = readFloat(parser, subTag, 0.0f); + break; + case "compass": + azimuth = readFloat(parser, subTag, -1.0f); + break; + case "compassAccuracy": + compassAccuracy = readInt(parser, subTag); + break; + case "name": + name = readString(parser, subTag); + break; + default: + depth++; + // ignore all other tags, but still recurse + // into them + } + } + + parser.require(XmlPullParser.END_TAG, null, tag); + + if(isWaypoint) { + dataHelper.wayPoint(trackId, location, name, null, null, azimuth, compassAccuracy, pressure); + } else { + dataHelper.track(trackId, location, azimuth, + compassAccuracy, pressure, newSegment, true); + } + } + + // Parses the contents of a track or route segment. If it encounters a + // trkpt tag, hands it off to readPoint + private void readSegment(XmlPullParser parser, String tag) + throws XmlPullParserException, IOException { + boolean segmentStart = true; + parser.require(XmlPullParser.START_TAG, null, tag); + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + String name = parser.getName(); + if (name.equals("trkpt")|| + name.equals("rtept")) { + readPoint(parser, name, segmentStart,false); + segmentStart = false; + } else { + skip(parser); + } + } + parser.require(XmlPullParser.END_TAG, null, tag); + } + + + // KML + private static final Pattern pat = + Pattern.compile("\\G\\s*([0-9.]+)\\s*,\\s*([0-9.]+)\\s*(?:,\\s*([0-9.]+)\\s*)?"); + + private Location nextCoordinate(Matcher m) { + Location location = new Location("import"); + storeDouble(m.group(1), location::setLongitude); + storeDouble(m.group(2), location::setLatitude); + storeDouble(m.group(3), location::setAltitude); + return location; + } + + private Matcher parseCoordinates(XmlPullParser parser, String tag) + throws XmlPullParserException, IOException + { + return pat.matcher(readString(parser, tag)); + } + + private void readKmlTrackSegment(XmlPullParser parser, String tag) + throws XmlPullParserException, IOException + { + parser.require(XmlPullParser.START_TAG, null, tag); + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + + String subTag = parser.getName(); + if("coordinates".equals(subTag)) { + boolean newSegment = true; + Matcher m = parseCoordinates(parser, subTag); + while (m.find()) { + Location point = nextCoordinate(m); + dataHelper.track(trackId, point, + -1.0f, 0, 0.0f, + newSegment, true); + newSegment = false; + } + } else + skip(parser); + } + parser.require(XmlPullParser.END_TAG, null, tag); + } + + private Location readKmlWayPoint(XmlPullParser parser, String tag) + throws XmlPullParserException, IOException + { + Location point=null; + parser.require(XmlPullParser.START_TAG, null, tag); + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + String subTag = parser.getName(); + if("coordinates".equals(subTag)) { + Matcher m = parseCoordinates(parser, subTag); + if (!m.find()) + return null; + point = nextCoordinate(m); + } else { + skip(parser); + } + } + parser.require(XmlPullParser.END_TAG, null, tag); + return point; + } + + private void readPlacemark(XmlPullParser parser, String tag) + throws XmlPullParserException, IOException { + + Location waypoint=null; + String wptName=null; + + parser.require(XmlPullParser.START_TAG, null, tag); + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + String subTag = parser.getName(); + switch(subTag) { + case "Point": + // a waypoint + waypoint = readKmlWayPoint(parser, subTag); + break; + case "name": + // the name of the waypoint or track + wptName = readString(parser, subTag); + break; + case "LineString": + // a track segment + readKmlTrackSegment(parser, subTag); + break; + default: + skip(parser); + } + } + parser.require(XmlPullParser.END_TAG, null, tag); + + if(waypoint != null) { + waypoint.setExtras(new Bundle()); + dataHelper.wayPoint(trackId, waypoint, wptName, + null, null, -1.0f, 0, 0.0f); + } + } + + public void reportPosition(long position, + long totalSize, + ProgressDialog pb) { + if(position > 30 && position <= totalSize) + pb.setProgress((int)position); + } + + + private void showException(String msg, Exception e) { + try { + new AlertDialog.Builder(context) + .setTitle("Exception received while "+msg) + .setMessage(Log.getStackTraceString(e)) + .setNeutralButton("Ok", + (dlg,id)->dlg.dismiss()) + .create() + .show(); + } catch(Exception e2) { + Log.v(TAG, "Exception while showing exception "+ + Log.getStackTraceString(e2)); + + } + } + + /** + * Import the given input stream into the given track + */ + public void doImport(AssetFileDescriptor afd, + Runnable completion) { + ProgressDialog pb = new ProgressDialog(context); + pb.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + pb.setIndeterminate(false); + pb.setCancelable(false); + pb.setProgress(0); + pb.setMax(100); + pb.setTitle("Import"); + pb.show(); + + new Thread(()-> { + try(InputStream is = afd.createInputStream()){ + Looper.prepare(); + long totSize = afd.getLength(); + if(totSize > 0) { + pb.setMax((int)totSize); + } + InputStreamWithPosition isp = + new InputStreamWithPosition(is, + p->reportPosition(p, totSize, pb)); + doImport(isp); + context.runOnUiThread(completion); + } catch(Exception e) { + Log.v(TAG, "Exception during import "+ + Log.getStackTraceString(e)); + context.runOnUiThread(()->showException("importing route", e)); + } finally { + pb.dismiss(); + } + }).start(); + } + + /** + * Import the given input stream into the given track + */ + private void doImport(InputStream is) + throws IOException, XmlPullParserException { + int event; + // Xml Pull parser + // https://www.tutorialspoint.com/android/android_xml_parsers.htm + // https://developer.android.com/reference/org/xmlpull/v1/XmlPullParser + // https://developer.android.com/training/basics/network-ops/xml + XmlPullParserFactory xmlFactoryObject = + XmlPullParserFactory.newInstance(); + XmlPullParser p = xmlFactoryObject.newPullParser(); + p.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); + p.setInput(is, null); + + while ((event=p.next()) != XmlPullParser.END_DOCUMENT) { + if(event == XmlPullParser.START_TAG) { + String name=p.getName(); + + // GPX + if("trkseg".equals(name) || + "rte".equals(name)) { + readSegment(p, name); + } else if (name.equals("wpt")) { + readPoint(p, name, false, true); + + // KML + } else if (name.equals("Placemark")) { + readPlacemark(p, name); + } + } + } + } +} diff --git a/app/src/main/java/net/osmtracker/overlay/PathOverlays.java b/app/src/main/java/net/osmtracker/overlay/PathOverlays.java new file mode 100644 index 000000000..2f6dfa593 --- /dev/null +++ b/app/src/main/java/net/osmtracker/overlay/PathOverlays.java @@ -0,0 +1,63 @@ +package net.osmtracker.overlay; + +import java.util.ArrayList; +import java.util.List; + +import org.osmdroid.views.MapView; +import org.osmdroid.views.overlay.PathOverlay; + +import android.content.Context; +import android.graphics.Color; + +/** + * Collection of Overlays, useful to draw interrupted paths + */ +public class PathOverlays { + private final float width; + private final Context ctx; + private final MapView osmView; + private final boolean[] havePoint = new boolean[] { false, false }; + + private final int[] curIdx=new int[] { 0, 0}; + + private final List> paths = new ArrayList<>(); + private final int[] colors = new int[] { Color.BLUE, Color.GREEN }; + + private void addPath(int slot) { + PathOverlay path = new PathOverlay(colors[slot], width, ctx); + paths.get(slot).add(path); + osmView.getOverlays().add(path); + } + + public void clearPath() { + for(int slot=0; slot<2; slot++) { + for(PathOverlay path : paths.get(slot)) + path.clearPath(); + curIdx[slot]=0; + } + } + + public PathOverlays(float width, + Context ctx, MapView osmView) { + this.width=width; + this.ctx=ctx; + this.osmView = osmView; + for(int slot=0; slot<2; slot++) + paths.add(new ArrayList<>()); + } + + public void addPoint(double lat, double lon, boolean isRoute) { + int slot= isRoute ? 1 : 0; + if(curIdx[slot] >= paths.get(slot).size()) + addPath(slot); + paths.get(slot).get(curIdx[slot]).addPoint(lat, lon); + havePoint[slot]=true; + } + + public void nextSegment(boolean isRoute) { + int slot= isRoute ? 1 : 0; + if(havePoint[slot]) + curIdx[slot]++; + havePoint[slot]=false; + } +} diff --git a/app/src/main/java/net/osmtracker/service/gps/GPSLogger.java b/app/src/main/java/net/osmtracker/service/gps/GPSLogger.java index ce596da58..353b6c7de 100644 --- a/app/src/main/java/net/osmtracker/service/gps/GPSLogger.java +++ b/app/src/main/java/net/osmtracker/service/gps/GPSLogger.java @@ -103,6 +103,8 @@ public class GPSLogger extends Service implements LocationListener { */ private PressureListener pressureListener = new PressureListener(); + private boolean newSeg = false; + /** * Receives Intent for way point tracking, and stop/start logging. */ @@ -129,7 +131,8 @@ public void onReceive(Context context, Intent intent) { dataHelper.wayPoint(trackId, lastLocation, name, link, uuid, sensorListener.getAzimuth(), sensorListener.getAccuracy(), pressureListener.getPressure()); // If there is a waypoint in the track, there should also be a trackpoint - dataHelper.track(currentTrackId, lastLocation, sensorListener.getAzimuth(), sensorListener.getAccuracy(), pressureListener.getPressure()); + dataHelper.track(currentTrackId, lastLocation, sensorListener.getAzimuth(), sensorListener.getAccuracy(), pressureListener.getPressure(),newSeg, false); + newSeg = false; } } } @@ -151,6 +154,7 @@ public void onReceive(Context context, Intent intent) { dataHelper.deleteWayPoint(uuid); } } else if (OSMTracker.INTENT_START_TRACKING.equals(intent.getAction()) ) { + newSeg = true; Bundle extras = intent.getExtras(); if (extras != null) { Long trackId = extras.getLong(TrackContentProvider.Schema.COL_TRACK_ID); @@ -306,7 +310,8 @@ public void onLocationChanged(Location location) { lastLocation = location; if (isTracking) { - dataHelper.track(currentTrackId, location, sensorListener.getAzimuth(), sensorListener.getAccuracy(), pressureListener.getPressure()); + dataHelper.track(currentTrackId, location, sensorListener.getAzimuth(), sensorListener.getAccuracy(), pressureListener.getPressure(), newSeg, false); + newSeg = false; } } } diff --git a/app/src/main/res/menu/trackmgr_contextmenu.xml b/app/src/main/res/menu/trackmgr_contextmenu.xml index 805e03ef8..7dd8e4d26 100644 --- a/app/src/main/res/menu/trackmgr_contextmenu.xml +++ b/app/src/main/res/menu/trackmgr_contextmenu.xml @@ -25,4 +25,7 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 42f3bc271..2e6a2a926 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -43,6 +43,7 @@ Export as GPX Share GPX Upload to OpenStreetMap + Import route from GPX Display Details Track #{0}