/*
 * Java GPX Library (@__identifier__@).
 * Copyright (c) @__year__@ Franz Wilhelmstötter
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Author:
 *    Franz Wilhelmstötter ([email protected])
 */
package io.jenetics.jpx;

import static java.lang.String.format;
import static java.util.Objects.hash;
import static java.util.Objects.requireNonNull;
import static io.jenetics.jpx.Lists.copyOf;
import static io.jenetics.jpx.Lists.copyTo;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;

import org.w3c.dom.Document;

import io.jenetics.jpx.GPX.Reader.Mode;

/**
 * GPX documents contain a metadata header, followed by way-points, routes, and
 * tracks. You can add your own elements to the extensions section of the GPX
 * document.
 * <p>
 * <em><b>Examples:</b></em>
 * <p>
 * <b>Creating a GPX object with one track-segment and 3 track-points</b>
 * <pre>{@code
 * final GPX gpx = GPX.builder()
 *     .addTrack(track -> track
 *         .addSegment(segment -> segment
 *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
 *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))
 *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162))))
 *     .build();
 * }</pre>
 *
 * <b>Reading a GPX file</b>
 * <pre>{@code
 * final GPX gpx = GPX.read("track.xml");
 * }</pre>
 *
 * <b>Reading erroneous GPX files</b>
 * <pre>{@code
 * final boolean lenient = true;
 * final GPX gpx = GPX.read("track.xml", lenient);
 * }</pre>
 *
 * This allows to read otherwise invalid GPX files, like
 * <pre>{@code
 * <?xml version="1.0" encoding="UTF-8"?>
 * <gpx version="1.1" creator="GPSBabel - http://www.gpsbabel.org" xmlns="http://www.topografix.com/GPX/1/0">
 *     <metadata>
 *         <time>2015-11-13T15:22:42.140Z</time>
 *         <bounds minlat="-37050536.000000000" minlon="-0.000000000" maxlat="48.359161377" maxlon="16.448385239"/>
 *     </metadata>
 *     <trk>
 *         <name>track-1</name>
 *         <desc>Log every 3 sec, 0 m</desc>
 *         <trkseg>
 *             <trkpt></trkpt>
 *             <trkpt lat="48.199352264" lon="16.403341293">
 *                 <ele>4325376.000000</ele>
 *                 <time>2015-10-23T17:07:08Z</time>
 *                 <speed>2.650000</speed>
 *                 <name>TP000001</name>
 *             </trkpt>
 *             <trkpt lat="6.376383781" lon="-0.000000000">
 *                 <ele>147573952589676412928.000000</ele>
 *                 <time>1992-07-19T10:10:58Z</time>
 *                 <speed>464.010010</speed>
 *                 <name>TP000002</name>
 *             </trkpt>
 *             <trkpt lat="-37050536.000000000" lon="0.000475423">
 *                 <ele>0.000000</ele>
 *                 <time>2025-12-17T05:10:27Z</time>
 *                 <speed>56528.671875</speed>
 *                 <name>TP000003</name>
 *             </trkpt>
 *             <trkpt></trkpt>
 *         </trkseg>
 *     </trk>
 * </gpx>
 * }</pre>
 *
 * which is read as
 * <pre>{@code
 * <?xml version="1.0" encoding="UTF-8"?>
 * <gpx xmlns="http://www.topografix.com/GPX/1/1" version="1.1" creator="JPX" >
 *     <metadata>
 *         <time>2015-11-13T15:22:42.140Z</time>
 *     </metadata>
 *     <trk>
 *         <name>track-1</name>
 *         <desc>Log every 3 sec, 0 m</desc>
 *         <trkseg>
 *             <trkpt lat="48.199352264" lon="16.403341293">
 *                 <ele>4325376.000000</ele>
 *                 <time>2015-10-23T17:07:08Z</time>
 *                 <speed>2.650000</speed>
 *                 <name>TP000001</name>
 *             </trkpt>
 *             <trkpt lat="6.376383781" lon="-0.000000000">
 *                 <ele>147573952589676412928.000000</ele>
 *                 <time>1992-07-19T10:10:58Z</time>
 *                 <speed>464.010010</speed>
 *                 <name>TP000002</name>
 *             </trkpt>
 *         </trkseg>
 *     </trk>
 * </gpx>
 * }</pre>
 *
 * @author <a href="mailto:[email protected]">Franz Wilhelmstötter</a>
 * @version 2.0
 * @since 1.0
 */
public final class GPX implements Serializable {

	private static final long serialVersionUID = 2L;

	/**
	 * Represents the available GPX versions.
	 *
	 * @version 1.3
	 * @since 1.3
	 */
	public enum Version {

		/**
		 * The GPX version 1.0. This version can be read and written.
		 *
		 * @see <a href="http://www.topografix.com/gpx_manual.asp">GPX 1.0</a>
		 */
		V10("1.0", "http://www.topografix.com/GPX/1/0"),

		/**
		 * The GPX version 1.1. This is the default version and can be read and
		 * written.
		 *
		 * @see <a href="http://www.topografix.com/GPX/1/1">GPX 1.1</a>
		 */
		V11("1.1", "http://www.topografix.com/GPX/1/1");

		private final String _value;
		private final String _namespaceURI;

		Version(final String value, final String namespaceURI) {
			_value = value;
			_namespaceURI = namespaceURI;
		}

		/**
		 * Return the version string value.
		 *
		 * @return the version string value
		 */
		public String getValue() {
			return _value;
		}

		/**
		 * Return the namespace URI of this version.
		 *
		 * @since 1.5
		 *
		 * @return the namespace URI of this version
		 */
		public String getNamespaceURI() {
			return _namespaceURI;
		}

		/**
		 * Return the version from the given {@code version} string. Allowed
		 * values are "1.0" and "1.1".
		 *
		 * @param version the version string
		 * @return the version from the given {@code version} string
		 * @throws IllegalArgumentException if the given {@code version} string
		 *         is neither "1.0" nor "1.1"
		 * @throws NullPointerException if the given {@code version} string is
		 *         {@code null}
		 */
		public static Version of(final String version) {
			switch (version) {
				case "1.0": return V10;
				case "1.1": return V11;
				default: throw new IllegalArgumentException(format(
					"Unknown version string: '%s'.", version
				));
			}
		}
	}

	private static final String _CREATOR = "JPX - https://github.com/jenetics/jpx";

