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}