Track.java
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  */
020 package io.jenetics.jpx;
021 
022 import static java.lang.String.format;
023 import static java.util.Objects.hash;
024 import static java.util.Objects.requireNonNull;
025 import static io.jenetics.jpx.Format.toIntString;
026 import static io.jenetics.jpx.Lists.copyOf;
027 import static io.jenetics.jpx.Lists.copyTo;
028 
029 import java.io.DataInput;
030 import java.io.DataOutput;
031 import java.io.IOException;
032 import java.io.InvalidObjectException;
033 import java.io.ObjectInputStream;
034 import java.io.Serial;
035 import java.io.Serializable;
036 import java.net.URI;
037 import java.util.ArrayList;
038 import java.util.Iterator;
039 import java.util.List;
040 import java.util.Objects;
041 import java.util.Optional;
042 import java.util.function.Consumer;
043 import java.util.function.Function;
044 import java.util.function.Predicate;
045 import java.util.stream.Stream;
046 
047 import org.w3c.dom.Document;
048 
049 import 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  */
074 public 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 outthrows 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 inthrows 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 }