	private final String _creator;
	private final Version _version;
	private final Metadata _metadata;
	private final List<WayPoint> _wayPoints;
	private final List<Route> _routes;
	private final List<Track> _tracks;
	private final Document _extensions;

	/**
	 * Create a new {@code GPX} object with the given data.
	 *
	 * @param creator the name or URL of the software that created your GPX
	 *        document. This allows others to inform the creator of a GPX
	 *        instance document that fails to validate.
	 * @param version the GPX version
	 * @param metadata the metadata about the GPS file
	 * @param wayPoints the way-points
	 * @param routes the routes
	 * @param tracks the tracks
	 * @param extensions the XML extensions document
	 * @throws NullPointerException if the {@code creator} or {@code version} is
	 *         {@code null}
	 */
	private GPX(
		final Version version,
		final String creator,
		final Metadata metadata,
		final List<WayPoint> wayPoints,
		final List<Route> routes,
		final List<Track> tracks,
		final Document extensions
	) {
		_version = requireNonNull(version);
		_creator = requireNonNull(creator);
		_metadata = metadata;
		_wayPoints = copyOf(wayPoints);
		_routes = copyOf(routes);
		_tracks = copyOf(tracks);
		_extensions = extensions;
	}

	/**
	 * Return the version number of the GPX file.
	 *
	 * @return the version number of the GPX file
	 */
	public String getVersion() {
		return _version._value;
	}

	/**
	 * Return the name or URL of the software that created your GPX document.
	 * This allows others to inform the creator of a GPX instance document that
	 * fails to validate.
	 *
	 * @return the name or URL of the software that created your GPX document
	 */
	public String getCreator() {
		return _creator;
	}

	/**
	 * Return the metadata of the GPX file.
	 *
	 * @return the metadata of the GPX file
	 */
	public Optional<Metadata> getMetadata() {
		return Optional.ofNullable(_metadata);
	}

	/**
	 * Return an unmodifiable list of the {@code GPX} way-points.
	 *
	 * @return an unmodifiable list of the {@code GPX} way-points.
	 */
	public List<WayPoint> getWayPoints() {
		return _wayPoints;
	}

	/**
	 * Return a stream with all {@code WayPoint}s of this {@code GPX} object.
	 *
	 * @return a stream with all {@code WayPoint}s of this {@code GPX} object
	 */
	public Stream<WayPoint> wayPoints() {
		return _wayPoints.stream();
	}

	/**
	 * Return an unmodifiable list of the {@code GPX} routes.
	 *
	 * @return an unmodifiable list of the {@code GPX} routes.
	 */
	public List<Route> getRoutes() {
		return _routes;
	}

	/**
	 * Return a stream of the {@code GPX} routes.
	 *
	 * @return a stream of the {@code GPX} routes.
	 */
	public Stream<Route> routes() {
		return _routes.stream();
	}

	/**
	 * Return an unmodifiable list of the {@code GPX} tracks.
	 *
	 * @return an unmodifiable list of the {@code GPX} tracks.
	 */
	public List<Track> getTracks() {
		return _tracks;
	}

	/**
	 * Return a stream of the {@code GPX} tracks.
	 *
	 * @return a stream of the {@code GPX} tracks.
	 */
	public Stream<Track> tracks() {
		return _tracks.stream();
	}

	/**
	 * Return the (cloned) extensions document. The root element of the returned
	 * document has the name {@code extensions}.
	 * <pre>{@code
	 * <extensions>
	 *     ...
	 * </extensions>
	 * }</pre>
	 *
	 * @since 1.5
	 *
	 * @return the extensions document
	 */
	public Optional<Document> getExtensions() {
		return Optional.ofNullable(_extensions).map(XML::clone);
	}

	/**
	 * Convert the <em>immutable</em> GPX object into a <em>mutable</em>
	 * builder initialized with the current GPX values.
	 *
	 * @since 1.1
	 *
	 * @return a new track builder initialized with the values of {@code this}
	 *         GPX object
	 */
	public Builder toBuilder() {
		return builder(_version, _creator)
			.metadata(_metadata)
			.wayPoints(_wayPoints)
			.routes(_routes)
			.tracks(_tracks)
			.extensions(_extensions);
	}

	@Override
	public String toString() {
		return format(
			"GPX[way-points=%s, routes=%s, tracks=%s]",
			getWayPoints().size(), getRoutes().size(), getTracks().size()
		);
	}

	@Override
	public int hashCode() {
		return hash(
			_creator,
			_version,
			_metadata,
			_wayPoints,
			_routes,
			_tracks
		);
	}

	@Override
	public boolean equals(final Object obj) {
		return obj == this ||
			obj instanceof GPX &&
			Objects.equals(((GPX)obj)._creator, _creator) &&
			Objects.equals(((GPX)obj)._version, _version) &&
			Objects.equals(((GPX)obj)._metadata, _metadata) &&
			Objects.equals(((GPX)obj)._wayPoints, _wayPoints) &&
			Objects.equals(((GPX)obj)._routes, _routes) &&
			Objects.equals(((GPX)obj)._tracks, _tracks);
	}

	/**
	 * Builder class for creating immutable {@code GPX} objects.
	 * <p>
	 * Creating a GPX object with one track-segment and 3 track-points:
	 * <pre>{@code
	 * final GPX gpx = GPX.builder()
	 *     .addTrack(track -> track
	 *         .addSegment(segment -> segment
	 *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
	 *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))
	 *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162))))
	 *     .build();
	 * }</pre>
	 */
	public static final class Builder {
		private String _creator;
		private Version _version;
		private Metadata _metadata;
		private final List<WayPoint> _wayPoints = new ArrayList<>();
		private final List<Route> _routes = new ArrayList<>();
		private final List<Track> _tracks = new ArrayList<>();
		private Document _extensions;

		private Builder(final Version version, final String creator) {
			_version = requireNonNull(version);
			_creator = requireNonNull(creator);
		}

		/**
		 * Set the GPX creator.
		 *
		 * @param creator the GPX creator
		 * @throws NullPointerException if the given argument is {@code null}
		 * @return {@code this} {@code Builder} for method chaining
		 */
		public Builder creator(final String creator) {
			_creator = requireNonNull(creator);
			return this;
		}

		/**
		 * Return the current creator value.
		 *
		 * @since 1.1
		 *
		 * @return the current creator value
		 */
		public String creator() {
			return _creator;
		}

