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.nio.charset.StandardCharsets.UTF_8; 024import static java.util.Locale.ENGLISH; 025import static java.util.Objects.hash; 026import static java.util.Objects.requireNonNull; 027import static io.jenetics.jpx.Lists.copyOf; 028import static io.jenetics.jpx.Lists.copyTo; 029 030import java.io.ByteArrayInputStream; 031import java.io.ByteArrayOutputStream; 032import java.io.DataInput; 033import java.io.DataInputStream; 034import java.io.DataOutput; 035import java.io.DataOutputStream; 036import java.io.File; 037import java.io.IOException; 038import java.io.InputStream; 039import java.io.InputStreamReader; 040import java.io.InvalidObjectException; 041import java.io.ObjectInputStream; 042import java.io.OutputStream; 043import java.io.OutputStreamWriter; 044import java.io.Serial; 045import java.io.Serializable; 046import java.io.UncheckedIOException; 047import java.net.URI; 048import java.nio.file.Files; 049import java.nio.file.Path; 050import java.nio.file.Paths; 051import java.text.NumberFormat; 052import java.time.Instant; 053import java.util.ArrayList; 054import java.util.List; 055import java.util.Objects; 056import java.util.Optional; 057import java.util.function.Consumer; 058import java.util.function.Function; 059import java.util.function.Predicate; 060import java.util.stream.Stream; 061 062import javax.xml.stream.XMLStreamException; 063import javax.xml.stream.XMLStreamReader; 064import javax.xml.stream.XMLStreamWriter; 065import javax.xml.transform.Result; 066import javax.xml.transform.Source; 067import javax.xml.transform.stream.StreamResult; 068import javax.xml.transform.stream.StreamSource; 069 070import org.w3c.dom.Document; 071 072/** 073 * GPX documents contain a metadata header, followed by way-points, routes, and 074 * tracks. You can add your own elements to the extensions section of the GPX 075 * document. 076 * <p> 077 * <em><b>Examples:</b></em> 078 * <p> 079 * <b>Creating a GPX object with one track-segment and 3 track-points</b> 080 * <pre>{@code 081 * final GPX gpx = GPX.builder() 082 * .addTrack(track -> track 083 * .addSegment(segment -> segment 084 * .addPoint(p -> p.lat(48.20100).lon(16.31651).ele(283)) 085 * .addPoint(p -> p.lat(48.20112).lon(16.31639).ele(278)) 086 * .addPoint(p -> p.lat(48.20126).lon(16.31601).ele(274)))) 087 * .build(); 088 * }</pre> 089 * 090 * <b>Writing a GPX file</b> 091 * <pre>{@code 092 * final var indent = new GPX.Writer.Indent(" "); 093 * GPX.Writer.of(indent).write(gpx, Path.of("points.gpx")); 094 * }</pre> 095 * 096 * This will produce the following output. 097 * <pre>{@code 098 * <gpx version="1.1" creator="JPX - https://github.com/jenetics/jpx" xmlns="http://www.topografix.com/GPX/1/1"> 099 * <trk> 100 * <trkseg> 101 * <trkpt lat="48.201" lon="16.31651"> 102 * <ele>283</ele> 103 * </trkpt> 104 * <trkpt lat="48.20112" lon="16.31639"> 105 * <ele>278</ele> 106 * </trkpt> 107 * <trkpt lat="48.20126" lon="16.31601"> 108 * <ele>274</ele> 109 * </trkpt> 110 * </trkseg> 111 * </trk> 112 * </gpx> 113 * }</pre> 114 * 115 * <b>Reading a GPX file</b> 116 * <pre>{@code 117 * final GPX gpx = GPX.read("points.xml"); 118 * }</pre> 119 * 120 * <b>Reading erroneous GPX files</b> 121 * <pre>{@code 122 * final GPX gpx = GPX.Reader.of(GPX.Reader.Mode.LENIENT).read("track.xml"); 123 * }</pre> 124 * 125 * This allows to read otherwise invalid GPX files, like 126 * <pre>{@code 127 * <?xml version="1.0" encoding="UTF-8"?> 128 * <gpx version="1.1" creator="GPSBabel - http://www.gpsbabel.org" xmlns="http://www.topografix.com/GPX/1/1"> 129 * <metadata> 130 * <time>2019-12-31T21:36:04.134Z</time> 131 * <bounds minlat="48.175186667" minlon="16.299580000" maxlat="48.199555000" maxlon="16.416933333"/> 132 * </metadata> 133 * <trk> 134 * <trkseg> 135 * <trkpt lat="48.184298333" lon="16.299580000"> 136 * <ele>0.000</ele> 137 * <time>2011-03-20T09:47:16Z</time> 138 * <geoidheight>43.5</geoidheight> 139 * <fix>2d</fix> 140 * <sat>3</sat> 141 * <hdop>4.200000</hdop> 142 * <vdop>1.000000</vdop> 143 * <pdop>4.300000</pdop> 144 * </trkpt> 145 * <trkpt lat="48.175186667" lon="16.303916667"> 146 * <ele>0.000</ele> 147 * <time>2011-03-20T09:51:31Z</time> 148 * <geoidheight>43.5</geoidheight> 149 * <fix>2d</fix> 150 * <sat>3</sat> 151 * <hdop>16.600000</hdop> 152 * <vdop>0.900000</vdop> 153 * <pdop>16.600000</pdop> 154 * </trkpt> 155 * </trkseg> 156 * </trk> 157 * </gpx> 158 * }</pre> 159 * 160 * which is read as (if you write it again) 161 * <pre>{@code 162 * <?xml version="1.0" encoding="UTF-8"?> 163 * <gpx version="1.1" creator="GPSBabel - http://www.gpsbabel.org" xmlns="http://www.topografix.com/GPX/1/1"> 164 * <metadata> 165 * <time>2019-12-31T21:36:04.134Z</time> 166 * <bounds minlat="48.175187" minlon="16.29958" maxlat="48.199555" maxlon="16.416933"></bounds> 167 * </metadata> 168 * <trk> 169 * <trkseg> 170 * <trkpt lat="48.184298" lon="16.29958"> 171 * <ele>0</ele> 172 * <time>2011-03-20T09:47:16Z</time> 173 * <geoidheight>43.5</geoidheight> 174 * <fix>2d</fix> 175 * <sat>3</sat> 176 * <hdop>4.2</hdop> 177 * <vdop>1</vdop> 178 * <pdop>4.3</pdop> 179 * </trkpt> 180 * <trkpt lat="48.175187" lon="16.303917"> 181 * <ele>0</ele> 182 * <time>2011-03-20T09:51:31Z</time> 183 * <geoidheight>43.5</geoidheight> 184 * <fix>2d</fix> 185 * <sat>3</sat> 186 * <hdop>16.6</hdop> 187 * <vdop>0.9</vdop> 188 * <pdop>16.6</pdop> 189 * </trkpt> 190 * </trkseg> 191 * </trk> 192 * </gpx> 193 * }</pre> 194 * 195 * <b>Converting a GPX object to an XML {@link Document}</b> 196 * <pre>{@code 197 * final GPX gpx = ...; 198 * 199 * final Document doc = XMLProvider.provider() 200 * .documentBuilderFactory() 201 * .newDocumentBuilder() 202 * .newDocument(); 203 * 204 * // The GPX data are written to the empty `doc` object. 205 * GPX.Writer.DEFAULT.write(gpx, new DOMResult(doc)); 206 * }</pre> 207 * 208 * @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a> 209 * @version 2.0 210 * @since 1.0 211 */ 212public final class GPX implements Serializable { 213 214 @Serial 215 private static final long serialVersionUID = 2L; 216 217 /** 218 * Represents the available GPX versions. 219 * 220 * @version 1.3 221 * @since 1.3 222 */ 223 public enum Version { 224 225 /** 226 * The GPX version 1.0. This version can be read and written. 227 * 228 * @see <a href="http://www.topografix.com/gpx_manual.asp">GPX 1.0</a> 229 */ 230 V10("1.0", "http://www.topografix.com/GPX/1/0"), 231 232 /** 233 * The GPX version 1.1. This is the default version and can be read and 234 * written. 235 * 236 * @see <a href="http://www.topografix.com/GPX/1/1">GPX 1.1</a> 237 */ 238 V11("1.1", "http://www.topografix.com/GPX/1/1"); 239 240 private final String _value; 241 private final String _namespaceURI; 242 243 Version(final String value, final String namespaceURI) { 244 _value = value; 245 _namespaceURI = namespaceURI; 246 } 247 248 /** 249 * Return the version string value. 250 * 251 * @return the version string value 252 */ 253 public String getValue() { 254 return _value; 255 } 256 257 /** 258 * Return the namespace URI of this version. 259 * 260 * @since 1.5 261 * 262 * @return the namespace URI of this version 263 */ 264 public String getNamespaceURI() { 265 return _namespaceURI; 266 } 267 268 /** 269 * Return the version from the given {@code version} string. Allowed 270 * values are "1.0" and "1.1". 271 * 272 * @param version the version string 273 * @return the version from the given {@code version} string 274 * @throws IllegalArgumentException if the given {@code version} string 275 * is neither "1.0" nor "1.1" 276 * @throws NullPointerException if the given {@code version} string is 277 * {@code null} 278 */ 279 public static Version of(final String version) { 280 return switch (version) { 281 case "1.0" -> V10; 282 case "1.1" -> V11; 283 default -> throw new IllegalArgumentException(format( 284 "Unknown version string: '%s'.", version 285 )); 286 }; 287 } 288 } 289 290 private static final String _CREATOR = "JPX - https://github.com/jenetics/jpx"; 291 292 private final String _creator; 293 private final Version _version; 294 private final Metadata _metadata; 295 private final List<WayPoint> _wayPoints; 296 private final List<Route> _routes; 297 private final List<Track> _tracks; 298 private final Document _extensions; 299 300 /** 301 * Create a new {@code GPX} object with the given data. 302 * 303 * @param creator the name or URL of the software that created your GPX 304 * document. This allows others to inform the creator of a GPX 305 * instance document that fails to validate. 306 * @param version the GPX version 307 * @param metadata the metadata about the GPS file 308 * @param wayPoints the way-points 309 * @param routes the routes 310 * @param tracks the tracks 311 * @param extensions the XML extensions document 312 * @throws NullPointerException if the {@code creator} or {@code version} is 313 * {@code null} 314 */ 315 private GPX( 316 final Version version, 317 final String creator, 318 final Metadata metadata, 319 final List<WayPoint> wayPoints, 320 final List<Route> routes, 321 final List<Track> tracks, 322 final Document extensions 323 ) { 324 _version = requireNonNull(version); 325 _creator = requireNonNull(creator); 326 _metadata = metadata; 327 _wayPoints = copyOf(wayPoints); 328 _routes = copyOf(routes); 329 _tracks = copyOf(tracks); 330 _extensions = extensions; 331 } 332 333 /** 334 * Return the version number of the GPX file. 335 * 336 * @return the version number of the GPX file 337 */ 338 public String getVersion() { 339 return _version._value; 340 } 341 342 /** 343 * Return the name or URL of the software that created your GPX document. 344 * This allows others to inform the creator of a GPX instance document that 345 * fails to validate. 346 * 347 * @return the name or URL of the software that created your GPX document 348 */ 349 public String getCreator() { 350 return _creator; 351 } 352 353 /** 354 * Return the metadata of the GPX file. 355 * 356 * @return the metadata of the GPX file 357 */ 358 public Optional<Metadata> getMetadata() { 359 return Optional.ofNullable(_metadata); 360 } 361 362 /** 363 * Return an unmodifiable list of the {@code GPX} way-points. 364 * 365 * @return an unmodifiable list of the {@code GPX} way-points. 366 */ 367 public List<WayPoint> getWayPoints() { 368 return _wayPoints; 369 } 370 371 /** 372 * Return a stream with all {@code WayPoint}s of this {@code GPX} object. 373 * 374 * @return a stream with all {@code WayPoint}s of this {@code GPX} object 375 */ 376 public Stream<WayPoint> wayPoints() { 377 return _wayPoints.stream(); 378 } 379 380 /** 381 * Return an unmodifiable list of the {@code GPX} routes. 382 * 383 * @return an unmodifiable list of the {@code GPX} routes. 384 */ 385 public List<Route> getRoutes() { 386 return _routes; 387 } 388 389 /** 390 * Return a stream of the {@code GPX} routes. 391 * 392 * @return a stream of the {@code GPX} routes. 393 */ 394 public Stream<Route> routes() { 395 return _routes.stream(); 396 } 397 398 /** 399 * Return an unmodifiable list of the {@code GPX} tracks. 400 * 401 * @return an unmodifiable list of the {@code GPX} tracks. 402 */ 403 public List<Track> getTracks() { 404 return _tracks; 405 } 406 407 /** 408 * Return a stream of the {@code GPX} tracks. 409 * 410 * @return a stream of the {@code GPX} tracks. 411 */ 412 public Stream<Track> tracks() { 413 return _tracks.stream(); 414 } 415 416 /** 417 * Return the (cloned) extensions document. The root element of the returned 418 * document has the name {@code extensions}. 419 * <pre>{@code 420 * <extensions> 421 * ... 422 * </extensions> 423 * }</pre> 424 * 425 * @since 1.5 426 * 427 * @return the extensions document 428 */ 429 public Optional<Document> getExtensions() { 430 return Optional.ofNullable(_extensions).map(XML::clone); 431 } 432 433 /** 434 * Convert the <em>immutable</em> GPX object into a <em>mutable</em> 435 * builder initialized with the current GPX values. 436 * 437 * @since 1.1 438 * 439 * @return a new track builder initialized with the values of {@code this} 440 * GPX object 441 */ 442 public Builder toBuilder() { 443 return builder(_version, _creator) 444 .metadata(_metadata) 445 .wayPoints(_wayPoints) 446 .routes(_routes) 447 .tracks(_tracks) 448 .extensions(_extensions); 449 } 450 451 @Override 452 public String toString() { 453 return format( 454 "GPX[way-points=%s, routes=%s, tracks=%s]", 455 getWayPoints().size(), getRoutes().size(), getTracks().size() 456 ); 457 } 458 459 @Override 460 public int hashCode() { 461 return hash( 462 _creator, 463 _version, 464 _metadata, 465 _wayPoints, 466 _routes, 467 _tracks 468 ); 469 } 470 471 @Override 472 public boolean equals(final Object obj) { 473 return obj == this || 474 obj instanceof GPX gpx && 475 Objects.equals(gpx._creator, _creator) && 476 Objects.equals(gpx._version, _version) && 477 Objects.equals(gpx._metadata, _metadata) && 478 Objects.equals(gpx._wayPoints, _wayPoints) && 479 Objects.equals(gpx._routes, _routes) && 480 Objects.equals(gpx._tracks, _tracks); 481 } 482 483 /** 484 * Builder class for creating immutable {@code GPX} objects. 485 * <p> 486 * Creating a GPX object with one track-segment and 3 track-points: 487 * <pre>{@code 488 * final GPX gpx = GPX.builder() 489 * .addTrack(track -> track 490 * .addSegment(segment -> segment 491 * .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160)) 492 * .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161)) 493 * .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162)))) 494 * .build(); 495 * }</pre> 496 */ 497 public static final class Builder { 498 private String _creator; 499 private Version _version; 500 private Metadata _metadata; 501 private final List<WayPoint> _wayPoints = new ArrayList<>(); 502 private final List<Route> _routes = new ArrayList<>(); 503 private final List<Track> _tracks = new ArrayList<>(); 504 private Document _extensions; 505 506 private Builder(final Version version, final String creator) { 507 _version = requireNonNull(version); 508 _creator = requireNonNull(creator); 509 } 510 511 /** 512 * Set the GPX creator. 513 * 514 * @param creator the GPX creator 515 * @throws NullPointerException if the given argument is {@code null} 516 * @return {@code this} {@code Builder} for method chaining 517 */ 518 public Builder creator(final String creator) { 519 _creator = requireNonNull(creator); 520 return this; 521 } 522 523 /** 524 * Return the current creator value. 525 * 526 * @since 1.1 527 * 528 * @return the current creator value 529 */ 530 public String creator() { 531 return _creator; 532 } 533 534 /** 535 * Set the GPX version. 536 * 537 * @since 1.3 538 * 539 * @param version the GPX version 540 * @throws NullPointerException if the given argument is {@code null} 541 * @return {@code this} {@code Builder} for method chaining 542 */ 543 public Builder version(final Version version) { 544 _version = requireNonNull(version); 545 return this; 546 } 547 548 /** 549 * Return the current version value. 550 * 551 * @since 1.1 552 * 553 * @return the current version value 554 */ 555 public String version() { 556 return _version._value; 557 } 558 559 /** 560 * Set the GPX metadata. 561 * 562 * @param metadata the GPX metadata 563 * @return {@code this} {@code Builder} for method chaining 564 */ 565 public Builder metadata(final Metadata metadata) { 566 _metadata = metadata; 567 return this; 568 } 569 570 /** 571 * Allows setting partial metadata without messing up with the 572 * {@link Metadata.Builder} class. 573 * <pre>{@code 574 * final GPX gpx = GPX.builder() 575 * .metadata(md -> md.author("Franz Wilhelmstötter")) 576 * .addTrack(...) 577 * .build(); 578 * }</pre> 579 * 580 * @param metadata the metadata consumer 581 * @return {@code this} {@code Builder} for method chaining 582 * @throws NullPointerException if the given argument is {@code null} 583 */ 584 public Builder metadata(final Consumer<? super Metadata.Builder> metadata) { 585 final Metadata.Builder builder = Metadata.builder(); 586 metadata.accept(builder); 587 588 final Metadata md = builder.build(); 589 _metadata = md.isEmpty() ? null : md; 590 591 return this; 592 } 593 594 /** 595 * Return the current metadata value. 596 * 597 * @since 1.1 598 * 599 * @return the current metadata value 600 */ 601 public Optional<Metadata> metadata() { 602 return Optional.ofNullable(_metadata); 603 } 604 605 /** 606 * Sets the way-points of the {@code GPX} object. The list of way-points 607 * may be {@code null}. 608 * 609 * @param wayPoints the {@code GPX} way-points 610 * @return {@code this} {@code Builder} for method chaining 611 * @throws NullPointerException if one of the way-points in the list is 612 * {@code null} 613 */ 614 public Builder wayPoints(final List<WayPoint> wayPoints) { 615 copyTo(wayPoints, _wayPoints); 616 return this; 617 } 618 619 /** 620 * Add one way-point to the {@code GPX} object. 621 * 622 * @param wayPoint the way-point to add 623 * @return {@code this} {@code Builder} for method chaining 624 * @throws NullPointerException if the given {@code wayPoint} is 625 * {@code null} 626 */ 627 public Builder addWayPoint(final WayPoint wayPoint) { 628 _wayPoints.add(requireNonNull(wayPoint)); 629 return this; 630 } 631 632 /** 633 * Add a way-point to the {@code GPX} object using a 634 * {@link WayPoint.Builder}. 635 * <pre>{@code 636 * final GPX gpx = GPX.builder() 637 * .addWayPoint(wp -> wp.lat(23.6).lon(13.5).ele(50)) 638 * .build(); 639 * }</pre> 640 * 641 * @param wayPoint the way-point to add, configured by the way-point 642 * builder 643 * @return {@code this} {@code Builder} for method chaining 644 * @throws NullPointerException if the given argument is {@code null} 645 */ 646 public Builder addWayPoint(final Consumer<? super WayPoint.Builder> wayPoint) { 647 final WayPoint.Builder builder = WayPoint.builder(); 648 wayPoint.accept(builder); 649 return addWayPoint(builder.build()); 650 } 651 652 /** 653 * Return the current way-points. The returned list is mutable. 654 * 655 * @since 1.1 656 * 657 * @return the current, mutable way-point list 658 */ 659 public List<WayPoint> wayPoints() { 660 return new NonNullList<>(_wayPoints); 661 } 662 663 /** 664 * Sets the routes of the {@code GPX} object. The list of routes may be 665 * {@code null}. 666 * 667 * @param routes the {@code GPX} routes 668 * @return {@code this} {@code Builder} for method chaining 669 * @throws NullPointerException if one of the routes is {@code null} 670 */ 671 public Builder routes(final List<Route> routes) { 672 copyTo(routes, _routes); 673 return this; 674 } 675 676 /** 677 * Add a route the {@code GPX} object. 678 * 679 * @param route the route to add 680 * @return {@code this} {@code Builder} for method chaining 681 * @throws NullPointerException if the given {@code route} is {@code null} 682 */ 683 public Builder addRoute(final Route route) { 684 _routes.add(requireNonNull(route)); 685 return this; 686 } 687 688 /** 689 * Add a route the {@code GPX} object. 690 * <pre>{@code 691 * final GPX gpx = GPX.builder() 692 * .addRoute(route -> route 693 * .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160)) 694 * .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))) 695 * .build(); 696 * }</pre> 697 * 698 * @param route the route to add, configured by the route builder 699 * @return {@code this} {@code Builder} for method chaining 700 * @throws NullPointerException if the given argument is {@code null} 701 */ 702 public Builder addRoute(final Consumer<? super Route.Builder> route) { 703 final Route.Builder builder = Route.builder(); 704 route.accept(builder); 705 return addRoute(builder.build()); 706 } 707 708 /** 709 * Return the current routes. The returned list is mutable. 710 * 711 * @since 1.1 712 * 713 * @return the current, mutable route list 714 */ 715 public List<Route> routes() { 716 return new NonNullList<>(_routes); 717 } 718 719 /** 720 * Sets the tracks of the {@code GPX} object. The list of tracks may be 721 * {@code null}. 722 * 723 * @param tracks the {@code GPX} tracks 724 * @return {@code this} {@code Builder} for method chaining 725 * @throws NullPointerException if one of the tracks is {@code null} 726 */ 727 public Builder tracks(final List<Track> tracks) { 728 copyTo(tracks, _tracks); 729 return this; 730 } 731 732 /** 733 * Add a track the {@code GPX} object. 734 * 735 * @param track the track to add 736 * @return {@code this} {@code Builder} for method chaining 737 * @throws NullPointerException if the given {@code track} is {@code null} 738 */ 739 public Builder addTrack(final Track track) { 740 _tracks.add(requireNonNull(track)); 741 return this; 742 } 743 744 /** 745 * Add a track the {@code GPX} object. 746 * <pre>{@code 747 * final GPX gpx = GPX.builder() 748 * .addTrack(track -> track 749 * .addSegment(segment -> segment 750 * .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160)) 751 * .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161)) 752 * .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162)))) 753 * .build(); 754 * }</pre> 755 * 756 * @param track the track to add, configured by the track builder 757 * @return {@code this} {@code Builder} for method chaining 758 * @throws NullPointerException if the given argument is {@code null} 759 */ 760 public Builder addTrack(final Consumer<? super Track.Builder> track) { 761 final Track.Builder builder = Track.builder(); 762 track.accept(builder); 763 return addTrack(builder.build()); 764 } 765 766 /** 767 * Return the current tracks. The returned list is mutable. 768 * 769 * @since 1.1 770 * 771 * @return the current, mutable track list 772 */ 773 public List<Track> tracks() { 774 return new NonNullList<>(_tracks); 775 } 776 777 778 /** 779 * Sets the extensions object, which may be {@code null}. The root 780 * element of the extensions document must be {@code extensions}. 781 * <pre>{@code 782 * <extensions> 783 * ... 784 * </extensions> 785 * }</pre> 786 * 787 * @since 1.5 788 * 789 * @param extensions the extensions document 790 * @return {@code this} {@code Builder} for method chaining 791 * @throws IllegalArgumentException if the root element is not the 792 * an {@code extensions} node 793 */ 794 public Builder extensions(final Document extensions) { 795 _extensions = XML.checkExtensions(extensions); 796 return this; 797 } 798 799 /** 800 * Return the current extensions 801 * 802 * @since 1.5 803 * 804 * @return the extensions document 805 */ 806 public Optional<Document> extensions() { 807 return Optional.ofNullable(_extensions); 808 } 809 810 /** 811 * Create an immutable {@code GPX} object from the current builder state. 812 * 813 * @return an immutable {@code GPX} object from the current builder state 814 */ 815 public GPX build() { 816 return of( 817 _version, 818 _creator, 819 _metadata, 820 _wayPoints, 821 _routes, 822 _tracks, 823 _extensions 824 ); 825 } 826 827 /** 828 * Return a new {@link WayPoint} filter. 829 * <pre>{@code 830 * final GPX filtered = gpx.toBuilder() 831 * .wayPointFilter() 832 * .filter(wp -> wp.getTime().isPresent()) 833 * .build()) 834 * .build(); 835 * }</pre> 836 * 837 * @since 1.1 838 * 839 * @return a new {@link WayPoint} filter 840 */ 841 public Filter<WayPoint, Builder> wayPointFilter() { 842 return new Filter<>() { 843 @Override 844 public Filter<WayPoint, Builder> filter( 845 final Predicate<? super WayPoint> predicate 846 ) { 847 wayPoints(_wayPoints.stream().filter(predicate).toList()); 848 return this; 849 } 850 851 @Override 852 public Filter<WayPoint, Builder> map( 853 final Function<? super WayPoint, ? extends WayPoint> mapper 854 ) { 855 wayPoints( 856 _wayPoints.stream() 857 .map(mapper) 858 .map(WayPoint.class::cast) 859 .toList() 860 ); 861 862 return this; 863 } 864 865 @Override 866 public Filter<WayPoint, Builder> flatMap( 867 final Function< 868 ? super WayPoint, 869 ? extends List<WayPoint>> mapper 870 ) { 871 wayPoints( 872 _wayPoints.stream() 873 .flatMap(wp -> mapper.apply(wp).stream()) 874 .toList() 875 ); 876 877 return this; 878 } 879 880 @Override 881 public Filter<WayPoint, Builder> listMap( 882 final Function< 883 ? super List<WayPoint>, 884 ? extends List<WayPoint>> mapper 885 ) { 886 wayPoints(mapper.apply(_wayPoints)); 887 888 return this; 889 } 890 891 @Override 892 public Builder build() { 893 return GPX.Builder.this; 894 } 895 896 }; 897 } 898 899 /** 900 * Return a new {@link Route} filter. 901 * <pre>{@code 902 * final GPX filtered = gpx.toBuilder() 903 * .routeFilter() 904 * .filter(Route::nonEmpty) 905 * .build()) 906 * .build(); 907 * }</pre> 908 * 909 * @since 1.1 910 * 911 * @return a new {@link Route} filter 912 */ 913 public Filter<Route, Builder> routeFilter() { 914 return new Filter<>() { 915 @Override 916 public Filter<Route, Builder> filter( 917 final Predicate<? super Route> predicate 918 ) { 919 routes( 920 _routes.stream() 921 .filter(predicate) 922 .toList() 923 ); 924 925 return this; 926 } 927 928 @Override 929 public Filter<Route, Builder> map( 930 final Function<? super Route, ? extends Route> mapper 931 ) { 932 routes( 933 _routes.stream() 934 .map(mapper) 935 .map(Route.class::cast) 936 .toList() 937 ); 938 939 return this; 940 } 941 942 @Override 943 public Filter<Route, Builder> flatMap( 944 final Function<? super Route, ? extends List<Route>> mapper) 945 { 946 routes( 947 _routes.stream() 948 .flatMap(route -> mapper.apply(route).stream()) 949 .toList() 950 ); 951 952 return this; 953 } 954 955 @Override 956 public Filter<Route, Builder> listMap( 957 final Function< 958 ? super List<Route>, 959 ? extends List<Route>> mapper 960 ) { 961 routes(mapper.apply(_routes)); 962 963 return this; 964 } 965 966 @Override 967 public Builder build() { 968 return GPX.Builder.this; 969 } 970 971 }; 972 } 973 974 /** 975 * Return a new {@link Track} filter. 976 * <pre>{@code 977 * final GPX merged = gpx.toBuilder() 978 * .trackFilter() 979 * .map(track -> track.toBuilder() 980 * .listMap(Filters::mergeSegments) 981 * .filter(TrackSegment::nonEmpty) 982 * .build()) 983 * .build() 984 * .build(); 985 * }</pre> 986 * 987 * @since 1.1 988 * 989 * @return a new {@link Track} filter 990 */ 991 public Filter<Track, Builder> trackFilter() { 992 return new Filter<>() { 993 @Override 994 public Filter<Track, Builder> filter( 995 final Predicate<? super Track> predicate 996 ) { 997 tracks(_tracks.stream().filter(predicate).toList()); 998 return this; 999 } 1000 1001 @Override 1002 public Filter<Track, Builder> map( 1003 final Function<? super Track, ? extends Track> mapper 1004 ) { 1005 tracks( 1006 _tracks.stream() 1007 .map(mapper) 1008 .map(Track.class::cast) 1009 .toList() 1010 ); 1011 1012 return this; 1013 } 1014 1015 @Override 1016 public Filter<Track, Builder> flatMap( 1017 final Function<? super Track, ? extends List<Track>> mapper 1018 ) { 1019 tracks( 1020 _tracks.stream() 1021 .flatMap(track -> mapper.apply(track).stream()) 1022 .toList() 1023 ); 1024 1025 return this; 1026 } 1027 1028 @Override 1029 public Filter<Track, Builder> listMap( 1030 final Function< 1031 ? super List<Track>, 1032 ? extends List<Track>> mapper 1033 ) { 1034 tracks(mapper.apply(_tracks)); 1035 1036 return this; 1037 } 1038 1039 @Override 1040 public Builder build() { 1041 return GPX.Builder.this; 1042 } 1043 1044 }; 1045 } 1046 1047 } 1048 1049 /** 1050 * Create a new GPX builder with the given GPX version and creator string. 1051 * 1052 * @since 1.3 1053 * 1054 * @param version the GPX version 1055 * @param creator the GPX creator 1056 * @return new GPX builder 1057 * @throws NullPointerException if one of the arguments is {@code null} 1058 */ 1059 public static Builder builder(final Version version, final String creator) { 1060 return new Builder(version, creator); 1061 } 1062 1063 1064 /** 1065 * Create a new GPX builder with the given GPX creator string. 1066 * 1067 * @param creator the GPX creator 1068 * @return new GPX builder 1069 * @throws NullPointerException if the given arguments is {@code null} 1070 */ 1071 public static Builder builder(final String creator) { 1072 return builder(Version.V11, creator); 1073 } 1074 1075 /** 1076 * Create a new GPX builder. 1077 * 1078 * @return new GPX builder 1079 */ 1080 public static Builder builder() { 1081 return builder(Version.V11, _CREATOR); 1082 } 1083 1084 1085 /** 1086 * Class for reading GPX files. A reader instance can be created by the 1087 * {@code GPX.reader} factory methods. 1088 * 1089 * @version 3.0 1090 * @since 1.3 1091 */ 1092 public static final class Reader { 1093 1094 /** 1095 * The possible GPX reader modes. 1096 * 1097 * @version 1.3 1098 * @since 1.3 1099 */ 1100 public enum Mode { 1101 1102 /** 1103 * In this mode the GPX reader tries to ignore invalid GPX values 1104 * and elements. 1105 */ 1106 LENIENT, 1107 1108 /** 1109 * Expects to read valid GPX files. 1110 */ 1111 STRICT 1112 } 1113 1114 /** 1115 * The <em>default </em>GPX reader, reading GPX files (v1.1) with 1116 * reading mode {@link Mode#STRICT}. 1117 * 1118 * @since 3.0 1119 */ 1120 public static final Reader DEFAULT = Reader.of(Version.V11, Mode.STRICT); 1121 1122 private final Version _version; 1123 private final Mode _mode; 1124 1125 private Reader(final Version version, final Mode mode) { 1126 _version = requireNonNull(version); 1127 _mode = requireNonNull(mode); 1128 } 1129 1130 /** 1131 * Return the GPX version {@code this} reader is able to read. 1132 * 1133 * @return the GPX version of {@code this} reader 1134 */ 1135 public Version version() { 1136 return _version; 1137 } 1138 1139 /** 1140 * Return the current reader mode. 1141 * 1142 * @return the current reader mode 1143 */ 1144 public Mode mode() { 1145 return _mode; 1146 } 1147 1148 /** 1149 * Read a GPX object from the given input {@code source}. This is the 1150 * most general method for reading a {@code GPX} object. 1151 * 1152 * @since 3.0 1153 * 1154 * @param source the input source from where the GPX date is read 1155 * @return the GPX object read from the source 1156 * @throws IOException if the GPX object can't be read 1157 * @throws NullPointerException if the given {@code source} is 1158 * {@code null} 1159 * @throws InvalidObjectException if the gpx input is invalid. 1160 * @throws UnsupportedOperationException if the defined 1161 * {@link javax.xml.stream.XMLInputFactory} doesn't support 1162 * the given {@code source} 1163 */ 1164 public GPX read(final Source source) 1165 throws IOException 1166 { 1167 try { 1168 final XMLStreamReader reader = XMLProvider.provider() 1169 .xmlInputFactory() 1170 .createXMLStreamReader(source); 1171 1172 try (var input = new XMLStreamReaderAdapter(reader)) { 1173 if (input.hasNext()) { 1174 input.next(); 1175 1176 final var format = NumberFormat.getNumberInstance(ENGLISH); 1177 final Function<String, Length> lengthParser = string -> 1178 Length.parse(string, format); 1179 1180 return GPX.xmlReader(_version, lengthParser) 1181 .read(input, _mode == Mode.LENIENT); 1182 } else { 1183 throw new InvalidObjectException("No 'gpx' element found."); 1184 } 1185 } catch (XMLStreamException e) { 1186 throw new InvalidObjectException( 1187 "Invalid GPX: " + e.getMessage() 1188 ); 1189 } catch (IllegalArgumentException e) { 1190 final var ioe = new InvalidObjectException(e.getMessage()); 1191 throw (InvalidObjectException)ioe.initCause(e); 1192 } 1193 } catch (XMLStreamException e) { 1194 throw new IOException(e); 1195 } 1196 } 1197 1198 /** 1199 * Read a GPX object from the given {@code input} stream. 1200 * 1201 * @param input the input stream from where the GPX date is read 1202 * @return the GPX object read from the in stream 1203 * @throws IOException if the GPX object can't be read 1204 * @throws NullPointerException if the given {@code input} stream is 1205 * {@code null} 1206 * @throws InvalidObjectException if the gpx input is invalid. 1207 */ 1208 public GPX read(final InputStream input) 1209 throws IOException 1210 { 1211 final var wrapper = new NonCloseableInputStream(input); 1212 try (var reader = new InputStreamReader(wrapper, UTF_8)) { 1213 return read(new StreamSource(reader)); 1214 } 1215 } 1216 1217 /** 1218 * Read a GPX object from the given {@code path}. 1219 * 1220 * @param path the input path from where the GPX date is read 1221 * @return the GPX object read from the input stream 1222 * @throws IOException if the GPX object can't be read 1223 * @throws NullPointerException if the given {@code input} stream is 1224 * {@code null} 1225 * @throws InvalidObjectException if the gpx input is invalid. 1226 */ 1227 public GPX read(final Path path) throws IOException { 1228 try (var input = Files.newInputStream(path)) { 1229 return read(input); 1230 } 1231 } 1232 1233 /** 1234 * Read a GPX object from the given {@code file}. 1235 * 1236 * @param file the input file from where the GPX date is read 1237 * @return the GPX object read from the input stream 1238 * @throws IOException if the GPX object can't be read 1239 * @throws NullPointerException if the given {@code input} stream is 1240 * {@code null} 1241 * @throws InvalidObjectException if the gpx input is invalid. 1242 */ 1243 public GPX read(final File file) throws IOException { 1244 return read(file.toPath()); 1245 } 1246 1247 /** 1248 * Read a GPX object from the given {@code path}. 1249 * 1250 * @param path the input path from where the GPX date is read 1251 * @return the GPX object read from the input stream 1252 * @throws IOException if the GPX object can't be read 1253 * @throws NullPointerException if the given {@code input} stream is 1254 * {@code null} 1255 * @throws InvalidObjectException if the gpx input is invalid. 1256 */ 1257 public GPX read(final String path) throws IOException { 1258 return read(Paths.get(path)); 1259 } 1260 1261 /** 1262 * Create a GPX object from the given GPX-XML string. 1263 * 1264 * @see GPX.Writer#toString(GPX) 1265 * 1266 * @param xml the GPX XML string 1267 * @return the GPX object created from the given XML string 1268 * @throws IllegalArgumentException if the given {@code xml} is not a 1269 * valid GPX XML string 1270 * @throws NullPointerException if the given {@code xml} string is 1271 * {@code null} 1272 */ 1273 public GPX fromString(final String xml) { 1274 try { 1275 return read(new ByteArrayInputStream(xml.getBytes())); 1276 } catch (InvalidObjectException e) { 1277 if (e.getCause() instanceof IllegalArgumentException iae) { 1278 throw iae; 1279 } else { 1280 throw new IllegalArgumentException(e); 1281 } 1282 } catch (IOException e) { 1283 throw new IllegalArgumentException(e); 1284 } 1285 } 1286 1287 /** 1288 * Create a GPX object from the given {@code byte[]} array. 1289 * 1290 * @see GPX.Writer#toByteArray(GPX) 1291 * 1292 * @param bytes the GPX {@code byte[]} array 1293 * @param offset the offset in the buffer of the first byte to read. 1294 * @param length the maximum number of bytes to read from the buffer. 1295 * @return the GPX object created from the given {@code byte[]} array 1296 * @throws IllegalArgumentException if the given {@code byte[]} array 1297 * doesn't represent a valid GPX object 1298 * @throws NullPointerException if the given {@code bytes} is {@code null} 1299 */ 1300 GPX formByteArray( 1301 final byte[] bytes, 1302 final int offset, 1303 final int length 1304 ) { 1305 final var in = new ByteArrayInputStream(bytes, offset, length); 1306 try (var din = new DataInputStream(in)) { 1307 return GPX.read(din); 1308 } catch (IOException e) { 1309 throw new IllegalArgumentException(e); 1310 } 1311 } 1312 1313 /** 1314 * Create a GPX object from the given {@code byte[]} array. 1315 * 1316 * @see GPX.Writer#toByteArray(GPX) 1317 * 1318 * @param bytes the GPX {@code byte[]} array 1319 * @return the GPX object created from the given {@code byte[]} array 1320 * @throws IllegalArgumentException if the given {@code byte[]} array 1321 * doesn't represent a valid GPX object 1322 * @throws NullPointerException if the given {@code bytes} is {@code null} 1323 */ 1324 GPX formByteArray(final byte[] bytes) { 1325 return formByteArray(bytes, 0, bytes.length); 1326 } 1327 1328 /* ********************************************************************* 1329 * Factory methods. 1330 * ********************************************************************/ 1331 1332 /** 1333 * Return a GPX reader, reading GPX files with the given version and in the 1334 * given reading mode. 1335 * 1336 * @since 3.0 1337 * 1338 * @param version the GPX version to read 1339 * @param mode the reading mode 1340 * @return a new GPX reader object 1341 * @throws NullPointerException if one of the arguments is {@code null} 1342 */ 1343 public static Reader of(final Version version, final Mode mode) { 1344 return new Reader(version, mode); 1345 } 1346 1347 /** 1348 * Return a GPX reader, reading GPX files with version 1.1 and in the given 1349 * reading mode. 1350 * 1351 * @since 3.0 1352 * 1353 * @param mode the reading mode 1354 * @return a new GPX reader object 1355 * @throws NullPointerException if one of the arguments is {@code null} 1356 */ 1357 public static Reader of(final Mode mode) { 1358 return new Reader(Version.V11, mode); 1359 } 1360 1361 } 1362 1363 /** 1364 * Class for writing GPX files. A writer instance can be created by the 1365 * {@code GPX.writer} factory methods. 1366 * 1367 * @version 3.0 1368 * @since 1.3 1369 */ 1370 public static final class Writer { 1371 1372 /** 1373 * Represents the indentation value, the writer is using. An indentation 1374 * string of {@code null} means that the GPX data is written as one XML 1375 * line. An empty string adds line feeds, but with no indentation. 1376 * 1377 * @since 3.0 1378 * 1379 * @param value the indentation value 1380 */ 1381 public record Indent(String value) { 1382 /** 1383 * This indentation lets the {@link Writer} write the GPX data into 1384 * one XML line. 1385 */ 1386 public static final Indent NULL = new Indent(null); 1387 1388 /** 1389 * No indentation, but with new-lines. 1390 */ 1391 public static final Indent NONE = new Indent(""); 1392 1393 /** 1394 * Indentation with 4 spaces. 1395 */ 1396 public static final Indent SPACE4 = new Indent(" "); 1397 1398 /** 1399 * Indentation with 2 spaces. 1400 */ 1401 public static final Indent SPACE2 = new Indent(" "); 1402 1403 /** 1404 * Indentation with tabs. 1405 */ 1406 public static final Indent TAB1 = new Indent("\t"); 1407 } 1408 1409 /** 1410 * The default value for the <em>maximum fraction digits</em>. 1411 */ 1412 public static final int DEFAULT_FRACTION_DIGITS = 8; 1413 1414 /** 1415 * The default GPX writer, with no indention and fraction digits 1416 * of {@link #DEFAULT_FRACTION_DIGITS}. 1417 * 1418 * @see #of(Indent, int) 1419 * @see #of(Indent) 1420 * 1421 * @since 3.0 1422 */ 1423 public static final Writer DEFAULT = 1424 new Writer(Indent.SPACE4, DEFAULT_FRACTION_DIGITS); 1425 1426 private final Indent _indent; 1427 private final int _maximumFractionDigits; 1428 1429 private Writer(final Indent indent, final int maximumFractionDigits) { 1430 _indent = requireNonNull(indent); 1431 _maximumFractionDigits = maximumFractionDigits; 1432 } 1433 1434 /** 1435 * Return the indentation string this GPX writer is using. 1436 * 1437 * @since 3.0 1438 * 1439 * @return the indentation string 1440 */ 1441 public Indent indent() { 1442 return _indent; 1443 } 1444 1445 /** 1446 * Return the maximum number of digits allowed in the fraction portion 1447 * of the written numbers like <em>latitude</em> and <em>longitude</em>. 1448 * 1449 * @return the maximum number of digits allowed in the fraction portion 1450 * of the written numbers 1451 */ 1452 public int maximumFractionDigits() { 1453 return _maximumFractionDigits; 1454 } 1455 1456 /** 1457 * Writes the given {@code gpx} object to the given {@code result}. This 1458 * is the most general way for writing {@link GPX} objects. 1459 * <p> 1460 * The following example shows how to create an XML-Document from a 1461 * given {@code GPX} object. 1462 * <pre>{@code 1463 * final GPX gpx = ...; 1464 * 1465 * final Document doc = XMLProvider.provider() 1466 * .documentBuilderFactory() 1467 * .newDocumentBuilder() 1468 * .newDocument(); 1469 * 1470 * // The GPX data are written to the empty `doc` object. 1471 * GPX.Writer.DEFAULT.write(gpx, new DOMResult(doc)); 1472 * }</pre> 1473 * 1474 * @since 3.0 1475 * 1476 * @param gpx the GPX object to write to the output 1477 * @param result the output <em>document</em> 1478 * @throws IOException if the writing of the GPX object fails 1479 * @throws NullPointerException if one of the given arguments is 1480 * {@code null} 1481 */ 1482 public void write(final GPX gpx, final Result result) 1483 throws IOException 1484 { 1485 try { 1486 final XMLStreamWriter writer = XMLProvider.provider() 1487 .xmlOutputFactory() 1488 .createXMLStreamWriter(result); 1489 1490 final XMLStreamWriterAdapter output = _indent.value() == null 1491 ? new XMLStreamWriterAdapter(writer) 1492 : new IndentingXMLStreamWriter(writer, _indent.value()); 1493 1494 try (output) { 1495 final var format = NumberFormat.getNumberInstance(ENGLISH); 1496 format.setMaximumFractionDigits(_maximumFractionDigits); 1497 format.setGroupingUsed(false); 1498 final Function<Number, String> formatter = value -> 1499 value != null ? format.format(value) : null; 1500 1501 output.writeStartDocument("UTF-8", "1.0"); 1502 GPX.xmlWriter(gpx._version, formatter).write(output, gpx); 1503 output.writeEndDocument(); 1504 } 1505 } catch (XMLStreamException e) { 1506 throw new IOException(e); 1507 } 1508 } 1509 1510 /** 1511 * Writes the given {@code gpx} object (in GPX XML format) to the given 1512 * {@code output} stream. <em>The caller of this method is responsible 1513 * for closing the given {@code output} stream.</em> 1514 * 1515 * @param gpx the GPX object to write to the output 1516 * @param output the output stream where the GPX object is written to 1517 * @throws IOException if the writing of the GPX object fails 1518 * @throws NullPointerException if one of the given arguments is 1519 * {@code null} 1520 */ 1521 public void write(final GPX gpx, final OutputStream output) 1522 throws IOException 1523 { 1524 final var wrapper = new NonCloseableOutputStream(output); 1525 try (var writer = new OutputStreamWriter(wrapper, UTF_8)) { 1526 write(gpx, new StreamResult(writer)); 1527 } 1528 } 1529 1530 /** 1531 * Writes the given {@code gpx} object (in GPX XML format) to the given 1532 * {@code path}. 1533 * 1534 * @param gpx the GPX object to write to the output 1535 * @param path the output path where the GPX object is written to 1536 * @throws IOException if the writing of the GPX object fails 1537 * @throws NullPointerException if one of the given arguments is 1538 * {@code null} 1539 */ 1540 public void write(final GPX gpx, final Path path) throws IOException { 1541 try (var out = Files.newOutputStream(path)) { 1542 write(gpx, out); 1543 } 1544 } 1545 1546 /** 1547 * Writes the given {@code gpx} object (in GPX XML format) to the given 1548 * {@code file}. 1549 * 1550 * @param gpx the GPX object to write to the output 1551 * @param file the output file where the GPX object is written to 1552 * @throws IOException if the writing of the GPX object fails 1553 * @throws NullPointerException if one of the given arguments is 1554 * {@code null} 1555 */ 1556 public void write(final GPX gpx, final File file) throws IOException { 1557 write(gpx, file.toPath()); 1558 } 1559 1560 /** 1561 * Writes the given {@code gpx} object (in GPX XML format) to the given 1562 * {@code path}. 1563 * 1564 * @param gpx the GPX object to write to the output 1565 * @param path the output path where the GPX object is written to 1566 * @throws IOException if the writing of the GPX object fails 1567 * @throws NullPointerException if one of the given arguments is 1568 * {@code null} 1569 */ 1570 public void write(final GPX gpx, final String path) throws IOException { 1571 write(gpx, Path.of(path)); 1572 } 1573 1574 /** 1575 * Create an XML string representation of the given {@code gpx} object. 1576 * 1577 * @see GPX.Reader#fromString(String) 1578 * 1579 * @param gpx the GPX object to convert to a string 1580 * @return the XML string representation of the given {@code gpx} object 1581 * @throws NullPointerException if the given GPX object is {@code null} 1582 */ 1583 public String toString(final GPX gpx) { 1584 final ByteArrayOutputStream out = new ByteArrayOutputStream(); 1585 try { 1586 write(gpx, out); 1587 return out.toString(); 1588 } catch (IOException e) { 1589 throw new UncheckedIOException(e); 1590 } 1591 } 1592 1593 /** 1594 * Converts the given {@code gpx} object into a {@code byte[]} array. 1595 * This method can be used for short term storage of GPX objects. 1596 * 1597 * @since 3.0 1598 * 1599 * @see GPX.Reader#formByteArray(byte[]) 1600 * 1601 * @param gpx the GPX object to convert to a {@code byte[]} array 1602 * @return the binary representation of the given {@code gpx} object 1603 * @throws NullPointerException if the given GPX object is {@code null} 1604 */ 1605 byte[] toByteArray(final GPX gpx) { 1606 final var out = new ByteArrayOutputStream(); 1607 try (var dout = new DataOutputStream(out)) { 1608 gpx.write(dout); 1609 } catch (IOException e) { 1610 throw new UncheckedIOException(e); 1611 } 1612 1613 return out.toByteArray(); 1614 } 1615 1616 /* ********************************************************************* 1617 * Factory methods. 1618 * ********************************************************************/ 1619 1620 /** 1621 * Return a new GPX writer with the given {@code indent} and number 1622 * formatter, which is used for formatting {@link WayPoint#getLatitude()}, 1623 * {@link WayPoint#getLongitude()}, ... 1624 * <p> 1625 * The example below shows the <em>lat</em> and <em>lon</em> values with 1626 * maximal 5 fractional digits. 1627 * <pre>{@code 1628 * <trkpt lat="45.78068" lon="12.55368"> 1629 * <ele>1.2</ele> 1630 * <time>2009-08-30T07:08:21Z</time> 1631 * </trkpt> 1632 * }</pre> 1633 * 1634 * The following table should give you a feeling about the accuracy of a 1635 * given fraction digits count, at the equator. 1636 * 1637 * <table class="striped"> 1638 * <caption><b>Maximum fraction digits accuracy</b></caption> 1639 * <thead> 1640 * <tr> 1641 * <th scope="row">Fraction digits</th> 1642 * <th scope="row">Degree</th> 1643 * <th scope="row">Distance</th> 1644 * </tr> 1645 * </thead> 1646 * <tbody> 1647 * <tr><td>0 </td><td>1 </td><td>111.31 km </td></tr> 1648 * <tr><td>1 </td><td>0.1 </td><td> 11.13 km </td></tr> 1649 * <tr><td>2 </td><td>0,01 </td><td> 1.1 km </td></tr> 1650 * <tr><td>3 </td><td>0.001 </td><td>111.3 m </td></tr> 1651 * <tr><td>4 </td><td>0.0001 </td><td> 11.1 m </td></tr> 1652 * <tr><td>5 </td><td>0.00001 </td><td> 1.11 m </td></tr> 1653 * <tr><td>6 </td><td>0.000001 </td><td> 0.1 m </td></tr> 1654 * <tr><td>7 </td><td>0.0000001 </td><td> 11.1 mm </td></tr> 1655 * <tr><td>8 </td><td>0.00000001 </td><td> 1.1 mm </td></tr> 1656 * <tr><td>9 </td><td>0.000000001 </td><td> 0.11 mm</td></tr> 1657 * </tbody> 1658 * </table> 1659 * 1660 * @see #of(Indent) 1661 * @see #DEFAULT 1662 * 1663 * @since 3.0 1664 * 1665 * @param indent the element indentation 1666 * @param maximumFractionDigits the maximum number of digits allowed in the 1667 * fraction portion of a number 1668 * @return a new GPX writer 1669 */ 1670 public static Writer of(final Indent indent, final int maximumFractionDigits) { 1671 return new Writer(indent, maximumFractionDigits); 1672 } 1673 1674 /** 1675 * Return a new GPX writer with the given {@code indent} and with 1676 * <em>maximum fraction digits</em> of 1677 * {@link Writer#DEFAULT_FRACTION_DIGITS}. 1678 * 1679 * @see #of(Indent, int) 1680 * @see #DEFAULT 1681 * 1682 * @since 3.0 1683 * 1684 * @param indent the element indentation 1685 * @return a new GPX writer 1686 */ 1687 public static Writer of(final Indent indent) { 1688 return new Writer(indent, DEFAULT_FRACTION_DIGITS); 1689 } 1690 1691 } 1692 1693 /* ************************************************************************* 1694 * Static object creation methods 1695 * ************************************************************************/ 1696 1697 /** 1698 * Create a new {@code GPX} object with the given data. 1699 * 1700 * @since 1.5 1701 * 1702 * @param creator the name or URL of the software that created your GPX 1703 * document. This allows others to inform the creator of a GPX 1704 * instance document that fails to validate. 1705 * @param version the GPX version 1706 * @param metadata the metadata about the GPS file 1707 * @param wayPoints the way-points 1708 * @param routes the routes 1709 * @param tracks the tracks 1710 * @param extensions the XML extensions 1711 * @return a new {@code GPX} object with the given data 1712 * @throws NullPointerException if the {@code creator}, {code wayPoints}, 1713 * {@code routes} or {@code tracks} is {@code null} 1714 */ 1715 public static GPX of( 1716 final Version version, 1717 final String creator, 1718 final Metadata metadata, 1719 final List<WayPoint> wayPoints, 1720 final List<Route> routes, 1721 final List<Track> tracks, 1722 final Document extensions 1723 ) { 1724 return new GPX( 1725 version, 1726 creator, 1727 metadata == null || metadata.isEmpty() ? null : metadata, 1728 wayPoints, 1729 routes, 1730 tracks, 1731 XML.extensions(XML.clone(extensions)) 1732 ); 1733 } 1734 1735 /** 1736 * Create a new {@code GPX} object with the given data. 1737 * 1738 * @param creator the name or URL of the software that created your GPX 1739 * document. This allows others to inform the creator of a GPX 1740 * instance document that fails to validate. 1741 * @param metadata the metadata about the GPS file 1742 * @param wayPoints the way-points 1743 * @param routes the routes 1744 * @param tracks the tracks 1745 * @return a new {@code GPX} object with the given data 1746 * @throws NullPointerException if the {@code creator}, {code wayPoints}, 1747 * {@code routes} or {@code tracks} is {@code null} 1748 */ 1749 public static GPX of( 1750 final String creator, 1751 final Metadata metadata, 1752 final List<WayPoint> wayPoints, 1753 final List<Route> routes, 1754 final List<Track> tracks 1755 ) { 1756 return of( 1757 Version.V11, 1758 creator, 1759 metadata, 1760 wayPoints, 1761 routes, 1762 tracks, 1763 null 1764 ); 1765 } 1766 1767 /** 1768 * Create a new {@code GPX} object with the given data. 1769 * 1770 * @since 1.5 1771 * 1772 * @param creator the name or URL of the software that created your GPX 1773 * document. This allows others to inform the creator of a GPX 1774 * instance document that fails to validate. 1775 * @param metadata the metadata about the GPS file 1776 * @param wayPoints the way-points 1777 * @param routes the routes 1778 * @param tracks the tracks 1779 * @param extensions the XML extensions 1780 * @return a new {@code GPX} object with the given data 1781 * @throws NullPointerException if the {@code creator}, {code wayPoints}, 1782 * {@code routes} or {@code tracks} is {@code null} 1783 */ 1784 public static GPX of( 1785 final String creator, 1786 final Metadata metadata, 1787 final List<WayPoint> wayPoints, 1788 final List<Route> routes, 1789 final List<Track> tracks, 1790 final Document extensions 1791 ) { 1792 return of( 1793 Version.V11, 1794 creator, 1795 metadata, 1796 wayPoints, 1797 routes, 1798 tracks, 1799 extensions 1800 ); 1801 } 1802 1803 /** 1804 * Create a new {@code GPX} object with the given data. 1805 * 1806 * @param creator the name or URL of the software that created your GPX 1807 * document. This allows others to inform the creator of a GPX 1808 * instance document that fails to validate. 1809 * @param version the GPX version 1810 * @param metadata the metadata about the GPS file 1811 * @param wayPoints the way-points 1812 * @param routes the routes 1813 * @param tracks the tracks 1814 * @return a new {@code GPX} object with the given data 1815 * @throws NullPointerException if the {@code creator}, {code wayPoints}, 1816 * {@code routes} or {@code tracks} is {@code null} 1817 */ 1818 public static GPX of( 1819 final Version version, 1820 final String creator, 1821 final Metadata metadata, 1822 final List<WayPoint> wayPoints, 1823 final List<Route> routes, 1824 final List<Track> tracks 1825 ) { 1826 return of( 1827 version, 1828 creator, 1829 metadata == null || metadata.isEmpty() ? null : metadata, 1830 wayPoints, 1831 routes, 1832 tracks, 1833 null 1834 ); 1835 } 1836 1837 1838 /* ************************************************************************* 1839 * Java object serialization 1840 * ************************************************************************/ 1841 1842 @Serial 1843 private Object writeReplace() { 1844 return new SerialProxy(SerialProxy.GPX_TYPE, this); 1845 } 1846 1847 @Serial 1848 private void readObject(final ObjectInputStream stream) 1849 throws InvalidObjectException 1850 { 1851 throw new InvalidObjectException("Serialization proxy required."); 1852 } 1853 1854 void write(final DataOutput out) throws IOException { 1855 IO.writeString(_version.getValue(), out); 1856 IO.writeString(_creator, out); 1857 IO.writeNullable(_metadata, Metadata::write, out); 1858 IO.writes(_wayPoints, WayPoint::write, out); 1859 IO.writes(_routes, Route::write, out); 1860 IO.writes(_tracks, Track::write, out); 1861 IO.writeNullable(_extensions, IO::write, out); 1862 } 1863 1864 static GPX read(final DataInput in) throws IOException { 1865 return new GPX( 1866 Version.of(IO.readString(in)), 1867 IO.readString(in), 1868 IO.readNullable(Metadata::read, in), 1869 IO.reads(WayPoint::read, in), 1870 IO.reads(Route::read, in), 1871 IO.reads(Track::read, in), 1872 IO.readNullable(IO::readDoc, in) 1873 ); 1874 } 1875 1876 /* ************************************************************************* 1877 * XML stream object serialization 1878 * ************************************************************************/ 1879 1880 private static String name(final GPX gpx) { 1881 return gpx.getMetadata() 1882 .flatMap(Metadata::getName) 1883 .orElse(null); 1884 } 1885 1886 private static String desc(final GPX gpx) { 1887 return gpx.getMetadata() 1888 .flatMap(Metadata::getDescription) 1889 .orElse(null); 1890 } 1891 1892 private static String author(final GPX gpx) { 1893 return gpx.getMetadata() 1894 .flatMap(Metadata::getAuthor) 1895 .flatMap(Person::getName) 1896 .orElse(null); 1897 } 1898 1899 private static String email(final GPX gpx) { 1900 return gpx.getMetadata() 1901 .flatMap(Metadata::getAuthor) 1902 .flatMap(Person::getEmail) 1903 .map(Email::getAddress) 1904 .orElse(null); 1905 } 1906 1907 private static String url(final GPX gpx) { 1908 return gpx.getMetadata() 1909 .flatMap(Metadata::getAuthor) 1910 .flatMap(Person::getLink) 1911 .map(Link::getHref) 1912 .map(URI::toString) 1913 .orElse(null); 1914 } 1915 1916 private static String urlname(final GPX gpx) { 1917 return gpx.getMetadata() 1918 .flatMap(Metadata::getAuthor) 1919 .flatMap(Person::getLink) 1920 .flatMap(Link::getText) 1921 .orElse(null); 1922 } 1923 1924 private static String time(final GPX gpx) { 1925 return gpx.getMetadata() 1926 .flatMap(Metadata::getTime) 1927 .map(TimeFormat::format) 1928 .orElse(null); 1929 } 1930 1931 private static String keywords(final GPX gpx) { 1932 return gpx.getMetadata() 1933 .flatMap(Metadata::getKeywords) 1934 .orElse(null); 1935 } 1936 1937 1938 // Define the needed writers for the different versions. 1939 private static XMLWriters<GPX> 1940 writers(final Function<? super Number, String> formatter) { 1941 return new XMLWriters<GPX>() 1942 .v00(XMLWriter.attr("version").map(gpx -> gpx._version._value)) 1943 .v00(XMLWriter.attr("creator").map(GPX::getCreator)) 1944 .v11(XMLWriter.ns(Version.V11.getNamespaceURI())) 1945 .v10(XMLWriter.ns(Version.V10.getNamespaceURI())) 1946 .v11(Metadata.writer(formatter).flatMap(GPX::getMetadata)) 1947 .v10(XMLWriter.elem("name").map(GPX::name)) 1948 .v10(XMLWriter.elem("desc").map(GPX::desc)) 1949 .v10(XMLWriter.elem("author").map(GPX::author)) 1950 .v10(XMLWriter.elem("email").map(GPX::email)) 1951 .v10(XMLWriter.elem("url").map(GPX::url)) 1952 .v10(XMLWriter.elem("urlname").map(GPX::urlname)) 1953 .v10(XMLWriter.elem("time").map(GPX::time)) 1954 .v10(XMLWriter.elem("keywords").map(GPX::keywords)) 1955 .v10(XMLWriter.elems(WayPoint.xmlWriter(Version.V10,"wpt", formatter)).map(GPX::getWayPoints)) 1956 .v11(XMLWriter.elems(WayPoint.xmlWriter(Version.V11,"wpt", formatter)).map(GPX::getWayPoints)) 1957 .v10(XMLWriter.elems(Route.xmlWriter(Version.V10, formatter)).map(GPX::getRoutes)) 1958 .v11(XMLWriter.elems(Route.xmlWriter(Version.V11, formatter)).map(GPX::getRoutes)) 1959 .v10(XMLWriter.elems(Track.xmlWriter(Version.V10, formatter)).map(GPX::getTracks)) 1960 .v11(XMLWriter.elems(Track.xmlWriter(Version.V11, formatter)).map(GPX::getTracks)) 1961 .v00(XMLWriter.doc("extensions").flatMap(GPX::getExtensions)); 1962 } 1963 1964 1965 // Define the needed readers for the different versions. 1966 private static XMLReaders 1967 readers(final Function<? super String, Length> lengthParser) { 1968 return new XMLReaders() 1969 .v00(XMLReader.attr("version").map(Version::of, Version.V11)) 1970 .v00(XMLReader.attr("creator")) 1971 .v11(Metadata.READER) 1972 .v10(XMLReader.elem("name")) 1973 .v10(XMLReader.elem("desc")) 1974 .v10(XMLReader.elem("author")) 1975 .v10(XMLReader.elem("email")) 1976 .v10(XMLReader.elem("url")) 1977 .v10(XMLReader.elem("urlname")) 1978 .v10(XMLReader.elem("time").map(TimeFormat::parse)) 1979 .v10(XMLReader.elem("keywords")) 1980 .v10(Bounds.READER) 1981 .v10(XMLReader.elems(WayPoint.xmlReader(Version.V10, "wpt", lengthParser))) 1982 .v11(XMLReader.elems(WayPoint.xmlReader(Version.V11, "wpt", lengthParser))) 1983 .v10(XMLReader.elems(Route.xmlReader(Version.V10, lengthParser))) 1984 .v11(XMLReader.elems(Route.xmlReader(Version.V11, lengthParser))) 1985 .v10(XMLReader.elems(Track.xmlReader(Version.V10, lengthParser))) 1986 .v11(XMLReader.elems(Track.xmlReader(Version.V11, lengthParser))) 1987 .v00(XMLReader.doc("extensions")); 1988 } 1989 1990 1991 static XMLWriter<GPX> xmlWriter( 1992 final Version version, 1993 final Function<? super Number, String> formatter 1994 ) { 1995 return XMLWriter.elem("gpx", writers(formatter).writers(version)); 1996 } 1997 1998 static XMLReader<GPX> xmlReader( 1999 final Version version, 2000 final Function<? super String, Length> lengthParser 2001 ) { 2002 return XMLReader.elem( 2003 version == Version.V10 ? GPX::toGPXv10 : GPX::toGPXv11, 2004 "gpx", 2005 readers(lengthParser).readers(version) 2006 ); 2007 } 2008 2009 @SuppressWarnings("unchecked") 2010 private static GPX toGPXv11(final Object[] v) { 2011 return new GPX( 2012 (Version)v[0], 2013 (String)v[1], 2014 (Metadata)v[2], 2015 (List<WayPoint>)v[3], 2016 (List<Route>)v[4], 2017 (List<Track>)v[5], 2018 XML.extensions((Document)v[6]) 2019 ); 2020 } 2021 2022 @SuppressWarnings("unchecked") 2023 private static GPX toGPXv10(final Object[] v) { 2024 return new GPX( 2025 (Version)v[0], 2026 (String)v[1], 2027 Metadata.of( 2028 (String)v[2], 2029 (String)v[3], 2030 Person.of( 2031 (String)v[4], 2032 v[5] != null 2033 ? Email.of((String)v[5]) 2034 : null, 2035 v[6] != null 2036 ? Link.of((String)v[6], (String)v[7], null) 2037 : null 2038 ), 2039 null, 2040 null, 2041 (Instant)v[8], 2042 (String)v[9], 2043 (Bounds)v[10] 2044 ), 2045 (List<WayPoint>)v[11], 2046 (List<Route>)v[12], 2047 (List<Track>)v[13], 2048 XML.extensions((Document)v[14]) 2049 ); 2050 } 2051 2052 2053 /* ************************************************************************* 2054 * Write and read GPX files 2055 * ************************************************************************/ 2056 2057 /** 2058 * Writes the given {@code gpx} object (in GPX XML format) to the given 2059 * {@code path}. 2060 * This method is a shortcut for 2061 * <pre>{@code 2062 * GPX.Writer.DEFAULT.write(gpx, path); 2063 * }</pre> 2064 * 2065 * @see Writer 2066 * 2067 * @since 1.1 2068 * 2069 * @param gpx the GPX object to write to the output 2070 * @param path the output path where the GPX object is written to 2071 * @throws IOException if the writing of the GPX object fails 2072 * @throws NullPointerException if one of the given arguments is {@code null} 2073 */ 2074 public static void write(final GPX gpx, final Path path) throws IOException { 2075 Writer.DEFAULT.write(gpx, path); 2076 } 2077 2078 /** 2079 * Read an GPX object from the given {@code input} stream. 2080 * This method is a shortcut for 2081 * <pre>{@code 2082 * GPX.Reader.DEFAULT.read(path); 2083 * }</pre> 2084 * 2085 * @see Reader 2086 * 2087 * @param path the input path from where the GPX date is read 2088 * @return the GPX object read from the input stream 2089 * @throws IOException if the GPX object can't be read 2090 * @throws NullPointerException if the given {@code input} stream is 2091 * {@code null} 2092 */ 2093 public static GPX read(final Path path) throws IOException { 2094 return Reader.DEFAULT.read(path); 2095 } 2096 2097}