001/*
002 * Java GPX Library (jpx-1.1.0).
003 * Copyright (c) 2016-2017 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.util.Objects.requireNonNull;
024import static io.jenetics.jpx.Lists.copy;
025import static io.jenetics.jpx.Lists.immutable;
026import static io.jenetics.jpx.Parsers.toMandatoryString;
027import static io.jenetics.jpx.XMLReader.attr;
028
029import java.io.BufferedInputStream;
030import java.io.BufferedOutputStream;
031import java.io.FileInputStream;
032import java.io.FileOutputStream;
033import java.io.IOException;
034import java.io.InputStream;
035import java.io.OutputStream;
036import java.io.Serializable;
037import java.nio.file.Path;
038import java.nio.file.Paths;
039import java.util.ArrayList;
040import java.util.List;
041import java.util.Objects;
042import java.util.Optional;
043import java.util.function.Consumer;
044import java.util.function.Function;
045import java.util.function.Predicate;
046import java.util.stream.Collectors;
047import java.util.stream.Stream;
048
049import javax.xml.stream.XMLInputFactory;
050import javax.xml.stream.XMLOutputFactory;
051import javax.xml.stream.XMLStreamException;
052import javax.xml.stream.XMLStreamReader;
053import javax.xml.stream.XMLStreamWriter;
054
055/**
056 * GPX documents contain a metadata header, followed by way-points, routes, and
057 * tracks. You can add your own elements to the extensions section of the GPX
058 * document.
059 * <h3>Examples</h3>
060 * <b>Creating a GPX object with one track-segment and 3 track-points</b>
061 * <pre>{@code
062 * final GPX gpx = GPX.builder()
063 *     .addTrack(track -> track
064 *         .addSegment(segment -> segment
065 *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
066 *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))
067 *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162))))
068 *     .build();
069 * }</pre>
070 *
071 * <h4>Reading a GPX file</h4>
072 * <pre>{@code
073 * final GPX gpx = GPX.read("track.xml");
074 * }</pre>
075 *
076 * <h4>Reading erroneous GPX files</h4>
077 * <pre>{@code
078 * final boolean lenient = true;
079 * final GPX gpx = GPX.read("track.xml", lenient);
080 * }</pre>
081 *
082 * This allows to read otherwise invalid GPX files, like
083 * <pre>{@code
084 * <?xml version="1.0" encoding="UTF-8"?>
085 * <gpx version="1.1" creator="GPSBabel - http://www.gpsbabel.org" xmlns="http://www.topografix.com/GPX/1/0">
086 *     <metadata>
087 *         <time>2015-11-13T15:22:42.140Z</time>
088 *         <bounds minlat="-37050536.000000000" minlon="-0.000000000" maxlat="48.359161377" maxlon="16.448385239"/>
089 *     </metadata>
090 *     <trk>
091 *         <name>track-1</name>
092 *         <desc>Log every 3 sec, 0 m</desc>
093 *         <trkseg>
094 *             <trkpt></trkpt>
095 *             <trkpt lat="48.199352264" lon="16.403341293">
096 *                 <ele>4325376.000000</ele>
097 *                 <time>2015-10-23T17:07:08Z</time>
098 *                 <speed>2.650000</speed>
099 *                 <name>TP000001</name>
100 *             </trkpt>
101 *             <trkpt lat="6.376383781" lon="-0.000000000">
102 *                 <ele>147573952589676412928.000000</ele>
103 *                 <time>1992-07-19T10:10:58Z</time>
104 *                 <speed>464.010010</speed>
105 *                 <name>TP000002</name>
106 *             </trkpt>
107 *             <trkpt lat="-37050536.000000000" lon="0.000475423">
108 *                 <ele>0.000000</ele>
109 *                 <time>2025-12-17T05:10:27Z</time>
110 *                 <speed>56528.671875</speed>
111 *                 <name>TP000003</name>
112 *             </trkpt>
113 *             <trkpt></trkpt>
114 *         </trkseg>
115 *     </trk>
116 * </gpx>
117 * }</pre>
118 *
119 * which is read as
120 * <pre>{@code
121 * <?xml version="1.0" encoding="UTF-8"?>
122 * <gpx version="1.1" creator="GPSBabel - http://www.gpsbabel.org" xmlns="http://www.topografix.com/GPX/1/0">
123 *     <metadata>
124 *         <time>2015-11-13T15:22:42.140Z</time>
125 *     </metadata>
126 *     <trk>
127 *         <name>track-1</name>
128 *         <desc>Log every 3 sec, 0 m</desc>
129 *         <trkseg>
130 *             <trkpt lat="48.199352264" lon="16.403341293">
131 *                 <ele>4325376.000000</ele>
132 *                 <time>2015-10-23T17:07:08Z</time>
133 *                 <speed>2.650000</speed>
134 *                 <name>TP000001</name>
135 *             </trkpt>
136 *             <trkpt lat="6.376383781" lon="-0.000000000">
137 *                 <ele>147573952589676412928.000000</ele>
138 *                 <time>1992-07-19T10:10:58Z</time>
139 *                 <speed>464.010010</speed>
140 *                 <name>TP000002</name>
141 *             </trkpt>
142 *         </trkseg>
143 *     </trk>
144 * </gpx>
145 * }</pre>
146 *
147 * @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
148 * @version 1.1
149 * @since 1.0
150 */
151public final class GPX implements Serializable {
152
153        private static final long serialVersionUID = 1L;
154
155        /**
156         * The default version number: 1.1.
157         */
158        public static final String VERSION = "1.1";
159
160        /**
161         * The default creator string.
162         */
163        public static final String CREATOR = "JPX - https://jenetics.github.io/jpx";
164
165        private final String _creator;
166        private final String _version;
167        private final Metadata _metadata;
168        private final List<WayPoint> _wayPoints;;
169        private final List<Route> _routes;
170        private final List<Track> _tracks;
171
172        /**
173         * Create a new {@code GPX} object with the given data.
174         *
175         * @param creator the name or URL of the software that created your GPX
176         *        document. This allows others to inform the creator of a GPX
177         *        instance document that fails to validate.
178         * @param version the GPX version
179         * @param metadata the metadata about the GPS file
180         * @param wayPoints the way-points
181         * @param routes the routes
182         * @param tracks the tracks
183         * @throws NullPointerException if the {@code creator} or {@code version} is
184         *         {@code null}
185         */
186        private GPX(
187                final String version,
188                final String creator,
189                final Metadata metadata,
190                final List<WayPoint> wayPoints,
191                final List<Route> routes,
192                final List<Track> tracks
193        ) {
194                _version = requireNonNull(version);
195                _creator = requireNonNull(creator);
196                _metadata = metadata;
197                _wayPoints = immutable(wayPoints);
198                _routes = immutable(routes);
199                _tracks = immutable(tracks);
200        }
201
202        /**
203         * Return the version number of the GPX file.
204         *
205         * @return the version number of the GPX file
206         */
207        public String getVersion() {
208                return _version;
209        }
210
211        /**
212         * Return the name or URL of the software that created your GPX document.
213         * This allows others to inform the creator of a GPX instance document that
214         * fails to validate.
215         *
216         * @return the name or URL of the software that created your GPX document
217         */
218        public String getCreator() {
219                return _creator;
220        }
221
222        /**
223         * Return the metadata of the GPX file.
224         *
225         * @return the metadata of the GPX file
226         */
227        public Optional<Metadata> getMetadata() {
228                return Optional.ofNullable(_metadata);
229        }
230
231        /**
232         * Return an unmodifiable list of the {@code GPX} way-points.
233         *
234         * @return an unmodifiable list of the {@code GPX} way-points.
235         */
236        public List<WayPoint> getWayPoints() {
237                return _wayPoints;
238        }
239
240        /**
241         * Return a stream with all {@code WayPoint}s of this {@code GPX} object.
242         *
243         * @return a stream with all {@code WayPoint}s of this {@code GPX} object
244         */
245        public Stream<WayPoint> wayPoints() {
246                return _wayPoints.stream();
247        }
248
249        /**
250         * Return an unmodifiable list of the {@code GPX} routes.
251         *
252         * @return an unmodifiable list of the {@code GPX} routes.
253         */
254        public List<Route> getRoutes() {
255                return _routes;
256        }
257
258        /**
259         * Return a stream of the {@code GPX} routes.
260         *
261         * @return a stream of the {@code GPX} routes.
262         */
263        public Stream<Route> routes() {
264                return _routes.stream();
265        }
266
267        /**
268         * Return an unmodifiable list of the {@code GPX} tracks.
269         *
270         * @return an unmodifiable list of the {@code GPX} tracks.
271         */
272        public List<Track> getTracks() {
273                return _tracks;
274        }
275
276        /**
277         * Return a stream of the {@code GPX} tracks.
278         *
279         * @return a stream of the {@code GPX} tracks.
280         */
281        public Stream<Track> tracks() {
282                return _tracks.stream();
283        }
284
285        /**
286         * Convert the <em>immutable</em> GPX object into a <em>mutable</em>
287         * builder initialized with the current GPX values.
288         *
289         * @since 1.1
290         *
291         * @return a new track builder initialized with the values of {@code this}
292         *         GPX object
293         */
294        public Builder toBuilder() {
295                return builder(_version, _creator)
296                        .metadata(_metadata)
297                        .wayPoints(_wayPoints)
298                        .routes(_routes)
299                        .tracks(_tracks);
300        }
301
302        @Override
303        public String toString() {
304                return format(
305                        "GPX[way-points=%s, routes=%s, tracks=%s]",
306                        getWayPoints().size(), getRoutes().size(), getTracks().size()
307                );
308        }
309
310        @Override
311        public int hashCode() {
312                int hash = 37;
313                hash += 17*Objects.hashCode(_creator) + 31;
314                hash += 17*Objects.hashCode(_version) + 31;
315                hash += 17*Objects.hashCode(_metadata) + 31;
316                hash += 17*Objects.hashCode(_wayPoints) + 31;
317                hash += 17*Objects.hashCode(_routes) + 31;
318                hash += 17*Objects.hashCode(_tracks) + 31;
319                return hash;
320        }
321
322        @Override
323        public boolean equals(final Object obj) {
324                return obj instanceof GPX &&
325                        Objects.equals(((GPX)obj)._creator, _creator) &&
326                        Objects.equals(((GPX)obj)._version, _version) &&
327                        Objects.equals(((GPX)obj)._metadata, _metadata) &&
328                        Objects.equals(((GPX)obj)._wayPoints, _wayPoints) &&
329                        Objects.equals(((GPX)obj)._routes, _routes) &&
330                        Objects.equals(((GPX)obj)._tracks, _tracks);
331        }
332
333
334        /**
335         * Builder class for creating immutable {@code GPX} objects.
336         * <p>
337         * Creating a GPX object with one track-segment and 3 track-points:
338         * <pre>{@code
339         * final GPX gpx = GPX.builder()
340         *     .addTrack(track -> track
341         *         .addSegment(segment -> segment
342         *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
343         *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))
344         *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162))))
345         *     .build();
346         * }</pre>
347         */
348        public static final class Builder {
349                private String _creator;
350                private String _version;
351                private Metadata _metadata;
352                private final List<WayPoint> _wayPoints = new ArrayList<>();
353                private final List<Route> _routes = new ArrayList<>();
354                private final List<Track> _tracks = new ArrayList<>();
355
356                private Builder(final String version, final String creator) {
357                        _version = requireNonNull(version);
358                        _creator = requireNonNull(creator);
359                }
360
361                /**
362                 * Set the GPX creator.
363                 *
364                 * @param creator the GPX creator
365                 * @throws NullPointerException if the given argument is {@code null}
366                 * @return {@code this} {@code Builder} for method chaining
367                 */
368                public Builder creator(final String creator) {
369                        _creator = requireNonNull(creator);
370                        return this;
371                }
372
373                /**
374                 * Return the current creator value.
375                 *
376                 * @since 1.1
377                 *
378                 * @return the current creator value
379                 */
380                public String creator() {
381                        return _creator;
382                }
383
384                /**
385                 * Return the current version value.
386                 *
387                 * @since 1.1
388                 *
389                 * @return the current version value
390                 */
391                public String version() {
392                        return _version;
393                }
394
395                /**
396                 * Set the GPX metadata.
397                 *
398                 * @param metadata the GPX metadata
399                 * @return {@code this} {@code Builder} for method chaining
400                 */
401                public Builder metadata(final Metadata metadata) {
402                        _metadata = metadata;
403                        return this;
404                }
405
406                /**
407                 * Allows to set partial metadata without messing up with the
408                 * {@link Metadata.Builder} class.
409                 * <pre>{@code
410                 * final GPX gpx = GPX.builder()
411                 *     .metadata(md -> md.author("Franz Wilhelmstötter"))
412                 *     .addTrack(...)
413                 *     .build();
414                 * }</pre>
415                 *
416                 * @param metadata the metadata consumer
417                 * @return {@code this} {@code Builder} for method chaining
418                 * @throws NullPointerException if the given argument is {@code null}
419                 */
420                public Builder metadata(final Consumer<Metadata.Builder> metadata) {
421                        final Metadata.Builder builder = Metadata.builder();
422                        metadata.accept(builder);
423                        _metadata = builder.build();
424
425                        return this;
426                }
427
428                /**
429                 * Return the current metadata value.
430                 *
431                 * @since 1.1
432                 *
433                 * @return the current metadata value
434                 */
435                public Optional<Metadata> metadata() {
436                        return Optional.ofNullable(_metadata);
437                }
438
439                /**
440                 * Sets the way-points of the {@code GPX} object. The list of way-points
441                 * may be {@code null}.
442                 *
443                 * @param wayPoints the {@code GPX} way-points
444                 * @return {@code this} {@code Builder} for method chaining
445                 * @throws NullPointerException if one of the way-points in the list is
446                 *         {@code null}
447                 */
448                public Builder wayPoints(final List<WayPoint> wayPoints) {
449                        copy(wayPoints, _wayPoints);
450                        return this;
451                }
452
453                /**
454                 * Add one way-point to the {@code GPX} object.
455                 *
456                 * @param wayPoint the way-point to add
457                 * @return {@code this} {@code Builder} for method chaining
458                 * @throws NullPointerException if the given {@code wayPoint} is
459                 *         {@code null}
460                 */
461                public Builder addWayPoint(final WayPoint wayPoint) {
462                        _wayPoints.add(requireNonNull(wayPoint));
463                        return this;
464                }
465
466                /**
467                 * Add a way-point to the {@code GPX} object using a
468                 * {@link WayPoint.Builder}.
469                 * <pre>{@code
470                 * final GPX gpx = GPX.builder()
471                 *     .addWayPoint(wp -> wp.lat(23.6).lon(13.5).ele(50))
472                 *     .build();
473                 * }</pre>
474                 *
475                 * @param wayPoint the way-point to add, configured by the way-point
476                 *        builder
477                 * @return {@code this} {@code Builder} for method chaining
478                 * @throws NullPointerException if the given argument is {@code null}
479                 */
480                public Builder addWayPoint(final Consumer<WayPoint.Builder> wayPoint) {
481                        final WayPoint.Builder builder = WayPoint.builder();
482                        wayPoint.accept(builder);
483                        return addWayPoint(builder.build());
484                }
485
486                /**
487                 * Return the current way-points. The returned list is mutable.
488                 *
489                 * @since 1.1
490                 *
491                 * @return the current, mutable way-point list
492                 */
493                public List<WayPoint> wayPoints() {
494                        return new NonNullList<>(_wayPoints);
495                }
496
497                /**
498                 * Sets the routes of the {@code GPX} object. The list of routes may be
499                 * {@code null}.
500                 *
501                 * @param routes the {@code GPX} routes
502                 * @return {@code this} {@code Builder} for method chaining
503                 * @throws NullPointerException if one of the routes is {@code null}
504                 */
505                public Builder routes(final List<Route> routes) {
506                        copy(routes, _routes);
507                        return this;
508                }
509
510                /**
511                 * Add a route the {@code GPX} object.
512                 *
513                 * @param route the route to add
514                 * @return {@code this} {@code Builder} for method chaining
515                 * @throws NullPointerException if the given {@code route} is {@code null}
516                 */
517                public Builder addRoute(final Route route) {
518                        _routes.add(requireNonNull(route));
519
520                        return this;
521                }
522
523                /**
524                 * Add a route the {@code GPX} object.
525                 * <pre>{@code
526                 * final GPX gpx = GPX.builder()
527                 *     .addRoute(route -> route
528                 *         .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
529                 *         .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161)))
530                 *     .build();
531                 * }</pre>
532                 *
533                 * @param route the route to add, configured by the route builder
534                 * @return {@code this} {@code Builder} for method chaining
535                 * @throws NullPointerException if the given argument is {@code null}
536                 */
537                public Builder addRoute(final Consumer<Route.Builder> route) {
538                        final Route.Builder builder = Route.builder();
539                        route.accept(builder);
540                        return addRoute(builder.build());
541                }
542
543                /**
544                 * Return the current routes. The returned list is mutable.
545                 *
546                 * @since 1.1
547                 *
548                 * @return the current, mutable route list
549                 */
550                public List<Route> routes() {
551                        return new NonNullList<>(_routes);
552                }
553
554                /**
555                 * Sets the tracks of the {@code GPX} object. The list of tracks may be
556                 * {@code null}.
557                 *
558                 * @param tracks the {@code GPX} tracks
559                 * @return {@code this} {@code Builder} for method chaining
560                 * @throws NullPointerException if one of the tracks is {@code null}
561                 */
562                public Builder tracks(final List<Track> tracks) {
563                        copy(tracks, _tracks);
564                        return this;
565                }
566
567                /**
568                 * Add a track the {@code GPX} object.
569                 *
570                 * @param track the track to add
571                 * @return {@code this} {@code Builder} for method chaining
572                 * @throws NullPointerException if the given {@code track} is {@code null}
573                 */
574                public Builder addTrack(final Track track) {
575                        _tracks.add(requireNonNull(track));
576                        return this;
577                }
578
579                /**
580                 * Add a track the {@code GPX} object.
581                 * <pre>{@code
582                 * final GPX gpx = GPX.builder()
583                 *     .addTrack(track -> track
584                 *         .addSegment(segment -> segment
585                 *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
586                 *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))
587                 *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162))))
588                 *     .build();
589                 * }</pre>
590                 *
591                 * @param track the track to add, configured by the track builder
592                 * @return {@code this} {@code Builder} for method chaining
593                 * @throws NullPointerException if the given argument is {@code null}
594                 */
595                public Builder addTrack(final Consumer<Track.Builder> track) {
596                        final Track.Builder builder = Track.builder();
597                        track.accept(builder);
598                        return addTrack(builder.build());
599                }
600
601                /**
602                 * Return the current tracks. The returned list is mutable.
603                 *
604                 * @since 1.1
605                 *
606                 * @return the current, mutable track list
607                 */
608                public List<Track> tracks() {
609                        return new NonNullList<>(_tracks);
610                }
611
612                /**
613                 * Create an immutable {@code GPX} object from the current builder state.
614                 *
615                 * @return an immutable {@code GPX} object from the current builder state
616                 */
617                public GPX build() {
618                        return new GPX(
619                                _version,
620                                _creator,
621                                _metadata,
622                                _wayPoints,
623                                _routes,
624                                _tracks
625                        );
626                }
627
628                /**
629                 * Return a new {@link WayPoint} filter.
630                 * <pre>{@code
631                 * final GPX filtered = gpx.toBuilder()
632                 *     .wayPointFilter()
633                 *         .filter(wp -> wp.getTime().isPresent())
634                 *         .build())
635                 *     .build();
636                 * }</pre>
637                 *
638                 * @since 1.1
639                 *
640                 * @return a new {@link WayPoint} filter
641                 */
642                public Filter<WayPoint, Builder> wayPointFilter() {
643                        return new Filter<WayPoint, Builder>() {
644
645                                @Override
646                                public Filter<WayPoint, Builder> filter(
647                                        final Predicate<? super WayPoint> predicate
648                                ) {
649                                        wayPoints(
650                                                _wayPoints.stream()
651                                                        .filter(predicate)
652                                                        .collect(Collectors.toList())
653                                        );
654
655                                        return this;
656                                }
657
658                                @Override
659                                public Filter<WayPoint, Builder> map(
660                                        final Function<? super WayPoint, ? extends WayPoint> mapper
661                                ) {
662                                        wayPoints(
663                                                _wayPoints.stream()
664                                                        .map(mapper)
665                                                        .collect(Collectors.toList())
666                                        );
667
668                                        return this;
669                                }
670
671                                @Override
672                                public Filter<WayPoint, Builder> flatMap(
673                                        final Function<
674                                                ? super WayPoint,
675                                                ? extends List<WayPoint>> mapper
676                                ) {
677                                        wayPoints(
678                                                _wayPoints.stream()
679                                                        .flatMap(wp -> mapper.apply(wp).stream())
680                                                        .collect(Collectors.toList())
681                                        );
682
683                                        return this;
684                                }
685
686                                @Override
687                                public Filter<WayPoint, Builder> listMap(
688                                        final Function<
689                                                ? super List<WayPoint>,
690                                                ? extends List<WayPoint>> mapper
691                                ) {
692                                        wayPoints(mapper.apply(_wayPoints));
693
694                                        return this;
695                                }
696
697                                @Override
698                                public Builder build() {
699                                        return GPX.Builder.this;
700                                }
701
702                        };
703                }
704
705                /**
706                 * Return a new {@link Route} filter.
707                 * <pre>{@code
708                 * final GPX filtered = gpx.toBuilder()
709                 *     .routeFilter()
710                 *         .filter(Route::nonEmpty)
711                 *         .build())
712                 *     .build();
713                 * }</pre>
714                 *
715                 * @since 1.1
716                 *
717                 * @return a new {@link Route} filter
718                 */
719                public Filter<Route, Builder> routeFilter() {
720                        return new Filter<Route, Builder>() {
721                                @Override
722                                public Filter<Route, Builder> filter(
723                                        final Predicate<? super Route> predicate
724                                ) {
725                                        routes(
726                                                _routes.stream()
727                                                        .filter(predicate)
728                                                        .collect(Collectors.toList())
729                                        );
730
731                                        return this;
732                                }
733
734                                @Override
735                                public Filter<Route, Builder> map(
736                                        final Function<? super Route, ? extends Route> mapper
737                                ) {
738                                        routes(
739                                                _routes.stream()
740                                                        .map(mapper)
741                                                        .collect(Collectors.toList())
742                                        );
743
744                                        return this;
745                                }
746
747                                @Override
748                                public Filter<Route, Builder> flatMap(
749                                        final Function<? super Route, ? extends List<Route>> mapper)
750                                {
751                                        routes(
752                                                _routes.stream()
753                                                        .flatMap(route -> mapper.apply(route).stream())
754                                                        .collect(Collectors.toList())
755                                        );
756
757                                        return this;
758                                }
759
760                                @Override
761                                public Filter<Route, Builder> listMap(
762                                        final Function<
763                                                ? super List<Route>,
764                                                ? extends List<Route>> mapper
765                                ) {
766                                        routes(mapper.apply(_routes));
767
768                                        return this;
769                                }
770
771                                @Override
772                                public Builder build() {
773                                        return GPX.Builder.this;
774                                }
775
776                        };
777                }
778
779                /**
780                 * Return a new {@link Track} filter.
781                 * <pre>{@code
782                 * final GPX merged = gpx.toBuilder()
783                 *     .trackFilter()
784                 *         .map(track -> track.toBuilder()
785                 *             .listMap(Filters::mergeSegments)
786                 *             .filter(TrackSegment::nonEmpty)
787                 *             .build())
788                 *         .build()
789                 *     .build();
790                 * }</pre>
791                 *
792                 * @since 1.1
793                 *
794                 * @return a new {@link Track} filter
795                 */
796                public Filter<Track, Builder> trackFilter() {
797                        return new Filter<Track, Builder>() {
798                                @Override
799                                public Filter<Track, Builder> filter(
800                                        final Predicate<? super Track> predicate
801                                ) {
802                                        tracks(
803                                                _tracks.stream()
804                                                        .filter(predicate)
805                                                        .collect(Collectors.toList())
806                                        );
807
808                                        return this;
809                                }
810
811                                @Override
812                                public Filter<Track, Builder> map(
813                                        final Function<? super Track, ? extends Track> mapper
814                                ) {
815                                        tracks(
816                                                _tracks.stream()
817                                                        .map(mapper)
818                                                        .collect(Collectors.toList())
819                                        );
820
821                                        return this;
822                                }
823
824                                @Override
825                                public Filter<Track, Builder> flatMap(
826                                        final Function<? super Track, ? extends List<Track>> mapper
827                                ) {
828                                        tracks(
829                                                _tracks.stream()
830                                                        .flatMap(track -> mapper.apply(track).stream())
831                                                        .collect(Collectors.toList())
832                                        );
833
834                                        return this;
835                                }
836
837                                @Override
838                                public Filter<Track, Builder> listMap(
839                                        final Function<
840                                                ? super List<Track>,
841                                                ? extends List<Track>> mapper
842                                ) {
843                                        tracks(mapper.apply(_tracks));
844
845                                        return this;
846                                }
847
848                                @Override
849                                public Builder build() {
850                                        return GPX.Builder.this;
851                                }
852
853                        };
854                }
855
856        }
857
858        /**
859         * Create a new GPX builder with the given GPX version and creator string.
860         *
861         * @param version the GPX version string
862         * @param creator the GPX creator
863         * @return new GPX builder
864         * @throws NullPointerException if one of the arguments is {@code null}
865         */
866        public static Builder builder(final String version, final String creator) {
867                return new Builder(version, creator);
868        }
869
870        /**
871         * Create a new GPX builder with the given GPX creator string.
872         *
873         * @param creator the GPX creator
874         * @return new GPX builder
875         * @throws NullPointerException if the given arguments is {@code null}
876         */
877        public static Builder builder(final String creator) {
878                return builder(VERSION, creator);
879        }
880
881        /**
882         * Create a new GPX builder.
883         *
884         * @return new GPX builder
885         */
886        public static Builder builder() {
887                return builder(VERSION, CREATOR);
888        }
889
890
891        /* *************************************************************************
892         *  Static object creation methods
893         * ************************************************************************/
894
895        /**
896         * Create a new {@code GPX} object with the given data.
897         *
898         * @param creator the name or URL of the software that created your GPX
899         *        document. This allows others to inform the creator of a GPX
900         *        instance document that fails to validate.
901         * @param metadata the metadata about the GPS file
902         * @param wayPoints the way-points
903         * @param routes the routes
904         * @param tracks the tracks
905         * @return a new {@code GPX} object with the given data
906         * @throws NullPointerException if the {@code creator}, {code wayPoints},
907         *         {@code routes} or {@code tracks} is {@code null}
908         */
909        public static GPX of(
910                final String creator,
911                final Metadata metadata,
912                final List<WayPoint> wayPoints,
913                final List<Route> routes,
914                final List<Track> tracks
915        ) {
916                return new GPX(
917                        VERSION,
918                        creator,
919                        metadata,
920                        wayPoints,
921                        routes,
922                        tracks
923                );
924        }
925
926        /**
927         * Create a new {@code GPX} object with the given data.
928         *
929         * @param creator the name or URL of the software that created your GPX
930         *        document. This allows others to inform the creator of a GPX
931         *        instance document that fails to validate.
932         * @param  version the GPX version
933         * @param metadata the metadata about the GPS file
934         * @param wayPoints the way-points
935         * @param routes the routes
936         * @param tracks the tracks
937         * @return a new {@code GPX} object with the given data
938         * @throws NullPointerException if the {@code creator}, {code wayPoints},
939         *         {@code routes} or {@code tracks} is {@code null}
940         */
941        public static GPX of(
942                final String version,
943                final String creator,
944                final Metadata metadata,
945                final List<WayPoint> wayPoints,
946                final List<Route> routes,
947                final List<Track> tracks
948        ) {
949                return new GPX(
950                        version,
951                        creator,
952                        metadata,
953                        wayPoints,
954                        routes,
955                        tracks
956                );
957        }
958
959
960        /* *************************************************************************
961         *  XML stream object serialization
962         * ************************************************************************/
963
964        /**
965         * Writes this {@code Link} object to the given XML stream {@code writer}.
966         *
967         * @param writer the XML data sink
968         * @throws XMLStreamException if an error occurs
969         */
970        void write(final XMLStreamWriter writer) throws XMLStreamException {
971                final XMLWriter xml = new XMLWriter(writer);
972
973                xml.write("gpx",
974                        xml.ns("http://www.topografix.com/GPX/1/1"),
975                        xml.attr("version", _version),
976                        xml.attr("creator", _creator),
977                        xml.elem(_metadata, Metadata::write),
978                        xml.elems(_wayPoints, (p, w) -> p.write("wpt", w)),
979                        xml.elems(_routes, Route::write),
980                        xml.elems(_tracks, Track::write)
981                );
982        }
983
984        @SuppressWarnings("unchecked")
985        static XMLReader<GPX> reader() {
986                final XML.Function<Object[], GPX> creator = a -> GPX.of(
987                        toMandatoryString(a[0], "GPX.version"),
988                        toMandatoryString(a[1], "GPX.creator"),
989                        (Metadata)a[2],
990                        (List<WayPoint>)a[3],
991                        (List<Route>)a[4],
992                        (List<Track>)a[5]
993                );
994
995                return XMLReader.of(creator, "gpx",
996                        attr("version"),
997                        attr("creator"),
998                        Metadata.reader(),
999                        XMLReader.ofList(WayPoint.reader("wpt")),
1000                        XMLReader.ofList(Route.reader()),
1001                        XMLReader.ofList(Track.reader())
1002                );
1003        }
1004
1005        /* *************************************************************************
1006         *  Load GPX from file.
1007         * ************************************************************************/
1008
1009        /**
1010         * Writes the given {@code gpx} object (in GPX XML format) to the given
1011         * {@code output} stream.
1012         *
1013         * @param gpx the GPX object to write to the output
1014         * @param output the output stream where the GPX object is written to
1015         * @throws IOException if the writing of the GPX object fails
1016         * @throws NullPointerException if one of the given arguments is {@code null}
1017         */
1018        public static void write(final GPX gpx, final OutputStream output)
1019                throws IOException
1020        {
1021                write(gpx, output, null);
1022        }
1023
1024        /**
1025         * Writes the given {@code gpx} object (in GPX XML format) to the given
1026         * {@code output} stream.
1027         *
1028         * @since 1.1
1029         *
1030         * @param gpx the GPX object to write to the output
1031         * @param path the output path where the GPX object is written to
1032         * @throws IOException if the writing of the GPX object fails
1033         * @throws NullPointerException if one of the given arguments is {@code null}
1034         */
1035        public static void write(final GPX gpx, final Path path) throws IOException {
1036                try (FileOutputStream out = new FileOutputStream(path.toFile());
1037                         BufferedOutputStream bout = new BufferedOutputStream(out))
1038                {
1039                        write(gpx, bout);
1040                }
1041        }
1042
1043        /**
1044         * Writes the given {@code gpx} object (in GPX XML format) to the given
1045         * {@code output} stream.
1046         *
1047         * @param gpx the GPX object to write to the output
1048         * @param path the output path where the GPX object is written to
1049         * @throws IOException if the writing of the GPX object fails
1050         * @throws NullPointerException if one of the given arguments is {@code null}
1051         */
1052        public static void write(final GPX gpx, final String path) throws IOException {
1053                write(gpx, Paths.get(path));
1054        }
1055
1056        /**
1057         * Writes the given {@code gpx} object (in GPX XML format) to the given
1058         * {@code output} stream.
1059         *
1060         * @param gpx the GPX object to write to the output
1061         * @param output the output stream where the GPX object is written to
1062         * @param indent the indent string for pretty printing. If the string is
1063         *        {@code null}, no pretty printing is performed.
1064         * @throws IOException if the writing of the GPX object fails
1065         * @throws NullPointerException if one of the given arguments is {@code null}
1066         */
1067        public static void write(
1068                final GPX gpx,
1069                final OutputStream output,
1070                final String indent
1071        )
1072                throws IOException
1073        {
1074                final XMLOutputFactory factory = XMLOutputFactory.newFactory();
1075                try {
1076                        final XMLStreamWriter writer = indent != null
1077                                ? new IndentingXMLWriter(
1078                                        factory.createXMLStreamWriter(output), indent)
1079                                : factory.createXMLStreamWriter(output);
1080
1081                        writer.writeStartDocument("UTF-8", "1.0");
1082                        gpx.write(writer);
1083                        writer.writeEndDocument();
1084                } catch (XMLStreamException e) {
1085                        throw new IOException(e);
1086                }
1087        }
1088
1089        /**
1090         * Writes the given {@code gpx} object (in GPX XML format) to the given
1091         * {@code output} stream.
1092         *
1093         * @param gpx the GPX object to write to the output
1094         * @param path the output path where the GPX object is written to
1095         * @param indent the indent string for pretty printing. If the string is
1096         *        {@code null}, no pretty printing is performed.
1097         * @throws IOException if the writing of the GPX object fails
1098         * @throws NullPointerException if one of the given arguments is {@code null}
1099         */
1100        public static void write(final GPX gpx, final Path path, final String indent)
1101                throws IOException
1102        {
1103                try (FileOutputStream out = new FileOutputStream(path.toFile());
1104                         BufferedOutputStream bout = new BufferedOutputStream(out))
1105                {
1106                        write(gpx, bout, indent);
1107                }
1108        }
1109
1110        /**
1111         * Writes the given {@code gpx} object (in GPX XML format) to the given
1112         * {@code output} stream.
1113         *
1114         * @param gpx the GPX object to write to the output
1115         * @param path the output path where the GPX object is written to
1116         * @param indent the indent string for pretty printing. If the string is
1117         *        {@code null}, no pretty printing is performed.
1118         * @throws IOException if the writing of the GPX object fails
1119         * @throws NullPointerException if one of the given arguments is {@code null}
1120         */
1121        public static void write(final GPX gpx, final String path, final String indent)
1122                throws IOException
1123        {
1124                write(gpx, Paths.get(path), indent);
1125        }
1126
1127        /**
1128         * Read an GPX object from the given {@code input} stream.
1129         *
1130         * @since 1.1
1131         *
1132         * @param input the input stream from where the GPX date is read
1133         * @param lenient if {@code true}, out-of-range and syntactical errors are
1134         *        ignored. E.g. a {@code WayPoint} with {@code lat} values not in
1135         *        the valid range of [-90..90] are ignored/skipped.
1136         * @return the GPX object read from the input stream
1137         * @throws IOException if the GPX object can't be read
1138         * @throws NullPointerException if the given {@code input} stream is
1139         *         {@code null}
1140         */
1141        public static GPX read(final InputStream input, final boolean lenient)
1142                throws IOException
1143        {
1144                final XMLInputFactory factory = XMLInputFactory.newFactory();
1145                try {
1146                        final XMLStreamReader reader = factory.createXMLStreamReader(input);
1147                        if (reader.hasNext()) {
1148                                reader.next();
1149                                return reader().read(reader, lenient);
1150                        } else {
1151                                throw new IOException("No 'gpx' element found.");
1152                        }
1153                } catch (XMLStreamException e) {
1154                        throw new IOException(e);
1155                }
1156        }
1157
1158        /**
1159         * Read an GPX object from the given {@code input} stream.
1160         *
1161         * @param input the input stream from where the GPX date is read
1162         * @return the GPX object read from the input stream
1163         * @throws IOException if the GPX object can't be read
1164         * @throws NullPointerException if the given {@code input} stream is
1165         *         {@code null}
1166         */
1167        public static GPX read(final InputStream input) throws IOException {
1168                return read(input, false);
1169        }
1170
1171        /**
1172         * Read an GPX object from the given {@code input} stream.
1173         *
1174         * @param path the input path from where the GPX date is read
1175         * @param lenient if {@code true}, out-of-range and syntactical errors are
1176         *        ignored. E.g. a {@code WayPoint} with {@code lat} values not in
1177         *        the valid range of [-90..90] are ignored/skipped.
1178         * @return the GPX object read from the input stream
1179         * @throws IOException if the GPX object can't be read
1180         * @throws NullPointerException if the given {@code input} stream is
1181         *         {@code null}
1182         */
1183        public static GPX read(final Path path, final boolean lenient)
1184                throws IOException
1185        {
1186                try (FileInputStream in = new FileInputStream(path.toFile());
1187                         BufferedInputStream bin = new BufferedInputStream(in))
1188                {
1189                        return read(bin, lenient);
1190                }
1191        }
1192
1193        /**
1194         * Read an GPX object from the given {@code input} stream.
1195         *
1196         * @param path the input path from where the GPX date is read
1197         * @return the GPX object read from the input stream
1198         * @throws IOException if the GPX object can't be read
1199         * @throws NullPointerException if the given {@code input} stream is
1200         *         {@code null}
1201         */
1202        public static GPX read(final Path path) throws IOException {
1203                return read(path, false);
1204        }
1205
1206        /**
1207         * Read an GPX object from the given {@code input} stream.
1208         *
1209         * @param path the input path from where the GPX date is read
1210         * @param lenient if {@code true}, out-of-range and syntactical errors are
1211         *        ignored. E.g. a {@code WayPoint} with {@code lat} values not in
1212         *        the valid range of [-90..90] are ignored/skipped.
1213         * @return the GPX object read from the input stream
1214         * @throws IOException if the GPX object can't be read
1215         * @throws NullPointerException if the given {@code input} stream is
1216         *         {@code null}
1217         */
1218        public static GPX read(final String path, final boolean lenient)
1219                throws IOException
1220        {
1221                return read(Paths.get(path), lenient);
1222        }
1223
1224        /**
1225         * Read an GPX object from the given {@code input} stream.
1226         *
1227         * @param path the input path from where the GPX date is read
1228         * @return the GPX object read from the input stream
1229         * @throws IOException if the GPX object can't be read
1230         * @throws NullPointerException if the given {@code input} stream is
1231         *         {@code null}
1232         */
1233        public static GPX read(final String path) throws IOException {
1234                return read(Paths.get(path), false);
1235        }
1236
1237}