		/**
		 * Set the GPX version.
		 *
		 * @since 1.3
		 *
		 * @param version the GPX version
		 * @throws NullPointerException if the given argument is {@code null}
		 * @return {@code this} {@code Builder} for method chaining
		 */
		public Builder version(final Version version) {
			_version = requireNonNull(version);
			return this;
		}

		/**
		 * Return the current version value.
		 *
		 * @since 1.1
		 *
		 * @return the current version value
		 */
		public String version() {
			return _version._value;
		}

		/**
		 * Set the GPX metadata.
		 *
		 * @param metadata the GPX metadata
		 * @return {@code this} {@code Builder} for method chaining
		 */
		public Builder metadata(final Metadata metadata) {
			_metadata = metadata;
			return this;
		}

		/**
		 * Allows to set partial metadata without messing up with the
		 * {@link Metadata.Builder} class.
		 * <pre>{@code
		 * final GPX gpx = GPX.builder()
		 *     .metadata(md -> md.author("Franz Wilhelmstötter"))
		 *     .addTrack(...)
		 *     .build();
		 * }</pre>
		 *
		 * @param metadata the metadata consumer
		 * @return {@code this} {@code Builder} for method chaining
		 * @throws NullPointerException if the given argument is {@code null}
		 */
		public Builder metadata(final Consumer<Metadata.Builder> metadata) {
			final Metadata.Builder builder = Metadata.builder();
			metadata.accept(builder);

			final Metadata md = builder.build();
			_metadata = md.isEmpty() ? null : md;

			return this;
		}

		/**
		 * Return the current metadata value.
		 *
		 * @since 1.1
		 *
		 * @return the current metadata value
		 */
		public Optional<Metadata> metadata() {
			return Optional.ofNullable(_metadata);
		}

		/**
		 * Sets the way-points of the {@code GPX} object. The list of way-points
		 * may be {@code null}.
		 *
		 * @param wayPoints the {@code GPX} way-points
		 * @return {@code this} {@code Builder} for method chaining
		 * @throws NullPointerException if one of the way-points in the list is
		 *         {@code null}
		 */
		public Builder wayPoints(final List<WayPoint> wayPoints) {
			copyTo(wayPoints, _wayPoints);
			return this;
		}

		/**
		 * Add one way-point to the {@code GPX} object.
		 *
		 * @param wayPoint the way-point to add
		 * @return {@code this} {@code Builder} for method chaining
		 * @throws NullPointerException if the given {@code wayPoint} is
		 *         {@code null}
		 */
		public Builder addWayPoint(final WayPoint wayPoint) {
			_wayPoints.add(requireNonNull(wayPoint));
			return this;
		}

		/**
		 * Add a way-point to the {@code GPX} object using a
		 * {@link WayPoint.Builder}.
		 * <pre>{@code
		 * final GPX gpx = GPX.builder()
		 *     .addWayPoint(wp -> wp.lat(23.6).lon(13.5).ele(50))
		 *     .build();
		 * }</pre>
		 *
		 * @param wayPoint the way-point to add, configured by the way-point
		 *        builder
		 * @return {@code this} {@code Builder} for method chaining
		 * @throws NullPointerException if the given argument is {@code null}
		 */
		public Builder addWayPoint(final Consumer<WayPoint.Builder> wayPoint) {
			final WayPoint.Builder builder = WayPoint.builder();
			wayPoint.accept(builder);
			return addWayPoint(builder.build());
		}

		/**
		 * Return the current way-points. The returned list is mutable.
		 *
		 * @since 1.1
		 *
		 * @return the current, mutable way-point list
		 */
		public List<WayPoint> wayPoints() {
			return new NonNullList<>(_wayPoints);
		}

		/**
		 * Sets the routes of the {@code GPX} object. The list of routes may be
		 * {@code null}.
		 *
		 * @param routes the {@code GPX} routes
		 * @return {@code this} {@code Builder} for method chaining
		 * @throws NullPointerException if one of the routes is {@code null}
		 */
		public Builder routes(final List<Route> routes) {
			copyTo(routes, _routes);
			return this;
		}

		/**
		 * Add a route the {@code GPX} object.
		 *
		 * @param route the route to add
		 * @return {@code this} {@code Builder} for method chaining
		 * @throws NullPointerException if the given {@code route} is {@code null}
		 */
		public Builder addRoute(final Route route) {
			_routes.add(requireNonNull(route));
			return this;
		}

		/**
		 * Add a route the {@code GPX} object.
		 * <pre>{@code
		 * final GPX gpx = GPX.builder()
		 *     .addRoute(route -> route
		 *         .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
		 *         .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161)))
		 *     .build();
		 * }</pre>
		 *
		 * @param route the route to add, configured by the route builder
		 * @return {@code this} {@code Builder} for method chaining
		 * @throws NullPointerException if the given argument is {@code null}
		 */
		public Builder addRoute(final Consumer<Route.Builder> route) {
			final Route.Builder builder = Route.builder();
			route.accept(builder);
			return addRoute(builder.build());
		}

		/**
		 * Return the current routes. The returned list is mutable.
		 *
		 * @since 1.1
		 *
		 * @return the current, mutable route list
		 */
		public List<Route> routes() {
			return new NonNullList<>(_routes);
		}

		/**
		 * Sets the tracks of the {@code GPX} object. The list of tracks may be
		 * {@code null}.
		 *
		 * @param tracks the {@code GPX} tracks
		 * @return {@code this} {@code Builder} for method chaining
		 * @throws NullPointerException if one of the tracks is {@code null}
		 */
		public Builder tracks(final List<Track> tracks) {
			copyTo(tracks, _tracks);
			return this;
		}

		/**
		 * Add a track the {@code GPX} object.
		 *
		 * @param track the track to add
		 * @return {@code this} {@code Builder} for method chaining
		 * @throws NullPointerException if the given {@code track} is {@code null}
		 */
		public Builder addTrack(final Track track) {
			_tracks.add(requireNonNull(track));
			return this;
		}

		/**
		 * Add a track the {@code GPX} object.
		 * <pre>{@code
		 * final GPX gpx = GPX.builder()
		 *     .addTrack(track -> track
		 *         .addSegment(segment -> segment
		 *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
		 *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))
		 *             .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162))))
		 *     .build();
		 * }</pre>
		 *
		 * @param track the track to add, configured by the track builder
		 * @return {@code this} {@code Builder} for method chaining
		 * @throws NullPointerException if the given argument is {@code null}
		 */
		public Builder addTrack(final Consumer<Track.Builder> track) {
			final Track.Builder builder = Track.builder();
			track.accept(builder);
			return addTrack(builder.build());
		}

