package com.revolsys.record.io.format.gpx;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Queue;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import org.apache.log4j.Logger;
import com.revolsys.geometry.model.Geometry;
import com.revolsys.geometry.model.GeometryFactory;
import com.revolsys.geometry.model.LineString;
import com.revolsys.geometry.model.Point;
import com.revolsys.geometry.model.coordinates.list.CoordinatesListUtil;
import com.revolsys.properties.BaseObjectWithProperties;
import com.revolsys.record.Record;
import com.revolsys.record.RecordFactory;
import com.revolsys.record.io.RecordReader;
import com.revolsys.record.io.format.xml.StaxReader;
import com.revolsys.record.schema.RecordDefinition;
import com.revolsys.spring.resource.Resource;
import com.revolsys.util.Dates;
import com.revolsys.util.MathUtil;
public class GpxIterator extends BaseObjectWithProperties
implements Iterator<Record>, RecordReader {
private static final Logger LOG = Logger.getLogger(GpxIterator.class);
private String baseName;
private Record currentRecord;
private File file;
private final GeometryFactory geometryFactory = GeometryFactory.floating3(4326);
private boolean hasNext = true;
private final StaxReader in;
private int index = 0;
private boolean loadNextObject = true;
private final Queue<Record> objects = new LinkedList<>();
private RecordFactory recordFactory;
private String schemaName = GpxConstants.GPX_NS_URI;
private String typePath;
public GpxIterator(final File file) throws IOException, XMLStreamException {
this(new FileReader(file));
}
public GpxIterator(final Reader in) throws IOException, XMLStreamException {
this(StaxReader.newXmlReader(in));
}
public GpxIterator(final Reader in, final RecordFactory recordFactory, final String path) {
this(StaxReader.newXmlReader(in));
this.recordFactory = recordFactory;
this.typePath = path;
}
public GpxIterator(final Resource resource, final RecordFactory recordFactory, final String path)
throws IOException {
this(StaxReader.newXmlReader(resource));
this.recordFactory = recordFactory;
this.typePath = path;
this.baseName = resource.getBaseName();
}
public GpxIterator(final StaxReader in) {
this.in = in;
try {
in.skipToStartElement();
skipMetaData();
} catch (final XMLStreamException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
@Override
public void close() {
if (this.in != null) {
this.in.close();
}
}
@Override
public RecordDefinition getRecordDefinition() {
return GpxConstants.GPX_TYPE;
}
public String getSchemaName() {
return this.schemaName;
}
@Override
public boolean hasNext() {
if (!this.hasNext) {
return false;
} else if (this.loadNextObject) {
return loadNextRecord();
} else {
return true;
}
}
@Override
public Iterator<Record> iterator() {
return this;
}
protected boolean loadNextRecord() {
try {
do {
this.currentRecord = parseRecord();
} while (this.currentRecord != null && this.typePath != null
&& !this.currentRecord.getRecordDefinition().getPath().equals(this.typePath));
this.loadNextObject = false;
if (this.currentRecord == null) {
close();
this.hasNext = false;
}
return this.hasNext;
} catch (final XMLStreamException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
@Override
public Record next() {
if (hasNext()) {
this.loadNextObject = true;
return this.currentRecord;
} else {
throw new NoSuchElementException();
}
}
protected Object parseAttribute(final Record record) {
final String fieldName = this.in.getLocalName();
final String stringValue = this.in.getElementText();
Object value;
if (stringValue == null) {
value = null;
} else if (fieldName.equals("time")) {
value = Dates.getDate("yyyy-MM-dd'T'HH:mm:ss'Z'", stringValue);
} else {
value = stringValue;
}
if (value != null) {
record.setValue(fieldName, value);
}
return value;
}
protected Record parsePoint(final String featureType, final double index)
throws XMLStreamException {
final Record record = this.recordFactory.newRecord(GpxConstants.GPX_TYPE);
record.setValue("dataset_name", this.baseName);
record.setValue("index", index);
record.setValue("feature_type", featureType);
final double lat = Double.parseDouble(this.in.getAttributeValue("", "lat"));
final double lon = Double.parseDouble(this.in.getAttributeValue("", "lon"));
double elevation = Double.NaN;
while (this.in.nextTag() == XMLStreamConstants.START_ELEMENT) {
if (this.in.getName().equals(GpxConstants.EXTENSION_ELEMENT)) {
this.in.skipSubTree();
} else if (this.in.getName().equals(GpxConstants.ELEVATION_ELEMENT)) {
elevation = Double.parseDouble(this.in.getElementText());
} else {
parseAttribute(record);
}
}
Point point = null;
if (Double.isNaN(elevation)) {
point = this.geometryFactory.point(lon, lat);
} else {
point = this.geometryFactory.point(lon, lat, elevation);
}
record.setValue("location", point);
return record;
}
private Record parseRecord() throws XMLStreamException {
if (!this.objects.isEmpty()) {
return this.objects.remove();
} else {
if (this.in.getEventType() != XMLStreamConstants.START_ELEMENT) {
this.in.skipToStartElement();
}
while (this.in.getEventType() == XMLStreamConstants.START_ELEMENT) {
final QName name = this.in.getName();
if (name.equals(GpxConstants.WAYPOINT_ELEMENT)) {
return parseWaypoint();
} else if (name.equals(GpxConstants.TRACK_ELEMENT)) {
return parseTrack();
} else if (name.equals(GpxConstants.ROUTE_ELEMENT)) {
return parseRoute();
} else {
this.in.skipSubTree();
this.in.nextTag();
}
}
return null;
}
}
private Record parseRoute() throws XMLStreamException {
this.index++;
final Record record = this.recordFactory.newRecord(GpxConstants.GPX_TYPE);
record.setValue("dataset_name", this.baseName);
record.setValue("index", this.index);
record.setValue("feature_type", "rte");
final List<Record> pointObjects = new ArrayList<>();
int axisCount = 2;
while (this.in.nextTag() == XMLStreamConstants.START_ELEMENT) {
if (this.in.getName().equals(GpxConstants.EXTENSION_ELEMENT)) {
this.in.skipSubTree();
} else if (this.in.getName().equals(GpxConstants.ROUTE_POINT_ELEMENT)) {
final double pointIndex = this.index + (pointObjects.size() + 1.0) / 10000;
final Record pointObject = parseRoutPoint(pointIndex);
pointObjects.add(pointObject);
final Point point = pointObject.getGeometry();
axisCount = Math.max(axisCount, point.getAxisCount());
} else {
parseAttribute(record);
}
}
final int vertexCount = pointObjects.size();
final double[] coordinates = new double[vertexCount * axisCount];
for (int i = 0; i < vertexCount; i++) {
final Record pointObject = pointObjects.get(i);
final Point point = pointObject.getGeometry();
CoordinatesListUtil.setCoordinates(coordinates, axisCount, i, point);
}
final LineString line;
if (vertexCount > 1) {
line = this.geometryFactory.lineString(axisCount, coordinates);
} else {
line = this.geometryFactory.lineString();
}
record.setGeometryValue(line);
this.objects.addAll(pointObjects);
return record;
}
private Record parseRoutPoint(final double index) throws XMLStreamException {
final String featureType = "rtept";
return parsePoint(featureType, index);
}
private Record parseTrack() throws XMLStreamException {
this.index++;
final Record record = this.recordFactory.newRecord(GpxConstants.GPX_TYPE);
record.setValue("dataset_name", this.baseName);
record.setValue("index", this.index);
record.setValue("feature_type", "trk");
int axisCount = 2;
final List<Geometry> parts = new ArrayList<>();
while (this.in.nextTag() == XMLStreamConstants.START_ELEMENT) {
if (this.in.getName().equals(GpxConstants.EXTENSION_ELEMENT)) {
this.in.skipSubTree();
} else if (this.in.getName().equals(GpxConstants.TRACK_SEGMENT_ELEMENT)) {
final Geometry part = parseTrackSegment();
parts.add(part);
if (part.getAxisCount() > axisCount) {
axisCount = part.getAxisCount();
}
} else {
parseAttribute(record);
}
}
final Geometry geometry = this.geometryFactory.convertAxisCount(axisCount).geometry(parts);
record.setGeometryValue(geometry);
return record;
}
private int parseTrackPoint(final List<Double> points) throws XMLStreamException {
final String lonText = this.in.getAttributeValue("", "lon");
final double lon = Double.parseDouble(lonText);
points.add(lon);
final String latText = this.in.getAttributeValue("", "lat");
final double lat = Double.parseDouble(latText);
points.add(lat);
int axisCount = 2;
double z = Double.NaN;
double m = Double.NaN;
while (this.in.nextTag() == XMLStreamConstants.START_ELEMENT) {
if (this.in.getName().equals(GpxConstants.EXTENSION_ELEMENT)
|| this.in.getName().equals(GpxConstants.TRACK_SEGMENT_ELEMENT)) {
this.in.skipSubTree();
} else {
if (this.in.getName().equals(GpxConstants.ELEVATION_ELEMENT)) {
final String elevationText = this.in.getElementText();
final double elevation = Double.parseDouble(elevationText);
z = elevation;
if (axisCount < 3) {
axisCount = 3;
}
} else if (this.in.getName().equals(GpxConstants.TIME_ELEMENT)) {
final String dateText = this.in.getElementText();
final Calendar calendar = Dates.getIsoCalendar(dateText);
final long time = calendar.getTimeInMillis();
m = time;
if (axisCount < 4) {
axisCount = 4;
}
} else {
// TODO decide if we want to handle the metadata on a track point
this.in.skipSubTree();
}
}
}
points.add(z);
points.add(m);
return axisCount;
}
private Geometry parseTrackSegment() throws XMLStreamException {
final List<Double> coordinates = new ArrayList<>();
int axisCount = 2;
while (this.in.nextTag() == XMLStreamConstants.START_ELEMENT) {
final int pointAxisCount = parseTrackPoint(coordinates);
axisCount = Math.max(axisCount, pointAxisCount);
}
if (coordinates.size() == axisCount) {
return this.geometryFactory.convertAxisCount(axisCount)
.point(MathUtil.toDoubleArray(coordinates));
} else {
return this.geometryFactory.convertAxisCount(axisCount).lineString(4,
MathUtil.toDoubleArray(coordinates));
}
}
private Record parseWaypoint() throws XMLStreamException {
this.index++;
final String featureType = "wpt";
return parsePoint(featureType, this.index);
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
public void setSchemaName(final String schemaName) {
this.schemaName = schemaName;
}
public void skipMetaData() throws XMLStreamException {
this.in.requireLocalName(GpxConstants.GPX_ELEMENT);
this.in.skipToStartElement();
if (this.in.getName().equals(GpxConstants.METADATA_ELEMENT)) {
this.in.skipSubTree();
this.in.skipToStartElement();
}
}
@Override
public String toString() {
return this.file.getAbsolutePath();
}
}