package net.osmand.plus;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.ColorInt;
import net.osmand.Location;
import net.osmand.PlatformUtil;
import net.osmand.data.LocationPoint;
import net.osmand.data.PointDescription;
import net.osmand.data.RotatedTileBox;
import net.osmand.plus.views.OsmandMapTileView;
import net.osmand.plus.views.Renderable;
import net.osmand.util.Algorithms;
import org.apache.commons.logging.Log;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Stack;
import java.util.TimeZone;
public class GPXUtilities {
public final static Log log = PlatformUtil.getLog(GPXUtilities.class);
private final static String GPX_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; //$NON-NLS-1$
private final static String GPX_TIME_FORMAT_MILLIS = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; //$NON-NLS-1$
private final static NumberFormat latLonFormat = new DecimalFormat("0.00#####", new DecimalFormatSymbols(
new Locale("EN", "US")));
public static class GPXExtensions {
Map<String, String> extensions = null;
public Map<String, String> getExtensionsToRead() {
if (extensions == null) {
return Collections.emptyMap();
}
return extensions;
}
@ColorInt
public int getColor(@ColorInt int defColor) {
if (extensions != null && extensions.containsKey("color")) {
try {
return Color.parseColor(extensions.get("color").toUpperCase());
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
}
return defColor;
}
public void setColor(int color) {
getExtensionsToWrite().put("color", Algorithms.colorToString(color));
}
public Map<String, String> getExtensionsToWrite() {
if (extensions == null) {
extensions = new LinkedHashMap<String, String>();
}
return extensions;
}
}
public static class Elevation {
public float distance;
public int time;
public float elevation;
}
public static class Speed {
public float distance;
public int time;
public float speed;
}
public static class WptPt extends GPXExtensions implements LocationPoint {
public double lat;
public double lon;
public String name = null;
public String link = null;
// previous undocumented feature 'category' ,now 'type'
public String category = null;
public String desc = null;
public String comment = null;
// by default
public long time = 0;
public double ele = Double.NaN;
public double speed = 0;
public double hdop = Double.NaN;
public boolean deleted = false;
public int colourARGB = 0; // point colour (used for altitude/speed colouring)
public double distance = 0.0; // cumulative distance, if in a track
public WptPt() {
}
// public WptPt(WptPt toCopy) {
// this.lat = toCopy.lat;
// this.lon = toCopy.lon;
// if (toCopy.name != null) {
// this.name = new String(toCopy.name);
// }
// if (toCopy.link != null) {
// this.link = new String(toCopy.link);
// }
// if (toCopy.category != null) {
// this.category = new String(toCopy.category);
// }
// this.time = toCopy.time;
// this.ele = toCopy.ele;
// this.speed = toCopy.speed;
// this.hdop = toCopy.hdop;
// this.deleted = toCopy.deleted;
// this.colourARGB = toCopy.colourARGB;
// this.distance = toCopy.distance;
// }
public void setDistance(double dist) {
distance = dist;
}
public double getDistance() {
return distance;
}
@Override
public int getColor() {
return getColor(0);
}
@Override
public double getLatitude() {
return lat;
}
@Override
public double getLongitude() {
return lon;
}
@Override
public PointDescription getPointDescription(Context ctx) {
return new PointDescription(PointDescription.POINT_TYPE_WPT, name);
}
public WptPt(double lat, double lon, long time, double ele, double speed, double hdop) {
this.lat = lat;
this.lon = lon;
this.time = time;
this.ele = ele;
this.speed = speed;
this.hdop = hdop;
}
@Override
public boolean isVisible() {
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((category == null) ? 0 : category.hashCode());
result = prime * result + ((desc == null) ? 0 : desc.hashCode());
result = prime * result + ((comment == null) ? 0 : comment.hashCode());
result = prime * result + ((lat == 0) ? 0 : Double.valueOf(lat).hashCode());
result = prime * result + ((lon == 0) ? 0 : Double.valueOf(lon).hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
WptPt other = (WptPt) obj;
return Algorithms.objectEquals(other.name, name)
&& Algorithms.objectEquals(other.category, category)
&& Algorithms.objectEquals(other.lat, lat)
&& Algorithms.objectEquals(other.lon, lon)
&& Algorithms.objectEquals(other.desc, desc);
}
}
public static class TrkSegment extends GPXExtensions {
public List<WptPt> points = new ArrayList<WptPt>();
private OsmandMapTileView view;
public List<Renderable.RenderableSegment> renders = new ArrayList<>();
public List<GPXTrackAnalysis> splitByDistance(double meters) {
return split(getDistanceMetric(), getTimeSplit(), meters);
}
public List<GPXTrackAnalysis> splitByTime(int seconds) {
return split(getTimeSplit(), getDistanceMetric(), seconds);
}
private List<GPXTrackAnalysis> split(SplitMetric metric, SplitMetric secondaryMetric, double metricLimit) {
List<SplitSegment> splitSegments = new ArrayList<GPXUtilities.SplitSegment>();
splitSegment(metric, secondaryMetric, metricLimit, splitSegments, this);
return convert(splitSegments);
}
public void drawRenderers(double zoom, Paint p, Canvas c, RotatedTileBox tb) {
for (Renderable.RenderableSegment rs : renders) {
rs.drawSegment(zoom, p, c, tb);
}
}
}
public static class Track extends GPXExtensions {
public String name = null;
public String desc = null;
public List<TrkSegment> segments = new ArrayList<TrkSegment>();
}
public static class Route extends GPXExtensions {
public String name = null;
public String desc = null;
public List<WptPt> points = new ArrayList<WptPt>();
}
public static class GPXTrackAnalysis {
public float totalDistance = 0;
public int totalTracks = 0;
public long startTime = Long.MAX_VALUE;
public long endTime = Long.MIN_VALUE;
public long timeSpan = 0;
//Next few lines for Issue 3222 heuristic testing only
//public long timeMoving0 = 0;
//public float totalDistanceMoving0 = 0;
public long timeMoving = 0;
public float totalDistanceMoving = 0;
public double diffElevationUp = 0;
public double diffElevationDown = 0;
public double avgElevation = 0;
public double minElevation = 99999;
public double maxElevation = -100;
public float maxSpeed = 0;
public float avgSpeed;
public int points;
public int wptPoints = 0;
public double metricEnd;
public double secondaryMetricEnd;
public WptPt locationStart;
public WptPt locationEnd;
public double left = 0;
public double right = 0;
public double top = 0;
public double bottom = 0;
public boolean isTimeSpecified() {
return startTime != Long.MAX_VALUE && startTime != 0;
}
public boolean isTimeMoving() {
return timeMoving != 0;
}
public boolean isElevationSpecified() {
return maxElevation != -100;
}
public boolean isBoundsCalculated() {
return left !=0 && right != 0 && top != 0 && bottom != 0;
}
public List<Elevation> elevationData;
public List<Speed> speedData;
public boolean hasElevationData;
public boolean hasSpeedData;
public boolean isSpeedSpecified() {
return avgSpeed > 0;
}
public static GPXTrackAnalysis segment(long filetimestamp, TrkSegment segment) {
return new GPXTrackAnalysis().prepareInformation(filetimestamp, new SplitSegment(segment));
}
public GPXTrackAnalysis prepareInformation(long filestamp, SplitSegment... splitSegments) {
float[] calculations = new float[1];
float totalElevation = 0;
int elevationPoints = 0;
int speedCount = 0;
int timeDiff = 0;
double totalSpeedSum = 0;
points = 0;
double channelThresMin = 5; // Minimum oscillation amplitude considered as noise for Up/Down analysis
double channelThres = channelThresMin; // Actual oscillation amplitude considered as noise, try depedency on current hdop/getAccuracy
double channelBase;
double channelTop;
double channelBottom;
boolean climb = false;
elevationData = new ArrayList<>();
speedData = new ArrayList<>();
for (SplitSegment s : splitSegments) {
final int numberOfPoints = s.getNumberOfPoints();
channelBase = 99999;
channelTop = channelBase;
channelBottom = channelBase;
channelThres = channelThresMin;
float segmentDistance = 0f;
metricEnd += s.metricEnd;
secondaryMetricEnd += s.secondaryMetricEnd;
points += numberOfPoints;
for (int j = 0; j < numberOfPoints; j++) {
WptPt point = s.get(j);
if (j == 0 && locationStart == null) {
locationStart = point;
}
if (j == numberOfPoints - 1) {
locationEnd = point;
}
long time = point.time;
if (time != 0) {
startTime = Math.min(startTime, time);
endTime = Math.max(endTime, time);
}
if (left == 0 && right == 0) {
left = point.getLongitude();
right = point.getLongitude();
top = point.getLatitude();
bottom = point.getLatitude();
} else {
left = Math.min(left, point.getLongitude());
right = Math.max(right, point.getLongitude());
top = Math.max(top, point.getLatitude());
bottom = Math.min(bottom, point.getLatitude());
}
double elevation = point.ele;
Elevation elevation1 = new Elevation();
if (!Double.isNaN(elevation)) {
totalElevation += elevation;
elevationPoints++;
minElevation = Math.min(elevation, minElevation);
maxElevation = Math.max(elevation, maxElevation);
elevation1.elevation = (float) elevation;
} else {
elevation1.elevation = Float.NaN;
}
float speed = (float) point.speed;
Speed speed1 = new Speed();
if (speed > 0) {
totalSpeedSum += speed;
maxSpeed = Math.max(speed, maxSpeed);
speedCount++;
speed1.speed = speed;
} else {
speed1.speed = 0;
}
// Trend channel approach for elevation gain/loss, Hardy 2015-09-22
// Self-adjusting turnarund threshold added for testing 2015-09-25: Current rule is now: "All up/down trends of amplitude <X are ignored to smooth the noise, where X is the maximum observed DOP value of any point which contributed to the current trend (but at least 5 m as the minimum noise threshold)".
if (!Double.isNaN(point.ele)) {
// Init channel
if (channelBase == 99999) {
channelBase = point.ele;
channelTop = channelBase;
channelBottom = channelBase;
channelThres = channelThresMin;
}
// Channel maintenance
if (point.ele > channelTop) {
channelTop = point.ele;
if (!Double.isNaN(point.hdop)) {
channelThres = Math.max(channelThres, 2.0 * point.hdop); //Use empirical 2*getAccuracy(vertical), this better serves very flat tracks or high dop tracks
}
} else if (point.ele < channelBottom) {
channelBottom = point.ele;
if (!Double.isNaN(point.hdop)) {
channelThres = Math.max(channelThres, 2.0 * point.hdop);
}
}
// Turnaround (breakout) detection
if ((point.ele <= (channelTop - channelThres)) && (climb == true)) {
if ((channelTop - channelBase) >= channelThres) {
diffElevationUp += channelTop - channelBase;
}
channelBase = channelTop;
channelBottom = point.ele;
climb = false;
channelThres = channelThresMin;
} else if ((point.ele >= (channelBottom + channelThres)) && (climb == false)) {
if ((channelBase - channelBottom) >= channelThres) {
diffElevationDown += channelBase - channelBottom;
}
channelBase = channelBottom;
channelTop = point.ele;
climb = true;
channelThres = channelThresMin;
}
// End detection without breakout
if (j == (numberOfPoints - 1)) {
if ((channelTop - channelBase) >= channelThres) {
diffElevationUp += channelTop - channelBase;
}
if ((channelBase - channelBottom) >= channelThres) {
diffElevationDown += channelBase - channelBottom;
}
}
}
if (j > 0) {
WptPt prev = s.get(j - 1);
// Old complete summation approach for elevation gain/loss
//if (!Double.isNaN(point.ele) && !Double.isNaN(prev.ele)) {
// double diff = point.ele - prev.ele;
// if (diff > 0) {
// diffElevationUp += diff;
// } else {
// diffElevationDown -= diff;
// }
//}
// totalDistance += MapUtils.getDistance(prev.lat, prev.lon, point.lat, point.lon);
// using ellipsoidal 'distanceBetween' instead of spherical haversine (MapUtils.getDistance) is
// a little more exact, also seems slightly faster:
net.osmand.Location.distanceBetween(prev.lat, prev.lon, point.lat, point.lon, calculations);
totalDistance += calculations[0];
segmentDistance += calculations[0];
point.distance = segmentDistance;
timeDiff = (int)((point.time - prev.time) / 1000);
// Motion detection:
// speed > 0 uses GPS chipset's motion detection
// calculations[0] > minDisplacment * time is heuristic needed because tracks may be filtered at recording time, so points at rest may not be present in file at all
if ((speed > 0) && (calculations[0] > 0.1 / 1000f * (point.time - prev.time)) && point.time != 0 && prev.time != 0) {
timeMoving = timeMoving + (point.time - prev.time);
totalDistanceMoving += calculations[0];
}
//Next few lines for Issue 3222 heuristic testing only
// if (speed > 0 && point.time != 0 && prev.time != 0) {
// timeMoving0 = timeMoving0 + (point.time - prev.time);
// totalDistanceMoving0 += calculations[0];
// }
}
elevation1.time = timeDiff;
elevation1.distance = (j > 0) ? calculations[0] : 0;
elevationData.add(elevation1);
if (!hasElevationData && !Float.isNaN(elevation1.elevation) && totalDistance > 0) {
hasElevationData = true;
}
speed1.time = timeDiff;
speed1.distance = elevation1.distance;
speedData.add(speed1);
if (!hasSpeedData && speed1.speed > 0 && totalDistance > 0) {
hasSpeedData = true;
}
}
}
if (totalDistance < 0) {
hasElevationData = false;
hasSpeedData = false;
}
if (!isTimeSpecified()) {
startTime = filestamp;
endTime = filestamp;
}
// OUTPUT:
// 1. Total distance, Start time, End time
// 2. Time span
timeSpan = endTime - startTime;
// 3. Time moving, if any
// 4. Elevation, eleUp, eleDown, if recorded
if (elevationPoints > 0) {
avgElevation = totalElevation / elevationPoints;
}
// 5. Max speed and Average speed, if any. Average speed is NOT overall (effective) speed, but only calculated for "moving" periods.
// Averaging speed values is less precise than totalDistanceMoving/timeMoving
if (speedCount > 0) {
if (timeMoving > 0) {
avgSpeed = (float) totalDistanceMoving / (float) timeMoving * 1000f;
} else {
avgSpeed = (float) totalSpeedSum / (float) speedCount;
}
} else {
avgSpeed = -1;
}
return this;
}
}
private static class SplitSegment {
TrkSegment segment;
double startCoeff = 0;
int startPointInd;
double endCoeff = 0;
int endPointInd;
double metricEnd;
double secondaryMetricEnd;
public SplitSegment(TrkSegment s) {
startPointInd = 0;
startCoeff = 0;
endPointInd = s.points.size() - 2;
endCoeff = 1;
this.segment = s;
}
public SplitSegment(TrkSegment s, int pointInd, double cf) {
this.segment = s;
this.startPointInd = pointInd;
this.startCoeff = cf;
}
public int getNumberOfPoints() {
return endPointInd - startPointInd + 2;
}
public WptPt get(int j) {
final int ind = j + startPointInd;
if (j == 0) {
if (startCoeff == 0) {
return segment.points.get(ind);
}
return approx(segment.points.get(ind), segment.points.get(ind + 1), startCoeff);
}
if (j == getNumberOfPoints() - 1) {
if (endCoeff == 1) {
return segment.points.get(ind);
}
return approx(segment.points.get(ind - 1), segment.points.get(ind), endCoeff);
}
return segment.points.get(ind);
}
private WptPt approx(WptPt w1, WptPt w2, double cf) {
long time = value(w1.time, w2.time, 0, cf);
double speed = value(w1.speed, w2.speed, 0, cf);
double ele = value(w1.ele, w2.ele, 0, cf);
double hdop = value(w1.hdop, w2.hdop, 0, cf);
double lat = value(w1.lat, w2.lat, -360, cf);
double lon = value(w1.lon, w2.lon, -360, cf);
return new WptPt(lat, lon, time, ele, speed, hdop);
}
private double value(double vl, double vl2, double none, double cf) {
if (vl == none || Double.isNaN(vl)) {
return vl2;
} else if (vl2 == none || Double.isNaN(vl2)) {
return vl;
}
return vl + cf * (vl2 - vl);
}
private long value(long vl, long vl2, long none, double cf) {
if (vl == none) {
return vl2;
} else if (vl2 == none) {
return vl;
}
return vl + ((long) (cf * (vl2 - vl)));
}
public double setLastPoint(int pointInd, double endCf) {
endCoeff = endCf;
endPointInd = pointInd;
return endCoeff;
}
}
private static SplitMetric getDistanceMetric() {
return new SplitMetric() {
private float[] calculations = new float[1];
@Override
public double metric(WptPt p1, WptPt p2) {
net.osmand.Location.distanceBetween(p1.lat, p1.lon, p2.lat, p2.lon, calculations);
return calculations[0];
}
};
}
private static SplitMetric getTimeSplit() {
return new SplitMetric() {
@Override
public double metric(WptPt p1, WptPt p2) {
if (p1.time != 0 && p2.time != 0) {
return (int) Math.abs((p2.time - p1.time) / 1000l);
}
return 0;
}
};
}
private abstract static class SplitMetric {
public abstract double metric(WptPt p1, WptPt p2);
}
private static void splitSegment(SplitMetric metric, SplitMetric secondaryMetric,
double metricLimit, List<SplitSegment> splitSegments,
TrkSegment segment) {
double currentMetricEnd = metricLimit;
double secondaryMetricEnd = 0;
SplitSegment sp = new SplitSegment(segment, 0, 0);
double total = 0;
WptPt prev = null;
for (int k = 0; k < segment.points.size(); k++) {
WptPt point = segment.points.get(k);
if (k > 0) {
double currentSegment = metric.metric(prev, point);
secondaryMetricEnd += secondaryMetric.metric(prev, point);
while (total + currentSegment > currentMetricEnd) {
double p = currentMetricEnd - total;
double cf = (p / currentSegment);
sp.setLastPoint(k - 1, cf);
sp.metricEnd = currentMetricEnd;
sp.secondaryMetricEnd = secondaryMetricEnd;
splitSegments.add(sp);
sp = new SplitSegment(segment, k - 1, cf);
currentMetricEnd += metricLimit;
prev = sp.get(0);
}
total += currentSegment;
}
prev = point;
}
if (segment.points.size() > 0
&& !(sp.endPointInd == segment.points.size() - 1 && sp.startCoeff == 1)) {
sp.metricEnd = total;
sp.secondaryMetricEnd = secondaryMetricEnd;
sp.setLastPoint(segment.points.size() - 2, 1);
splitSegments.add(sp);
}
}
private static List<GPXTrackAnalysis> convert(List<SplitSegment> splitSegments) {
List<GPXTrackAnalysis> ls = new ArrayList<GPXUtilities.GPXTrackAnalysis>();
for (SplitSegment s : splitSegments) {
GPXTrackAnalysis a = new GPXTrackAnalysis();
a.prepareInformation(0, s);
ls.add(a);
}
return ls;
}
public static class GPXFile extends GPXExtensions {
public String author;
public List<Track> tracks = new ArrayList<Track>();
public List<WptPt> points = new ArrayList<WptPt>();
public List<Route> routes = new ArrayList<Route>();
public String warning = null;
public String path = "";
public boolean showCurrentTrack;
public long modifiedTime = 0;
public boolean isCloudmadeRouteFile() {
return "cloudmade".equalsIgnoreCase(author);
}
public GPXTrackAnalysis getAnalysis(long fileTimestamp) {
GPXTrackAnalysis g = new GPXTrackAnalysis();
g.wptPoints = points.size();
List<SplitSegment> splitSegments = new ArrayList<GPXUtilities.SplitSegment>();
for (int i = 0; i < tracks.size(); i++) {
Track subtrack = tracks.get(i);
for (TrkSegment segment : subtrack.segments) {
g.totalTracks++;
if (segment.points.size() > 1) {
splitSegments.add(new SplitSegment(segment));
}
}
}
g.prepareInformation(fileTimestamp, splitSegments.toArray(new SplitSegment[splitSegments.size()]));
return g;
}
public boolean hasRtePt() {
for (Route r : routes) {
if (r.points.size() > 0) {
return true;
}
}
return false;
}
public boolean hasWptPt() {
return points.size() > 0;
}
public boolean hasTrkPt() {
for (Track t : tracks) {
for (TrkSegment ts : t.segments) {
if (ts.points.size() > 0) {
return true;
}
}
}
return false;
}
public WptPt addWptPt(double lat, double lon, long time, String description, String name, String category, int color) {
double latAdjusted = Double.parseDouble(latLonFormat.format(lat));
double lonAdjusted = Double.parseDouble(latLonFormat.format(lon));
final WptPt pt = new WptPt(latAdjusted, lonAdjusted, time, Double.NaN, 0, Double.NaN);
pt.name = name;
pt.category = category;
pt.desc = description;
if (color != 0) {
pt.setColor(color);
}
points.add(pt);
modifiedTime = System.currentTimeMillis();
return pt;
}
public void updateWptPt(WptPt pt, double lat, double lon, long time, String description, String name, String category, int color) {
int index = points.indexOf(pt);
double latAdjusted = Double.parseDouble(latLonFormat.format(lat));
double lonAdjusted = Double.parseDouble(latLonFormat.format(lon));
pt.lat = latAdjusted;
pt.lon = lonAdjusted;
pt.time = time;
pt.desc = description;
pt.name = name;
pt.category = category;
if (color != 0) {
pt.setColor(color);
}
if (index != -1) {
points.set(index, pt);
}
modifiedTime = System.currentTimeMillis();
}
public boolean deleteWptPt(WptPt pt) {
modifiedTime = System.currentTimeMillis();
return points.remove(pt);
}
public boolean deleteRtePt(WptPt pt) {
modifiedTime = System.currentTimeMillis();
for (Route route : routes) {
if (route.points.remove(pt)) {
return true;
}
}
return false;
}
public List<TrkSegment> processRoutePoints() {
List<TrkSegment> tpoints = new ArrayList<TrkSegment>();
if (routes.size() > 0) {
for (Route r : routes) {
int routeColor = r.getColor(getColor(0));
if (r.points.size() > 0) {
TrkSegment sgmt = new TrkSegment();
tpoints.add(sgmt);
sgmt.points.addAll(r.points);
sgmt.setColor(routeColor);
}
}
}
return tpoints;
}
public List<TrkSegment> proccessPoints() {
List<TrkSegment> tpoints = new ArrayList<TrkSegment>();
for (Track t : tracks) {
int trackColor = t.getColor(getColor(0));
for (TrkSegment ts : t.segments) {
if (ts.points.size() > 0) {
TrkSegment sgmt = new TrkSegment();
tpoints.add(sgmt);
sgmt.points.addAll(ts.points);
sgmt.setColor(trackColor);
}
}
}
return tpoints;
}
public WptPt getLastPoint() {
if (tracks.size() > 0) {
Track tk = tracks.get(tracks.size() - 1);
if (tk.segments.size() > 0) {
TrkSegment ts = tk.segments.get(tk.segments.size() - 1);
if (ts.points.size() > 0) {
return ts.points.get(ts.points.size() - 1);
}
}
}
return null;
}
public WptPt findPointToShow() {
for (Track t : tracks) {
for (TrkSegment s : t.segments) {
if (s.points.size() > 0) {
return s.points.get(0);
}
}
}
for (Route s : routes) {
if (s.points.size() > 0) {
return s.points.get(0);
}
}
if (points.size() > 0) {
return points.get(0);
}
return null;
}
public boolean isEmpty() {
for (Track t : tracks) {
if (t.segments != null) {
for (TrkSegment s : t.segments) {
boolean tracksEmpty = s.points.isEmpty();
if (!tracksEmpty) {
return false;
}
}
}
}
return points.isEmpty() && routes.isEmpty();
}
}
public static String asString(GPXFile file, OsmandApplication ctx) {
final Writer writer = new StringWriter();
GPXUtilities.writeGpx(writer, file, ctx);
return writer.toString();
}
public static String writeGpxFile(File fout, GPXFile file, OsmandApplication ctx) {
Writer output = null;
try {
output = new OutputStreamWriter(new FileOutputStream(fout), "UTF-8"); //$NON-NLS-1$
return writeGpx(output, file, ctx);
} catch (IOException e) {
log.error("Error saving gpx", e); //$NON-NLS-1$
return ctx.getString(R.string.error_occurred_saving_gpx);
} finally {
if (output != null) {
try {
output.close();
} catch (IOException ignore) {
// ignore
}
}
}
}
public static String writeGpx(Writer output, GPXFile file, OsmandApplication ctx) {
try {
SimpleDateFormat format = new SimpleDateFormat(GPX_TIME_FORMAT, Locale.US);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
XmlSerializer serializer = PlatformUtil.newSerializer();
serializer.setOutput(output);
serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); //$NON-NLS-1$
serializer.startDocument("UTF-8", true); //$NON-NLS-1$
serializer.startTag(null, "gpx"); //$NON-NLS-1$
serializer.attribute(null, "version", "1.1"); //$NON-NLS-1$ //$NON-NLS-2$
if (file.author == null) {
serializer.attribute(null, "creator", Version.getAppName(ctx)); //$NON-NLS-1$
} else {
serializer.attribute(null, "creator", file.author); //$NON-NLS-1$
}
serializer.attribute(null, "xmlns", "http://www.topografix.com/GPX/1/1"); //$NON-NLS-1$ //$NON-NLS-2$
serializer.attribute(null, "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
serializer.attribute(null, "xsi:schemaLocation",
"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd");
for (Track track : file.tracks) {
serializer.startTag(null, "trk"); //$NON-NLS-1$
writeNotNullText(serializer, "name", track.name);
writeNotNullText(serializer, "desc", track.desc);
for (TrkSegment segment : track.segments) {
serializer.startTag(null, "trkseg"); //$NON-NLS-1$
for (WptPt p : segment.points) {
serializer.startTag(null, "trkpt"); //$NON-NLS-1$
writeWpt(format, serializer, p);
serializer.endTag(null, "trkpt"); //$NON-NLS-1$
}
serializer.endTag(null, "trkseg"); //$NON-NLS-1$
}
writeExtensions(serializer, track);
serializer.endTag(null, "trk"); //$NON-NLS-1$
}
for (Route track : file.routes) {
serializer.startTag(null, "rte"); //$NON-NLS-1$
writeNotNullText(serializer, "name", track.name);
writeNotNullText(serializer, "desc", track.desc);
for (WptPt p : track.points) {
serializer.startTag(null, "rtept"); //$NON-NLS-1$
writeWpt(format, serializer, p);
serializer.endTag(null, "rtept"); //$NON-NLS-1$
}
writeExtensions(serializer, track);
serializer.endTag(null, "rte"); //$NON-NLS-1$
}
for (WptPt l : file.points) {
serializer.startTag(null, "wpt"); //$NON-NLS-1$
writeWpt(format, serializer, l);
serializer.endTag(null, "wpt"); //$NON-NLS-1$
}
serializer.endTag(null, "gpx"); //$NON-NLS-1$
serializer.flush();
serializer.endDocument();
} catch (RuntimeException e) {
log.error("Error saving gpx", e); //$NON-NLS-1$
return ctx.getString(R.string.error_occurred_saving_gpx);
} catch (IOException e) {
log.error("Error saving gpx", e); //$NON-NLS-1$
return ctx.getString(R.string.error_occurred_saving_gpx);
}
return null;
}
private static void writeNotNullText(XmlSerializer serializer, String tag, String value) throws IOException {
if (value != null) {
serializer.startTag(null, tag);
serializer.text(value);
serializer.endTag(null, tag);
}
}
private static void writeExtensions(XmlSerializer serializer, GPXExtensions p) throws IOException {
if (!p.getExtensionsToRead().isEmpty()) {
serializer.startTag(null, "extensions");
for (Map.Entry<String, String> s : p.getExtensionsToRead().entrySet()) {
writeNotNullText(serializer, s.getKey(), s.getValue());
}
serializer.endTag(null, "extensions");
}
}
private static void writeWpt(SimpleDateFormat format, XmlSerializer serializer, WptPt p) throws IOException {
serializer.attribute(null, "lat", latLonFormat.format(p.lat)); //$NON-NLS-1$ //$NON-NLS-2$
serializer.attribute(null, "lon", latLonFormat.format(p.lon)); //$NON-NLS-1$ //$NON-NLS-2$
if (!Double.isNaN(p.ele)) {
writeNotNullText(serializer, "ele", (float) p.ele + "");
}
if (p.time != 0) {
writeNotNullText(serializer, "time", format.format(new Date(p.time)));
}
writeNotNullText(serializer, "name", p.name);
writeNotNullText(serializer, "desc", p.desc);
if (p.link != null) {
serializer.startTag(null, "link");
serializer.attribute(null, "href", p.link);
serializer.endTag(null, "link");
}
writeNotNullText(serializer, "type", p.category);
if (p.comment != null) {
writeNotNullText(serializer, "cmt", p.comment);
}
if (!Double.isNaN(p.hdop)) {
writeNotNullText(serializer, "hdop", p.hdop + "");
}
if (p.speed > 0) {
p.getExtensionsToWrite().put("speed", p.speed + "");
}
writeExtensions(serializer, p);
}
public static class GPXFileResult {
public ArrayList<List<Location>> locations = new ArrayList<List<Location>>();
public ArrayList<WptPt> wayPoints = new ArrayList<WptPt>();
// special case for cloudmate gpx : they discourage common schema
// by using waypoint as track points and rtept are not very close to real way
// such as wpt. However they provide additional information into gpx.
public boolean cloudMadeFile;
public String error;
public Location findFistLocation() {
for (List<Location> l : locations) {
for (Location ls : l) {
if (ls != null) {
return ls;
}
}
}
return null;
}
}
private static String readText(XmlPullParser parser, String key) throws XmlPullParserException, IOException {
int tok;
String text = null;
while ((tok = parser.next()) != XmlPullParser.END_DOCUMENT) {
if (tok == XmlPullParser.END_TAG && parser.getName().equals(key)) {
break;
} else if (tok == XmlPullParser.TEXT) {
if (text == null) {
text = parser.getText();
} else {
text += parser.getText();
}
}
}
return text;
}
public static GPXFile loadGPXFile(Context ctx, File f) {
FileInputStream fis = null;
try {
fis = new FileInputStream(f);
GPXFile file = loadGPXFile(ctx, fis);
file.path = f.getAbsolutePath();
try {
fis.close();
} catch (IOException e) {
}
return file;
} catch (FileNotFoundException e) {
GPXFile res = new GPXFile();
res.path = f.getAbsolutePath();
log.error("Error reading gpx", e); //$NON-NLS-1$
res.warning = ctx.getString(R.string.error_reading_gpx);
return res;
} finally {
try {
if (fis != null)
fis.close();
} catch (IOException ignore) {
// ignore
}
}
}
public static GPXFile loadGPXFile(Context ctx, InputStream f) {
GPXFile res = new GPXFile();
SimpleDateFormat format = new SimpleDateFormat(GPX_TIME_FORMAT, Locale.US);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
SimpleDateFormat formatMillis = new SimpleDateFormat(GPX_TIME_FORMAT_MILLIS, Locale.US);
formatMillis.setTimeZone(TimeZone.getTimeZone("UTC"));
try {
XmlPullParser parser = PlatformUtil.newXMLPullParser();
parser.setInput(getUTF8Reader(f)); //$NON-NLS-1$
Stack<GPXExtensions> parserState = new Stack<GPXExtensions>();
boolean extensionReadMode = false;
parserState.push(res);
int tok;
while ((tok = parser.next()) != XmlPullParser.END_DOCUMENT) {
if (tok == XmlPullParser.START_TAG) {
Object parse = parserState.peek();
String tag = parser.getName();
if (extensionReadMode && parse instanceof GPXExtensions) {
String value = readText(parser, tag);
if (value != null) {
((GPXExtensions) parse).getExtensionsToWrite().put(tag, value);
if (tag.equals("speed") && parse instanceof WptPt) {
try {
((WptPt) parse).speed = Float.parseFloat(value);
} catch (NumberFormatException e) {
}
}
}
} else if (parse instanceof GPXExtensions && tag.equals("extensions")) {
extensionReadMode = true;
} else {
if (parse instanceof GPXFile) {
if (parser.getName().equals("gpx")) {
((GPXFile) parse).author = parser.getAttributeValue("", "creator");
}
if (parser.getName().equals("trk")) {
Track track = new Track();
((GPXFile) parse).tracks.add(track);
parserState.push(track);
}
if (parser.getName().equals("rte")) {
Route route = new Route();
((GPXFile) parse).routes.add(route);
parserState.push(route);
}
if (parser.getName().equals("wpt")) {
WptPt wptPt = parseWptAttributes(parser);
((GPXFile) parse).points.add(wptPt);
parserState.push(wptPt);
}
} else if (parse instanceof Route) {
if (parser.getName().equals("name")) {
((Route) parse).name = readText(parser, "name");
}
if (parser.getName().equals("desc")) {
((Route) parse).desc = readText(parser, "desc");
}
if (parser.getName().equals("rtept")) {
WptPt wptPt = parseWptAttributes(parser);
((Route) parse).points.add(wptPt);
parserState.push(wptPt);
}
} else if (parse instanceof Track) {
if (parser.getName().equals("name")) {
((Track) parse).name = readText(parser, "name");
}
if (parser.getName().equals("desc")) {
((Track) parse).desc = readText(parser, "desc");
}
if (parser.getName().equals("trkseg")) {
TrkSegment trkSeg = new TrkSegment();
((Track) parse).segments.add(trkSeg);
parserState.push(trkSeg);
}
} else if (parse instanceof TrkSegment) {
if (parser.getName().equals("trkpt")) {
WptPt wptPt = parseWptAttributes(parser);
((TrkSegment) parse).points.add(wptPt);
parserState.push(wptPt);
}
// main object to parse
} else if (parse instanceof WptPt) {
if (parser.getName().equals("name")) {
((WptPt) parse).name = readText(parser, "name");
} else if (parser.getName().equals("desc")) {
((WptPt) parse).desc = readText(parser, "desc");
} else if (parser.getName().equals("cmt")) {
((WptPt) parse).comment = readText(parser, "cmt");
} else if (parser.getName().equals("speed")) {
try {
String value = readText(parser, "speed");
((WptPt) parse).speed = Float.parseFloat(value);
((WptPt) parse).getExtensionsToWrite().put("speed", value);
} catch (NumberFormatException e) {
}
} else if (parser.getName().equals("link")) {
((WptPt) parse).link = parser.getAttributeValue("", "href");
} else if (tag.equals("category")) {
((WptPt) parse).category = readText(parser, "category");
} else if (tag.equals("type")) {
if (((WptPt) parse).category == null) {
((WptPt) parse).category = readText(parser, "type");
}
} else if (parser.getName().equals("ele")) {
String text = readText(parser, "ele");
if (text != null) {
try {
((WptPt) parse).ele = Float.parseFloat(text);
} catch (NumberFormatException e) {
}
}
} else if (parser.getName().equals("hdop")) {
String text = readText(parser, "hdop");
if (text != null) {
try {
((WptPt) parse).hdop = Float.parseFloat(text);
} catch (NumberFormatException e) {
}
}
} else if (parser.getName().equals("time")) {
String text = readText(parser, "time");
if (text != null) {
try {
((WptPt) parse).time = format.parse(text).getTime();
} catch (ParseException e1) {
try {
((WptPt) parse).time = formatMillis.parse(text).getTime();
} catch (ParseException e2) {
}
}
}
}
}
}
} else if (tok == XmlPullParser.END_TAG) {
Object parse = parserState.peek();
String tag = parser.getName();
if (parse instanceof GPXExtensions && tag.equals("extensions")) {
extensionReadMode = false;
}
if (tag.equals("trkpt")) {
Object pop = parserState.pop();
assert pop instanceof WptPt;
} else if (tag.equals("wpt")) {
Object pop = parserState.pop();
assert pop instanceof WptPt;
} else if (tag.equals("rtept")) {
Object pop = parserState.pop();
assert pop instanceof WptPt;
} else if (tag.equals("trk")) {
Object pop = parserState.pop();
assert pop instanceof Track;
} else if (tag.equals("rte")) {
Object pop = parserState.pop();
assert pop instanceof Route;
} else if (tag.equals("trkseg")) {
Object pop = parserState.pop();
assert pop instanceof TrkSegment;
}
}
}
// if (convertCloudmadeSource && res.isCloudmadeRouteFile()) {
// Track tk = new Track();
// res.tracks.add(tk);
// TrkSegment segment = new TrkSegment();
// tk.segments.add(segment);
//
// for (WptPt wp : res.points) {
// segment.points.add(wp);
// }
// res.points.clear();
// }
} catch (RuntimeException e) {
log.error("Error reading gpx", e); //$NON-NLS-1$
res.warning = ctx.getString(R.string.error_reading_gpx) + " " + e.getMessage();
} catch (XmlPullParserException e) {
log.error("Error reading gpx", e); //$NON-NLS-1$
res.warning = ctx.getString(R.string.error_reading_gpx) + " " + e.getMessage();
} catch (IOException e) {
log.error("Error reading gpx", e); //$NON-NLS-1$
res.warning = ctx.getString(R.string.error_reading_gpx) + " " + e.getMessage();
}
return res;
}
private static Reader getUTF8Reader(InputStream f) throws IOException {
BufferedInputStream bis = new BufferedInputStream(f);
assert bis.markSupported();
bis.mark(3);
boolean reset = true;
byte[] t = new byte[3];
bis.read(t);
if (t[0] == ((byte) 0xef) && t[1] == ((byte) 0xbb) && t[2] == ((byte) 0xbf)) {
reset = false;
}
if (reset) {
bis.reset();
}
return new InputStreamReader(bis, "UTF-8");
}
private static WptPt parseWptAttributes(XmlPullParser parser) {
WptPt wpt = new WptPt();
try {
wpt.lat = Double.parseDouble(parser.getAttributeValue("", "lat")); //$NON-NLS-1$ //$NON-NLS-2$
wpt.lon = Double.parseDouble(parser.getAttributeValue("", "lon")); //$NON-NLS-1$ //$NON-NLS-2$
} catch (NumberFormatException e) {
}
return wpt;
}
public static void mergeGPXFileInto(GPXFile to, GPXFile from) {
if (from == null) {
return;
}
if (from.showCurrentTrack) {
to.showCurrentTrack = true;
}
if (from.points != null) {
to.points.addAll(from.points);
}
if (from.tracks != null) {
to.tracks.addAll(from.tracks);
}
if (from.routes != null) {
to.routes.addAll(from.routes);
}
if (from.warning != null) {
to.warning = from.warning;
}
}
}