		/**
		 * Return the current tracks. The returned list is mutable.
		 *
		 * @since 1.1
		 *
		 * @return the current, mutable track list
		 */
		public List<Track> tracks() {
			return new NonNullList<>(_tracks);
		}


		/**
		 * Sets the extensions object, which may be {@code null}. The root
		 * element of the extensions document must be {@code extensions}.
		 * <pre>{@code
		 * <extensions>
		 *     ...
		 * </extensions>
		 * }</pre>
		 *
		 * @since 1.5
		 *
		 * @param extensions the extensions document
		 * @return {@code this} {@code Builder} for method chaining
		 * @throws IllegalArgumentException if the root element is not the
		 *         an {@code extensions} node
		 */
		public Builder extensions(final Document extensions) {
			_extensions = XML.checkExtensions(extensions);
			return this;
		}

		/**
		 * Return the current extensions
		 *
		 * @since 1.5
		 *
		 * @return the extensions document
		 */
		public Optional<Document> extensions() {
			return Optional.ofNullable(_extensions);
		}

		/**
		 * Create an immutable {@code GPX} object from the current builder state.
		 *
		 * @return an immutable {@code GPX} object from the current builder state
		 */
		public GPX build() {
			return of(
				_version,
				_creator,
				_metadata,
				_wayPoints,
				_routes,
				_tracks,
				_extensions
			);
		}

		/**
		 * Return a new {@link WayPoint} filter.
		 * <pre>{@code
		 * final GPX filtered = gpx.toBuilder()
		 *     .wayPointFilter()
		 *         .filter(wp -> wp.getTime().isPresent())
		 *         .build())
		 *     .build();
		 * }</pre>
		 *
		 * @since 1.1
		 *
		 * @return a new {@link WayPoint} filter
		 */
		public Filter<WayPoint, Builder> wayPointFilter() {
			return new Filter<>() {
				@Override
				public Filter<WayPoint, Builder> filter(
					final Predicate<? super WayPoint> predicate
				) {
					wayPoints(
						_wayPoints.stream()
							.filter(predicate)
							.collect(Collectors.toList())
					);

					return this;
				}

				@Override
				public Filter<WayPoint, Builder> map(
					final Function<? super WayPoint, ? extends WayPoint> mapper
				) {
					wayPoints(
						_wayPoints.stream()
							.map(mapper)
							.collect(Collectors.toUnmodifiableList())
					);

					return this;
				}

				@Override
				public Filter<WayPoint, Builder> flatMap(
					final Function<
						? super WayPoint,
						? extends List<WayPoint>> mapper
				) {
					wayPoints(
						_wayPoints.stream()
							.flatMap(wp -> mapper.apply(wp).stream())
							.collect(Collectors.toUnmodifiableList())
					);

					return this;
				}

				@Override
				public Filter<WayPoint, Builder> listMap(
					final Function<
						? super List<WayPoint>,
						? extends List<WayPoint>> mapper
				) {
					wayPoints(mapper.apply(_wayPoints));

					return this;
				}

				@Override
				public Builder build() {
					return GPX.Builder.this;
				}

			};
		}

		/**
		 * Return a new {@link Route} filter.
		 * <pre>{@code
		 * final GPX filtered = gpx.toBuilder()
		 *     .routeFilter()
		 *         .filter(Route::nonEmpty)
		 *         .build())
		 *     .build();
		 * }</pre>
		 *
		 * @since 1.1
		 *
		 * @return a new {@link Route} filter
		 */
		public Filter<Route, Builder> routeFilter() {
			return new Filter<>() {
				@Override
				public Filter<Route, Builder> filter(
					final Predicate<? super Route> predicate
				) {
					routes(
						_routes.stream()
							.filter(predicate)
							.collect(Collectors.toUnmodifiableList())
					);

					return this;
				}

				@Override
				public Filter<Route, Builder> map(
					final Function<? super Route, ? extends Route> mapper
				) {
					routes(
						_routes.stream()
							.map(mapper)
							.collect(Collectors.toUnmodifiableList())
					);

					return this;
				}

				@Override
				public Filter<Route, Builder> flatMap(
					final Function<? super Route, ? extends List<Route>> mapper)
				{
					routes(
						_routes.stream()
							.flatMap(route -> mapper.apply(route).stream())
							.collect(Collectors.toUnmodifiableList())
					);

					return this;
				}

				@Override
				public Filter<Route, Builder> listMap(
					final Function<
						? super List<Route>,
						? extends List<Route>> mapper
				) {
					routes(mapper.apply(_routes));

					return this;
				}

				@Override
				public Builder build() {
					return GPX.Builder.this;
				}

			};
		}

		/**
		 * Return a new {@link Track} filter.
		 * <pre>{@code
		 * final GPX merged = gpx.toBuilder()
		 *     .trackFilter()
		 *         .map(track -> track.toBuilder()
		 *             .listMap(Filters::mergeSegments)
		 *             .filter(TrackSegment::nonEmpty)
		 *             .build())
		 *         .build()
		 *     .build();
		 * }</pre>
		 *
		 * @since 1.1
		 *
		 * @return a new {@link Track} filter
		 */
		public Filter<Track, Builder> trackFilter() {
			return new Filter<>() {
				@Override
				public Filter<Track, Builder> filter(
					final Predicate<? super Track> predicate
				) {
					tracks(
						_tracks.stream()
							.filter(predicate)
							.collect(Collectors.toUnmodifiableList())
					);

					return this;
				}

				@Override
				public Filter<Track, Builder> map(
					final Function<? super Track, ? extends Track> mapper
				) {
					tracks(
						_tracks.stream()
							.map(mapper)
							.collect(Collectors.toUnmodifiableList())
					);

					return this;
				}

				@Override
				public Filter<Track, Builder> flatMap(
					final Function<? super Track, ? extends List<Track>> mapper
				) {
					tracks(
						_tracks.stream()
							.flatMap(track -> mapper.apply(track).stream())
							.collect(Collectors.toUnmodifiableList())
					);

					return this;
				}

				@Override
				public Filter<Track, Builder> listMap(
					final Function<
						? super List<Track>,
						? extends List<Track>> mapper
				) {
					tracks(mapper.apply(_tracks));

					return this;
				}

				@Override
				public Builder build() {
					return GPX.Builder.this;
				}

			};
		}

	}

