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