0001 /*
0002 * Java GPX Library (jpx-3.1.0).
0003 * Copyright (c) 2016-2023 Franz Wilhelmstötter
0004 *
0005 * Licensed under the Apache License, Version 2.0 (the "License");
0006 * you may not use this file except in compliance with the License.
0007 * You may obtain a copy of the License at
0008 *
0009 * http://www.apache.org/licenses/LICENSE-2.0
0010 *
0011 * Unless required by applicable law or agreed to in writing, software
0012 * distributed under the License is distributed on an "AS IS" BASIS,
0013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0014 * See the License for the specific language governing permissions and
0015 * limitations under the License.
0016 *
0017 * Author:
0018 * Franz Wilhelmstötter (franz.wilhelmstoetter@gmail.com)
0019 */
0020 package io.jenetics.jpx;
0021
0022 import static java.lang.String.format;
0023 import static java.nio.charset.StandardCharsets.UTF_8;
0024 import static java.util.Locale.ENGLISH;
0025 import static java.util.Objects.hash;
0026 import static java.util.Objects.requireNonNull;
0027 import static io.jenetics.jpx.Lists.copyOf;
0028 import static io.jenetics.jpx.Lists.copyTo;
0029
0030 import java.io.ByteArrayInputStream;
0031 import java.io.ByteArrayOutputStream;
0032 import java.io.DataInput;
0033 import java.io.DataInputStream;
0034 import java.io.DataOutput;
0035 import java.io.DataOutputStream;
0036 import java.io.File;
0037 import java.io.IOException;
0038 import java.io.InputStream;
0039 import java.io.InputStreamReader;
0040 import java.io.InvalidObjectException;
0041 import java.io.ObjectInputStream;
0042 import java.io.OutputStream;
0043 import java.io.OutputStreamWriter;
0044 import java.io.Serial;
0045 import java.io.Serializable;
0046 import java.io.UncheckedIOException;
0047 import java.net.URI;
0048 import java.nio.file.Files;
0049 import java.nio.file.Path;
0050 import java.nio.file.Paths;
0051 import java.text.NumberFormat;
0052 import java.time.Instant;
0053 import java.util.ArrayList;
0054 import java.util.List;
0055 import java.util.Objects;
0056 import java.util.Optional;
0057 import java.util.function.Consumer;
0058 import java.util.function.Function;
0059 import java.util.function.Predicate;
0060 import java.util.stream.Stream;
0061
0062 import javax.xml.stream.XMLStreamException;
0063 import javax.xml.stream.XMLStreamReader;
0064 import javax.xml.stream.XMLStreamWriter;
0065 import javax.xml.transform.Result;
0066 import javax.xml.transform.Source;
0067 import javax.xml.transform.stream.StreamResult;
0068 import javax.xml.transform.stream.StreamSource;
0069
0070 import org.w3c.dom.Document;
0071
0072 /**
0073 * GPX documents contain a metadata header, followed by way-points, routes, and
0074 * tracks. You can add your own elements to the extensions section of the GPX
0075 * document.
0076 * <p>
0077 * <em><b>Examples:</b></em>
0078 * <p>
0079 * <b>Creating a GPX object with one track-segment and 3 track-points</b>
0080 * <pre>{@code
0081 * final GPX gpx = GPX.builder()
0082 * .addTrack(track -> track
0083 * .addSegment(segment -> segment
0084 * .addPoint(p -> p.lat(48.20100).lon(16.31651).ele(283))
0085 * .addPoint(p -> p.lat(48.20112).lon(16.31639).ele(278))
0086 * .addPoint(p -> p.lat(48.20126).lon(16.31601).ele(274))))
0087 * .build();
0088 * }</pre>
0089 *
0090 * <b>Writing a GPX file</b>
0091 * <pre>{@code
0092 * final var indent = new GPX.Writer.Indent(" ");
0093 * GPX.Writer.of(indent).write(gpx, Path.of("points.gpx"));
0094 * }</pre>
0095 *
0096 * This will produce the following output.
0097 * <pre>{@code
0098 * <gpx version="1.1" creator="JPX - https://github.com/jenetics/jpx" xmlns="http://www.topografix.com/GPX/1/1">
0099 * <trk>
0100 * <trkseg>
0101 * <trkpt lat="48.201" lon="16.31651">
0102 * <ele>283</ele>
0103 * </trkpt>
0104 * <trkpt lat="48.20112" lon="16.31639">
0105 * <ele>278</ele>
0106 * </trkpt>
0107 * <trkpt lat="48.20126" lon="16.31601">
0108 * <ele>274</ele>
0109 * </trkpt>
0110 * </trkseg>
0111 * </trk>
0112 * </gpx>
0113 * }</pre>
0114 *
0115 * <b>Reading a GPX file</b>
0116 * <pre>{@code
0117 * final GPX gpx = GPX.read("points.xml");
0118 * }</pre>
0119 *
0120 * <b>Reading erroneous GPX files</b>
0121 * <pre>{@code
0122 * final GPX gpx = GPX.Reader.of(GPX.Reader.Mode.LENIENT).read("track.xml");
0123 * }</pre>
0124 *
0125 * This allows to read otherwise invalid GPX files, like
0126 * <pre>{@code
0127 * <?xml version="1.0" encoding="UTF-8"?>
0128 * <gpx version="1.1" creator="GPSBabel - http://www.gpsbabel.org" xmlns="http://www.topografix.com/GPX/1/1">
0129 * <metadata>
0130 * <time>2019-12-31T21:36:04.134Z</time>
0131 * <bounds minlat="48.175186667" minlon="16.299580000" maxlat="48.199555000" maxlon="16.416933333"/>
0132 * </metadata>
0133 * <trk>
0134 * <trkseg>
0135 * <trkpt lat="48.184298333" lon="16.299580000">
0136 * <ele>0.000</ele>
0137 * <time>2011-03-20T09:47:16Z</time>
0138 * <geoidheight>43.5</geoidheight>
0139 * <fix>2d</fix>
0140 * <sat>3</sat>
0141 * <hdop>4.200000</hdop>
0142 * <vdop>1.000000</vdop>
0143 * <pdop>4.300000</pdop>
0144 * </trkpt>
0145 * <trkpt lat="48.175186667" lon="16.303916667">
0146 * <ele>0.000</ele>
0147 * <time>2011-03-20T09:51:31Z</time>
0148 * <geoidheight>43.5</geoidheight>
0149 * <fix>2d</fix>
0150 * <sat>3</sat>
0151 * <hdop>16.600000</hdop>
0152 * <vdop>0.900000</vdop>
0153 * <pdop>16.600000</pdop>
0154 * </trkpt>
0155 * </trkseg>
0156 * </trk>
0157 * </gpx>
0158 * }</pre>
0159 *
0160 * which is read as (if you write it again)
0161 * <pre>{@code
0162 * <?xml version="1.0" encoding="UTF-8"?>
0163 * <gpx version="1.1" creator="GPSBabel - http://www.gpsbabel.org" xmlns="http://www.topografix.com/GPX/1/1">
0164 * <metadata>
0165 * <time>2019-12-31T21:36:04.134Z</time>
0166 * <bounds minlat="48.175187" minlon="16.29958" maxlat="48.199555" maxlon="16.416933"></bounds>
0167 * </metadata>
0168 * <trk>
0169 * <trkseg>
0170 * <trkpt lat="48.184298" lon="16.29958">
0171 * <ele>0</ele>
0172 * <time>2011-03-20T09:47:16Z</time>
0173 * <geoidheight>43.5</geoidheight>
0174 * <fix>2d</fix>
0175 * <sat>3</sat>
0176 * <hdop>4.2</hdop>
0177 * <vdop>1</vdop>
0178 * <pdop>4.3</pdop>
0179 * </trkpt>
0180 * <trkpt lat="48.175187" lon="16.303917">
0181 * <ele>0</ele>
0182 * <time>2011-03-20T09:51:31Z</time>
0183 * <geoidheight>43.5</geoidheight>
0184 * <fix>2d</fix>
0185 * <sat>3</sat>
0186 * <hdop>16.6</hdop>
0187 * <vdop>0.9</vdop>
0188 * <pdop>16.6</pdop>
0189 * </trkpt>
0190 * </trkseg>
0191 * </trk>
0192 * </gpx>
0193 * }</pre>
0194 *
0195 * <b>Converting a GPX object to an XML {@link Document}</b>
0196 * <pre>{@code
0197 * final GPX gpx = ...;
0198 *
0199 * final Document doc = XMLProvider.provider()
0200 * .documentBuilderFactory()
0201 * .newDocumentBuilder()
0202 * .newDocument();
0203 *
0204 * // The GPX data are written to the empty `doc` object.
0205 * GPX.Writer.DEFAULT.write(gpx, new DOMResult(doc));
0206 * }</pre>
0207 *
0208 * @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
0209 * @version 2.0
0210 * @since 1.0
0211 */
0212 public final class GPX implements Serializable {
0213
0214 @Serial
0215 private static final long serialVersionUID = 2L;
0216
0217 /**
0218 * Represents the available GPX versions.
0219 *
0220 * @version 1.3
0221 * @since 1.3
0222 */
0223 public enum Version {
0224
0225 /**
0226 * The GPX version 1.0. This version can be read and written.
0227 *
0228 * @see <a href="http://www.topografix.com/gpx_manual.asp">GPX 1.0</a>
0229 */
0230 V10("1.0", "http://www.topografix.com/GPX/1/0"),
0231
0232 /**
0233 * The GPX version 1.1. This is the default version and can be read and
0234 * written.
0235 *
0236 * @see <a href="http://www.topografix.com/GPX/1/1">GPX 1.1</a>
0237 */
0238 V11("1.1", "http://www.topografix.com/GPX/1/1");
0239
0240 private final String _value;
0241 private final String _namespaceURI;
0242
0243 Version(final String value, final String namespaceURI) {
0244 _value = value;
0245 _namespaceURI = namespaceURI;
0246 }
0247
0248 /**
0249 * Return the version string value.
0250 *
0251 * @return the version string value
0252 */
0253 public String getValue() {
0254 return _value;
0255 }
0256
0257 /**
0258 * Return the namespace URI of this version.
0259 *
0260 * @since 1.5
0261 *
0262 * @return the namespace URI of this version
0263 */
0264 public String getNamespaceURI() {
0265 return _namespaceURI;
0266 }
0267
0268 /**
0269 * Return the version from the given {@code version} string. Allowed
0270 * values are "1.0" and "1.1".
0271 *
0272 * @param version the version string
0273 * @return the version from the given {@code version} string
0274 * @throws IllegalArgumentException if the given {@code version} string
0275 * is neither "1.0" nor "1.1"
0276 * @throws NullPointerException if the given {@code version} string is
0277 * {@code null}
0278 */
0279 public static Version of(final String version) {
0280 return switch (version) {
0281 case "1.0" -> V10;
0282 case "1.1" -> V11;
0283 default -> throw new IllegalArgumentException(format(
0284 "Unknown version string: '%s'.", version
0285 ));
0286 };
0287 }
0288 }
0289
0290 private static final String _CREATOR = "JPX - https://github.com/jenetics/jpx";
0291
0292 private final String _creator;
0293 private final Version _version;
0294 private final Metadata _metadata;
0295 private final List<WayPoint> _wayPoints;
0296 private final List<Route> _routes;
0297 private final List<Track> _tracks;
0298 private final Document _extensions;
0299
0300 /**
0301 * Create a new {@code GPX} object with the given data.
0302 *
0303 * @param creator the name or URL of the software that created your GPX
0304 * document. This allows others to inform the creator of a GPX
0305 * instance document that fails to validate.
0306 * @param version the GPX version
0307 * @param metadata the metadata about the GPS file
0308 * @param wayPoints the way-points
0309 * @param routes the routes
0310 * @param tracks the tracks
0311 * @param extensions the XML extensions document
0312 * @throws NullPointerException if the {@code creator} or {@code version} is
0313 * {@code null}
0314 */
0315 private GPX(
0316 final Version version,
0317 final String creator,
0318 final Metadata metadata,
0319 final List<WayPoint> wayPoints,
0320 final List<Route> routes,
0321 final List<Track> tracks,
0322 final Document extensions
0323 ) {
0324 _version = requireNonNull(version);
0325 _creator = requireNonNull(creator);
0326 _metadata = metadata;
0327 _wayPoints = copyOf(wayPoints);
0328 _routes = copyOf(routes);
0329 _tracks = copyOf(tracks);
0330 _extensions = extensions;
0331 }
0332
0333 /**
0334 * Return the version number of the GPX file.
0335 *
0336 * @return the version number of the GPX file
0337 */
0338 public String getVersion() {
0339 return _version._value;
0340 }
0341
0342 /**
0343 * Return the name or URL of the software that created your GPX document.
0344 * This allows others to inform the creator of a GPX instance document that
0345 * fails to validate.
0346 *
0347 * @return the name or URL of the software that created your GPX document
0348 */
0349 public String getCreator() {
0350 return _creator;
0351 }
0352
0353 /**
0354 * Return the metadata of the GPX file.
0355 *
0356 * @return the metadata of the GPX file
0357 */
0358 public Optional<Metadata> getMetadata() {
0359 return Optional.ofNullable(_metadata);
0360 }
0361
0362 /**
0363 * Return an unmodifiable list of the {@code GPX} way-points.
0364 *
0365 * @return an unmodifiable list of the {@code GPX} way-points.
0366 */
0367 public List<WayPoint> getWayPoints() {
0368 return _wayPoints;
0369 }
0370
0371 /**
0372 * Return a stream with all {@code WayPoint}s of this {@code GPX} object.
0373 *
0374 * @return a stream with all {@code WayPoint}s of this {@code GPX} object
0375 */
0376 public Stream<WayPoint> wayPoints() {
0377 return _wayPoints.stream();
0378 }
0379
0380 /**
0381 * Return an unmodifiable list of the {@code GPX} routes.
0382 *
0383 * @return an unmodifiable list of the {@code GPX} routes.
0384 */
0385 public List<Route> getRoutes() {
0386 return _routes;
0387 }
0388
0389 /**
0390 * Return a stream of the {@code GPX} routes.
0391 *
0392 * @return a stream of the {@code GPX} routes.
0393 */
0394 public Stream<Route> routes() {
0395 return _routes.stream();
0396 }
0397
0398 /**
0399 * Return an unmodifiable list of the {@code GPX} tracks.
0400 *
0401 * @return an unmodifiable list of the {@code GPX} tracks.
0402 */
0403 public List<Track> getTracks() {
0404 return _tracks;
0405 }
0406
0407 /**
0408 * Return a stream of the {@code GPX} tracks.
0409 *
0410 * @return a stream of the {@code GPX} tracks.
0411 */
0412 public Stream<Track> tracks() {
0413 return _tracks.stream();
0414 }
0415
0416 /**
0417 * Return the (cloned) extensions document. The root element of the returned
0418 * document has the name {@code extensions}.
0419 * <pre>{@code
0420 * <extensions>
0421 * ...
0422 * </extensions>
0423 * }</pre>
0424 *
0425 * @since 1.5
0426 *
0427 * @return the extensions document
0428 */
0429 public Optional<Document> getExtensions() {
0430 return Optional.ofNullable(_extensions).map(XML::clone);
0431 }
0432
0433 /**
0434 * Convert the <em>immutable</em> GPX object into a <em>mutable</em>
0435 * builder initialized with the current GPX values.
0436 *
0437 * @since 1.1
0438 *
0439 * @return a new track builder initialized with the values of {@code this}
0440 * GPX object
0441 */
0442 public Builder toBuilder() {
0443 return builder(_version, _creator)
0444 .metadata(_metadata)
0445 .wayPoints(_wayPoints)
0446 .routes(_routes)
0447 .tracks(_tracks)
0448 .extensions(_extensions);
0449 }
0450
0451 @Override
0452 public String toString() {
0453 return format(
0454 "GPX[way-points=%s, routes=%s, tracks=%s]",
0455 getWayPoints().size(), getRoutes().size(), getTracks().size()
0456 );
0457 }
0458
0459 @Override
0460 public int hashCode() {
0461 return hash(
0462 _creator,
0463 _version,
0464 _metadata,
0465 _wayPoints,
0466 _routes,
0467 _tracks
0468 );
0469 }
0470
0471 @Override
0472 public boolean equals(final Object obj) {
0473 return obj == this ||
0474 obj instanceof GPX gpx &&
0475 Objects.equals(gpx._creator, _creator) &&
0476 Objects.equals(gpx._version, _version) &&
0477 Objects.equals(gpx._metadata, _metadata) &&
0478 Objects.equals(gpx._wayPoints, _wayPoints) &&
0479 Objects.equals(gpx._routes, _routes) &&
0480 Objects.equals(gpx._tracks, _tracks);
0481 }
0482
0483 /**
0484 * Builder class for creating immutable {@code GPX} objects.
0485 * <p>
0486 * Creating a GPX object with one track-segment and 3 track-points:
0487 * <pre>{@code
0488 * final GPX gpx = GPX.builder()
0489 * .addTrack(track -> track
0490 * .addSegment(segment -> segment
0491 * .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
0492 * .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))
0493 * .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162))))
0494 * .build();
0495 * }</pre>
0496 */
0497 public static final class Builder {
0498 private String _creator;
0499 private Version _version;
0500 private Metadata _metadata;
0501 private final List<WayPoint> _wayPoints = new ArrayList<>();
0502 private final List<Route> _routes = new ArrayList<>();
0503 private final List<Track> _tracks = new ArrayList<>();
0504 private Document _extensions;
0505
0506 private Builder(final Version version, final String creator) {
0507 _version = requireNonNull(version);
0508 _creator = requireNonNull(creator);
0509 }
0510
0511 /**
0512 * Set the GPX creator.
0513 *
0514 * @param creator the GPX creator
0515 * @throws NullPointerException if the given argument is {@code null}
0516 * @return {@code this} {@code Builder} for method chaining
0517 */
0518 public Builder creator(final String creator) {
0519 _creator = requireNonNull(creator);
0520 return this;
0521 }
0522
0523 /**
0524 * Return the current creator value.
0525 *
0526 * @since 1.1
0527 *
0528 * @return the current creator value
0529 */
0530 public String creator() {
0531 return _creator;
0532 }
0533
0534 /**
0535 * Set the GPX version.
0536 *
0537 * @since 1.3
0538 *
0539 * @param version the GPX version
0540 * @throws NullPointerException if the given argument is {@code null}
0541 * @return {@code this} {@code Builder} for method chaining
0542 */
0543 public Builder version(final Version version) {
0544 _version = requireNonNull(version);
0545 return this;
0546 }
0547
0548 /**
0549 * Return the current version value.
0550 *
0551 * @since 1.1
0552 *
0553 * @return the current version value
0554 */
0555 public String version() {
0556 return _version._value;
0557 }
0558
0559 /**
0560 * Set the GPX metadata.
0561 *
0562 * @param metadata the GPX metadata
0563 * @return {@code this} {@code Builder} for method chaining
0564 */
0565 public Builder metadata(final Metadata metadata) {
0566 _metadata = metadata;
0567 return this;
0568 }
0569
0570 /**
0571 * Allows setting partial metadata without messing up with the
0572 * {@link Metadata.Builder} class.
0573 * <pre>{@code
0574 * final GPX gpx = GPX.builder()
0575 * .metadata(md -> md.author("Franz Wilhelmstötter"))
0576 * .addTrack(...)
0577 * .build();
0578 * }</pre>
0579 *
0580 * @param metadata the metadata consumer
0581 * @return {@code this} {@code Builder} for method chaining
0582 * @throws NullPointerException if the given argument is {@code null}
0583 */
0584 public Builder metadata(final Consumer<? super Metadata.Builder> metadata) {
0585 final Metadata.Builder builder = Metadata.builder();
0586 metadata.accept(builder);
0587
0588 final Metadata md = builder.build();
0589 _metadata = md.isEmpty() ? null : md;
0590
0591 return this;
0592 }
0593
0594 /**
0595 * Return the current metadata value.
0596 *
0597 * @since 1.1
0598 *
0599 * @return the current metadata value
0600 */
0601 public Optional<Metadata> metadata() {
0602 return Optional.ofNullable(_metadata);
0603 }
0604
0605 /**
0606 * Sets the way-points of the {@code GPX} object. The list of way-points
0607 * may be {@code null}.
0608 *
0609 * @param wayPoints the {@code GPX} way-points
0610 * @return {@code this} {@code Builder} for method chaining
0611 * @throws NullPointerException if one of the way-points in the list is
0612 * {@code null}
0613 */
0614 public Builder wayPoints(final List<WayPoint> wayPoints) {
0615 copyTo(wayPoints, _wayPoints);
0616 return this;
0617 }
0618
0619 /**
0620 * Add one way-point to the {@code GPX} object.
0621 *
0622 * @param wayPoint the way-point to add
0623 * @return {@code this} {@code Builder} for method chaining
0624 * @throws NullPointerException if the given {@code wayPoint} is
0625 * {@code null}
0626 */
0627 public Builder addWayPoint(final WayPoint wayPoint) {
0628 _wayPoints.add(requireNonNull(wayPoint));
0629 return this;
0630 }
0631
0632 /**
0633 * Add a way-point to the {@code GPX} object using a
0634 * {@link WayPoint.Builder}.
0635 * <pre>{@code
0636 * final GPX gpx = GPX.builder()
0637 * .addWayPoint(wp -> wp.lat(23.6).lon(13.5).ele(50))
0638 * .build();
0639 * }</pre>
0640 *
0641 * @param wayPoint the way-point to add, configured by the way-point
0642 * builder
0643 * @return {@code this} {@code Builder} for method chaining
0644 * @throws NullPointerException if the given argument is {@code null}
0645 */
0646 public Builder addWayPoint(final Consumer<? super WayPoint.Builder> wayPoint) {
0647 final WayPoint.Builder builder = WayPoint.builder();
0648 wayPoint.accept(builder);
0649 return addWayPoint(builder.build());
0650 }
0651
0652 /**
0653 * Return the current way-points. The returned list is mutable.
0654 *
0655 * @since 1.1
0656 *
0657 * @return the current, mutable way-point list
0658 */
0659 public List<WayPoint> wayPoints() {
0660 return new NonNullList<>(_wayPoints);
0661 }
0662
0663 /**
0664 * Sets the routes of the {@code GPX} object. The list of routes may be
0665 * {@code null}.
0666 *
0667 * @param routes the {@code GPX} routes
0668 * @return {@code this} {@code Builder} for method chaining
0669 * @throws NullPointerException if one of the routes is {@code null}
0670 */
0671 public Builder routes(final List<Route> routes) {
0672 copyTo(routes, _routes);
0673 return this;
0674 }
0675
0676 /**
0677 * Add a route the {@code GPX} object.
0678 *
0679 * @param route the route to add
0680 * @return {@code this} {@code Builder} for method chaining
0681 * @throws NullPointerException if the given {@code route} is {@code null}
0682 */
0683 public Builder addRoute(final Route route) {
0684 _routes.add(requireNonNull(route));
0685 return this;
0686 }
0687
0688 /**
0689 * Add a route the {@code GPX} object.
0690 * <pre>{@code
0691 * final GPX gpx = GPX.builder()
0692 * .addRoute(route -> route
0693 * .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
0694 * .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161)))
0695 * .build();
0696 * }</pre>
0697 *
0698 * @param route the route to add, configured by the route builder
0699 * @return {@code this} {@code Builder} for method chaining
0700 * @throws NullPointerException if the given argument is {@code null}
0701 */
0702 public Builder addRoute(final Consumer<? super Route.Builder> route) {
0703 final Route.Builder builder = Route.builder();
0704 route.accept(builder);
0705 return addRoute(builder.build());
0706 }
0707
0708 /**
0709 * Return the current routes. The returned list is mutable.
0710 *
0711 * @since 1.1
0712 *
0713 * @return the current, mutable route list
0714 */
0715 public List<Route> routes() {
0716 return new NonNullList<>(_routes);
0717 }
0718
0719 /**
0720 * Sets the tracks of the {@code GPX} object. The list of tracks may be
0721 * {@code null}.
0722 *
0723 * @param tracks the {@code GPX} tracks
0724 * @return {@code this} {@code Builder} for method chaining
0725 * @throws NullPointerException if one of the tracks is {@code null}
0726 */
0727 public Builder tracks(final List<Track> tracks) {
0728 copyTo(tracks, _tracks);
0729 return this;
0730 }
0731
0732 /**
0733 * Add a track the {@code GPX} object.
0734 *
0735 * @param track the track to add
0736 * @return {@code this} {@code Builder} for method chaining
0737 * @throws NullPointerException if the given {@code track} is {@code null}
0738 */
0739 public Builder addTrack(final Track track) {
0740 _tracks.add(requireNonNull(track));
0741 return this;
0742 }
0743
0744 /**
0745 * Add a track the {@code GPX} object.
0746 * <pre>{@code
0747 * final GPX gpx = GPX.builder()
0748 * .addTrack(track -> track
0749 * .addSegment(segment -> segment
0750 * .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
0751 * .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))
0752 * .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162))))
0753 * .build();
0754 * }</pre>
0755 *
0756 * @param track the track to add, configured by the track builder
0757 * @return {@code this} {@code Builder} for method chaining
0758 * @throws NullPointerException if the given argument is {@code null}
0759 */
0760 public Builder addTrack(final Consumer<? super Track.Builder> track) {
0761 final Track.Builder builder = Track.builder();
0762 track.accept(builder);
0763 return addTrack(builder.build());
0764 }
0765
0766 /**
0767 * Return the current tracks. The returned list is mutable.
0768 *
0769 * @since 1.1
0770 *
0771 * @return the current, mutable track list
0772 */
0773 public List<Track> tracks() {
0774 return new NonNullList<>(_tracks);
0775 }
0776
0777
0778 /**
0779 * Sets the extensions object, which may be {@code null}. The root
0780 * element of the extensions document must be {@code extensions}.
0781 * <pre>{@code
0782 * <extensions>
0783 * ...
0784 * </extensions>
0785 * }</pre>
0786 *
0787 * @since 1.5
0788 *
0789 * @param extensions the extensions document
0790 * @return {@code this} {@code Builder} for method chaining
0791 * @throws IllegalArgumentException if the root element is not the
0792 * an {@code extensions} node
0793 */
0794 public Builder extensions(final Document extensions) {
0795 _extensions = XML.checkExtensions(extensions);
0796 return this;
0797 }
0798
0799 /**
0800 * Return the current extensions
0801 *
0802 * @since 1.5
0803 *
0804 * @return the extensions document
0805 */
0806 public Optional<Document> extensions() {
0807 return Optional.ofNullable(_extensions);
0808 }
0809
0810 /**
0811 * Create an immutable {@code GPX} object from the current builder state.
0812 *
0813 * @return an immutable {@code GPX} object from the current builder state
0814 */
0815 public GPX build() {
0816 return of(
0817 _version,
0818 _creator,
0819 _metadata,
0820 _wayPoints,
0821 _routes,
0822 _tracks,
0823 _extensions
0824 );
0825 }
0826
0827 /**
0828 * Return a new {@link WayPoint} filter.
0829 * <pre>{@code
0830 * final GPX filtered = gpx.toBuilder()
0831 * .wayPointFilter()
0832 * .filter(wp -> wp.getTime().isPresent())
0833 * .build())
0834 * .build();
0835 * }</pre>
0836 *
0837 * @since 1.1
0838 *
0839 * @return a new {@link WayPoint} filter
0840 */
0841 public Filter<WayPoint, Builder> wayPointFilter() {
0842 return new Filter<>() {
0843 @Override
0844 public Filter<WayPoint, Builder> filter(
0845 final Predicate<? super WayPoint> predicate
0846 ) {
0847 wayPoints(_wayPoints.stream().filter(predicate).toList());
0848 return this;
0849 }
0850
0851 @Override
0852 public Filter<WayPoint, Builder> map(
0853 final Function<? super WayPoint, ? extends WayPoint> mapper
0854 ) {
0855 wayPoints(
0856 _wayPoints.stream()
0857 .map(mapper)
0858 .map(WayPoint.class::cast)
0859 .toList()
0860 );
0861
0862 return this;
0863 }
0864
0865 @Override
0866 public Filter<WayPoint, Builder> flatMap(
0867 final Function<
0868 ? super WayPoint,
0869 ? extends List<WayPoint>> mapper
0870 ) {
0871 wayPoints(
0872 _wayPoints.stream()
0873 .flatMap(wp -> mapper.apply(wp).stream())
0874 .toList()
0875 );
0876
0877 return this;
0878 }
0879
0880 @Override
0881 public Filter<WayPoint, Builder> listMap(
0882 final Function<
0883 ? super List<WayPoint>,
0884 ? extends List<WayPoint>> mapper
0885 ) {
0886 wayPoints(mapper.apply(_wayPoints));
0887
0888 return this;
0889 }
0890
0891 @Override
0892 public Builder build() {
0893 return GPX.Builder.this;
0894 }
0895
0896 };
0897 }
0898
0899 /**
0900 * Return a new {@link Route} filter.
0901 * <pre>{@code
0902 * final GPX filtered = gpx.toBuilder()
0903 * .routeFilter()
0904 * .filter(Route::nonEmpty)
0905 * .build())
0906 * .build();
0907 * }</pre>
0908 *
0909 * @since 1.1
0910 *
0911 * @return a new {@link Route} filter
0912 */
0913 public Filter<Route, Builder> routeFilter() {
0914 return new Filter<>() {
0915 @Override
0916 public Filter<Route, Builder> filter(
0917 final Predicate<? super Route> predicate
0918 ) {
0919 routes(
0920 _routes.stream()
0921 .filter(predicate)
0922 .toList()
0923 );
0924
0925 return this;
0926 }
0927
0928 @Override
0929 public Filter<Route, Builder> map(
0930 final Function<? super Route, ? extends Route> mapper
0931 ) {
0932 routes(
0933 _routes.stream()
0934 .map(mapper)
0935 .map(Route.class::cast)
0936 .toList()
0937 );
0938
0939 return this;
0940 }
0941
0942 @Override
0943 public Filter<Route, Builder> flatMap(
0944 final Function<? super Route, ? extends List<Route>> mapper)
0945 {
0946 routes(
0947 _routes.stream()
0948 .flatMap(route -> mapper.apply(route).stream())
0949 .toList()
0950 );
0951
0952 return this;
0953 }
0954
0955 @Override
0956 public Filter<Route, Builder> listMap(
0957 final Function<
0958 ? super List<Route>,
0959 ? extends List<Route>> mapper
0960 ) {
0961 routes(mapper.apply(_routes));
0962
0963 return this;
0964 }
0965
0966 @Override
0967 public Builder build() {
0968 return GPX.Builder.this;
0969 }
0970
0971 };
0972 }
0973
0974 /**
0975 * Return a new {@link Track} filter.
0976 * <pre>{@code
0977 * final GPX merged = gpx.toBuilder()
0978 * .trackFilter()
0979 * .map(track -> track.toBuilder()
0980 * .listMap(Filters::mergeSegments)
0981 * .filter(TrackSegment::nonEmpty)
0982 * .build())
0983 * .build()
0984 * .build();
0985 * }</pre>
0986 *
0987 * @since 1.1
0988 *
0989 * @return a new {@link Track} filter
0990 */
0991 public Filter<Track, Builder> trackFilter() {
0992 return new Filter<>() {
0993 @Override
0994 public Filter<Track, Builder> filter(
0995 final Predicate<? super Track> predicate
0996 ) {
0997 tracks(_tracks.stream().filter(predicate).toList());
0998 return this;
0999 }
1000
1001 @Override
1002 public Filter<Track, Builder> map(
1003 final Function<? super Track, ? extends Track> mapper
1004 ) {
1005 tracks(
1006 _tracks.stream()
1007 .map(mapper)
1008 .map(Track.class::cast)
1009 .toList()
1010 );
1011
1012 return this;
1013 }
1014
1015 @Override
1016 public Filter<Track, Builder> flatMap(
1017 final Function<? super Track, ? extends List<Track>> mapper
1018 ) {
1019 tracks(
1020 _tracks.stream()
1021 .flatMap(track -> mapper.apply(track).stream())
1022 .toList()
1023 );
1024
1025 return this;
1026 }
1027
1028 @Override
1029 public Filter<Track, Builder> listMap(
1030 final Function<
1031 ? super List<Track>,
1032 ? extends List<Track>> mapper
1033 ) {
1034 tracks(mapper.apply(_tracks));
1035
1036 return this;
1037 }
1038
1039 @Override
1040 public Builder build() {
1041 return GPX.Builder.this;
1042 }
1043
1044 };
1045 }
1046
1047 }
1048
1049 /**
1050 * Create a new GPX builder with the given GPX version and creator string.
1051 *
1052 * @since 1.3
1053 *
1054 * @param version the GPX version
1055 * @param creator the GPX creator
1056 * @return new GPX builder
1057 * @throws NullPointerException if one of the arguments is {@code null}
1058 */
1059 public static Builder builder(final Version version, final String creator) {
1060 return new Builder(version, creator);
1061 }
1062
1063
1064 /**
1065 * Create a new GPX builder with the given GPX creator string.
1066 *
1067 * @param creator the GPX creator
1068 * @return new GPX builder
1069 * @throws NullPointerException if the given arguments is {@code null}
1070 */
1071 public static Builder builder(final String creator) {
1072 return builder(Version.V11, creator);
1073 }
1074
1075 /**
1076 * Create a new GPX builder.
1077 *
1078 * @return new GPX builder
1079 */
1080 public static Builder builder() {
1081 return builder(Version.V11, _CREATOR);
1082 }
1083
1084
1085 /**
1086 * Class for reading GPX files. A reader instance can be created by the
1087 * {@code GPX.reader} factory methods.
1088 *
1089 * @version 3.0
1090 * @since 1.3
1091 */
1092 public static final class Reader {
1093
1094 /**
1095 * The possible GPX reader modes.
1096 *
1097 * @version 1.3
1098 * @since 1.3
1099 */
1100 public enum Mode {
1101
1102 /**
1103 * In this mode the GPX reader tries to ignore invalid GPX values
1104 * and elements.
1105 */
1106 LENIENT,
1107
1108 /**
1109 * Expects to read valid GPX files.
1110 */
1111 STRICT
1112 }
1113
1114 /**
1115 * The <em>default </em>GPX reader, reading GPX files (v1.1) with
1116 * reading mode {@link Mode#STRICT}.
1117 *
1118 * @since 3.0
1119 */
1120 public static final Reader DEFAULT = Reader.of(Version.V11, Mode.STRICT);
1121
1122 private final Version _version;
1123 private final Mode _mode;
1124
1125 private Reader(final Version version, final Mode mode) {
1126 _version = requireNonNull(version);
1127 _mode = requireNonNull(mode);
1128 }
1129
1130 /**
1131 * Return the GPX version {@code this} reader is able to read.
1132 *
1133 * @return the GPX version of {@code this} reader
1134 */
1135 public Version version() {
1136 return _version;
1137 }
1138
1139 /**
1140 * Return the current reader mode.
1141 *
1142 * @return the current reader mode
1143 */
1144 public Mode mode() {
1145 return _mode;
1146 }
1147
1148 /**
1149 * Read a GPX object from the given input {@code source}. This is the
1150 * most general method for reading a {@code GPX} object.
1151 *
1152 * @since 3.0
1153 *
1154 * @param source the input source from where the GPX date is read
1155 * @return the GPX object read from the source
1156 * @throws IOException if the GPX object can't be read
1157 * @throws NullPointerException if the given {@code source} is
1158 * {@code null}
1159 * @throws InvalidObjectException if the gpx input is invalid.
1160 * @throws UnsupportedOperationException if the defined
1161 * {@link javax.xml.stream.XMLInputFactory} doesn't support
1162 * the given {@code source}
1163 */
1164 public GPX read(final Source source)
1165 throws IOException
1166 {
1167 try {
1168 final XMLStreamReader reader = XMLProvider.provider()
1169 .xmlInputFactory()
1170 .createXMLStreamReader(source);
1171
1172 try (var input = new XMLStreamReaderAdapter(reader)) {
1173 if (input.hasNext()) {
1174 input.next();
1175
1176 final var format = NumberFormat.getNumberInstance(ENGLISH);
1177 final Function<String, Length> lengthParser = string ->
1178 Length.parse(string, format);
1179
1180 return GPX.xmlReader(_version, lengthParser)
1181 .read(input, _mode == Mode.LENIENT);
1182 } else {
1183 throw new InvalidObjectException("No 'gpx' element found.");
1184 }
1185 } catch (XMLStreamException e) {
1186 throw new InvalidObjectException(
1187 "Invalid GPX: " + e.getMessage()
1188 );
1189 } catch (IllegalArgumentException e) {
1190 final var ioe = new InvalidObjectException(e.getMessage());
1191 throw (InvalidObjectException)ioe.initCause(e);
1192 }
1193 } catch (XMLStreamException e) {
1194 throw new IOException(e);
1195 }
1196 }
1197
1198 /**
1199 * Read a GPX object from the given {@code input} stream.
1200 *
1201 * @param input the input stream from where the GPX date is read
1202 * @return the GPX object read from the in stream
1203 * @throws IOException if the GPX object can't be read
1204 * @throws NullPointerException if the given {@code input} stream is
1205 * {@code null}
1206 * @throws InvalidObjectException if the gpx input is invalid.
1207 */
1208 public GPX read(final InputStream input)
1209 throws IOException
1210 {
1211 final var wrapper = new NonCloseableInputStream(input);
1212 try (var reader = new InputStreamReader(wrapper, UTF_8)) {
1213 return read(new StreamSource(reader));
1214 }
1215 }
1216
1217 /**
1218 * Read a GPX object from the given {@code path}.
1219 *
1220 * @param path the input path from where the GPX date is read
1221 * @return the GPX object read from the input stream
1222 * @throws IOException if the GPX object can't be read
1223 * @throws NullPointerException if the given {@code input} stream is
1224 * {@code null}
1225 * @throws InvalidObjectException if the gpx input is invalid.
1226 */
1227 public GPX read(final Path path) throws IOException {
1228 try (var input = Files.newInputStream(path)) {
1229 return read(input);
1230 }
1231 }
1232
1233 /**
1234 * Read a GPX object from the given {@code file}.
1235 *
1236 * @param file the input file from where the GPX date is read
1237 * @return the GPX object read from the input stream
1238 * @throws IOException if the GPX object can't be read
1239 * @throws NullPointerException if the given {@code input} stream is
1240 * {@code null}
1241 * @throws InvalidObjectException if the gpx input is invalid.
1242 */
1243 public GPX read(final File file) throws IOException {
1244 return read(file.toPath());
1245 }
1246
1247 /**
1248 * Read a GPX object from the given {@code path}.
1249 *
1250 * @param path the input path from where the GPX date is read
1251 * @return the GPX object read from the input stream
1252 * @throws IOException if the GPX object can't be read
1253 * @throws NullPointerException if the given {@code input} stream is
1254 * {@code null}
1255 * @throws InvalidObjectException if the gpx input is invalid.
1256 */
1257 public GPX read(final String path) throws IOException {
1258 return read(Paths.get(path));
1259 }
1260
1261 /**
1262 * Create a GPX object from the given GPX-XML string.
1263 *
1264 * @see GPX.Writer#toString(GPX)
1265 *
1266 * @param xml the GPX XML string
1267 * @return the GPX object created from the given XML string
1268 * @throws IllegalArgumentException if the given {@code xml} is not a
1269 * valid GPX XML string
1270 * @throws NullPointerException if the given {@code xml} string is
1271 * {@code null}
1272 */
1273 public GPX fromString(final String xml) {
1274 try {
1275 return read(new ByteArrayInputStream(xml.getBytes()));
1276 } catch (InvalidObjectException e) {
1277 if (e.getCause() instanceof IllegalArgumentException iae) {
1278 throw iae;
1279 } else {
1280 throw new IllegalArgumentException(e);
1281 }
1282 } catch (IOException e) {
1283 throw new IllegalArgumentException(e);
1284 }
1285 }
1286
1287 /**
1288 * Create a GPX object from the given {@code byte[]} array.
1289 *
1290 * @see GPX.Writer#toByteArray(GPX)
1291 *
1292 * @param bytes the GPX {@code byte[]} array
1293 * @param offset the offset in the buffer of the first byte to read.
1294 * @param length the maximum number of bytes to read from the buffer.
1295 * @return the GPX object created from the given {@code byte[]} array
1296 * @throws IllegalArgumentException if the given {@code byte[]} array
1297 * doesn't represent a valid GPX object
1298 * @throws NullPointerException if the given {@code bytes} is {@code null}
1299 */
1300 GPX formByteArray(
1301 final byte[] bytes,
1302 final int offset,
1303 final int length
1304 ) {
1305 final var in = new ByteArrayInputStream(bytes, offset, length);
1306 try (var din = new DataInputStream(in)) {
1307 return GPX.read(din);
1308 } catch (IOException e) {
1309 throw new IllegalArgumentException(e);
1310 }
1311 }
1312
1313 /**
1314 * Create a GPX object from the given {@code byte[]} array.
1315 *
1316 * @see GPX.Writer#toByteArray(GPX)
1317 *
1318 * @param bytes the GPX {@code byte[]} array
1319 * @return the GPX object created from the given {@code byte[]} array
1320 * @throws IllegalArgumentException if the given {@code byte[]} array
1321 * doesn't represent a valid GPX object
1322 * @throws NullPointerException if the given {@code bytes} is {@code null}
1323 */
1324 GPX formByteArray(final byte[] bytes) {
1325 return formByteArray(bytes, 0, bytes.length);
1326 }
1327
1328 /* *********************************************************************
1329 * Factory methods.
1330 * ********************************************************************/
1331
1332 /**
1333 * Return a GPX reader, reading GPX files with the given version and in the
1334 * given reading mode.
1335 *
1336 * @since 3.0
1337 *
1338 * @param version the GPX version to read
1339 * @param mode the reading mode
1340 * @return a new GPX reader object
1341 * @throws NullPointerException if one of the arguments is {@code null}
1342 */
1343 public static Reader of(final Version version, final Mode mode) {
1344 return new Reader(version, mode);
1345 }
1346
1347 /**
1348 * Return a GPX reader, reading GPX files with version 1.1 and in the given
1349 * reading mode.
1350 *
1351 * @since 3.0
1352 *
1353 * @param mode the reading mode
1354 * @return a new GPX reader object
1355 * @throws NullPointerException if one of the arguments is {@code null}
1356 */
1357 public static Reader of(final Mode mode) {
1358 return new Reader(Version.V11, mode);
1359 }
1360
1361 }
1362
1363 /**
1364 * Class for writing GPX files. A writer instance can be created by the
1365 * {@code GPX.writer} factory methods.
1366 *
1367 * @version 3.0
1368 * @since 1.3
1369 */
1370 public static final class Writer {
1371
1372 /**
1373 * Represents the indentation value, the writer is using. An indentation
1374 * string of {@code null} means that the GPX data is written as one XML
1375 * line. An empty string adds line feeds, but with no indentation.
1376 *
1377 * @since 3.0
1378 *
1379 * @param value the indentation value
1380 */
1381 public record Indent(String value) {
1382 /**
1383 * This indentation lets the {@link Writer} write the GPX data into
1384 * one XML line.
1385 */
1386 public static final Indent NULL = new Indent(null);
1387
1388 /**
1389 * No indentation, but with new-lines.
1390 */
1391 public static final Indent NONE = new Indent("");
1392
1393 /**
1394 * Indentation with 4 spaces.
1395 */
1396 public static final Indent SPACE4 = new Indent(" ");
1397
1398 /**
1399 * Indentation with 2 spaces.
1400 */
1401 public static final Indent SPACE2 = new Indent(" ");
1402
1403 /**
1404 * Indentation with tabs.
1405 */
1406 public static final Indent TAB1 = new Indent("\t");
1407 }
1408
1409 /**
1410 * The default value for the <em>maximum fraction digits</em>.
1411 */
1412 public static final int DEFAULT_FRACTION_DIGITS = 8;
1413
1414 /**
1415 * The default GPX writer, with no indention and fraction digits
1416 * of {@link #DEFAULT_FRACTION_DIGITS}.
1417 *
1418 * @see #of(Indent, int)
1419 * @see #of(Indent)
1420 *
1421 * @since 3.0
1422 */
1423 public static final Writer DEFAULT =
1424 new Writer(Indent.SPACE4, DEFAULT_FRACTION_DIGITS);
1425
1426 private final Indent _indent;
1427 private final int _maximumFractionDigits;
1428
1429 private Writer(final Indent indent, final int maximumFractionDigits) {
1430 _indent = requireNonNull(indent);
1431 _maximumFractionDigits = maximumFractionDigits;
1432 }
1433
1434 /**
1435 * Return the indentation string this GPX writer is using.
1436 *
1437 * @since 3.0
1438 *
1439 * @return the indentation string
1440 */
1441 public Indent indent() {
1442 return _indent;
1443 }
1444
1445 /**
1446 * Return the maximum number of digits allowed in the fraction portion
1447 * of the written numbers like <em>latitude</em> and <em>longitude</em>.
1448 *
1449 * @return the maximum number of digits allowed in the fraction portion
1450 * of the written numbers
1451 */
1452 public int maximumFractionDigits() {
1453 return _maximumFractionDigits;
1454 }
1455
1456 /**
1457 * Writes the given {@code gpx} object to the given {@code result}. This
1458 * is the most general way for writing {@link GPX} objects.
1459 * <p>
1460 * The following example shows how to create an XML-Document from a
1461 * given {@code GPX} object.
1462 * <pre>{@code
1463 * final GPX gpx = ...;
1464 *
1465 * final Document doc = XMLProvider.provider()
1466 * .documentBuilderFactory()
1467 * .newDocumentBuilder()
1468 * .newDocument();
1469 *
1470 * // The GPX data are written to the empty `doc` object.
1471 * GPX.Writer.DEFAULT.write(gpx, new DOMResult(doc));
1472 * }</pre>
1473 *
1474 * @since 3.0
1475 *
1476 * @param gpx the GPX object to write to the output
1477 * @param result the output <em>document</em>
1478 * @throws IOException if the writing of the GPX object fails
1479 * @throws NullPointerException if one of the given arguments is
1480 * {@code null}
1481 */
1482 public void write(final GPX gpx, final Result result)
1483 throws IOException
1484 {
1485 try {
1486 final XMLStreamWriter writer = XMLProvider.provider()
1487 .xmlOutputFactory()
1488 .createXMLStreamWriter(result);
1489
1490 final XMLStreamWriterAdapter output = _indent.value() == null
1491 ? new XMLStreamWriterAdapter(writer)
1492 : new IndentingXMLStreamWriter(writer, _indent.value());
1493
1494 try (output) {
1495 final var format = NumberFormat.getNumberInstance(ENGLISH);
1496 format.setMaximumFractionDigits(_maximumFractionDigits);
1497 format.setGroupingUsed(false);
1498 final Function<Number, String> formatter = value ->
1499 value != null ? format.format(value) : null;
1500
1501 output.writeStartDocument("UTF-8", "1.0");
1502 GPX.xmlWriter(gpx._version, formatter).write(output, gpx);
1503 output.writeEndDocument();
1504 }
1505 } catch (XMLStreamException e) {
1506 throw new IOException(e);
1507 }
1508 }
1509
1510 /**
1511 * Writes the given {@code gpx} object (in GPX XML format) to the given
1512 * {@code output} stream. <em>The caller of this method is responsible
1513 * for closing the given {@code output} stream.</em>
1514 *
1515 * @param gpx the GPX object to write to the output
1516 * @param output the output stream where the GPX object is written to
1517 * @throws IOException if the writing of the GPX object fails
1518 * @throws NullPointerException if one of the given arguments is
1519 * {@code null}
1520 */
1521 public void write(final GPX gpx, final OutputStream output)
1522 throws IOException
1523 {
1524 final var wrapper = new NonCloseableOutputStream(output);
1525 try (var writer = new OutputStreamWriter(wrapper, UTF_8)) {
1526 write(gpx, new StreamResult(writer));
1527 }
1528 }
1529
1530 /**
1531 * Writes the given {@code gpx} object (in GPX XML format) to the given
1532 * {@code path}.
1533 *
1534 * @param gpx the GPX object to write to the output
1535 * @param path the output path where the GPX object is written to
1536 * @throws IOException if the writing of the GPX object fails
1537 * @throws NullPointerException if one of the given arguments is
1538 * {@code null}
1539 */
1540 public void write(final GPX gpx, final Path path) throws IOException {
1541 try (var out = Files.newOutputStream(path)) {
1542 write(gpx, out);
1543 }
1544 }
1545
1546 /**
1547 * Writes the given {@code gpx} object (in GPX XML format) to the given
1548 * {@code file}.
1549 *
1550 * @param gpx the GPX object to write to the output
1551 * @param file the output file where the GPX object is written to
1552 * @throws IOException if the writing of the GPX object fails
1553 * @throws NullPointerException if one of the given arguments is
1554 * {@code null}
1555 */
1556 public void write(final GPX gpx, final File file) throws IOException {
1557 write(gpx, file.toPath());
1558 }
1559
1560 /**
1561 * Writes the given {@code gpx} object (in GPX XML format) to the given
1562 * {@code path}.
1563 *
1564 * @param gpx the GPX object to write to the output
1565 * @param path the output path where the GPX object is written to
1566 * @throws IOException if the writing of the GPX object fails
1567 * @throws NullPointerException if one of the given arguments is
1568 * {@code null}
1569 */
1570 public void write(final GPX gpx, final String path) throws IOException {
1571 write(gpx, Path.of(path));
1572 }
1573
1574 /**
1575 * Create an XML string representation of the given {@code gpx} object.
1576 *
1577 * @see GPX.Reader#fromString(String)
1578 *
1579 * @param gpx the GPX object to convert to a string
1580 * @return the XML string representation of the given {@code gpx} object
1581 * @throws NullPointerException if the given GPX object is {@code null}
1582 */
1583 public String toString(final GPX gpx) {
1584 final ByteArrayOutputStream out = new ByteArrayOutputStream();
1585 try {
1586 write(gpx, out);
1587 return out.toString();
1588 } catch (IOException e) {
1589 throw new UncheckedIOException(e);
1590 }
1591 }
1592
1593 /**
1594 * Converts the given {@code gpx} object into a {@code byte[]} array.
1595 * This method can be used for short term storage of GPX objects.
1596 *
1597 * @since 3.0
1598 *
1599 * @see GPX.Reader#formByteArray(byte[])
1600 *
1601 * @param gpx the GPX object to convert to a {@code byte[]} array
1602 * @return the binary representation of the given {@code gpx} object
1603 * @throws NullPointerException if the given GPX object is {@code null}
1604 */
1605 byte[] toByteArray(final GPX gpx) {
1606 final var out = new ByteArrayOutputStream();
1607 try (var dout = new DataOutputStream(out)) {
1608 gpx.write(dout);
1609 } catch (IOException e) {
1610 throw new UncheckedIOException(e);
1611 }
1612
1613 return out.toByteArray();
1614 }
1615
1616 /* *********************************************************************
1617 * Factory methods.
1618 * ********************************************************************/
1619
1620 /**
1621 * Return a new GPX writer with the given {@code indent} and number
1622 * formatter, which is used for formatting {@link WayPoint#getLatitude()},
1623 * {@link WayPoint#getLongitude()}, ...
1624 * <p>
1625 * The example below shows the <em>lat</em> and <em>lon</em> values with
1626 * maximal 5 fractional digits.
1627 * <pre>{@code
1628 * <trkpt lat="45.78068" lon="12.55368">
1629 * <ele>1.2</ele>
1630 * <time>2009-08-30T07:08:21Z</time>
1631 * </trkpt>
1632 * }</pre>
1633 *
1634 * The following table should give you a feeling about the accuracy of a
1635 * given fraction digits count, at the equator.
1636 *
1637 * <table class="striped">
1638 * <caption><b>Maximum fraction digits accuracy</b></caption>
1639 * <thead>
1640 * <tr>
1641 * <th scope="row">Fraction digits</th>
1642 * <th scope="row">Degree</th>
1643 * <th scope="row">Distance</th>
1644 * </tr>
1645 * </thead>
1646 * <tbody>
1647 * <tr><td>0 </td><td>1 </td><td>111.31 km </td></tr>
1648 * <tr><td>1 </td><td>0.1 </td><td> 11.13 km </td></tr>
1649 * <tr><td>2 </td><td>0,01 </td><td> 1.1 km </td></tr>
1650 * <tr><td>3 </td><td>0.001 </td><td>111.3 m </td></tr>
1651 * <tr><td>4 </td><td>0.0001 </td><td> 11.1 m </td></tr>
1652 * <tr><td>5 </td><td>0.00001 </td><td> 1.11 m </td></tr>
1653 * <tr><td>6 </td><td>0.000001 </td><td> 0.1 m </td></tr>
1654 * <tr><td>7 </td><td>0.0000001 </td><td> 11.1 mm </td></tr>
1655 * <tr><td>8 </td><td>0.00000001 </td><td> 1.1 mm </td></tr>
1656 * <tr><td>9 </td><td>0.000000001 </td><td> 0.11 mm</td></tr>
1657 * </tbody>
1658 * </table>
1659 *
1660 * @see #of(Indent)
1661 * @see #DEFAULT
1662 *
1663 * @since 3.0
1664 *
1665 * @param indent the element indentation
1666 * @param maximumFractionDigits the maximum number of digits allowed in the
1667 * fraction portion of a number
1668 * @return a new GPX writer
1669 */
1670 public static Writer of(final Indent indent, final int maximumFractionDigits) {
1671 return new Writer(indent, maximumFractionDigits);
1672 }
1673
1674 /**
1675 * Return a new GPX writer with the given {@code indent} and with
1676 * <em>maximum fraction digits</em> of
1677 * {@link Writer#DEFAULT_FRACTION_DIGITS}.
1678 *
1679 * @see #of(Indent, int)
1680 * @see #DEFAULT
1681 *
1682 * @since 3.0
1683 *
1684 * @param indent the element indentation
1685 * @return a new GPX writer
1686 */
1687 public static Writer of(final Indent indent) {
1688 return new Writer(indent, DEFAULT_FRACTION_DIGITS);
1689 }
1690
1691 }
1692
1693 /* *************************************************************************
1694 * Static object creation methods
1695 * ************************************************************************/
1696
1697 /**
1698 * Create a new {@code GPX} object with the given data.
1699 *
1700 * @since 1.5
1701 *
1702 * @param creator the name or URL of the software that created your GPX
1703 * document. This allows others to inform the creator of a GPX
1704 * instance document that fails to validate.
1705 * @param version the GPX version
1706 * @param metadata the metadata about the GPS file
1707 * @param wayPoints the way-points
1708 * @param routes the routes
1709 * @param tracks the tracks
1710 * @param extensions the XML extensions
1711 * @return a new {@code GPX} object with the given data
1712 * @throws NullPointerException if the {@code creator}, {code wayPoints},
1713 * {@code routes} or {@code tracks} is {@code null}
1714 */
1715 public static GPX of(
1716 final Version version,
1717 final String creator,
1718 final Metadata metadata,
1719 final List<WayPoint> wayPoints,
1720 final List<Route> routes,
1721 final List<Track> tracks,
1722 final Document extensions
1723 ) {
1724 return new GPX(
1725 version,
1726 creator,
1727 metadata == null || metadata.isEmpty() ? null : metadata,
1728 wayPoints,
1729 routes,
1730 tracks,
1731 XML.extensions(XML.clone(extensions))
1732 );
1733 }
1734
1735 /**
1736 * Create a new {@code GPX} object with the given data.
1737 *
1738 * @param creator the name or URL of the software that created your GPX
1739 * document. This allows others to inform the creator of a GPX
1740 * instance document that fails to validate.
1741 * @param metadata the metadata about the GPS file
1742 * @param wayPoints the way-points
1743 * @param routes the routes
1744 * @param tracks the tracks
1745 * @return a new {@code GPX} object with the given data
1746 * @throws NullPointerException if the {@code creator}, {code wayPoints},
1747 * {@code routes} or {@code tracks} is {@code null}
1748 */
1749 public static GPX of(
1750 final String creator,
1751 final Metadata metadata,
1752 final List<WayPoint> wayPoints,
1753 final List<Route> routes,
1754 final List<Track> tracks
1755 ) {
1756 return of(
1757 Version.V11,
1758 creator,
1759 metadata,
1760 wayPoints,
1761 routes,
1762 tracks,
1763 null
1764 );
1765 }
1766
1767 /**
1768 * Create a new {@code GPX} object with the given data.
1769 *
1770 * @since 1.5
1771 *
1772 * @param creator the name or URL of the software that created your GPX
1773 * document. This allows others to inform the creator of a GPX
1774 * instance document that fails to validate.
1775 * @param metadata the metadata about the GPS file
1776 * @param wayPoints the way-points
1777 * @param routes the routes
1778 * @param tracks the tracks
1779 * @param extensions the XML extensions
1780 * @return a new {@code GPX} object with the given data
1781 * @throws NullPointerException if the {@code creator}, {code wayPoints},
1782 * {@code routes} or {@code tracks} is {@code null}
1783 */
1784 public static GPX of(
1785 final String creator,
1786 final Metadata metadata,
1787 final List<WayPoint> wayPoints,
1788 final List<Route> routes,
1789 final List<Track> tracks,
1790 final Document extensions
1791 ) {
1792 return of(
1793 Version.V11,
1794 creator,
1795 metadata,
1796 wayPoints,
1797 routes,
1798 tracks,
1799 extensions
1800 );
1801 }
1802
1803 /**
1804 * Create a new {@code GPX} object with the given data.
1805 *
1806 * @param creator the name or URL of the software that created your GPX
1807 * document. This allows others to inform the creator of a GPX
1808 * instance document that fails to validate.
1809 * @param version the GPX version
1810 * @param metadata the metadata about the GPS file
1811 * @param wayPoints the way-points
1812 * @param routes the routes
1813 * @param tracks the tracks
1814 * @return a new {@code GPX} object with the given data
1815 * @throws NullPointerException if the {@code creator}, {code wayPoints},
1816 * {@code routes} or {@code tracks} is {@code null}
1817 */
1818 public static GPX of(
1819 final Version version,
1820 final String creator,
1821 final Metadata metadata,
1822 final List<WayPoint> wayPoints,
1823 final List<Route> routes,
1824 final List<Track> tracks
1825 ) {
1826 return of(
1827 version,
1828 creator,
1829 metadata == null || metadata.isEmpty() ? null : metadata,
1830 wayPoints,
1831 routes,
1832 tracks,
1833 null
1834 );
1835 }
1836
1837
1838 /* *************************************************************************
1839 * Java object serialization
1840 * ************************************************************************/
1841
1842 @Serial
1843 private Object writeReplace() {
1844 return new SerialProxy(SerialProxy.GPX_TYPE, this);
1845 }
1846
1847 @Serial
1848 private void readObject(final ObjectInputStream stream)
1849 throws InvalidObjectException
1850 {
1851 throw new InvalidObjectException("Serialization proxy required.");
1852 }
1853
1854 void write(final DataOutput out) throws IOException {
1855 IO.writeString(_version.getValue(), out);
1856 IO.writeString(_creator, out);
1857 IO.writeNullable(_metadata, Metadata::write, out);
1858 IO.writes(_wayPoints, WayPoint::write, out);
1859 IO.writes(_routes, Route::write, out);
1860 IO.writes(_tracks, Track::write, out);
1861 IO.writeNullable(_extensions, IO::write, out);
1862 }
1863
1864 static GPX read(final DataInput in) throws IOException {
1865 return new GPX(
1866 Version.of(IO.readString(in)),
1867 IO.readString(in),
1868 IO.readNullable(Metadata::read, in),
1869 IO.reads(WayPoint::read, in),
1870 IO.reads(Route::read, in),
1871 IO.reads(Track::read, in),
1872 IO.readNullable(IO::readDoc, in)
1873 );
1874 }
1875
1876 /* *************************************************************************
1877 * XML stream object serialization
1878 * ************************************************************************/
1879
1880 private static String name(final GPX gpx) {
1881 return gpx.getMetadata()
1882 .flatMap(Metadata::getName)
1883 .orElse(null);
1884 }
1885
1886 private static String desc(final GPX gpx) {
1887 return gpx.getMetadata()
1888 .flatMap(Metadata::getDescription)
1889 .orElse(null);
1890 }
1891
1892 private static String author(final GPX gpx) {
1893 return gpx.getMetadata()
1894 .flatMap(Metadata::getAuthor)
1895 .flatMap(Person::getName)
1896 .orElse(null);
1897 }
1898
1899 private static String email(final GPX gpx) {
1900 return gpx.getMetadata()
1901 .flatMap(Metadata::getAuthor)
1902 .flatMap(Person::getEmail)
1903 .map(Email::getAddress)
1904 .orElse(null);
1905 }
1906
1907 private static String url(final GPX gpx) {
1908 return gpx.getMetadata()
1909 .flatMap(Metadata::getAuthor)
1910 .flatMap(Person::getLink)
1911 .map(Link::getHref)
1912 .map(URI::toString)
1913 .orElse(null);
1914 }
1915
1916 private static String urlname(final GPX gpx) {
1917 return gpx.getMetadata()
1918 .flatMap(Metadata::getAuthor)
1919 .flatMap(Person::getLink)
1920 .flatMap(Link::getText)
1921 .orElse(null);
1922 }
1923
1924 private static String time(final GPX gpx) {
1925 return gpx.getMetadata()
1926 .flatMap(Metadata::getTime)
1927 .map(TimeFormat::format)
1928 .orElse(null);
1929 }
1930
1931 private static String keywords(final GPX gpx) {
1932 return gpx.getMetadata()
1933 .flatMap(Metadata::getKeywords)
1934 .orElse(null);
1935 }
1936
1937
1938 // Define the needed writers for the different versions.
1939 private static XMLWriters<GPX>
1940 writers(final Function<? super Number, String> formatter) {
1941 return new XMLWriters<GPX>()
1942 .v00(XMLWriter.attr("version").map(gpx -> gpx._version._value))
1943 .v00(XMLWriter.attr("creator").map(GPX::getCreator))
1944 .v11(XMLWriter.ns(Version.V11.getNamespaceURI()))
1945 .v10(XMLWriter.ns(Version.V10.getNamespaceURI()))
1946 .v11(Metadata.writer(formatter).flatMap(GPX::getMetadata))
1947 .v10(XMLWriter.elem("name").map(GPX::name))
1948 .v10(XMLWriter.elem("desc").map(GPX::desc))
1949 .v10(XMLWriter.elem("author").map(GPX::author))
1950 .v10(XMLWriter.elem("email").map(GPX::email))
1951 .v10(XMLWriter.elem("url").map(GPX::url))
1952 .v10(XMLWriter.elem("urlname").map(GPX::urlname))
1953 .v10(XMLWriter.elem("time").map(GPX::time))
1954 .v10(XMLWriter.elem("keywords").map(GPX::keywords))
1955 .v10(XMLWriter.elems(WayPoint.xmlWriter(Version.V10,"wpt", formatter)).map(GPX::getWayPoints))
1956 .v11(XMLWriter.elems(WayPoint.xmlWriter(Version.V11,"wpt", formatter)).map(GPX::getWayPoints))
1957 .v10(XMLWriter.elems(Route.xmlWriter(Version.V10, formatter)).map(GPX::getRoutes))
1958 .v11(XMLWriter.elems(Route.xmlWriter(Version.V11, formatter)).map(GPX::getRoutes))
1959 .v10(XMLWriter.elems(Track.xmlWriter(Version.V10, formatter)).map(GPX::getTracks))
1960 .v11(XMLWriter.elems(Track.xmlWriter(Version.V11, formatter)).map(GPX::getTracks))
1961 .v00(XMLWriter.doc("extensions").flatMap(GPX::getExtensions));
1962 }
1963
1964
1965 // Define the needed readers for the different versions.
1966 private static XMLReaders
1967 readers(final Function<? super String, Length> lengthParser) {
1968 return new XMLReaders()
1969 .v00(XMLReader.attr("version").map(Version::of, Version.V11))
1970 .v00(XMLReader.attr("creator"))
1971 .v11(Metadata.READER)
1972 .v10(XMLReader.elem("name"))
1973 .v10(XMLReader.elem("desc"))
1974 .v10(XMLReader.elem("author"))
1975 .v10(XMLReader.elem("email"))
1976 .v10(XMLReader.elem("url"))
1977 .v10(XMLReader.elem("urlname"))
1978 .v10(XMLReader.elem("time").map(TimeFormat::parse))
1979 .v10(XMLReader.elem("keywords"))
1980 .v10(Bounds.READER)
1981 .v10(XMLReader.elems(WayPoint.xmlReader(Version.V10, "wpt", lengthParser)))
1982 .v11(XMLReader.elems(WayPoint.xmlReader(Version.V11, "wpt", lengthParser)))
1983 .v10(XMLReader.elems(Route.xmlReader(Version.V10, lengthParser)))
1984 .v11(XMLReader.elems(Route.xmlReader(Version.V11, lengthParser)))
1985 .v10(XMLReader.elems(Track.xmlReader(Version.V10, lengthParser)))
1986 .v11(XMLReader.elems(Track.xmlReader(Version.V11, lengthParser)))
1987 .v00(XMLReader.doc("extensions"));
1988 }
1989
1990
1991 static XMLWriter<GPX> xmlWriter(
1992 final Version version,
1993 final Function<? super Number, String> formatter
1994 ) {
1995 return XMLWriter.elem("gpx", writers(formatter).writers(version));
1996 }
1997
1998 static XMLReader<GPX> xmlReader(
1999 final Version version,
2000 final Function<? super String, Length> lengthParser
2001 ) {
2002 return XMLReader.elem(
2003 version == Version.V10 ? GPX::toGPXv10 : GPX::toGPXv11,
2004 "gpx",
2005 readers(lengthParser).readers(version)
2006 );
2007 }
2008
2009 @SuppressWarnings("unchecked")
2010 private static GPX toGPXv11(final Object[] v) {
2011 return new GPX(
2012 (Version)v[0],
2013 (String)v[1],
2014 (Metadata)v[2],
2015 (List<WayPoint>)v[3],
2016 (List<Route>)v[4],
2017 (List<Track>)v[5],
2018 XML.extensions((Document)v[6])
2019 );
2020 }
2021
2022 @SuppressWarnings("unchecked")
2023 private static GPX toGPXv10(final Object[] v) {
2024 return new GPX(
2025 (Version)v[0],
2026 (String)v[1],
2027 Metadata.of(
2028 (String)v[2],
2029 (String)v[3],
2030 Person.of(
2031 (String)v[4],
2032 v[5] != null
2033 ? Email.of((String)v[5])
2034 : null,
2035 v[6] != null
2036 ? Link.of((String)v[6], (String)v[7], null)
2037 : null
2038 ),
2039 null,
2040 null,
2041 (Instant)v[8],
2042 (String)v[9],
2043 (Bounds)v[10]
2044 ),
2045 (List<WayPoint>)v[11],
2046 (List<Route>)v[12],
2047 (List<Track>)v[13],
2048 XML.extensions((Document)v[14])
2049 );
2050 }
2051
2052
2053 /* *************************************************************************
2054 * Write and read GPX files
2055 * ************************************************************************/
2056
2057 /**
2058 * Writes the given {@code gpx} object (in GPX XML format) to the given
2059 * {@code path}.
2060 * This method is a shortcut for
2061 * <pre>{@code
2062 * GPX.Writer.DEFAULT.write(gpx, path);
2063 * }</pre>
2064 *
2065 * @see Writer
2066 *
2067 * @since 1.1
2068 *
2069 * @param gpx the GPX object to write to the output
2070 * @param path the output path where the GPX object is written to
2071 * @throws IOException if the writing of the GPX object fails
2072 * @throws NullPointerException if one of the given arguments is {@code null}
2073 */
2074 public static void write(final GPX gpx, final Path path) throws IOException {
2075 Writer.DEFAULT.write(gpx, path);
2076 }
2077
2078 /**
2079 * Read an GPX object from the given {@code input} stream.
2080 * This method is a shortcut for
2081 * <pre>{@code
2082 * GPX.Reader.DEFAULT.read(path);
2083 * }</pre>
2084 *
2085 * @see Reader
2086 *
2087 * @param path the input path from where the GPX date is read
2088 * @return the GPX object read from the input stream
2089 * @throws IOException if the GPX object can't be read
2090 * @throws NullPointerException if the given {@code input} stream is
2091 * {@code null}
2092 */
2093 public static GPX read(final Path path) throws IOException {
2094 return Reader.DEFAULT.read(path);
2095 }
2096
2097 }
|