	/**
	 * Create a new GPX builder with the given GPX version and creator string.
	 *
	 * @since 1.3
	 *
	 * @param version the GPX version
	 * @param creator the GPX creator
	 * @return new GPX builder
	 * @throws NullPointerException if one of the arguments is {@code null}
	 */
	public static Builder builder(final Version version, final String creator) {
		return new Builder(version, creator);
	}


	/**
	 * Create a new GPX builder with the given GPX creator string.
	 *
	 * @param creator the GPX creator
	 * @return new GPX builder
	 * @throws NullPointerException if the given arguments is {@code null}
	 */
	public static Builder builder(final String creator) {
		return builder(Version.V11, creator);
	}

	/**
	 * Create a new GPX builder.
	 *
	 * @return new GPX builder
	 */
	public static Builder builder() {
		return builder(Version.V11, _CREATOR);
	}


	/**
	 * Class for reading GPX files. A reader instance can be created by the
	 * {@code GPX.reader} factory methods.
	 *
	 * @see GPX#reader()
	 * @see GPX#reader(Version, Reader.Mode)
	 *
	 * @version 1.3
	 * @since 1.3
	 */
	public static final class Reader {

		/**
		 * The possible GPX reader modes.
		 *
		 * @version 1.3
		 * @since 1.3
		 */
		public enum Mode {

			/**
			 * In this mode the GPX reader tries to ignore invalid GPX values
			 * and elements.
			 */
			LENIENT,

			/**
			 * Expects to read valid GPX files.
			 */
			STRICT
		}

		private final XMLReader<GPX> _reader;
		private final Mode _mode;

		private Reader(final XMLReader<GPX> reader, final Mode mode) {
			_reader = requireNonNull(reader);
			_mode = requireNonNull(mode);
		}

		/**
		 * Return the current reader mode.
		 *
		 * @return the current reader mode
		 */
		public Mode getMode() {
			return _mode;
		}

		/**
		 * Read a GPX object from the given {@code input} stream.
		 *
		 * @param input the input stream from where the GPX date is read
		 * @return the GPX object read from the input stream
		 * @throws IOException if the GPX object can't be read
		 * @throws NullPointerException if the given {@code input} stream is
		 *         {@code null}
		 * @throws InvalidObjectException if the gpx input is invalid.
		 */
		public GPX read(final InputStream input)
			throws IOException, InvalidObjectException
		{
			final XMLInputFactory factory = XMLProvider.provider().xmlInputFactory();
			try  (XMLStreamReaderAdapter reader = new XMLStreamReaderAdapter(
						factory.createXMLStreamReader(input)))
			{
				if (reader.hasNext()) {
					reader.next();
					return _reader.read(reader, _mode == Mode.LENIENT);
				} else {
					throw new InvalidObjectException("No 'gpx' element found.");
				}
			} catch (XMLStreamException e) {
				throw new InvalidObjectException("Invalid 'gpx' input.");
			} catch (IllegalArgumentException e) {
				throw (InvalidObjectException)new InvalidObjectException(e.getMessage())
						.initCause(e);
			}
		}

		/**
		 * Read a GPX object from the given {@code input} stream.
		 *
		 * @param file the input file from where the GPX date is read
		 * @return the GPX object read from the input stream
		 * @throws IOException if the GPX object can't be read
		 * @throws NullPointerException if the given {@code input} stream is
		 *         {@code null}
		 */
		public GPX read(final File file) throws IOException {
			try (FileInputStream fin = new FileInputStream(file);
				 BufferedInputStream bin = new BufferedInputStream(fin))
			{
				return read(bin);
			}
		}

		/**
		 * Read a GPX object from the given {@code input} stream.
		 *
		 * @param path the input path from where the GPX date is read
		 * @return the GPX object read from the input stream
		 * @throws IOException if the GPX object can't be read
		 * @throws NullPointerException if the given {@code input} stream is
		 *         {@code null}
		 */
		public GPX read(final Path path) throws IOException {
			return read(path.toFile());
		}

		/**
		 * Read a GPX object from the given {@code input} stream.
		 *
		 * @param path the input path from where the GPX date is read
		 * @return the GPX object read from the input stream
		 * @throws IOException if the GPX object can't be read
		 * @throws NullPointerException if the given {@code input} stream is
		 *         {@code null}
		 */
		public GPX read(final String path) throws IOException {
			return read(Paths.get(path));
		}

		/**
		 * Create a GPX object from the given GPX-XML string.
		 *
		 * @param xml the GPX XML string
		 * @return the GPX object created from the given XML string
		 * @throws IllegalArgumentException if the given {@code xml} is not a
		 *         valid GPX XML string
		 * @throws NullPointerException if the given {@code xml} string is
		 *         {@code null}
		 */
		public GPX fromString(final String xml) {
			final byte[] bytes = xml.getBytes();
			final ByteArrayInputStream in = new ByteArrayInputStream(bytes);
			try {
				return read(in);
			} catch (InvalidObjectException e) {
				if (e.getCause() instanceof IllegalArgumentException) {
					throw (IllegalArgumentException)e.getCause();
				}
				throw new IllegalArgumentException(e);
			} catch (IOException e) {
				throw new IllegalArgumentException(e);
			}
		}

	}

	/**
	 * Class for writing GPX files. A writer instance can be created by the
	 * {@code GPX.writer} factory methods.
	 *
	 * @see GPX#writer()
	 * @see GPX#writer(String)
	 *
	 * @version 1.3
	 * @since 1.3
	 */
	public static final class Writer {

		private final String _indent;

		private Writer(final String indent) {
			_indent = indent;
		}

		/**
		 * Return the indentation string this GPX writer is using. If the
		 * indentation string is {@link Optional#empty()}, the GPX file consists
		 * of one line.
		 *
		 * @return the indentation string
		 */
		public Optional<String> getIndent() {
			return Optional.ofNullable(_indent);
		}

		/**
		 * Writes the given {@code gpx} object (in GPX XML format) to the given
		 * {@code output} stream.
		 *
		 * @param gpx the GPX object to write to the output
		 * @param output the output stream where the GPX object is written to
		 * @throws IOException if the writing of the GPX object fails
		 * @throws NullPointerException if one of the given arguments is
		 *         {@code null}
		 */
		public void write(final GPX gpx, final OutputStream output)
			throws IOException
		{
			final XMLOutputFactory factory = XMLProvider.provider().xmlOutputFactory();
			try (XMLStreamWriterAdapter xml = writer(factory, output)) {
				xml.writeStartDocument("UTF-8", "1.0");
				GPX.xmlWriter(gpx._version).write(xml, gpx);
				xml.writeEndDocument();
			} catch (XMLStreamException e) {
				throw new IOException(e);
			}
		}

		private XMLStreamWriterAdapter writer(
			final XMLOutputFactory factory,
			final OutputStream output
		)
			throws XMLStreamException
		{
			final NonCloseableOutputStream out =
				new NonCloseableOutputStream(output);

			return _indent == null
				? new XMLStreamWriterAdapter(factory
					.createXMLStreamWriter(out, "UTF-8"))
				: new IndentingXMLStreamWriter(factory
					.createXMLStreamWriter(out, "UTF-8"), _indent);
		}

		/**
		 * Writes the given {@code gpx} object (in GPX XML format) to the given
		 * {@code output} stream.
		 *
		 * @param gpx the GPX object to write to the output
		 * @param file the output file where the GPX object is written to
		 * @throws IOException if the writing of the GPX object fails
		 * @throws NullPointerException if one of the given arguments is
		 *         {@code null}
		 */
		public void write(final GPX gpx, final File file) throws IOException {
			try (FileOutputStream out = new FileOutputStream(file);
				 BufferedOutputStream bout = new BufferedOutputStream(out))
			{
				write(gpx, bout);
			}
		}

		/**
		 * Writes the given {@code gpx} object (in GPX XML format) to the given
		 * {@code output} stream.
		 *
		 * @param gpx the GPX object to write to the output
		 * @param path the output path where the GPX object is written to
		 * @throws IOException if the writing of the GPX object fails
		 * @throws NullPointerException if one of the given arguments is
		 *         {@code null}
		 */
		public void write(final GPX gpx, final Path path) throws IOException {
			write(gpx, path.toFile());
		}

		/**
		 * Writes the given {@code gpx} object (in GPX XML format) to the given
		 * {@code output} stream.
		 *
		 * @param gpx the GPX object to write to the output
		 * @param path the output path where the GPX object is written to
		 * @throws IOException if the writing of the GPX object fails
		 * @throws NullPointerException if one of the given arguments is
		 *         {@code null}
		 */
		public void write(final GPX gpx, final String path) throws IOException {
			write(gpx, Paths.get(path));
		}

		/**
		 * Create a XML string representation of the given {@code gpx} object.
		 *
		 * @param gpx the GPX object to convert to a string
		 * @return the XML string representation of the given {@code gpx} object
		 * @throws NullPointerException if the given given GPX object is
		 *         {@code null}
		 */
		public String toString(final GPX gpx) {
			final ByteArrayOutputStream out = new ByteArrayOutputStream();
			try {
				write(gpx, out);
				return new String(out.toByteArray());
			} catch (IOException e) {
				throw new IllegalStateException("Unexpected error.", e);
			}
		}

	}

	/* *************************************************************************
	 *  Static object creation methods
	 * ************************************************************************/

	/**
	 * Create a new {@code GPX} object with the given data.
	 *
	 * @since 1.5
	 *
	 * @param creator the name or URL of the software that created your GPX
	 *        document. This allows others to inform the creator of a GPX
	 *        instance document that fails to validate.
	 * @param  version the GPX version
	 * @param metadata the metadata about the GPS file
	 * @param wayPoints the way-points
	 * @param routes the routes
	 * @param tracks the tracks
	 * @param extensions the XML extensions
	 * @return a new {@code GPX} object with the given data
	 * @throws NullPointerException if the {@code creator}, {code wayPoints},
	 *         {@code routes} or {@code tracks} is {@code null}
	 */
	public static GPX of(
		final Version version,
		final String creator,
		final Metadata metadata,
		final List<WayPoint> wayPoints,
		final List<Route> routes,
		final List<Track> tracks,
		final Document extensions
	) {
		return new GPX(
			version,
			creator,
			metadata == null || metadata.isEmpty() ? null : metadata,
			wayPoints,
			routes,
			tracks,
			XML.extensions(XML.clone(extensions))
		);
	}

	/**
	 * Create a new {@code GPX} object with the given data.
	 *
	 * @param creator the name or URL of the software that created your GPX
	 *        document. This allows others to inform the creator of a GPX
	 *        instance document that fails to validate.
	 * @param metadata the metadata about the GPS file
	 * @param wayPoints the way-points
	 * @param routes the routes
	 * @param tracks the tracks
	 * @return a new {@code GPX} object with the given data
	 * @throws NullPointerException if the {@code creator}, {code wayPoints},
	 *         {@code routes} or {@code tracks} is {@code null}
	 */
	public static GPX of(
		final String creator,
		final Metadata metadata,
		final List<WayPoint> wayPoints,
		final List<Route> routes,
		final List<Track> tracks
	) {
		return of(
			Version.V11,
			creator,
			metadata,
			wayPoints,
			routes,
			tracks,
			null
		);
	}

	/**
	 * Create a new {@code GPX} object with the given data.
	 *
	 * @since 1.5
	 *
	 * @param creator the name or URL of the software that created your GPX
	 *        document. This allows others to inform the creator of a GPX
	 *        instance document that fails to validate.
	 * @param metadata the metadata about the GPS file
	 * @param wayPoints the way-points
	 * @param routes the routes
	 * @param tracks the tracks
	 * @param extensions the XML extensions
	 * @return a new {@code GPX} object with the given data
	 * @throws NullPointerException if the {@code creator}, {code wayPoints},
	 *         {@code routes} or {@code tracks} is {@code null}
	 */
	public static GPX of(
		final String creator,
		final Metadata metadata,
		final List<WayPoint> wayPoints,
		final List<Route> routes,
		final List<Track> tracks,
		final Document extensions
	) {
		return of(
			Version.V11,
			creator,
			metadata,
			wayPoints,
			routes,
			tracks,
			extensions
		);
	}

	/**
	 * Create a new {@code GPX} object with the given data.
	 *
	 * @param creator the name or URL of the software that created your GPX
	 *        document. This allows others to inform the creator of a GPX
	 *        instance document that fails to validate.
	 * @param  version the GPX version
	 * @param metadata the metadata about the GPS file
	 * @param wayPoints the way-points
	 * @param routes the routes
	 * @param tracks the tracks
	 * @return a new {@code GPX} object with the given data
	 * @throws NullPointerException if the {@code creator}, {code wayPoints},
	 *         {@code routes} or {@code tracks} is {@code null}
	 */
	public static GPX of(
		final Version version,
		final String creator,
		final Metadata metadata,
		final List<WayPoint> wayPoints,
		final List<Route> routes,
		final List<Track> tracks
	) {
		return of(
			version,
			creator,
			metadata == null || metadata.isEmpty() ? null : metadata,
			wayPoints,
			routes,
			tracks,
			null
		);
	}



