/*
* Licensed to GraphHopper GmbH under one or more contributor
* license agreements. See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*
* GraphHopper GmbH licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.graphhopper.matching;
import com.graphhopper.routing.Path;
import com.graphhopper.util.Constants;
import com.graphhopper.util.DistanceCalc;
import com.graphhopper.util.GPXEntry;
import com.graphhopper.util.Helper;
import com.graphhopper.util.Instruction;
import com.graphhopper.util.InstructionList;
import com.graphhopper.util.PointList;
import com.graphhopper.util.Translation;
import java.io.*;
import java.text.SimpleDateFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.TimeZone;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* A simple utility method to import from and export to GPX files.
* <p>
* @author Peter Karich
*/
public class GPXFile {
static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
static final String DATE_FORMAT_Z = "yyyy-MM-dd'T'HH:mm:ss'Z'";
static final String DATE_FORMAT_Z_MS = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
private final List<GPXEntry> entries;
private boolean includeElevation = false;
private InstructionList instructions;
public GPXFile() {
entries = new ArrayList<GPXEntry>();
}
public GPXFile(List<GPXEntry> entries) {
this.entries = entries;
}
public GPXFile(MatchResult mr, InstructionList il) {
this.instructions = il;
this.entries = new ArrayList<GPXEntry>(mr.getEdgeMatches().size());
// TODO fetch time from GPX or from calculated route?
long time = 0;
for (int emIndex = 0; emIndex < mr.getEdgeMatches().size(); emIndex++) {
EdgeMatch em = mr.getEdgeMatches().get(emIndex);
PointList pl = em.getEdgeState().fetchWayGeometry(emIndex == 0 ? 3 : 2);
if (pl.is3D()) {
includeElevation = true;
}
for (int i = 0; i < pl.size(); i++) {
if (pl.is3D()) {
entries.add(new GPXEntry(pl.getLatitude(i), pl.getLongitude(i), pl.getElevation(i), time));
} else {
entries.add(new GPXEntry(pl.getLatitude(i), pl.getLongitude(i), time));
}
}
}
}
public List<GPXEntry> getEntries() {
return entries;
}
public GPXFile doImport(String fileStr) {
try {
return doImport(new FileInputStream(fileStr), 20);
} catch (FileNotFoundException ex) {
throw new RuntimeException(ex);
}
}
/**
* This method creates a GPXFile object filled with lat,lon values from the
* xml inputstream is.
*
* @param defaultSpeed if no time element is found the time value will be
* guessed from the distance and this provided default speed in kph.
*/
public GPXFile doImport(InputStream is, double defaultSpeed) {
SimpleDateFormat formatter = new SimpleDateFormat(DATE_FORMAT);
SimpleDateFormat formatterZ = new SimpleDateFormat(DATE_FORMAT_Z);
SimpleDateFormat formatterZMS = new SimpleDateFormat(DATE_FORMAT_Z_MS);
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(false);
factory.setIgnoringElementContentWhitespace(true);
DistanceCalc distCalc = Helper.DIST_PLANE;
try {
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(is);
NodeList nl = doc.getElementsByTagName("trkpt");
double prevLat = 0, prevLon = 0;
long prevMillis = 0;
for (int index = 0; index < nl.getLength(); index++) {
Node n = nl.item(index);
if (!(n instanceof Element)) {
continue;
}
Element e = (Element) n;
double lat = Double.parseDouble(e.getAttribute("lat"));
double lon = Double.parseDouble(e.getAttribute("lon"));
NodeList timeNodes = e.getElementsByTagName("time");
long millis = prevMillis;
if (timeNodes.getLength() == 0) {
if (index > 0) {
millis += Math.round(distCalc.calcDist(prevLat, prevLon, lat, lon) * 3600 / defaultSpeed);
}
} else {
String text = timeNodes.item(0).getTextContent();
if (text.contains("Z")) {
try {
// Try whole second matching
millis = formatterZ.parse(text).getTime();
} catch (ParseException ex) {
// Error: try looking at milliseconds
millis = formatterZMS.parse(text).getTime();
}
} else {
millis = formatter.parse(revertTZHack(text)).getTime();
}
}
NodeList eleNodes = e.getElementsByTagName("ele");
if (eleNodes.getLength() == 0) {
entries.add(new GPXEntry(lat, lon, millis));
} else {
double ele = Double.parseDouble(eleNodes.item(0).getTextContent());
entries.add(new GPXEntry(lat, lon, ele, millis));
}
prevLat = lat;
prevLon = lon;
prevMillis = millis;
}
return this;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Hack to parse time in Java and convert +0100 into +01:00
*/
private static String revertTZHack(String str) {
return str.substring(0, str.length() - 3) + str.substring(str.length() - 2);
}
@Override
public String toString() {
return "entries " + entries.size() + ", " + entries;
}
// TODO DUPLICATE CODE from GraphHopper InstructionList!
//
public String createString() {
long startTimeMillis = 0;
SimpleDateFormat formatter = new SimpleDateFormat(DATE_FORMAT_Z);
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
String header = "<?xml version='1.0' encoding='UTF-8' standalone='no' ?>"
+ "<gpx xmlns='http://www.topografix.com/GPX/1/1' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'"
+ " creator='Graphhopper MapMatching " + Constants.VERSION + "' version='1.1'"
// This xmlns:gh acts only as ID, no valid URL necessary.
// Use a separate namespace for custom extensions to make basecamp happy.
+ " xmlns:gh='https://graphhopper.com/public/schema/gpx/1.1'>"
+ "\n<metadata>"
+ "<copyright author=\"OpenStreetMap contributors\"/>"
+ "<link href='http://graphhopper.com'>"
+ "<text>GraphHopper GPX</text>"
+ "</link>"
+ "<time>" + formatter.format(startTimeMillis) + "</time>"
+ "</metadata>";
StringBuilder gpxOutput = new StringBuilder(header);
gpxOutput.append("\n<trk><name>").append("GraphHopper MapMatching").append("</name>");
if (instructions != null && !instructions.isEmpty()) {
gpxOutput.append("\n<rte>");
Instruction nextInstr = null;
for (Instruction currInstr : instructions) {
if (null != nextInstr) {
instructions.createRteptBlock(gpxOutput, nextInstr, currInstr);
}
nextInstr = currInstr;
}
instructions.createRteptBlock(gpxOutput, nextInstr, null);
gpxOutput.append("\n</rte>");
}
gpxOutput.append("<trkseg>");
for (GPXEntry entry : entries) {
gpxOutput.append("\n<trkpt lat='").append(Helper.round6(entry.getLat()));
gpxOutput.append("' lon='").append(Helper.round6(entry.getLon())).append("'>");
if (includeElevation) {
gpxOutput.append("<ele>").append(Helper.round2(entry.getEle())).append("</ele>");
}
gpxOutput.append("<time>").append(formatter.format(startTimeMillis + entry.getTime())).append("</time>");
gpxOutput.append("</trkpt>");
}
gpxOutput.append("</trkseg>");
gpxOutput.append("</trk>");
// we could now use 'wpt' for via points
gpxOutput.append("</gpx>");
return gpxOutput.toString().replaceAll("\\'", "\"");
}
public GPXFile doExport(String gpxFile) {
BufferedWriter writer = null;
try {
writer = new BufferedWriter(new FileWriter(gpxFile));
writer.append(createString());
return this;
} catch (IOException ex) {
throw new RuntimeException(ex);
} finally {
Helper.close(writer);
}
}
public static void write(Path path, String gpxFile, Translation translation) {
BufferedWriter writer = null;
try {
writer = new BufferedWriter(new FileWriter(gpxFile));
writer.append(path.calcInstructions(translation).createGPX());
} catch (IOException ex) {
throw new RuntimeException(ex);
} finally {
Helper.close(writer);
}
}
}