001/*
002 * Java GPX Library (jpx-3.1.0).
003 * Copyright (c) 2016-2023 Franz Wilhelmstötter
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 *
017 * Author:
018 *    Franz Wilhelmstötter (franz.wilhelmstoetter@gmail.com)
019 */
020package io.jenetics.jpx;
021
022import static java.lang.String.format;
023import static java.nio.charset.StandardCharsets.UTF_8;
024import static java.util.Locale.ENGLISH;
025import static java.util.Objects.hash;
026import static java.util.Objects.requireNonNull;
027import static io.jenetics.jpx.Lists.copyOf;
028import static io.jenetics.jpx.Lists.copyTo;
029
030import java.io.ByteArrayInputStream;
031import java.io.ByteArrayOutputStream;
032import java.io.DataInput;
033import java.io.DataInputStream;
034import java.io.DataOutput;
035import java.io.DataOutputStream;
036import java.io.File;
037import java.io.IOException;
038import java.io.InputStream;
039import java.io.InputStreamReader;
040import java.io.InvalidObjectException;
041import java.io.ObjectInputStream;
042import java.io.OutputStream;
043import java.io.OutputStreamWriter;
044import java.io.Serial;
045import java.io.Serializable;
046import java.io.UncheckedIOException;
047import java.net.URI;
048import java.nio.file.Files;
049import java.nio.file.Path;
050import java.nio.file.Paths;
051import java.text.NumberFormat;
052import java.time.Instant;
053import java.util.ArrayList;
054import java.util.List;
055import java.util.Objects;
056import java.util.Optional;
057import java.util.function.Consumer;
058import java.util.function.Function;
059import java.util.function.Predicate;
060import java.util.stream.Stream;
061
062import javax.xml.stream.XMLStreamException;
063import javax.xml.stream.XMLStreamReader;
064import javax.xml.stream.XMLStreamWriter;
065import javax.xml.transform.Result;
066import javax.xml.transform.Source;
067import javax.xml.transform.stream.StreamResult;
068import javax.xml.transform.stream.StreamSource;
069
070import org.w3c.dom.Document;
071
072/**
073 * GPX documents contain a metadata header, followed by way-points, routes, and
074 * tracks. You can add your own elements to the extensions section of the GPX
075 * document.
076 * <p>
077 * <em><b>Examples:</b></em>
078 * <p>
079 * <b>Creating a GPX object with one track-segment and 3 track-points</b>
080 * <pre>{@code
081 * final GPX gpx = GPX.builder()
082 *     .addTrack(track -> track
083 *         .addSegment(segment -> segment
084 *             .addPoint(p -> p.lat(48.20100).lon(16.31651).ele(283))
085 *             .addPoint(p -> p.lat(48.20112).lon(16.31639).ele(278))
086 *             .addPoint(p -> p.lat(48.20126).lon(16.31601).ele(274))))
087 *     .build();
088 * }</pre>
089 *
090 * <b>Writing a GPX file</b>
091 * <pre>{@code
092 * final var indent = new GPX.Writer.Indent("    ");
093 * GPX.Writer.of(indent).write(gpx, Path.of("points.gpx"));
094 * }</pre>
095 *
096 * This will produce the following output.
097 * <pre>{@code
098 * <gpx version="1.1" creator="JPX - https://github.com/jenetics/jpx" xmlns="http://www.topografix.com/GPX/1/1">
099 *     <trk>
100 *         <trkseg>
101 *             <trkpt lat="48.201" lon="16.31651">
102 *                 <ele>283</ele>
103 *             </trkpt>
104 *             <trkpt lat="48.20112" lon="16.31639">
105 *                 <ele>278</ele>
106 *             </trkpt>
107 *             <trkpt lat="48.20126" lon="16.31601">
108 *                 <ele>274</ele>
109 *             </trkpt>
110 *         </trkseg>
111 *     </trk>
112 * </gpx>
113 * }</pre>
114 *
115 * <b>Reading a GPX file</b>
116 * <pre>{@code
117 * final GPX gpx = GPX.read("points.xml");
118 * }</pre>
119 *
120 * <b>Reading erroneous GPX files</b>
121 * <pre>{@code
122 * final GPX gpx = GPX.Reader.of(GPX.Reader.Mode.LENIENT).read("track.xml");
123 * }</pre>
124 *
125 * This allows to read otherwise invalid GPX files, like
126 * <pre>{@code
127 * <?xml version="1.0" encoding="UTF-8"?>
128 * <gpx version="1.1" creator="GPSBabel - http://www.gpsbabel.org" xmlns="http://www.topografix.com/GPX/1/1">
129 *   <metadata>
130 *     <time>2019-12-31T21:36:04.134Z</time>
131 *     <bounds minlat="48.175186667" minlon="16.299580000" maxlat="48.199555000" maxlon="16.416933333"/>
132 *   </metadata>
133 *   <trk>
134 *     <trkseg>
135 *       <trkpt lat="48.184298333" lon="16.299580000">
136 *         <ele>0.000</ele>
137 *         <time>2011-03-20T09:47:16Z</time>
138 *         <geoidheight>43.5</geoidheight>
139 *         <fix>2d</fix>
140 *         <sat>3</sat>
141 *         <hdop>4.200000</hdop>
142 *         <vdop>1.000000</vdop>
143 *         <pdop>4.300000</pdop>
144 *       </trkpt>
145 *       <trkpt lat="48.175186667" lon="16.303916667">
146 *         <ele>0.000</ele>
147 *         <time>2011-03-20T09:51:31Z</time>
148 *         <geoidheight>43.5</geoidheight>
149 *         <fix>2d</fix>
150 *         <sat>3</sat>
151 *         <hdop>16.600000</hdop>
152 *         <vdop>0.900000</vdop>
153 *         <pdop>16.600000</pdop>
154 *       </trkpt>
155 *     </trkseg>
156 *   </trk>
157 * </gpx>
158 * }</pre>
159 *
160 * which is read as (if you write it again)
161 * <pre>{@code
162 * <?xml version="1.0" encoding="UTF-8"?>
163 * <gpx version="1.1" creator="GPSBabel - http://www.gpsbabel.org" xmlns="http://www.topografix.com/GPX/1/1">
164 *     <metadata>
165 *         <time>2019-12-31T21:36:04.134Z</time>
166 *         <bounds minlat="48.175187" minlon="16.29958" maxlat="48.199555" maxlon="16.416933"></bounds>
167 *     </metadata>
168 *     <trk>
169 *         <trkseg>
170 *             <trkpt lat="48.184298" lon="16.29958">
171 *                 <ele>0</ele>
172 *                 <time>2011-03-20T09:47:16Z</time>
173 *                 <geoidheight>43.5</geoidheight>
174 *                 <fix>2d</fix>
175 *                 <sat>3</sat>
176 *                 <hdop>4.2</hdop>
177 *                 <vdop>1</vdop>
178 *                 <pdop>4.3</pdop>
179 *             </trkpt>
180 *             <trkpt lat="48.175187" lon="16.303917">
181 *                 <ele>0</ele>
182 *                 <time>2011-03-20T09:51:31Z</time>
183 *                 <geoidheight>43.5</geoidheight>
184 *                 <fix>2d</fix>
185 *                 <sat>3</sat>
186 *                 <hdop>16.6</hdop>
187 *                 <vdop>0.9</vdop>
188 *                 <pdop>16.6</pdop>
189 *             </trkpt>
190 *         </trkseg>
191 *     </trk>
192 * </gpx>
193 * }</pre>
194 *
195 * <b>Converting a GPX object to an XML {@link Document}</b>
196 * <pre>{@code
197 * final GPX gpx = ...;
198 *
199 * final Document doc = XMLProvider.provider()
200 *     .documentBuilderFactory()
201 *     .newDocumentBuilder()
202 *     .newDocument();
203 *
204 * // The GPX data are written to the empty `doc` object.
205 * GPX.Writer.DEFAULT.write(gpx, new DOMResult(doc));
206 * }</pre>
207 *
208 * @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
209 * @version 2.0
210 * @since 1.0
211 */
212public final class GPX implements Serializable {
213
214        @Serial
215        private static final long serialVersionUID = 2L;
216
217        /**
218         * Represents the available GPX versions.
219         *
220         * @version 1.3
221         * @since 1.3
222         */
223        public enum Version {
224
225                /**
226                 * The GPX version 1.0. This version can be read and written.
227                 *
228                 * @see <a href="http://www.topografix.com/gpx_manual.asp">GPX 1.0</a>
229                 */
230                V10("1.0", "http://www.topografix.com/GPX/1/0"),
231
232                /**
233                 * The GPX version 1.1. This is the default version and can be read and
234                 * written.
235                 *
236                 * @see <a href="http://www.topografix.com/GPX/1/1">GPX 1.1</a>
237                 */
238                V11("1.1", "http://www.topografix.com/GPX/1/1");
239
240                private final String _value;
241                private final String _namespaceURI;
242
243                Version(final String value, final String namespaceURI) {
244                        _value = value;
245                        _namespaceURI = namespaceURI;
246                }
247
248                /**
249                 * Return the version string value.
250                 *
251                 * @return the version string value
252                 */
253                public String getValue() {
254                        return _value;
255                }
256
257                /**
258                 * Return the namespace URI of this version.
259                 *
260                 * @since 1.5
261                 *
262                 * @return the namespace URI of this version
263                 */
264                public String getNamespaceURI() {
265                        return _namespaceURI;
266                }
267
268                /**
269                 * Return the version from the given {@code version} string. Allowed
270                 * values are "1.0" and "1.1".
271                 *
272                 * @param version the version string
273                 * @return the version from the given {@code version} string
274                 * @throws IllegalArgumentException if the given {@code version} string
275                 *         is neither "1.0" nor "1.1"
276                 * @throws NullPointerException if the given {@code version} string is
277                 *         {@code null}
278                 */
279                public static Version of(final String version) {
280                        return switch (version) {
281                                case "1.0" -> V10;
282                                case "1.1" -> V11;
283                                default -> throw new IllegalArgumentException(format(
284                                        "Unknown version string: '%s'.", version
285                                ));
286                        };
287                }
288        }
289
290        private static final String _CREATOR = "JPX - https://github.com/jenetics/jpx";
291
292        private final String _creator;
293        private final Version _version;
294        private final Metadata _metadata;
295        private final List<WayPoint> _wayPoints;
296        private final List<Route> _routes;
297        private final List<Track> _tracks;
298        private final Document _extensions;
299
300        /**
301         * Create a new {@code GPX} object with the given data.
302         *
303         * @param creator the name or URL of the software that created your GPX
304         *        document. This allows others to inform the creator of a GPX
305         *        instance document that fails to validate.
306         * @param version the GPX version
307         * @param metadata the metadata about the GPS file
308         * @param wayPoints the way-points
309         * @param routes the routes
310         * @param tracks the tracks
311         * @param extensions the XML extensions document
312         * @throws NullPointerException if the {@code creator} or {@code version} is
313         *         {@code null}
314         */
315        private GPX(
316                final Version version,
317                final String creator,
318                final Metadata metadata,
319                final List<WayPoint> wayPoints,
320                final List<Route> routes,
321                final List<Track> tracks,
322                final Document extensions
323        ) {
324                _version = requireNonNull(version);
325                _creator = requireNonNull(creator);
326                _metadata = metadata;
327                _wayPoints = copyOf(wayPoints);
328                _routes = copyOf(routes);
329                _tracks = copyOf(tracks);
330                _extensions = extensions;
331        }
332
333        /**
334         * Return the version number of the GPX file.
335         *
336         * @return the version number of the GPX file
337         */
338        public String getVersion() {
339                return _version._value;
340        }
341
342        /**
343         * Return the name or URL of the software that created your GPX document.
344         * This allows others to inform the creator of a GPX instance document that
345         * fails to validate.
346         *
347         * @return the name or URL of the software that created your GPX document
348         */
349        public String getCreator() {
350                return _creator;
351        }
352
353        /**
354         * Return the metadata of the GPX file.
355         *
356         * @return the metadata of the GPX file
357         */
358        public Optional<Metadata> getMetadata() {
359                return Optional.ofNullable(_metadata);
360        }
361
362        /**
363         * Return an unmodifiable list of the {@code GPX} way-points.
364         *
365         * @return an unmodifiable list of the {@code GPX} way-points.
366         */
367        public List<WayPoint> getWayPoints() {
368                return _wayPoints;
369        }
370
371        /**
372         * Return a stream with all {@code WayPoint}s of this {@code GPX} object.
373         *
374         * @return a stream with all {@code WayPoint}s of this {@code GPX} object
375         */
376        public Stream<WayPoint> wayPoints() {
377                return _wayPoints.stream();
378        }
379
380        /**
381         * Return an unmodifiable list of the {@code GPX} routes.
382         *
383         * @return an unmodifiable list of the {@code GPX} routes.
384         */
385        public List<Route> getRoutes() {
386                return _routes;
387        }
388
389        /**
390         * Return a stream of the {@code GPX} routes.
391         *
392         * @return a stream of the {@code GPX} routes.
393         */
394        public Stream<Route> routes() {
395                return _routes.stream();
396        }
397
398        /**
399         * Return an unmodifiable list of the {@code GPX} tracks.
400         *
401         * @return an unmodifiable list of the {@code GPX} tracks.
402         */
403        public List<Track> getTracks() {
404                return _tracks;
405        }
406
407        /**
408         * Return a stream of the {@code GPX} tracks.
409         *
410         * @return a stream of the {@code GPX} tracks.
411         */
412        public Stream<Track> tracks() {
413                return _tracks.stream();
414        }
415
416        /**
417         * Return the (cloned) extensions document. The root element of the returned
418         * document has the name {@code extensions}.
419         * <pre>{@code
420         * <extensions>
421         *     ...
422         * </extensions>
423         * }</pre>
424         *
425         * @since 1.5
426         *
427         * @return the extensions document
428         */
429        public Optional<Document> getExtensions() {
430                return Optional.ofNullable(_extensions).map(XML::clone);
431        }
432
433        /**
434         * Convert the <em>immutable</em> GPX object into a <em>mutable</em>
435         * builder initialized with the current GPX values.
436         *
437         * @since 1.1
438         *
439         * @return a new track builder initialized with the values of {@code this}
440         *         GPX object
441         */
442        public Builder toBuilder() {
443                return builder(_version, _creator)
444                        .metadata(_metadata)
445                        .wayPoints(_wayPoints)
446                        .routes(_routes)
447                        .tracks(_tracks)
448                        .extensions(_extensions);
449        }
450
451        @Override
452        public String toString() {
453                return format(
454                        "GPX[way-points=%s, routes=%s, tracks=%s]",
455                        getWayPoints().size(), getRoutes().size(), getTracks().size()
456                );
457        }
458
459        @Override
460        public int hashCode() {
461                return hash(
462                        _creator,
463                        _version,
464                        _metadata,
465                        _wayPoints,
466                        _routes,
467                        _tracks
468                );
469        }
470
471        @Override
472        public boolean equals(final Object obj) {
473                return obj == this ||
474                        obj instanceof GPX gpx &&
475                        Objects.equals(gpx._creator, _creator) &&
476                        Objects.equals(gpx._version, _version) &&
477                        Objects.equals(gpx._metadata, _metadata) &&
478                        Objects.equals(gpx._wayPoints, _wayPoints) &&
479                        Objects.equals(gpx._routes, _routes) &&
480                        Objects.equals(gpx._tracks, _tracks);
481        }
482
483        /**
484         * Builder class for creating immutable {@code GPX} objects.
485         * <p>
486         * Creating a GPX object with one track-segment and 3 track-points:
487         * <pre>{@code
488         * final GPX gpx = GPX.builder()
489         *     .addTrack(track -> track
490         *         .addSegment(segment -> segment
491         *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
492         *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))
493         *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162))))
494         *     .build();
495         * }</pre>
496         */
497        public static final class Builder {
498                private String _creator;
499                private Version _version;
500                private Metadata _metadata;
501                private final List<WayPoint> _wayPoints = new ArrayList<>();
502                private final List<Route> _routes = new ArrayList<>();
503                private final List<Track> _tracks = new ArrayList<>();
504                private Document _extensions;
505
506                private Builder(final Version version, final String creator) {
507                        _version = requireNonNull(version);
508                        _creator = requireNonNull(creator);
509                }
510
511                /**
512                 * Set the GPX creator.
513                 *
514                 * @param creator the GPX creator
515                 * @throws NullPointerException if the given argument is {@code null}
516                 * @return {@code this} {@code Builder} for method chaining
517                 */
518                public Builder creator(final String creator) {
519                        _creator = requireNonNull(creator);
520                        return this;
521                }
522
523                /**
524                 * Return the current creator value.
525                 *
526                 * @since 1.1
527                 *
528                 * @return the current creator value
529                 */
530                public String creator() {
531                        return _creator;
532                }
533
534                /**
535                 * Set the GPX version.
536                 *
537                 * @since 1.3
538                 *
539                 * @param version the GPX version
540                 * @throws NullPointerException if the given argument is {@code null}
541                 * @return {@code this} {@code Builder} for method chaining
542                 */
543                public Builder version(final Version version) {
544                        _version = requireNonNull(version);
545                        return this;
546                }
547
548                /**
549                 * Return the current version value.
550                 *
551                 * @since 1.1
552                 *
553                 * @return the current version value
554                 */
555                public String version() {
556                        return _version._value;
557                }
558
559                /**
560                 * Set the GPX metadata.
561                 *
562                 * @param metadata the GPX metadata
563                 * @return {@code this} {@code Builder} for method chaining
564                 */
565                public Builder metadata(final Metadata metadata) {
566                        _metadata = metadata;
567                        return this;
568                }
569
570                /**
571                 * Allows setting partial metadata without messing up with the
572                 * {@link Metadata.Builder} class.
573                 * <pre>{@code
574                 * final GPX gpx = GPX.builder()
575                 *     .metadata(md -> md.author("Franz Wilhelmstötter"))
576                 *     .addTrack(...)
577                 *     .build();
578                 * }</pre>
579                 *
580                 * @param metadata the metadata consumer
581                 * @return {@code this} {@code Builder} for method chaining
582                 * @throws NullPointerException if the given argument is {@code null}
583                 */
584                public Builder metadata(final Consumer<? super Metadata.Builder> metadata) {
585                        final Metadata.Builder builder = Metadata.builder();
586                        metadata.accept(builder);
587
588                        final Metadata md = builder.build();
589                        _metadata = md.isEmpty() ? null : md;
590
591                        return this;
592                }
593
594                /**
595                 * Return the current metadata value.
596                 *
597                 * @since 1.1
598                 *
599                 * @return the current metadata value
600                 */
601                public Optional<Metadata> metadata() {
602                        return Optional.ofNullable(_metadata);
603                }
604
605                /**
606                 * Sets the way-points of the {@code GPX} object. The list of way-points
607                 * may be {@code null}.
608                 *
609                 * @param wayPoints the {@code GPX} way-points
610                 * @return {@code this} {@code Builder} for method chaining
611                 * @throws NullPointerException if one of the way-points in the list is
612                 *         {@code null}
613                 */
614                public Builder wayPoints(final List<WayPoint> wayPoints) {
615                        copyTo(wayPoints, _wayPoints);
616                        return this;
617                }
618
619                /**
620                 * Add one way-point to the {@code GPX} object.
621                 *
622                 * @param wayPoint the way-point to add
623                 * @return {@code this} {@code Builder} for method chaining
624                 * @throws NullPointerException if the given {@code wayPoint} is
625                 *         {@code null}
626                 */
627                public Builder addWayPoint(final WayPoint wayPoint) {
628                        _wayPoints.add(requireNonNull(wayPoint));
629                        return this;
630                }
631
632                /**
633                 * Add a way-point to the {@code GPX} object using a
634                 * {@link WayPoint.Builder}.
635                 * <pre>{@code
636                 * final GPX gpx = GPX.builder()
637                 *     .addWayPoint(wp -> wp.lat(23.6).lon(13.5).ele(50))
638                 *     .build();
639                 * }</pre>
640                 *
641                 * @param wayPoint the way-point to add, configured by the way-point
642                 *        builder
643                 * @return {@code this} {@code Builder} for method chaining
644                 * @throws NullPointerException if the given argument is {@code null}
645                 */
646                public Builder addWayPoint(final Consumer<? super WayPoint.Builder> wayPoint) {
647                        final WayPoint.Builder builder = WayPoint.builder();
648                        wayPoint.accept(builder);
649                        return addWayPoint(builder.build());
650                }
651
652                /**
653                 * Return the current way-points. The returned list is mutable.
654                 *
655                 * @since 1.1
656                 *
657                 * @return the current, mutable way-point list
658                 */
659                public List<WayPoint> wayPoints() {
660                        return new NonNullList<>(_wayPoints);
661                }
662
663                /**
664                 * Sets the routes of the {@code GPX} object. The list of routes may be
665                 * {@code null}.
666                 *
667                 * @param routes the {@code GPX} routes
668                 * @return {@code this} {@code Builder} for method chaining
669                 * @throws NullPointerException if one of the routes is {@code null}
670                 */
671                public Builder routes(final List<Route> routes) {
672                        copyTo(routes, _routes);
673                        return this;
674                }
675
676                /**
677                 * Add a route the {@code GPX} object.
678                 *
679                 * @param route the route to add
680                 * @return {@code this} {@code Builder} for method chaining
681                 * @throws NullPointerException if the given {@code route} is {@code null}
682                 */
683                public Builder addRoute(final Route route) {
684                        _routes.add(requireNonNull(route));
685                        return this;
686                }
687
688                /**
689                 * Add a route the {@code GPX} object.
690                 * <pre>{@code
691                 * final GPX gpx = GPX.builder()
692                 *     .addRoute(route -> route
693                 *         .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
694                 *         .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161)))
695                 *     .build();
696                 * }</pre>
697                 *
698                 * @param route the route to add, configured by the route builder
699                 * @return {@code this} {@code Builder} for method chaining
700                 * @throws NullPointerException if the given argument is {@code null}
701                 */
702                public Builder addRoute(final Consumer<? super Route.Builder> route) {
703                        final Route.Builder builder = Route.builder();
704                        route.accept(builder);
705                        return addRoute(builder.build());
706                }
707
708                /**
709                 * Return the current routes. The returned list is mutable.
710                 *
711                 * @since 1.1
712                 *
713                 * @return the current, mutable route list
714                 */
715                public List<Route> routes() {
716                        return new NonNullList<>(_routes);
717                }
718
719                /**
720                 * Sets the tracks of the {@code GPX} object. The list of tracks may be
721                 * {@code null}.
722                 *
723                 * @param tracks the {@code GPX} tracks
724                 * @return {@code this} {@code Builder} for method chaining
725                 * @throws NullPointerException if one of the tracks is {@code null}
726                 */
727                public Builder tracks(final List<Track> tracks) {
728                        copyTo(tracks, _tracks);
729                        return this;
730                }
731
732                /**
733                 * Add a track the {@code GPX} object.
734                 *
735                 * @param track the track to add
736                 * @return {@code this} {@code Builder} for method chaining
737                 * @throws NullPointerException if the given {@code track} is {@code null}
738                 */
739                public Builder addTrack(final Track track) {
740                        _tracks.add(requireNonNull(track));
741                        return this;
742                }
743
744                /**
745                 * Add a track the {@code GPX} object.
746                 * <pre>{@code
747                 * final GPX gpx = GPX.builder()
748                 *     .addTrack(track -> track
749                 *         .addSegment(segment -> segment
750                 *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
751                 *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))
752                 *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162))))
753                 *     .build();
754                 * }</pre>
755                 *
756                 * @param track the track to add, configured by the track builder
757                 * @return {@code this} {@code Builder} for method chaining
758                 * @throws NullPointerException if the given argument is {@code null}
759                 */
760                public Builder addTrack(final Consumer<? super Track.Builder> track) {
761                        final Track.Builder builder = Track.builder();
762                        track.accept(builder);
763                        return addTrack(builder.build());
764                }
765
766                /**
767                 * Return the current tracks. The returned list is mutable.
768                 *
769                 * @since 1.1
770                 *
771                 * @return the current, mutable track list
772                 */
773                public List<Track> tracks() {
774                        return new NonNullList<>(_tracks);
775                }
776
777
778                /**
779                 * Sets the extensions object, which may be {@code null}. The root
780                 * element of the extensions document must be {@code extensions}.
781                 * <pre>{@code
782                 * <extensions>
783                 *     ...
784                 * </extensions>
785                 * }</pre>
786                 *
787                 * @since 1.5
788                 *
789                 * @param extensions the extensions document
790                 * @return {@code this} {@code Builder} for method chaining
791                 * @throws IllegalArgumentException if the root element is not the
792                 *         an {@code extensions} node
793                 */
794                public Builder extensions(final Document extensions) {
795                        _extensions = XML.checkExtensions(extensions);
796                        return this;
797                }
798
799                /**
800                 * Return the current extensions
801                 *
802                 * @since 1.5
803                 *
804                 * @return the extensions document
805                 */
806                public Optional<Document> extensions() {
807                        return Optional.ofNullable(_extensions);
808                }
809
810                /**
811                 * Create an immutable {@code GPX} object from the current builder state.
812                 *
813                 * @return an immutable {@code GPX} object from the current builder state
814                 */
815                public GPX build() {
816                        return of(
817                                _version,
818                                _creator,
819                                _metadata,
820                                _wayPoints,
821                                _routes,
822                                _tracks,
823                                _extensions
824                        );
825                }
826
827                /**
828                 * Return a new {@link WayPoint} filter.
829                 * <pre>{@code
830                 * final GPX filtered = gpx.toBuilder()
831                 *     .wayPointFilter()
832                 *         .filter(wp -> wp.getTime().isPresent())
833                 *         .build())
834                 *     .build();
835                 * }</pre>
836                 *
837                 * @since 1.1
838                 *
839                 * @return a new {@link WayPoint} filter
840                 */
841                public Filter<WayPoint, Builder> wayPointFilter() {
842                        return new Filter<>() {
843                                @Override
844                                public Filter<WayPoint, Builder> filter(
845                                        final Predicate<? super WayPoint> predicate
846                                ) {
847                                        wayPoints(_wayPoints.stream().filter(predicate).toList());
848                                        return this;
849                                }
850
851                                @Override
852                                public Filter<WayPoint, Builder> map(
853                                        final Function<? super WayPoint, ? extends WayPoint> mapper
854                                ) {
855                                        wayPoints(
856                                                _wayPoints.stream()
857                                                        .map(mapper)
858                                                        .map(WayPoint.class::cast)
859                                                        .toList()
860                                        );
861
862                                        return this;
863                                }
864
865                                @Override
866                                public Filter<WayPoint, Builder> flatMap(
867                                        final Function<
868                                                ? super WayPoint,
869                                                ? extends List<WayPoint>> mapper
870                                ) {
871                                        wayPoints(
872                                                _wayPoints.stream()
873                                                        .flatMap(wp -> mapper.apply(wp).stream())
874                                                        .toList()
875                                        );
876
877                                        return this;
878                                }
879
880                                @Override
881                                public Filter<WayPoint, Builder> listMap(
882                                        final Function<
883                                                ? super List<WayPoint>,
884                                                ? extends List<WayPoint>> mapper
885                                ) {
886                                        wayPoints(mapper.apply(_wayPoints));
887
888                                        return this;
889                                }
890
891                                @Override
892                                public Builder build() {
893                                        return GPX.Builder.this;
894                                }
895
896                        };
897                }
898
899                /**
900                 * Return a new {@link Route} filter.
901                 * <pre>{@code
902                 * final GPX filtered = gpx.toBuilder()
903                 *     .routeFilter()
904                 *         .filter(Route::nonEmpty)
905                 *         .build())
906                 *     .build();
907                 * }</pre>
908                 *
909                 * @since 1.1
910                 *
911                 * @return a new {@link Route} filter
912                 */
913                public Filter<Route, Builder> routeFilter() {
914                        return new Filter<>() {
915                                @Override
916                                public Filter<Route, Builder> filter(
917                                        final Predicate<? super Route> predicate
918                                ) {
919                                        routes(
920                                                _routes.stream()
921                                                        .filter(predicate)
922                                                        .toList()
923                                        );
924
925                                        return this;
926                                }
927
928                                @Override
929                                public Filter<Route, Builder> map(
930                                        final Function<? super Route, ? extends Route> mapper
931                                ) {
932                                        routes(
933                                                _routes.stream()
934                                                        .map(mapper)
935                                                        .map(Route.class::cast)
936                                                        .toList()
937                                        );
938
939                                        return this;
940                                }
941
942                                @Override
943                                public Filter<Route, Builder> flatMap(
944                                        final Function<? super Route, ? extends List<Route>> mapper)
945                                {
946                                        routes(
947                                                _routes.stream()
948                                                        .flatMap(route -> mapper.apply(route).stream())
949                                                        .toList()
950                                        );
951
952                                        return this;
953                                }
954
955                                @Override
956                                public Filter<Route, Builder> listMap(
957                                        final Function<
958                                                ? super List<Route>,
959                                                ? extends List<Route>> mapper
960                                ) {
961                                        routes(mapper.apply(_routes));
962
963                                        return this;
964                                }
965
966                                @Override
967                                public Builder build() {
968                                        return GPX.Builder.this;
969                                }
970
971                        };
972                }
973
974                /**
975                 * Return a new {@link Track} filter.
976                 * <pre>{@code
977                 * final GPX merged = gpx.toBuilder()
978                 *     .trackFilter()
979                 *         .map(track -> track.toBuilder()
980                 *             .listMap(Filters::mergeSegments)
981                 *             .filter(TrackSegment::nonEmpty)
982                 *             .build())
983                 *         .build()
984                 *     .build();
985                 * }</pre>
986                 *
987                 * @since 1.1
988                 *
989                 * @return a new {@link Track} filter
990                 */
991                public Filter<Track, Builder> trackFilter() {
992                        return new Filter<>() {
993                                @Override
994                                public Filter<Track, Builder> filter(
995                                        final Predicate<? super Track> predicate
996                                ) {
997                                        tracks(_tracks.stream().filter(predicate).toList());
998                                        return this;
999                                }
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}