	/* *************************************************************************
	 *  Java object serialization
	 * ************************************************************************/

	private Object writeReplace() {
		return new Serial(Serial.GPX_TYPE, this);
	}

	private void readObject(final ObjectInputStream stream)
		throws InvalidObjectException
	{
		throw new InvalidObjectException("Serialization proxy required.");
	}

	void write(final DataOutput out) throws IOException {
		IO.writeString(_version.getValue(), out);
		IO.writeString(_creator, out);
		IO.writeNullable(_metadata, Metadata::write, out);
		IO.writes(_wayPoints, WayPoint::write, out);
		IO.writes(_routes, Route::write, out);
		IO.writes(_tracks, Track::write, out);
		IO.writeNullable(_extensions, IO::write, out);
	}

	static GPX read(final DataInput in) throws IOException {
		return new GPX(
			Version.of(IO.readString(in)),
			IO.readString(in),
			IO.readNullable(Metadata::read, in),
			IO.reads(WayPoint::read, in),
			IO.reads(Route::read, in),
			IO.reads(Track::read, in),
			IO.readNullable(IO::readDoc, in)
		);
	}

	/* *************************************************************************
	 *  XML stream object serialization
	 * ************************************************************************/

	private static String name(final GPX gpx) {
		return gpx.getMetadata()
			.flatMap(Metadata::getName)
			.orElse(null);
	}

	private static String desc(final GPX gpx) {
		return gpx.getMetadata()
			.flatMap(Metadata::getDescription)
			.orElse(null);
	}

	private static String author(final GPX gpx) {
		return gpx.getMetadata()
			.flatMap(Metadata::getAuthor)
			.flatMap(Person::getName)
			.orElse(null);
	}

	private static String email(final GPX gpx) {
		return gpx.getMetadata()
			.flatMap(Metadata::getAuthor)
			.flatMap(Person::getEmail)
			.map(Email::getAddress)
			.orElse(null);
	}

	private static String url(final GPX gpx) {
		return gpx.getMetadata()
			.flatMap(Metadata::getAuthor)
			.flatMap(Person::getLink)
			.map(Link::getHref)
			.map(URI::toString)
			.orElse(null);
	}

	private static String urlname(final GPX gpx) {
		return gpx.getMetadata()
			.flatMap(Metadata::getAuthor)
			.flatMap(Person::getLink)
			.flatMap(Link::getText)
			.orElse(null);
	}

	private static String time(final GPX gpx) {
		return gpx.getMetadata()
			.flatMap(Metadata::getTime)
			.map(ZonedDateTimeFormat::format)
			.orElse(null);
	}

	private static String keywords(final GPX gpx) {
		return gpx.getMetadata()
			.flatMap(Metadata::getKeywords)
			.orElse(null);
	}


	// Define the needed writers for the different versions.
	private static final XMLWriters<GPX> WRITERS = new XMLWriters<GPX>()
		.v00(XMLWriter.attr("version").map(gpx -> gpx._version._value))
		.v00(XMLWriter.attr("creator").map(gpx -> gpx._creator))
		.v11(XMLWriter.ns(Version.V11.getNamespaceURI()))
		.v10(XMLWriter.ns(Version.V10.getNamespaceURI()))
		.v11(Metadata.WRITER.map(gpx -> gpx._metadata))
		.v10(XMLWriter.elem("name").map(GPX::name))
		.v10(XMLWriter.elem("desc").map(GPX::desc))
		.v10(XMLWriter.elem("author").map(GPX::author))
		.v10(XMLWriter.elem("email").map(GPX::email))
		.v10(XMLWriter.elem("url").map(GPX::url))
		.v10(XMLWriter.elem("urlname").map(GPX::urlname))
		.v10(XMLWriter.elem("time").map(GPX::time))
		.v10(XMLWriter.elem("keywords").map(GPX::keywords))
		.v10(XMLWriter.elems(WayPoint.xmlWriter(Version.V10,"wpt")).map(gpx -> gpx._wayPoints))
		.v11(XMLWriter.elems(WayPoint.xmlWriter(Version.V11,"wpt")).map(gpx -> gpx._wayPoints))
		.v10(XMLWriter.elems(Route.xmlWriter(Version.V10)).map(gpx -> gpx._routes))
		.v11(XMLWriter.elems(Route.xmlWriter(Version.V11)).map(gpx -> gpx._routes))
		.v10(XMLWriter.elems(Track.xmlWriter(Version.V10)).map(gpx -> gpx._tracks))
		.v11(XMLWriter.elems(Track.xmlWriter(Version.V11)).map(gpx -> gpx._tracks))
		.v00(XMLWriter.doc("extensions").map(gpx -> gpx._extensions));


	// Define the needed readers for the different versions.
	private static final XMLReaders READERS = new XMLReaders()
		.v00(XMLReader.attr("version").map(Version::of))
		.v00(XMLReader.attr("creator"))
		.v11(Metadata.READER)
		.v10(XMLReader.elem("name"))
		.v10(XMLReader.elem("desc"))
		.v10(XMLReader.elem("author"))
		.v10(XMLReader.elem("email"))
		.v10(XMLReader.elem("url"))
		.v10(XMLReader.elem("urlname"))
		.v10(XMLReader.elem("time").map(ZonedDateTimeFormat::parse))
		.v10(XMLReader.elem("keywords"))
		.v10(Bounds.READER)
		.v10(XMLReader.elems(WayPoint.xmlReader(Version.V10, "wpt")))
		.v11(XMLReader.elems(WayPoint.xmlReader(Version.V11, "wpt")))
		.v10(XMLReader.elems(Route.xmlReader(Version.V10)))
		.v11(XMLReader.elems(Route.xmlReader(Version.V11)))
		.v10(XMLReader.elems(Track.xmlReader(Version.V10)))
		.v11(XMLReader.elems(Track.xmlReader(Version.V11)))
		.v00(XMLReader.doc("extensions"));


	static XMLWriter<GPX> xmlWriter(final Version version) {
		return XMLWriter.elem("gpx", WRITERS.writers(version));
	}

