GPX.java
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 paththrows 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 filethrows 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 paththrows 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(valuenull;
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 paththrows 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 filethrows 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 paththrows 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></td><td>1           </td><td>111.31 km  </td></tr>
1648          *         <tr><td></td><td>0.1         </td><td> 11.13 km  </td></tr>
1649          *         <tr><td></td><td>0,01        </td><td>  1.1 km   </td></tr>
1650          *         <tr><td></td><td>0.001       </td><td>111.3 m    </td></tr>
1651          *         <tr><td></td><td>0.0001      </td><td> 11.1 m    </td></tr>
1652          *         <tr><td></td><td>0.00001     </td><td>  1.11 m   </td></tr>
1653          *         <tr><td></td><td>0.000001    </td><td>    0.1 m  </td></tr>
1654          *         <tr><td></td><td>0.0000001   </td><td> 11.1 mm   </td></tr>
1655          *         <tr><td></td><td>0.00000001  </td><td>  1.1 mm   </td></tr>
1656          *         <tr><td></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 outthrows 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 inthrows 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 paththrows 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 paththrows IOException {
2094         return Reader.DEFAULT.read(path);
2095     }
2096 
2097 }