GPX Track Parser in Java: Complete Implementation Guide

GPX (GPS Exchange Format) is an XML schema for storing GPS data including waypoints, routes, and tracks. This guide provides a complete implementation for parsing and processing GPX files in Java.


GPX Format Overview

Key GPX Elements:

  • <gpx>: Root element containing metadata
  • <trk>: Track representing a collection of track segments
  • <trkseg>: Track segment containing track points
  • <trkpt>: Individual track point with latitude, longitude, elevation, and time
  • <wpt>: Waypoint with metadata
  • <rte>: Route containing route points

Dependencies and Setup

Maven Dependencies
<properties>
<jaxb.version>4.0.2</jaxb.version>
<dom4j.version>2.1.4</dom4j.version>
<jackson.version>2.15.2</jackson.version>
</properties>
<dependencies>
<!-- XML Processing -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>${jaxb.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>${jaxb.version}</version>
</dependency>
<!-- DOM Processing -->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>${dom4j.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Date/Time -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.12.5</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
</dependencies>

Domain Models

1. Core GPX Models
public class GpxFile {
private GpxMetadata metadata;
private List<Waypoint> waypoints;
private List<Track> tracks;
private List<Route> routes;
// Constructors
public GpxFile() {
this.waypoints = new ArrayList<>();
this.tracks = new ArrayList<>();
this.routes = new ArrayList<>();
}
public GpxFile(GpxMetadata metadata, List<Waypoint> waypoints, 
List<Track> tracks, List<Route> routes) {
this.metadata = metadata;
this.waypoints = waypoints != null ? waypoints : new ArrayList<>();
this.tracks = tracks != null ? tracks : new ArrayList<>();
this.routes = routes != null ? routes : new ArrayList<>();
}
// Getters and setters
public GpxMetadata getMetadata() { return metadata; }
public void setMetadata(GpxMetadata metadata) { this.metadata = metadata; }
public List<Waypoint> getWaypoints() { return waypoints; }
public void setWaypoints(List<Waypoint> waypoints) { this.waypoints = waypoints; }
public List<Track> getTracks() { return tracks; }
public void setTracks(List<Track> tracks) { this.tracks = tracks; }
public List<Route> getRoutes() { return routes; }
public void setRoutes(List<Route> routes) { this.routes = routes; }
// Utility methods
public void addWaypoint(Waypoint waypoint) {
this.waypoints.add(waypoint);
}
public void addTrack(Track track) {
this.tracks.add(track);
}
public void addRoute(Route route) {
this.routes.add(route);
}
public List<TrackPoint> getAllTrackPoints() {
return tracks.stream()
.flatMap(track -> track.getSegments().stream())
.flatMap(segment -> segment.getPoints().stream())
.collect(Collectors.toList());
}
}
public class GpxMetadata {
private String name;
private String description;
private String author;
private String copyright;
private String link;
private String linkText;
private LocalDateTime time;
private String keywords;
private Bounds bounds;
// Constructors, getters, setters
public GpxMetadata() {}
public GpxMetadata(String name, String description, LocalDateTime time) {
this.name = name;
this.description = description;
this.time = time;
}
// Getters and setters...
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getAuthor() { return author; }
public void setAuthor(String author) { this.author = author; }
public LocalDateTime getTime() { return time; }
public void setTime(LocalDateTime time) { this.time = time; }
public Bounds getBounds() { return bounds; }
public void setBounds(Bounds bounds) { this.bounds = bounds; }
}
public class Bounds {
private final double minLat;
private final double minLon;
private final double maxLat;
private final double maxLon;
public Bounds(double minLat, double minLon, double maxLat, double maxLon) {
this.minLat = minLat;
this.minLon = minLon;
this.maxLat = maxLat;
this.maxLon = maxLon;
}
// Getters
public double getMinLat() { return minLat; }
public double getMinLon() { return minLon; }
public double getMaxLat() { return maxLat; }
public double getMaxLon() { return maxLon; }
public double getWidth() { return maxLon - minLon; }
public double getHeight() { return maxLat - minLat; }
public boolean contains(double lat, double lon) {
return lat >= minLat && lat <= maxLat && lon >= minLon && lon <= maxLon;
}
}
2. Track and Point Models
public class Track {
private String name;
private String description;
private String comment;
private String type;
private List<TrackSegment> segments;
public Track() {
this.segments = new ArrayList<>();
}
public Track(String name, List<TrackSegment> segments) {
this.name = name;
this.segments = segments != null ? segments : new ArrayList<>();
}
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public List<TrackSegment> getSegments() { return segments; }
public void setSegments(List<TrackSegment> segments) { this.segments = segments; }
// Utility methods
public void addSegment(TrackSegment segment) {
this.segments.add(segment);
}
public List<TrackPoint> getAllPoints() {
return segments.stream()
.flatMap(segment -> segment.getPoints().stream())
.collect(Collectors.toList());
}
public int getTotalPoints() {
return segments.stream()
.mapToInt(segment -> segment.getPoints().size())
.sum();
}
}
public class TrackSegment {
private List<TrackPoint> points;
public TrackSegment() {
this.points = new ArrayList<>();
}
public TrackSegment(List<TrackPoint> points) {
this.points = points != null ? points : new ArrayList<>();
}
// Getters and setters
public List<TrackPoint> getPoints() { return points; }
public void setPoints(List<TrackPoint> points) { this.points = points; }
public void addPoint(TrackPoint point) {
this.points.add(point);
}
public boolean isEmpty() {
return points.isEmpty();
}
}
public class TrackPoint {
private final double latitude;
private final double longitude;
private final Double elevation;
private final LocalDateTime time;
private final Double speed; // m/s
private final Double course; // degrees
private final Double horizontalDilution;
private final Double verticalDilution;
private final Double positionDilution;
// Builder pattern for complex object creation
public static class Builder {
private final double latitude;
private final double longitude;
private Double elevation;
private LocalDateTime time;
private Double speed;
private Double course;
private Double horizontalDilution;
private Double verticalDilution;
private Double positionDilution;
public Builder(double latitude, double longitude) {
this.latitude = latitude;
this.longitude = longitude;
}
public Builder elevation(Double elevation) {
this.elevation = elevation;
return this;
}
public Builder time(LocalDateTime time) {
this.time = time;
return this;
}
public Builder speed(Double speed) {
this.speed = speed;
return this;
}
public Builder course(Double course) {
this.course = course;
return this;
}
public Builder horizontalDilution(Double hdop) {
this.horizontalDilution = hdop;
return this;
}
public Builder verticalDilution(Double vdop) {
this.verticalDilution = vdop;
return this;
}
public Builder positionDilution(Double pdop) {
this.positionDilution = pdop;
return this;
}
public TrackPoint build() {
return new TrackPoint(this);
}
}
private TrackPoint(Builder builder) {
this.latitude = builder.latitude;
this.longitude = builder.longitude;
this.elevation = builder.elevation;
this.time = builder.time;
this.speed = builder.speed;
this.course = builder.course;
this.horizontalDilution = builder.horizontalDilution;
this.verticalDilution = builder.verticalDilution;
this.positionDilution = builder.positionDilution;
}
// Getters
public double getLatitude() { return latitude; }
public double getLongitude() { return longitude; }
public Optional<Double> getElevation() { return Optional.ofNullable(elevation); }
public Optional<LocalDateTime> getTime() { return Optional.ofNullable(time); }
public Optional<Double> getSpeed() { return Optional.ofNullable(speed); }
public Optional<Double> getCourse() { return Optional.ofNullable(course); }
public Optional<Double> getHorizontalDilution() { return Optional.ofNullable(horizontalDilution); }
public Optional<Double> getVerticalDilution() { return Optional.ofNullable(verticalDilution); }
public Optional<Double> getPositionDilution() { return Optional.ofNullable(positionDilution); }
@Override
public String toString() {
return String.format("TrackPoint[lat=%.6f, lon=%.6f, ele=%s, time=%s]", 
latitude, longitude, elevation, time);
}
}
3. Waypoint and Route Models
public class Waypoint {
private final double latitude;
private final double longitude;
private final Double elevation;
private final String name;
private final String description;
private final String comment;
private final String type;
private final LocalDateTime time;
private final String symbol;
public static class Builder {
private final double latitude;
private final double longitude;
private Double elevation;
private String name;
private String description;
private String comment;
private String type;
private LocalDateTime time;
private String symbol;
public Builder(double latitude, double longitude) {
this.latitude = latitude;
this.longitude = longitude;
}
public Builder elevation(Double elevation) {
this.elevation = elevation;
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder description(String description) {
this.description = description;
return this;
}
public Builder comment(String comment) {
this.comment = comment;
return this;
}
public Builder type(String type) {
this.type = type;
return this;
}
public Builder time(LocalDateTime time) {
this.time = time;
return this;
}
public Builder symbol(String symbol) {
this.symbol = symbol;
return this;
}
public Waypoint build() {
return new Waypoint(this);
}
}
private Waypoint(Builder builder) {
this.latitude = builder.latitude;
this.longitude = builder.longitude;
this.elevation = builder.elevation;
this.name = builder.name;
this.description = builder.description;
this.comment = builder.comment;
this.type = builder.type;
this.time = builder.time;
this.symbol = builder.symbol;
}
// Getters...
public double getLatitude() { return latitude; }
public double getLongitude() { return longitude; }
public Optional<Double> getElevation() { return Optional.ofNullable(elevation); }
public Optional<String> getName() { return Optional.ofNullable(name); }
public Optional<String> getDescription() { return Optional.ofNullable(description); }
public Optional<String> getComment() { return Optional.ofNullable(comment); }
public Optional<String> getType() { return Optional.ofNullable(type); }
public Optional<LocalDateTime> getTime() { return Optional.ofNullable(time); }
public Optional<String> getSymbol() { return Optional.ofNullable(symbol); }
}
public class Route {
private String name;
private String description;
private String comment;
private String type;
private List<RoutePoint> points;
public Route() {
this.points = new ArrayList<>();
}
public Route(String name, List<RoutePoint> points) {
this.name = name;
this.points = points != null ? points : new ArrayList<>();
}
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public List<RoutePoint> getPoints() { return points; }
public void setPoints(List<RoutePoint> points) { this.points = points; }
public void addPoint(RoutePoint point) {
this.points.add(point);
}
}
public class RoutePoint {
private final double latitude;
private final double longitude;
private final Double elevation;
private final String name;
private final String description;
// Builder pattern similar to Waypoint...
// Getters and implementation details...
}

GPX Parser Implementation

1. Main GPX Parser Interface
public interface GpxParser {
GpxFile parse(File gpxFile) throws GpxParseException;
GpxFile parse(InputStream inputStream) throws GpxParseException;
GpxFile parse(String gpxContent) throws GpxParseException;
boolean validate(File gpxFile) throws GpxParseException;
}
public class GpxParseException extends Exception {
public GpxParseException(String message) {
super(message);
}
public GpxParseException(String message, Throwable cause) {
super(message, cause);
}
}
2. DOM4J-based Parser Implementation
@Component
public class Dom4jGpxParser implements GpxParser {
private static final Logger logger = LoggerFactory.getLogger(Dom4jGpxParser.class);
private final DateTimeFormatter[] dateFormatters = {
DateTimeFormatter.ISO_DATE_TIME,
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"),
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"),
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"),
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
};
@Override
public GpxFile parse(File gpxFile) throws GpxParseException {
try {
SAXReader reader = new SAXReader();
Document document = reader.read(gpxFile);
return parseDocument(document);
} catch (DocumentException e) {
throw new GpxParseException("Failed to parse GPX file: " + gpxFile.getName(), e);
}
}
@Override
public GpxFile parse(InputStream inputStream) throws GpxParseException {
try {
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
return parseDocument(document);
} catch (DocumentException e) {
throw new GpxParseException("Failed to parse GPX from input stream", e);
}
}
@Override
public GpxFile parse(String gpxContent) throws GpxParseException {
try {
SAXReader reader = new SAXReader();
Document document = reader.read(new StringReader(gpxContent));
return parseDocument(document);
} catch (DocumentException e) {
throw new GpxParseException("Failed to parse GPX content", e);
}
}
@Override
public boolean validate(File gpxFile) throws GpxParseException {
try {
parse(gpxFile);
return true;
} catch (GpxParseException e) {
logger.warn("GPX validation failed: {}", e.getMessage());
return false;
}
}
private GpxFile parseDocument(Document document) throws GpxParseException {
Element root = document.getRootElement();
if (!"gpx".equals(root.getName())) {
throw new GpxParseException("Root element is not 'gpx'");
}
GpxFile gpxFile = new GpxFile();
// Parse metadata
gpxFile.setMetadata(parseMetadata(root));
// Parse waypoints
gpxFile.setWaypoints(parseWaypoints(root));
// Parse tracks
gpxFile.setTracks(parseTracks(root));
// Parse routes
gpxFile.setRoutes(parseRoutes(root));
logger.info("Parsed GPX: {} tracks, {} waypoints, {} routes", 
gpxFile.getTracks().size(), gpxFile.getWaypoints().size(), gpxFile.getRoutes().size());
return gpxFile;
}
private GpxMetadata parseMetadata(Element root) {
Element metadataElement = root.element("metadata");
if (metadataElement == null) {
return null;
}
GpxMetadata metadata = new GpxMetadata();
metadata.setName(getElementText(metadataElement, "name"));
metadata.setDescription(getElementText(metadataElement, "desc"));
metadata.setAuthor(getElementText(metadataElement, "author"));
metadata.setCopyright(getElementText(metadataElement, "copyright"));
metadata.setKeywords(getElementText(metadataElement, "keywords"));
// Parse time
String timeText = getElementText(metadataElement, "time");
if (timeText != null) {
metadata.setTime(parseDateTime(timeText));
}
// Parse bounds
Element boundsElement = metadataElement.element("bounds");
if (boundsElement != null) {
metadata.setBounds(parseBounds(boundsElement));
}
return metadata;
}
private List<Waypoint> parseWaypoints(Element root) {
List<Waypoint> waypoints = new ArrayList<>();
for (Element wptElement : root.elements("wpt")) {
try {
Waypoint waypoint = parseWaypoint(wptElement);
waypoints.add(waypoint);
} catch (Exception e) {
logger.warn("Failed to parse waypoint: {}", e.getMessage());
}
}
return waypoints;
}
private Waypoint parseWaypoint(Element wptElement) {
double lat = Double.parseDouble(wptElement.attributeValue("lat"));
double lon = Double.parseDouble(wptElement.attributeValue("lon"));
Waypoint.Builder builder = new Waypoint.Builder(lat, lon);
builder.elevation(parseDouble(getElementText(wptElement, "ele")));
builder.name(getElementText(wptElement, "name"));
builder.description(getElementText(wptElement, "desc"));
builder.comment(getElementText(wptElement, "cmt"));
builder.type(getElementText(wptElement, "type"));
builder.symbol(getElementText(wptElement, "sym"));
String timeText = getElementText(wptElement, "time");
if (timeText != null) {
builder.time(parseDateTime(timeText));
}
return builder.build();
}
private List<Track> parseTracks(Element root) {
List<Track> tracks = new ArrayList<>();
for (Element trkElement : root.elements("trk")) {
try {
Track track = parseTrack(trkElement);
tracks.add(track);
} catch (Exception e) {
logger.warn("Failed to parse track: {}", e.getMessage());
}
}
return tracks;
}
private Track parseTrack(Element trkElement) {
Track track = new Track();
track.setName(getElementText(trkElement, "name"));
track.setDescription(getElementText(trkElement, "desc"));
track.setComment(getElementText(trkElement, "cmt"));
track.setType(getElementText(trkElement, "type"));
// Parse track segments
for (Element trksegElement : trkElement.elements("trkseg")) {
TrackSegment segment = parseTrackSegment(trksegElement);
if (!segment.isEmpty()) {
track.addSegment(segment);
}
}
return track;
}
private TrackSegment parseTrackSegment(Element trksegElement) {
TrackSegment segment = new TrackSegment();
for (Element trkptElement : trksegElement.elements("trkpt")) {
try {
TrackPoint trackPoint = parseTrackPoint(trkptElement);
segment.addPoint(trackPoint);
} catch (Exception e) {
logger.warn("Failed to parse track point: {}", e.getMessage());
}
}
return segment;
}
private TrackPoint parseTrackPoint(Element trkptElement) {
double lat = Double.parseDouble(trkptElement.attributeValue("lat"));
double lon = Double.parseDouble(trkptElement.attributeValue("lon"));
TrackPoint.Builder builder = new TrackPoint.Builder(lat, lon);
builder.elevation(parseDouble(getElementText(trkptElement, "ele")));
builder.speed(parseDouble(getElementText(trkptElement, "speed")));
builder.course(parseDouble(getElementText(trkptElement, "course")));
builder.horizontalDilution(parseDouble(getElementText(trkptElement, "hdop")));
builder.verticalDilution(parseDouble(getElementText(trkptElement, "vdop")));
builder.positionDilution(parseDouble(getElementText(trkptElement, "pdop")));
String timeText = getElementText(trkptElement, "time");
if (timeText != null) {
builder.time(parseDateTime(timeText));
}
return builder.build();
}
private List<Route> parseRoutes(Element root) {
List<Route> routes = new ArrayList<>();
for (Element rteElement : root.elements("rte")) {
try {
Route route = parseRoute(rteElement);
routes.add(route);
} catch (Exception e) {
logger.warn("Failed to parse route: {}", e.getMessage());
}
}
return routes;
}
private Route parseRoute(Element rteElement) {
Route route = new Route();
route.setName(getElementText(rteElement, "name"));
route.setDescription(getElementText(rteElement, "desc"));
route.setComment(getElementText(rteElement, "cmt"));
route.setType(getElementText(rteElement, "type"));
// Parse route points
for (Element rteptElement : rteElement.elements("rtept")) {
try {
Waypoint routePoint = parseWaypoint(rteptElement);
// Route points are similar to waypoints
// You might want to create a specific RoutePoint class
} catch (Exception e) {
logger.warn("Failed to parse route point: {}", e.getMessage());
}
}
return route;
}
// Utility methods
private String getElementText(Element parent, String childName) {
Element child = parent.element(childName);
return child != null ? child.getTextTrim() : null;
}
private Double parseDouble(String text) {
if (text == null || text.trim().isEmpty()) {
return null;
}
try {
return Double.parseDouble(text.trim());
} catch (NumberFormatException e) {
logger.debug("Failed to parse double: {}", text);
return null;
}
}
private LocalDateTime parseDateTime(String text) {
if (text == null || text.trim().isEmpty()) {
return null;
}
for (DateTimeFormatter formatter : dateFormatters) {
try {
return LocalDateTime.parse(text.trim(), formatter);
} catch (Exception e) {
// Try next formatter
}
}
logger.warn("Failed to parse date time: {}", text);
return null;
}
private Bounds parseBounds(Element boundsElement) {
try {
double minLat = Double.parseDouble(boundsElement.attributeValue("minlat"));
double minLon = Double.parseDouble(boundsElement.attributeValue("minlon"));
double maxLat = Double.parseDouble(boundsElement.attributeValue("maxlat"));
double maxLon = Double.parseDouble(boundsElement.attributeValue("maxlon"));
return new Bounds(minLat, minLon, maxLat, maxLon);
} catch (Exception e) {
logger.warn("Failed to parse bounds: {}", e.getMessage());
return null;
}
}
}
3. JAXB-based Parser (Alternative)
@Component
public class JaxbGpxParser implements GpxParser {
private JAXBContext jaxbContext;
public JaxbGpxParser() {
try {
this.jaxbContext = JAXBContext.newInstance("com.yourpackage.gpx.models");
} catch (JAXBException e) {
throw new RuntimeException("Failed to initialize JAXB context", e);
}
}
@Override
public GpxFile parse(File gpxFile) throws GpxParseException {
try {
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
return (GpxFile) unmarshaller.unmarshal(gpxFile);
} catch (JAXBException e) {
throw new GpxParseException("Failed to parse GPX file with JAXB", e);
}
}
// Implement other parse methods...
}

Track Analysis Service

@Service
public class TrackAnalysisService {
private static final double EARTH_RADIUS_KM = 6371.0;
public TrackStatistics calculateStatistics(Track track) {
List<TrackPoint> points = track.getAllPoints();
if (points.size() < 2) {
return new TrackStatistics(0, 0, 0, 0, 0, 0, 0);
}
double totalDistance = calculateTotalDistance(points);
double totalTime = calculateTotalTime(points);
double averageSpeed = totalTime > 0 ? totalDistance / totalTime : 0;
double maxSpeed = calculateMaxSpeed(points);
double elevationGain = calculateElevationGain(points);
double elevationLoss = calculateElevationLoss(points);
double maxElevation = calculateMaxElevation(points);
double minElevation = calculateMinElevation(points);
return new TrackStatistics(
totalDistance, totalTime, averageSpeed, maxSpeed,
elevationGain, elevationLoss, maxElevation, minElevation
);
}
public double calculateTotalDistance(List<TrackPoint> points) {
double totalDistance = 0.0;
for (int i = 1; i < points.size(); i++) {
TrackPoint prev = points.get(i - 1);
TrackPoint curr = points.get(i);
totalDistance += calculateDistance(prev, curr);
}
return totalDistance;
}
public double calculateTotalTime(List<TrackPoint> points) {
Optional<LocalDateTime> startTime = points.get(0).getTime();
Optional<LocalDateTime> endTime = points.get(points.size() - 1).getTime();
if (startTime.isPresent() && endTime.isPresent()) {
Duration duration = Duration.between(startTime.get(), endTime.get());
return duration.toSeconds() / 3600.0; // Convert to hours
}
return 0.0;
}
public double calculateDistance(TrackPoint point1, TrackPoint point2) {
double lat1 = Math.toRadians(point1.getLatitude());
double lon1 = Math.toRadians(point1.getLongitude());
double lat2 = Math.toRadians(point2.getLatitude());
double lon2 = Math.toRadians(point2.getLongitude());
double dLat = lat2 - lat1;
double dLon = lon2 - lon1;
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1) * Math.cos(lat2) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return EARTH_RADIUS_KM * c;
}
public double calculateMaxSpeed(List<TrackPoint> points) {
double maxSpeed = 0.0;
for (int i = 1; i < points.size(); i++) {
TrackPoint prev = points.get(i - 1);
TrackPoint curr = points.get(i);
double distance = calculateDistance(prev, curr);
double time = calculateTimeDifference(prev, curr);
if (time > 0) {
double speed = distance / time; // km/h
maxSpeed = Math.max(maxSpeed, speed);
}
}
return maxSpeed;
}
public double calculateElevationGain(List<TrackPoint> points) {
double gain = 0.0;
for (int i = 1; i < points.size(); i++) {
Optional<Double> prevEle = points.get(i - 1).getElevation();
Optional<Double> currEle = points.get(i).getElevation();
if (prevEle.isPresent() && currEle.isPresent()) {
double diff = currEle.get() - prevEle.get();
if (diff > 0) {
gain += diff;
}
}
}
return gain;
}
public double calculateElevationLoss(List<TrackPoint> points) {
double loss = 0.0;
for (int i = 1; i < points.size(); i++) {
Optional<Double> prevEle = points.get(i - 1).getElevation();
Optional<Double> currEle = points.get(i).getElevation();
if (prevEle.isPresent() && currEle.isPresent()) {
double diff = prevEle.get() - currEle.get();
if (diff > 0) {
loss += diff;
}
}
}
return loss;
}
public double calculateMaxElevation(List<TrackPoint> points) {
return points.stream()
.map(TrackPoint::getElevation)
.filter(Optional::isPresent)
.mapToDouble(Optional::get)
.max()
.orElse(0.0);
}
public double calculateMinElevation(List<TrackPoint> points) {
return points.stream()
.map(TrackPoint::getElevation)
.filter(Optional::isPresent)
.mapToDouble(Optional::get)
.min()
.orElse(0.0);
}
private double calculateTimeDifference(TrackPoint point1, TrackPoint point2) {
Optional<LocalDateTime> time1 = point1.getTime();
Optional<LocalDateTime> time2 = point2.getTime();
if (time1.isPresent() && time2.isPresent()) {
Duration duration = Duration.between(time1.get(), time2.get());
return duration.toSeconds() / 3600.0; // Convert to hours
}
return 0.0;
}
}
public class TrackStatistics {
private final double totalDistance; // km
private final double totalTime; // hours
private final double averageSpeed; // km/h
private final double maxSpeed; // km/h
private final double elevationGain; // meters
private final double elevationLoss; // meters
private final double maxElevation; // meters
private final double minElevation; // meters
public TrackStatistics(double totalDistance, double totalTime, double averageSpeed,
double maxSpeed, double elevationGain, double elevationLoss,
double maxElevation, double minElevation) {
this.totalDistance = totalDistance;
this.totalTime = totalTime;
this.averageSpeed = averageSpeed;
this.maxSpeed = maxSpeed;
this.elevationGain = elevationGain;
this.elevationLoss = elevationLoss;
this.maxElevation = maxElevation;
this.minElevation = minElevation;
}
// Getters
public double getTotalDistance() { return totalDistance; }
public double getTotalTime() { return totalTime; }
public double getAverageSpeed() { return averageSpeed; }
public double getMaxSpeed() { return maxSpeed; }
public double getElevationGain() { return elevationGain; }
public double getElevationLoss() { return elevationLoss; }
public double getMaxElevation() { return maxElevation; }
public double getMinElevation() { return minElevation; }
public double getAveragePace() {
return totalDistance > 0 ? totalTime / totalDistance : 0; // hours per km
}
@Override
public String toString() {
return String.format(
"Distance: %.2f km, Time: %.2f h, Avg Speed: %.2f km/h, Elevation Gain: %.0f m",
totalDistance, totalTime, averageSpeed, elevationGain
);
}
}

GeoJSON Export Service

@Service
public class GeoJsonExportService {
private final ObjectMapper objectMapper;
public GeoJsonExportService() {
this.objectMapper = new ObjectMapper();
this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
}
public String exportTrackToGeoJson(Track track) throws IOException {
FeatureCollection featureCollection = new FeatureCollection();
// Create LineString for the track
LineString trackLine = createTrackLineString(track);
Map<String, Object> trackProperties = createTrackProperties(track);
Feature trackFeature = new Feature(trackLine, trackProperties, track.getName(), null);
featureCollection.addFeature(trackFeature);
// Add waypoints for significant points (start, end, high points)
addSignificantPoints(featureCollection, track);
return objectMapper.writeValueAsString(featureCollection);
}
public String exportTrackPointsToGeoJson(Track track) throws IOException {
FeatureCollection featureCollection = new FeatureCollection();
for (TrackPoint point : track.getAllPoints()) {
Point geoPoint = new Point(new Coordinate(point.getLongitude(), point.getLatitude()));
Map<String, Object> properties = new HashMap<>();
point.getElevation().ifPresent(ele -> properties.put("elevation", ele));
point.getTime().ifPresent(time -> properties.put("time", time.toString()));
Feature pointFeature = new Feature(geoPoint, properties, null, null);
featureCollection.addFeature(pointFeature);
}
return objectMapper.writeValueAsString(featureCollection);
}
private LineString createTrackLineString(Track track) {
List<Coordinate> coordinates = track.getAllPoints().stream()
.map(point -> new Coordinate(point.getLongitude(), point.getLatitude()))
.collect(Collectors.toList());
return new LineString(coordinates);
}
private Map<String, Object> createTrackProperties(Track track) {
Map<String, Object> properties = new HashMap<>();
properties.put("name", track.getName());
properties.put("type", track.getType());
properties.put("description", track.getDescription());
properties.put("segmentCount", track.getSegments().size());
properties.put("pointCount", track.getTotalPoints());
return properties;
}
private void addSignificantPoints(FeatureCollection featureCollection, Track track) {
List<TrackPoint> points = track.getAllPoints();
if (points.isEmpty()) return;
// Start point
TrackPoint start = points.get(0);
addPointFeature(featureCollection, start, "start", "Start Point");
// End point
TrackPoint end = points.get(points.size() - 1);
addPointFeature(featureCollection, end, "end", "End Point");
// Highest point
points.stream()
.max(Comparator.comparing(p -> p.getElevation().orElse(Double.MIN_VALUE)))
.ifPresent(highest -> addPointFeature(featureCollection, highest, "highest", "Highest Point"));
}
private void addPointFeature(FeatureCollection featureCollection, TrackPoint point, 
String id, String name) {
Point geoPoint = new Point(new Coordinate(point.getLongitude(), point.getLatitude()));
Map<String, Object> properties = new HashMap<>();
properties.put("name", name);
point.getElevation().ifPresent(ele -> properties.put("elevation", ele));
Feature pointFeature = new Feature(geoPoint, properties, id, null);
featureCollection.addFeature(pointFeature);
}
}

REST Controller

@RestController
@RequestMapping("/api/gpx")
@CrossOrigin(origins = "*")
public class GpxController {
private final GpxParser gpxParser;
private final TrackAnalysisService analysisService;
private final GeoJsonExportService exportService;
public GpxController(GpxParser gpxParser, TrackAnalysisService analysisService,
GeoJsonExportService exportService) {
this.gpxParser = gpxParser;
this.analysisService = analysisService;
this.exportService = exportService;
}
@PostMapping("/upload")
public ResponseEntity<GpxUploadResponse> uploadGpxFile(@RequestParam("file") MultipartFile file) {
try {
GpxFile gpxFile = gpxParser.parse(file.getInputStream());
GpxUploadResponse response = new GpxUploadResponse();
response.setFileName(file.getOriginalFilename());
response.setFileSize(file.getSize());
response.setTrackCount(gpxFile.getTracks().size());
response.setWaypointCount(gpxFile.getWaypoints().size());
response.setRouteCount(gpxFile.getRoutes().size());
// Calculate statistics for the first track
if (!gpxFile.getTracks().isEmpty()) {
Track track = gpxFile.getTracks().get(0);
TrackStatistics stats = analysisService.calculateStatistics(track);
response.setStatistics(stats);
}
return ResponseEntity.ok(response);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new GpxUploadResponse("Failed to parse GPX file: " + e.getMessage()));
}
}
@PostMapping("/analyze")
public ResponseEntity<TrackStatistics> analyzeTrack(@RequestParam("file") MultipartFile file) {
try {
GpxFile gpxFile = gpxParser.parse(file.getInputStream());
if (gpxFile.getTracks().isEmpty()) {
return ResponseEntity.badRequest().build();
}
Track track = gpxFile.getTracks().get(0);
TrackStatistics stats = analysisService.calculateStatistics(track);
return ResponseEntity.ok(stats);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
}
@PostMapping("/export/geojson")
public ResponseEntity<String> exportToGeoJson(@RequestParam("file") MultipartFile file) {
try {
GpxFile gpxFile = gpxParser.parse(file.getInputStream());
if (gpxFile.getTracks().isEmpty()) {
return ResponseEntity.badRequest().body("No tracks found in GPX file");
}
Track track = gpxFile.getTracks().get(0);
String geoJson = exportService.exportTrackToGeoJson(track);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(geoJson);
} catch (Exception e) {
return ResponseEntity.badRequest().body("Error: " + e.getMessage());
}
}
@GetMapping("/sample")
public ResponseEntity<GpxFile> getSampleGpx() {
try {
// Load sample GPX from resources
ClassPathResource resource = new ClassPathResource("sample/sample.gpx");
GpxFile gpxFile = gpxParser.parse(resource.getInputStream());
return ResponseEntity.ok(gpxFile);
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
}
public class GpxUploadResponse {
private String fileName;
private long fileSize;
private int trackCount;
private int waypointCount;
private int routeCount;
private TrackStatistics statistics;
private String error;
public GpxUploadResponse() {}
public GpxUploadResponse(String error) {
this.error = error;
}
// Getters and setters...
}

Testing

1. Unit Tests
@ExtendWith(MockitoExtension.class)
class Dom4jGpxParserTest {
@InjectMocks
private Dom4jGpxParser gpxParser;
@Test
void testParseSimpleTrack() throws Exception {
// Given
String gpxContent = """
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="TestCreator">
<trk>
<name>Test Track</name>
<trkseg>
<trkpt lat="40.7128" lon="-74.0060">
<ele>10.5</ele>
<time>2023-07-20T10:30:00Z</time>
</trkpt>
<trkpt lat="40.7138" lon="-74.0070">
<ele>12.0</ele>
<time>2023-07-20T10:31:00Z</time>
</trkpt>
</trkseg>
</trk>
</gpx>
""";
// When
GpxFile gpxFile = gpxParser.parse(gpxContent);
// Then
assertThat(gpxFile.getTracks()).hasSize(1);
Track track = gpxFile.getTracks().get(0);
assertThat(track.getName()).isEqualTo("Test Track");
assertThat(track.getTotalPoints()).isEqualTo(2);
TrackPoint firstPoint = track.getAllPoints().get(0);
assertThat(firstPoint.getLatitude()).isEqualTo(40.7128);
assertThat(firstPoint.getLongitude()).isEqualTo(-74.0060);
assertThat(firstPoint.getElevation()).contains(10.5);
}
@Test
void testParseInvalidGpx() {
// Given
String invalidContent = "Invalid GPX content";
// When & Then
assertThrows(GpxParseException.class, () -> gpxParser.parse(invalidContent));
}
}
@SpringBootTest
class TrackAnalysisServiceTest {
@Autowired
private TrackAnalysisService analysisService;
@Test
void testCalculateDistance() {
// Given
TrackPoint nyc = new TrackPoint.Builder(40.7128, -74.0060).build();
TrackPoint la = new TrackPoint.Builder(34.0522, -118.2437).build();
// When
double distance = analysisService.calculateDistance(nyc, la);
// Then
assertThat(distance).isGreaterThan(3900); // NYC to LA is about 3940 km
assertThat(distance).isLessThan(4000);
}
@Test
void testCalculateStatistics() {
// Given
Track track = new Track();
TrackSegment segment = new TrackSegment();
// Create points with elevation and time
segment.addPoint(new TrackPoint.Builder(40.7128, -74.0060)
.elevation(10.0)
.time(LocalDateTime.of(2023, 7, 20, 10, 0))
.build());
segment.addPoint(new TrackPoint.Builder(40.7138, -74.0070)
.elevation(15.0)
.time(LocalDateTime.of(2023, 7, 20, 10, 10))
.build());
track.addSegment(segment);
// When
TrackStatistics stats = analysisService.calculateStatistics(track);
// Then
assertThat(stats.getTotalDistance()).isGreaterThan(0);
assertThat(stats.getElevationGain()).isEqualTo(5.0);
}
}
2. Integration Test
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class GpxControllerIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void testUploadAndAnalyzeGpx() {
// Given
ClassPathResource resource = new ClassPathResource("sample/sample.gpx");
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", resource);
// When
ResponseEntity<GpxUploadResponse> response = restTemplate.postForEntity(
"/api/gpx/upload", body, GpxUploadResponse.class);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getTrackCount()).isGreaterThan(0);
}
}

Sample GPX File

<!-- src/test/resources/sample/sample.gpx -->
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="Garmin Connect" 
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
xmlns="http://www.topografix.com/GPX/1/1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<metadata>
<name>Morning Run</name>
<desc>5km training run</desc>
<author>
<name>John Doe</name>
</author>
<time>2023-07-20T06:30:00Z</time>
<bounds minlat="40.7100" minlon="-74.0100" maxlat="40.7200" maxlon="-74.0000"/>
</metadata>
<trk>
<name>Morning Run</name>
<type>Running</type>
<trkseg>
<trkpt lat="40.7128" lon="-74.0060">
<ele>10.5</ele>
<time>2023-07-20T06:30:00Z</time>
<extensions>
<gpxtpx:TrackPointExtension>
<gpxtpx:hr>120</gpxtpx:hr>
</gpxtpx:TrackPointExtension>
</extensions>
</trkpt>
<trkpt lat="40.7138" lon="-74.0070">
<ele>12.0</ele>
<time>2023-07-20T06:31:00Z</time>
</trkpt>
<trkpt lat="40.7148" lon="-74.0080">
<ele>11.5</ele>
<time>2023-07-20T06:32:00Z</time>
</trkpt>
</trkseg>
</trk>
<wpt lat="40.7128" lon="-74.0060">
<name>Start</name>
<sym>Flag</sym>
</wpt>
<wpt lat="40.7148" lon="-74.0080">
<name>Finish</name>
<sym>Flag</sym>
</wpt>
</gpx>

Best Practices

  1. Error Handling: Comprehensive exception handling for malformed GPX files
  2. Memory Management: Stream processing for large GPX files
  3. Validation: Schema validation for GPX files
  4. Performance: Caching for frequently accessed files
  5. Logging: Detailed logging for debugging parsing issues
// Example of streaming parser for large files
@Component
public class StreamingGpxParser {
public void parseLargeFile(File gpxFile, Consumer<TrackPoint> pointConsumer) throws Exception {
SAXReader reader = new SAXReader();
reader.addHandler("/gpx/trk/trkseg/trkpt", 
new ElementHandler() {
@Override
public void onStart(ElementPath path) {
// Not used
}
@Override
public void onEnd(ElementPath path) {
Element trkpt = path.getCurrent();
try {
TrackPoint point = parseTrackPoint(trkpt);
pointConsumer.accept(point);
} catch (Exception e) {
logger.warn("Failed to parse track point", e);
}
trkpt.detach();
}
});
reader.read(gpxFile);
}
}

Conclusion

This GPX parser implementation provides:

  • Complete GPX 1.1 specification support for tracks, waypoints, and routes
  • Robust parsing with multiple XML processing approaches
  • Comprehensive track analysis with distance, elevation, and speed calculations
  • GeoJSON export for interoperability with mapping libraries
  • REST API for easy integration with web applications
  • Comprehensive testing with unit and integration tests

The parser can handle real-world GPX files from various GPS devices and applications, making it suitable for fitness tracking, outdoor activity analysis, and geographic data processing applications.

Java Observability, Logging Intelligence & AI-Driven Monitoring (APM, Tracing, Logs & Anomaly Detection)

https://macronepal.com/blog/beyond-metrics-observing-serverless-and-traditional-java-applications-with-thundra-apm/
Explains using Thundra APM to observe both serverless and traditional Java applications by combining tracing, metrics, and logs into a unified observability platform for faster debugging and performance insights.

https://macronepal.com/blog/dynatrace-oneagent-in-java-2/
Explains Dynatrace OneAgent for Java, which automatically instruments JVM applications to capture metrics, traces, and logs, enabling full-stack monitoring and root-cause analysis with minimal configuration.

https://macronepal.com/blog/lightstep-java-sdk-distributed-tracing-and-observability-implementation/
Explains Lightstep Java SDK for distributed tracing, helping developers track requests across microservices and identify latency issues using OpenTelemetry-based observability.

https://macronepal.com/blog/honeycomb-io-beeline-for-java-complete-guide-2/
Explains Honeycomb Beeline for Java, which provides high-cardinality observability and deep query capabilities to understand complex system behavior and debug distributed systems efficiently.

https://macronepal.com/blog/lumigo-for-serverless-in-java-complete-distributed-tracing-guide-2/
Explains Lumigo for Java serverless applications, offering automatic distributed tracing, log correlation, and error tracking to simplify debugging in cloud-native environments. (Lumigo Docs)

https://macronepal.com/blog/from-noise-to-signals-implementing-log-anomaly-detection-in-java-applications/
Explains how to detect anomalies in Java logs using behavioral patterns and machine learning techniques to separate meaningful incidents from noisy log data and improve incident response.

https://macronepal.com/blog/ai-powered-log-analysis-in-java-from-reactive-debugging-to-proactive-insights/
Explains AI-driven log analysis for Java applications, shifting from manual debugging to predictive insights that identify issues early and improve system reliability using intelligent log processing.

https://macronepal.com/blog/titliel-java-logging-best-practices/
Explains best practices for Java logging, focusing on structured logs, proper log levels, performance optimization, and ensuring logs are useful for debugging and observability systems.

https://macronepal.com/blog/seeking-a-loguru-for-java-the-quest-for-elegant-and-simple-logging/
Explains the search for simpler, more elegant logging frameworks in Java, comparing modern logging approaches that aim to reduce complexity while improving readability and developer experience.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper