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
- Error Handling: Comprehensive exception handling for malformed GPX files
- Memory Management: Stream processing for large GPX files
- Validation: Schema validation for GPX files
- Performance: Caching for frequently accessed files
- 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.