/**
Copyright 2015 Tim Engler, Rareventure LLC
This file is part of Tiny Travel Tracker.
Tiny Travel Tracker is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Tiny Travel Tracker is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Tiny Travel Tracker. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Taken from mytrack
*
* Copyright 2010 Google Inc.
*
* Licensed 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.rareventure.gps2.gpx;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import java.util.TimeZone;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import com.rareventure.android.Util;
import android.location.Location;
/**
* Imports GPX file to My Tracks.
*/
public class GpxReader extends DefaultHandler {
private static final DateTimeFormatter XML_DATE_TIME_FORMAT = ISODateTimeFormat
.dateTime();
// GPX tag names and attributes
private static final String TAG_ALTITUDE = "ele";
private static final String TAG_DESCRIPTION = "desc";
private static final String TAG_NAME = "name";
private static final String TAG_TIME = "time";
private static final String TAG_TRACK = "trk";
private static final String TAG_TRACK_POINT = "trkpt";
private static final String TAG_TRACK_SEGMENT = "trkseg";
private static final String TAG_GPX = "gpx";
private static final String ATT_LAT = "lat";
private static final String ATT_LON = "lon";
private static final String TAG_EXTENSIONS = "extensions";
public static final String TAG_NEW_TIME_ZONE = "new_time_zone";
public interface GpxReaderCallback {
public void readGpx();
public void readTrk();
public void readTrkSeg();
/**
*
* @param lon
* @param lat
* @param elevation
* @param timeMs
* @param tz local timezone associated with gps point. If null, it means
* that timezone is unknown
*/
public void readTrkPt(double lon, double lat, double elevation,
long timeMs, TimeZone tz);
}
private GpxReaderCallback grc;
private String content;
private boolean isInTrackElement;
private int trackChildDepth;
private int numberOfTrackSegments;
private Locator locator;
private double latitudeValue;
private double longitudeValue;
private double elevation;
private long timeMs;
private TimeZone tz;
public GpxReader(GpxReaderCallback grc) {
this.grc = grc;
}
public void doIt(InputStream inputStream)
throws ParserConfigurationException, SAXException, IOException {
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
SAXParser saxParser = saxParserFactory.newSAXParser();
saxParser.parse(inputStream, this);
}
@Override
public void characters(char[] ch, int start, int length)
throws SAXException {
String newContent = new String(ch, start, length);
if (content == null) {
content = newContent;
} else {
/*
* In 99% of the cases, a single call to this method will be made
* for each sequence of characters we're interested in, so we'll
* rarely be concatenating strings, thus not justifying the use of a
* StringBuilder.
*/
content += newContent;
}
}
@Override
public void startElement(String uri, String localName, String name,
Attributes attributes) throws SAXException {
if (isInTrackElement) {
trackChildDepth++;
if (localName.equals(TAG_TRACK)) {
throw new SAXException(
createErrorMessage("Invalid GPX. Already inside a track."));
} else if (localName.equals(TAG_TRACK_SEGMENT)) {
onTrackSegmentElementStart();
} else if (localName.equals(TAG_TRACK_POINT)) {
onTrackPointElementStart(attributes);
} else if (localName.equals(TAG_NEW_TIME_ZONE)) {
onNewTimeZoneElementStart(attributes);
}
} else if (localName.equals(TAG_TRACK)) {
isInTrackElement = true;
trackChildDepth = 0;
onTrackElementStart();
} else if (localName.equals(TAG_GPX)) {
grc.readGpx();
}
}
@Override
public void endElement(String uri, String localName, String name)
throws SAXException {
if (!isInTrackElement) {
content = null;
return;
}
if (localName.equals(TAG_TRACK)) {
onTrackElementEnd();
isInTrackElement = false;
trackChildDepth = 0;
} else if (localName.equals(TAG_NAME)) {
// we are only interested in the first level name element
if (trackChildDepth == 1) {
onNameElementEnd();
}
} else if (localName.equals(TAG_DESCRIPTION)) {
// we are only interested in the first level description element
if (trackChildDepth == 1) {
onDescriptionElementEnd();
}
} else if (localName.equals(TAG_TRACK_SEGMENT)) {
onTrackSegmentElementEnd();
} else if (localName.equals(TAG_TRACK_POINT)) {
onTrackPointElementEnd();
} else if (localName.equals(TAG_ALTITUDE)) {
onAltitudeElementEnd();
} else if (localName.equals(TAG_EXTENSIONS)) {
onExtensionsElementEnd();
} else if (localName.equals(TAG_NEW_TIME_ZONE)) {
onNewTimeZoneElementEnd();
} else if (localName.equals(TAG_TIME)) {
onTimeElementEnd();
}
trackChildDepth--;
// reset element content
content = null;
}
private void onNewTimeZoneElementEnd() {
}
private void onExtensionsElementEnd() {
}
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
/**
* On track element start.
*/
private void onTrackElementStart() {
this.grc.readTrk();
numberOfTrackSegments = 0;
}
/**
* On track element end.
*/
private void onTrackElementEnd() {
}
/**
* On name element end.
*/
private void onNameElementEnd() {
// if (content != null) {
// track.setName(content.toString().trim());
// }
}
/**
* On description element end.
*/
private void onDescriptionElementEnd() {
// if (content != null) {
// track.setDescription(content.toString().trim());
// }
}
/**
* On track segment start.
*/
private void onTrackSegmentElementStart() {
numberOfTrackSegments++;
this.grc.readTrkSeg();
}
/**
* On track segment element end.
*/
private void onTrackSegmentElementEnd() {
}
/**
* On track point element start.
*
* @param attributes
* the attributes
*/
private void onTrackPointElementStart(Attributes attributes)
throws SAXException {
String latitude = attributes.getValue(ATT_LAT);
String longitude = attributes.getValue(ATT_LON);
if (latitude == null || longitude == null) {
throw new SAXException(
createErrorMessage("Point with no longitude or latitude."));
}
try {
latitudeValue = Double.parseDouble(latitude);
longitudeValue = Double.parseDouble(longitude);
} catch (NumberFormatException e) {
throw new SAXException(
createErrorMessage("Unable to parse latitude/longitude: "
+ latitude + "/" + longitude), e);
}
}
private void onNewTimeZoneElementStart(Attributes attributes)
throws SAXException {
String tzId = attributes.getValue("id");
if (tzId == null) {
throw new SAXException(
createErrorMessage("New Time zone extension with no id."));
}
//if we can't understand the timezone, we let it be null
tz = Util.parseTimeZone(tzId);
}
/**
* Checks if a given location is a valid (i.e. physically possible) location
* on Earth. Note: The special separator locations (which have latitude =
* 100) will not qualify as valid. Neither will locations with lat=0 and
* lng=0 as these are most likely "bad" measurements which often cause
* trouble.
*
* @param location
* the location to test
* @return true if the location is a valid location.
*/
public static boolean isValidLocation(double lon, double lat) {
return Math.abs(lat) <= 90 && Math.abs(lon) <= 180;
}
/**
* On track point element end.
*/
private void onTrackPointElementEnd() throws SAXException {
grc.readTrkPt(longitudeValue, latitudeValue, elevation, timeMs, tz);
}
/**
* On altitude element end.
*/
private void onAltitudeElementEnd() throws SAXException {
try {
elevation = Double.parseDouble(content);
} catch (NumberFormatException e) {
throw new SAXException(
createErrorMessage("Unable to parse altitude: " + content),
e);
}
}
/**
* On time element end. Sets location time and doing additional calculations
* as this is the last value required for the location. Also sets the start
* time for the trip statistics builder as there is no start time in the
* track root element.
*/
private void onTimeElementEnd() throws SAXException {
// Parse the time
try {
timeMs = XML_DATE_TIME_FORMAT.parseMillis(content.trim());
} catch (IllegalArgumentException e) {
throw new SAXException(createErrorMessage("Unable to parse time: "
+ content), e);
}
}
/**
* Creates an error message.
*
* @param message
* the message
*/
public String createErrorMessage(String message) {
return String.format(Locale.US,
"Parsing error at line: %d column: %d. %s",
locator.getLineNumber(), locator.getColumnNumber(), message);
}
}