	static XMLReader<GPX> xmlReader(final Version version) {
		return XMLReader.elem(
			version == Version.V10 ? GPX::toGPXv10 : GPX::toGPXv11,
			"gpx",
			READERS.readers(version)
		);
	}

	@SuppressWarnings("unchecked")
	private static GPX toGPXv11(final Object[] v) {
		return new GPX(
			(Version)v[0],
			(String)v[1],
			(Metadata)v[2],
			(List<WayPoint>)v[3],
			(List<Route>)v[4],
			(List<Track>)v[5],
			XML.extensions((Document)v[6])
		);
	}

	@SuppressWarnings("unchecked")
	private static GPX toGPXv10(final Object[] v) {
		return new GPX(
			(Version)v[0],
			(String)v[1],
			Metadata.of(
				(String)v[2],
				(String)v[3],
				Person.of(
					(String)v[4],
					v[5] != null
						? Email.of((String)v[5])
						: null,
					v[6] != null
						? Link.of((String)v[6], (String)v[7], null)
						: null
				),
				null,
				null,
				(ZonedDateTime)v[8],
				(String)v[9],
				(Bounds)v[10]
			),
			(List<WayPoint>)v[11],
			(List<Route>)v[12],
			(List<Track>)v[13],
			XML.extensions((Document)v[14])
		);
	}


	/* *************************************************************************
	 *  Write and read GPX files
	 * ************************************************************************/

	/**
	 * Return a new GPX writer with the given {@code indent}.
	 *
	 * @since 1.3
	 *
	 * @see #writer()
	 *
	 * @param indent the element indentation
	 * @return a new GPX writer
	 */
	public static Writer writer(final String indent) {
		return new Writer(indent);
	}

	/**
	 * Return a new GPX writer with no indentation.
	 *
	 * @since 1.3
	 *
	 * @see #writer(String)
	 *
	 * @return a new GPX writer
	 */
	public static Writer writer() {
		return new Writer(null);
	}

	/**
	 * Writes the given {@code gpx} object (in GPX XML format) to the given
	 * {@code output} stream.
	 *
	 * @param gpx the GPX object to write to the output
	 * @param output the output stream where the GPX object is written to
	 * @throws IOException if the writing of the GPX object fails
	 * @throws NullPointerException if one of the given arguments is {@code null}
	 */
	public static void write(final GPX gpx, final OutputStream output)
		throws IOException
	{
		writer().write(gpx, output);
	}

	/**
	 * Writes the given {@code gpx} object (in GPX XML format) to the given
	 * {@code output} stream.
	 *
	 * @since 1.1
	 *
	 * @param gpx the GPX object to write to the output
	 * @param path the output path where the GPX object is written to
	 * @throws IOException if the writing of the GPX object fails
	 * @throws NullPointerException if one of the given arguments is {@code null}
	 */
	public static void write(final GPX gpx, final Path path) throws IOException {
		writer().write(gpx, path);
	}



	/**
	 * Return a GPX reader, reading GPX files with the given version and in the
	 * given reading mode.
	 *
	 * @since 1.3
	 *
	 * @see #reader()
	 *
	 * @param version the GPX version to read
	 * @param mode the reading mode
	 * @return a new GPX reader object
	 * @throws NullPointerException if one of the arguments is {@code null}
	 */
	public static Reader reader(final Version version, final Mode mode) {
		return new Reader(GPX.xmlReader(version), mode);
	}

	/**
	 * Return a GPX reader, reading GPX files with the given version and in
	 * strict reading mode.
	 *
	 * @since 1.3
	 *
	 * @see #reader()
	 *
	 * @param version the GPX version to read
	 * @return a new GPX reader object
	 * @throws NullPointerException if one of the arguments is {@code null}
	 */
	public static Reader reader(final Version version) {
		return new Reader(GPX.xmlReader(version), Mode.STRICT);
	}

	/**
	 * Return a GPX reader, reading GPX files with version 1.1 and in the given
	 * reading mode.
	 *
	 * @since 1.3
	 *
	 * @see #reader()
	 *
	 * @param mode the reading mode
	 * @return a new GPX reader object
	 * @throws NullPointerException if one of the arguments is {@code null}
	 */
	public static Reader reader(final Mode mode) {
		return new Reader(GPX.xmlReader(Version.V11), mode);
	}

	/**
	 * Return a GPX reader, reading GPX files (v1.1) with reading mode
	 * {@link Mode#STRICT}.
	 *
	 * @since 1.3
	 *
	 * @see #reader(Version, Reader.Mode)
	 *
	 * @return a new GPX reader object
	 */
	public static Reader reader() {
		return reader(Version.V11, Mode.STRICT);
	}


	/**
	 * Read an GPX object from the given {@code input} stream. This method is a
	 * shortcut for
	 * <pre>{@code
	 * final GPX gpx = GPX.reader().read(input);
	 * }</pre>
	 *
	 * @param input the input stream from where the GPX date is read
	 * @return the GPX object read from the input stream
	 * @throws IOException if the GPX object can't be read
	 * @throws NullPointerException if the given {@code input} stream is
	 *         {@code null}
	 * @throws InvalidObjectException if the gpx input is invalid.
	 */
	public static GPX read(final InputStream input) throws IOException {
		return reader(Version.V11, Mode.STRICT).read(input);
	}



	/**
	 * Read an GPX object from the given {@code input} stream. This method is a
	 * shortcut for
	 * <pre>{@code
	 * final GPX gpx = GPX.reader().read(path);
	 * }</pre>
	 *
	 * @param path the input path from where the GPX date is read
	 * @return the GPX object read from the input stream
	 * @throws IOException if the GPX object can't be read
	 * @throws NullPointerException if the given {@code input} stream is
	 *         {@code null}
	 */
	public static GPX read(final Path path) throws IOException {
		return reader(Version.V11, Mode.STRICT).read(path);
	}



	/**
	 * Read an GPX object from the given {@code input} stream. This method is a
	 * shortcut for
	 * <pre>{@code
	 * final GPX gpx = GPX.reader().read(path);
	 * }</pre>
	 *
	 * @param path the input path from where the GPX date is read
	 * @return the GPX object read from the input stream
	 * @throws IOException if the GPX object can't be read
	 * @throws NullPointerException if the given {@code input} stream is
	 *         {@code null}
	 */
	public static GPX read(final String path) throws IOException {
		return reader(Version.V11, Mode.STRICT).read(path);
	}

}