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.util.Objects.hash;
024import static java.util.Objects.requireNonNull;
025import static io.jenetics.jpx.Format.toIntString;
026import static io.jenetics.jpx.Lists.copyOf;
027import static io.jenetics.jpx.Lists.copyTo;
028
029import java.io.DataInput;
030import java.io.DataOutput;
031import java.io.IOException;
032import java.io.InvalidObjectException;
033import java.io.ObjectInputStream;
034import java.io.Serial;
035import java.io.Serializable;
036import java.net.URI;
037import java.util.ArrayList;
038import java.util.Iterator;
039import java.util.List;
040import java.util.Objects;
041import java.util.Optional;
042import java.util.function.Consumer;
043import java.util.function.Function;
044import java.util.function.Predicate;
045import java.util.stream.Stream;
046
047import org.w3c.dom.Document;
048
049import io.jenetics.jpx.GPX.Version;
050
051/**
052 * Represents a GPX track - an ordered list of points describing a path.
053 * <p>
054 * Creating a Track object with one track-segment and 3 track-points:
055 * <pre>{@code
056 * final Track track = Track.builder()
057 *     .name("Track 1")
058 *     .description("Mountain bike tour.")
059 *     .addSegment(segment -> segment
060 *         .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
061 *         .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))
062 *         .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162))))
063 *     .addSegment(segment -> segment
064 *         .addPoint(p -> p.lat(46.2081743).lon(16.3738189).ele(160))
065 *         .addPoint(p -> p.lat(47.2081743).lon(16.3738189).ele(161))
066 *         .addPoint(p -> p.lat(49.2081743).lon(16.3738189).ele(162))))
067 *     .build();
068 * }</pre>
069 *
070 * @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
071 * @version 1.5
072 * @since 1.0
073 */
074public final class Track implements Iterable<TrackSegment>, Serializable {
075
076        @Serial
077        private static final long serialVersionUID = 2L;
078
079        private final String _name;
080        private final String _comment;
081        private final String _description;
082        private final String _source;
083        private final List<Link> _links;
084        private final UInt _number;
085        private final String _type;
086        private final Document _extensions;
087        private final List<TrackSegment> _segments;
088
089        /**
090         * Create a new {@code Track} with the given parameters.
091         *
092         * @param name the GPS name of the track
093         * @param comment the GPS comment for the track
094         * @param description user description of the track
095         * @param source the source of data. Included to give user some idea of
096         *        reliability and accuracy of data.
097         * @param links the links to external information about track
098         * @param number the GPS track number
099         * @param type the type (classification) of track
100         * @param extensions the extensions document
101         * @param segments the track-segments holds a list of track-points which are
102         *        logically connected in order. To represent a single GPS track
103         *        where GPS reception was lost, or the GPS receiver was turned off,
104         *        start a new track-segment for each continuous span of track data.
105         */
106        private Track(
107                final String name,
108                final String comment,
109                final String description,
110                final String source,
111                final List<Link> links,
112                final UInt number,
113                final String type,
114                final Document extensions,
115                final List<TrackSegment> segments
116        ) {
117                _name = name;
118                _comment = comment;
119                _description = description;
120                _source = source;
121                _links = copyOf(links);
122                _number = number;
123                _type = type;
124                _extensions = extensions;
125                _segments = copyOf(segments);
126        }
127
128        /**
129         * Return the track name.
130         *
131         * @return the track name
132         */
133        public Optional<String> getName() {
134                return Optional.ofNullable(_name);
135        }
136
137        /**
138         * Return the GPS comment of the track.
139         *
140         * @return the GPS comment of the track
141         */
142        public Optional<String> getComment() {
143                return Optional.ofNullable(_comment);
144        }
145
146        /**
147         * Return the text description of the track.
148         *
149         * @return the text description of the track
150         */
151        public Optional<String> getDescription() {
152                return Optional.ofNullable(_description);
153        }
154
155        /**
156         * Return the source of data. Included to give user some idea of reliability
157         * and accuracy of data.
158         *
159         * @return the source of data
160         */
161        public Optional<String> getSource() {
162                return Optional.ofNullable(_source);
163        }
164
165        /**
166         * Return the links to external information about the track.
167         *
168         * @return the links to external information about the track
169         */
170        public List<Link> getLinks() {
171                return _links;
172        }
173
174        /**
175         * Return the GPS track number.
176         *
177         * @return the GPS track number
178         */
179        public Optional<UInt> getNumber() {
180                return Optional.ofNullable(_number);
181        }
182
183        /**
184         * Return the type (classification) of the track.
185         *
186         * @return the type (classification) of the track
187         */
188        public Optional<String> getType() {
189                return Optional.ofNullable(_type);
190        }
191
192        /**
193         * Return the (cloned) extensions document. The root element of the returned
194         * document has the name {@code extensions}.
195         * <pre>{@code
196         * <extensions>
197         *     ...
198         * </extensions>
199         * }</pre>
200         *
201         * @since 1.5
202         *
203         * @return the extensions document
204         * @throws org.w3c.dom.DOMException if the document could not be cloned,
205         *         because of an erroneous XML configuration
206         */
207        public Optional<Document> getExtensions() {
208                return Optional.ofNullable(_extensions).map(XML::clone);
209        }
210
211        /**
212         * Return the sequence of route points.
213         *
214         * @return the sequence of route points
215         */
216        public List<TrackSegment> getSegments() {
217                return _segments;
218        }
219
220        /**
221         * Return a stream of {@link TrackSegment} objects this track contains.
222         *
223         * @return a stream of {@link TrackSegment} objects this track contains
224         */
225        public Stream<TrackSegment> segments() {
226                return _segments.stream();
227        }
228
229        @Override
230        public Iterator<TrackSegment> iterator() {
231                return _segments.iterator();
232        }
233
234        /**
235         * Convert the <em>immutable</em> track object into a <em>mutable</em>
236         * builder initialized with the current track values.
237         *
238         * @since 1.1
239         *
240         * @return a new track builder initialized with the values of {@code this}
241         *         track
242         */
243        public Builder toBuilder() {
244                return builder()
245                        .name(_name)
246                        .cmt(_comment)
247                        .desc(_description)
248                        .src(_source)
249                        .links(_links)
250                        .number(_number)
251                        .extensions(_extensions)
252                        .segments(_segments);
253        }
254
255        /**
256         * Return {@code true} if all track properties are {@code null} or empty.
257         *
258         * @return {@code true} if all track properties are {@code null} or empty
259         */
260        public boolean isEmpty() {
261                return _name == null &&
262                        _comment == null &&
263                        _description == null &&
264                        _source == null &&
265                        _links.isEmpty() &&
266                        _number == null &&
267                        _extensions == null &&
268                        (_segments.isEmpty() ||
269                                _segments.stream().allMatch(TrackSegment::isEmpty));
270        }
271
272        /**
273         * Return {@code true} if not all track properties are {@code null} or empty.
274         *
275         * @since 1.1
276         *
277         * @return {@code true} if not all track properties are {@code null} or empty
278         */
279        public boolean nonEmpty() {
280                return !isEmpty();
281        }
282
283        @Override
284        public int hashCode() {
285                return hash(
286                        _name,
287                        _comment,
288                        _description,
289                        _source,
290                        _type,
291                        Lists.hashCode(_links),
292                        _number,
293                        _segments
294                );
295        }
296
297        @Override
298        public boolean equals(final Object obj) {
299                return obj == this ||
300                        obj instanceof Track track &&
301                        Objects.equals(track._name, _name) &&
302                        Objects.equals(track._comment, _comment) &&
303                        Objects.equals(track._description, _description) &&
304                        Objects.equals(track._source, _source) &&
305                        Objects.equals(track._type, _type) &&
306                        Lists.equals(track._links, _links) &&
307                        Objects.equals(track._number, _number) &&
308                        Objects.equals(track._segments, _segments);
309        }
310
311        @Override
312        public String toString() {
313                return format("Track[name=%s, segments=%s]", _name, _segments);
314        }
315
316        /**
317         * Builder class for creating immutable {@code Track} objects.
318         * <p>
319         * Creating a Track object with one track-segment and 3 track-points:
320         * <pre>{@code
321         * final Track track = Track.builder()
322         *     .name("Track 1")
323         *     .description("Mountain bike tour.")
324         *     .addSegment(segment -> segment
325         *         .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
326         *         .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))
327         *         .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162))))
328         *     .addSegment(segment -> segment
329         *         .addPoint(p -> p.lat(46.2081743).lon(16.3738189).ele(160))
330         *         .addPoint(p -> p.lat(47.2081743).lon(16.3738189).ele(161))
331         *         .addPoint(p -> p.lat(49.2081743).lon(16.3738189).ele(162))))
332         *     .build();
333         * }</pre>
334         */
335        public static final class Builder implements Filter<TrackSegment, Track> {
336                private String _name;
337                private String _comment;
338                private String _description;
339                private String _source;
340                private final List<Link> _links = new ArrayList<>();
341                private UInt _number;
342                private String _type;
343                private Document _extensions;
344                private final List<TrackSegment> _segments = new ArrayList<>();
345
346                private Builder() {
347                }
348
349                /**
350                 * Set the name of the track.
351                 *
352                 * @param name the track name
353                 * @return {@code this} {@code Builder} for method chaining
354                 */
355                public Builder name(final String name) {
356                        _name = name;
357                        return this;
358                }
359
360                /**
361                 * Return the current name value.
362                 *
363                 * @since 1.1
364                 *
365                 * @return the current name value
366                 */
367                public Optional<String> name() {
368                        return Optional.ofNullable(_name);
369                }
370
371                /**
372                 * Set the comment of the track.
373                 *
374                 * @param comment the track comment
375                 * @return {@code this} {@code Builder} for method chaining
376                 */
377                public Builder cmt(final String comment) {
378                        _comment = comment;
379                        return this;
380                }
381
382                public Optional<String> cmt() {
383                        return Optional.ofNullable(_comment);
384                }
385
386                /**
387                 * Set the description of the track.
388                 *
389                 * @param description the track description
390                 * @return {@code this} {@code Builder} for method chaining
391                 */
392                public Builder desc(final String description) {
393                        _description = description;
394                        return this;
395                }
396
397                /**
398                 * Return the current description value.
399                 *
400                 * @since 1.1
401                 *
402                 * @return the current description value
403                 */
404                public Optional<String> desc() {
405                        return Optional.ofNullable(_description);
406                }
407
408                /**
409                 * Set the source of the track.
410                 *
411                 * @param source the track source
412                 * @return {@code this} {@code Builder} for method chaining
413                 */
414                public Builder src(final String source) {
415                        _source = source;
416                        return this;
417                }
418
419                /**
420                 * Return the current source value.
421                 *
422                 * @since 1.1
423                 *
424                 * @return the current source value
425                 */
426                public Optional<String> src() {
427                        return Optional.ofNullable(_source);
428                }
429
430                /**
431                 * Set the track links. The link list may be {@code null}.
432                 *
433                 * @param links the track links
434                 * @return {@code this} {@code Builder} for method chaining
435                 * @throws NullPointerException if one of the links in the list is
436                 *         {@code null}
437                 */
438                public Builder links(final List<Link> links) {
439                        copyTo(links, _links);
440                        return this;
441                }
442
443                /**
444                 * Add the given {@code link} to the track
445                 *
446                 * @param link the link to add to the track
447                 * @return {@code this} {@code Builder} for method chaining
448                 */
449                public Builder addLink(final Link link) {
450                        _links.add(requireNonNull(link));
451
452                        return this;
453                }
454
455                /**
456                 * Add the given {@code link} to the track
457                 *
458                 * @param href the link to add to the track
459                 * @return {@code this} {@code Builder} for method chaining
460                 * @throws NullPointerException if the given {@code href} is {@code null}
461                 * @throws IllegalArgumentException if the given {@code href} is not a
462                 *         valid URL
463                 */
464                public Builder addLink(final String href) {
465                        return addLink(Link.of(href));
466                }
467
468                /**
469                 * Return the current links. The returned link list is mutable.
470                 *
471                 * @since 1.1
472                 *
473                 * @return the current links
474                 */
475                public List<Link> links() {
476                        return new NonNullList<>(_links);
477                }
478
479                /**
480                 * Set the track number.
481                 *
482                 * @param number the track number
483                 * @return {@code this} {@code Builder} for method chaining
484                 */
485                public Builder number(final UInt number) {
486                        _number = number;
487                        return this;
488                }
489
490                /**
491                 * Set the track number.
492                 *
493                 * @param number the track number
494                 * @return {@code this} {@code Builder} for method chaining
495                 * @throws IllegalArgumentException if the given {@code value} is smaller
496                 *         than zero
497                 */
498                public Builder number(final int number) {
499                        _number = UInt.of(number);
500                        return this;
501                }
502
503                /**
504                 * Return the current number value.
505                 *
506                 * @since 1.1
507                 *
508                 * @return the current number value
509                 */
510                public Optional<UInt> number() {
511                        return Optional.ofNullable(_number);
512                }
513
514                /**
515                 * Set the track type.
516                 *
517                 * @param type the track type
518                 * @return {@code this} {@code Builder} for method chaining
519                 */
520                public Builder type(final String type) {
521                        _type = type;
522                        return this;
523                }
524
525                /**
526                 * Return the current type value.
527                 *
528                 * @since 1.1
529                 *
530                 * @return the current type value
531                 */
532                public Optional<String> type() {
533                        return Optional.ofNullable(_type);
534                }
535
536                /**
537                 * Sets the extensions object, which may be {@code null}. The root
538                 * element of the extensions document must be {@code extensions}.
539                 * <pre>{@code
540                 * <extensions>
541                 *     ...
542                 * </extensions>
543                 * }</pre>
544                 *
545                 * @since 1.5
546                 *
547                 * @param extensions the document
548                 * @return {@code this} {@code Builder} for method chaining
549                 * @throws IllegalArgumentException if the root element is not the
550                 *         an {@code extensions} node
551                 */
552                public Builder extensions(final Document extensions) {
553                        _extensions = XML.checkExtensions(extensions);
554                        return this;
555                }
556
557                /**
558                 * Return the current extensions
559                 *
560                 * @since 1.5
561                 *
562                 * @return the extensions document
563                 */
564                public Optional<Document> extensions() {
565                        return Optional.ofNullable(_extensions);
566                }
567
568                /**
569                 * Set the track segments of the track. The list may be {@code null}.
570                 *
571                 * @param segments the track segments
572                 * @return {@code this} {@code Builder} for method chaining
573                 * @throws NullPointerException if one of the segments in the list is
574                 *         {@code null}
575                 */
576                public Builder segments(final List<TrackSegment> segments) {
577                        copyTo(segments, _segments);
578                        return this;
579                }
580
581                /**
582                 * Add a track segment to the track.
583                 *
584                 * @param segment the track segment added to the track
585                 * @return {@code this} {@code Builder} for method chaining
586                 * @throws NullPointerException if the given argument is {@code null}
587                 */
588                public Builder addSegment(final TrackSegment segment) {
589                        _segments.add(requireNonNull(segment));
590                        return this;
591                }
592
593                /**
594                 * Add a track segment to the track, via the given builder.
595                 * <pre>{@code
596                 * final Track track = Track.builder()
597                 *     .name("Track 1")
598                 *     .description("Mountain bike tour.")
599                 *     .addSegment(segment -> segment
600                 *         .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
601                 *         .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))
602                 *         .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162))))
603                 *     .addSegment(segment -> segment
604                 *         .addPoint(p -> p.lat(46.2081743).lon(16.3738189).ele(160))
605                 *         .addPoint(p -> p.lat(47.2081743).lon(16.3738189).ele(161))
606                 *         .addPoint(p -> p.lat(49.2081743).lon(16.3738189).ele(162))))
607                 *     .build();
608                 * }</pre>
609                 *
610                 * @param segment the track segment
611                 * @return {@code this} {@code Builder} for method chaining
612                 * @throws NullPointerException if the given argument is {@code null}
613                 */
614                public Builder addSegment(final Consumer<? super TrackSegment.Builder> segment) {
615                        final TrackSegment.Builder builder = TrackSegment.builder();
616                        segment.accept(builder);
617                        return addSegment(builder.build());
618                }
619
620                /**
621                 * Return the current track segments. The returned segment list is
622                 * mutable.
623                 *
624                 * @since 1.1
625                 *
626                 * @return the current track segments
627                 */
628                public List<TrackSegment> segments() {
629                        return new NonNullList<>(_segments);
630                }
631
632                @Override
633                public Builder filter(final Predicate<? super TrackSegment> predicate) {
634                        segments(_segments.stream().filter(predicate).toList());
635                        return this;
636                }
637
638                @Override
639                public Builder map(
640                        final Function<? super TrackSegment, ? extends TrackSegment> mapper
641                ) {
642                        segments(
643                                _segments.stream()
644                                        .map(mapper)
645                                        .map(TrackSegment.class::cast)
646                                        .toList()
647                        );
648                        return this;
649                }
650
651                @Override
652                public Builder flatMap(
653                        final Function<
654                                ? super TrackSegment,
655                                ? extends List<TrackSegment>> mapper
656                ) {
657                        segments(
658                                _segments.stream()
659                                        .flatMap(segment -> mapper.apply(segment).stream())
660                                        .toList()
661                        );
662                        return this;
663                }
664
665                @Override
666                public Builder listMap(
667                        final Function<
668                                ? super List<TrackSegment>,
669                                ? extends List<TrackSegment>> mapper
670                ) {
671                        segments(mapper.apply(_segments));
672                        return this;
673                }
674
675                /**
676                 * Create a new GPX track from the current builder state.
677                 *
678                 * @return a new GPX track from the current builder state
679                 */
680                @Override
681                public Track build() {
682                        return of(
683                                _name,
684                                _comment,
685                                _description,
686                                _source,
687                                _links,
688                                _number,
689                                _type,
690                                _extensions,
691                                _segments
692                        );
693                }
694        }
695
696        public static Builder builder() {
697                return new Builder();
698        }
699
700
701        /* *************************************************************************
702         *  Static object creation methods
703         * ************************************************************************/
704
705        /**
706         * Create a new {@code Track} with the given parameters.
707         *
708         * @since 1.5
709         *
710         * @param name the GPS name of the track
711         * @param comment the GPS comment for the track
712         * @param description user description of the track
713         * @param source the source of data. Included to give user some idea of
714         *        reliability and accuracy of data.
715         * @param links the links to external information about track
716         * @param number the GPS track number
717         * @param type the type (classification) of track
718         * @param extensions the extensions document
719         * @param segments the track-segments holds a list of track-points which are
720         *        logically connected in order. To represent a single GPS track
721         *        where GPS reception was lost, or the GPS receiver was turned off,
722         *        start a new track-segment for each continuous span of track data.
723         * @return a new {@code Track} with the given parameters
724         * @throws NullPointerException if the {@code links} or the {@code segments}
725         *         sequence is {@code null}
726         */
727        public static Track of(
728                final String name,
729                final String comment,
730                final String description,
731                final String source,
732                final List<Link> links,
733                final UInt number,
734                final String type,
735                final Document extensions,
736                final List<TrackSegment> segments
737        ) {
738                return new Track(
739                        name,
740                        comment,
741                        description,
742                        source,
743                        links,
744                        number,
745                        type,
746                        XML.extensions(XML.clone(extensions)),
747                        segments
748                );
749        }
750
751        /**
752         * Create a new {@code Track} with the given parameters.
753         *
754         * @param name the GPS name of the track
755         * @param comment the GPS comment for the track
756         * @param description user description of the track
757         * @param source the source of data. Included to give user some idea of
758         *        reliability and accuracy of data.
759         * @param links the links to external information about track
760         * @param number the GPS track number
761         * @param type the type (classification) of track
762         * @param segments the track-segments holds a list of track-points which are
763         *        logically connected in order. To represent a single GPS track
764         *        where GPS reception was lost, or the GPS receiver was turned off,
765         *        start a new track-segment for each continuous span of track data.
766         * @return a new {@code Track} with the given parameters
767         * @throws NullPointerException if the {@code links} or the {@code segments}
768         *         sequence is {@code null}
769         */
770        public static Track of(
771                final String name,
772                final String comment,
773                final String description,
774                final String source,
775                final List<Link> links,
776                final UInt number,
777                final String type,
778                final List<TrackSegment> segments
779        ) {
780                return of(
781                        name,
782                        comment,
783                        description,
784                        source,
785                        links,
786                        number,
787                        type,
788                        null,
789                        segments
790                );
791        }
792
793
794        /* *************************************************************************
795         *  Java object serialization
796         * ************************************************************************/
797
798        @Serial
799        private Object writeReplace() {
800                return new SerialProxy(SerialProxy.TRACK, this);
801        }
802
803        @Serial
804        private void readObject(final ObjectInputStream stream)
805                throws InvalidObjectException
806        {
807                throw new InvalidObjectException("Serialization proxy required.");
808        }
809
810        void write(final DataOutput out) throws IOException {
811                IO.writeNullableString(_name, out);
812                IO.writeNullableString(_comment, out);
813                IO.writeNullableString(_description, out);
814                IO.writeNullableString(_source, out);
815                IO.writes(_links, Link::write, out);
816                IO.writeNullable(_number, UInt::write, out);
817                IO.writeNullableString(_type, out);
818                IO.writeNullable(_extensions, IO::write, out);
819                IO.writes(_segments, TrackSegment::write, out);
820        }
821
822        static Track read(final DataInput in) throws IOException {
823                return new Track(
824                        IO.readNullableString(in),
825                        IO.readNullableString(in),
826                        IO.readNullableString(in),
827                        IO.readNullableString(in),
828                        IO.reads(Link::read, in),
829                        IO.readNullable(UInt::read, in),
830                        IO.readNullableString(in),
831                        IO.readNullable(IO::readDoc, in),
832                        IO.reads(TrackSegment::read, in)
833                );
834        }
835
836        /* *************************************************************************
837         *  XML stream object serialization
838         * ************************************************************************/
839
840        private static String url(final Track track) {
841                return track.getLinks().isEmpty()
842                        ? null
843                        : track.getLinks().get(0).getHref().toString();
844        }
845
846        private static String urlname(final Track track) {
847                return track.getLinks().isEmpty()
848                        ? null
849                        : track.getLinks().get(0).getText().orElse(null);
850        }
851
852        // Define the needed writers for the different versions.
853        private static XMLWriters<Track>
854        writers(final Function<? super Number, String> formatter) {
855                return new XMLWriters<Track>()
856                        .v00(XMLWriter.elem("name").map(t -> t._name))
857                        .v00(XMLWriter.elem("cmt").map(r -> r._comment))
858                        .v00(XMLWriter.elem("desc").map(r -> r._description))
859                        .v00(XMLWriter.elem("src").map(r -> r._source))
860                        .v11(XMLWriter.elems(Link.WRITER).map(r -> r._links))
861                        .v10(XMLWriter.elem("url").map(Track::url))
862                        .v10(XMLWriter.elem("urlname").map(Track::urlname))
863                        .v00(XMLWriter.elem("number").map(r -> toIntString(r._number)))
864                        .v00(XMLWriter.elem("type").map(r -> r._type))
865                        .v00(XMLWriter.doc("extensions").map(gpx -> gpx._extensions))
866                        .v10(XMLWriter.elems(TrackSegment.xmlWriter(Version.V10, formatter)).map(r -> r._segments))
867                        .v11(XMLWriter.elems(TrackSegment.xmlWriter(Version.V11, formatter)).map(r -> r._segments));
868        }
869
870        // Define the needed readers for the different versions.
871        private static XMLReaders
872        readers(final Function<? super String, Length> lengthParser) {
873                return new XMLReaders()
874                        .v00(XMLReader.elem("name"))
875                        .v00(XMLReader.elem("cmt"))
876                        .v00(XMLReader.elem("desc"))
877                        .v00(XMLReader.elem("src"))
878                        .v11(XMLReader.elems(Link.READER))
879                        .v10(XMLReader.elem("url").map(Format::parseURI))
880                        .v10(XMLReader.elem("urlname"))
881                        .v00(XMLReader.elem("number").map(UInt::parse))
882                        .v00(XMLReader.elem("type"))
883                        .v00(XMLReader.doc("extensions"))
884                        .v10(XMLReader.elems(TrackSegment.xmlReader(Version.V10, lengthParser)))
885                        .v11(XMLReader.elems(TrackSegment.xmlReader(Version.V11, lengthParser)));
886        }
887
888        static XMLWriter<Track> xmlWriter(
889                final Version version,
890                final Function<? super Number, String> formatter
891        ) {
892                return XMLWriter.elem("trk", writers(formatter).writers(version));
893        }
894
895        static XMLReader<Track> xmlReader(
896                final Version version,
897                final Function<? super String, Length> lengthParser
898        ) {
899                return XMLReader.elem(
900                        version == Version.V10 ? Track::toTrackV10 : Track::toTrackV11,
901                        "trk",
902                        readers(lengthParser).readers(version)
903                );
904        }
905
906        @SuppressWarnings("unchecked")
907        private static Track toTrackV11(final Object[] v) {
908                return new Track(
909                        (String)v[0],
910                        (String)v[1],
911                        (String)v[2],
912                        (String)v[3],
913                        (List<Link>)v[4],
914                        (UInt)v[5],
915                        (String)v[6],
916                        XML.extensions((Document)v[7]),
917                        (List<TrackSegment>)v[8]
918                );
919        }
920
921        @SuppressWarnings("unchecked")
922        private static Track toTrackV10(final Object[] v) {
923                return new Track(
924                        (String)v[0],
925                        (String)v[1],
926                        (String)v[2],
927                        (String)v[3],
928                        v[4] != null
929                                ? List.of(Link.of((URI)v[4], (String)v[5], null))
930                                : null,
931                        (UInt)v[6],
932                        (String)v[7],
933                        XML.extensions((Document)v[8]),
934                        (List<TrackSegment>)v[9]
935                );
936        }
937
938}