001/*
002 * Java GPX Library (jpx-3.1.0).
003 * Copyright (c) 2016-2023 Franz Wilhelmstötter
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 *
017 * Author:
018 *    Franz Wilhelmstötter (franz.wilhelmstoetter@gmail.com)
019 */
020package io.jenetics.jpx;
021
022import static java.lang.String.format;
023import static java.util.Objects.requireNonNull;
024import static io.jenetics.jpx.Lists.copyOf;
025import static io.jenetics.jpx.Lists.copyTo;
026import static io.jenetics.jpx.XMLWriter.elem;
027
028import java.io.DataInput;
029import java.io.DataOutput;
030import java.io.IOException;
031import java.io.InvalidObjectException;
032import java.io.ObjectInputStream;
033import java.io.Serial;
034import java.io.Serializable;
035import java.util.ArrayList;
036import java.util.Iterator;
037import java.util.List;
038import java.util.Objects;
039import java.util.Optional;
040import java.util.function.Consumer;
041import java.util.function.Function;
042import java.util.function.Predicate;
043import java.util.stream.Stream;
044
045import org.w3c.dom.Document;
046
047import io.jenetics.jpx.GPX.Version;
048
049/**
050 * A Track Segment holds a list of Track Points which are logically connected in
051 * order. To represent a single GPS track where GPS reception was lost, or the
052 * GPS receiver was turned off, start a new Track Segment for each continuous
053 * span of track data.
054 *
055 * @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
056 * @version 1.5
057 * @since 1.0
058 */
059public final class TrackSegment implements Iterable<WayPoint>, Serializable {
060
061        @Serial
062        private static final long serialVersionUID = 2L;
063
064        private final List<WayPoint> _points;
065        private final Document _extensions;
066
067        /**
068         * Create a new track-segment with the given points.
069         *
070         * @param points the points of the track-segment
071         */
072        private TrackSegment(final List<WayPoint> points, final Document extensions) {
073                _points = copyOf(points);
074                _extensions = extensions;
075        }
076
077        /**
078         * Return the track-points of this segment.
079         *
080         * @return the track-points of this segment
081         */
082        public List<WayPoint> getPoints() {
083                return _points;
084        }
085
086        /**
087         * Return a stream of {@link WayPoint} objects this track-segments contains.
088         *
089         * @return a stream of {@link WayPoint} objects this track-segment contains
090         */
091        public Stream<WayPoint> points() {
092                return _points.stream();
093        }
094
095        @Override
096        public Iterator<WayPoint> iterator() {
097                return _points.iterator();
098        }
099
100
101        /**
102         * Return the (cloned) extensions document. The root element of the returned
103         * document has the name {@code extensions}.
104         * <pre>{@code
105         * <extensions>
106         *     ...
107         * </extensions>
108         * }</pre>
109         *
110         * @since 1.5
111         *
112         * @return the extensions document
113         * @throws org.w3c.dom.DOMException if the document could not be cloned,
114         *         because of an erroneous XML configuration
115         */
116        public Optional<Document> getExtensions() {
117                return Optional.ofNullable(_extensions).map(XML::clone);
118        }
119
120        /**
121         * Convert the <em>immutable</em> track-segment object into a
122         * <em>mutable</em> builder initialized with the current track-segment
123         * values.
124         *
125         * @since 1.1
126         *
127         * @return a new track-segment builder initialized with the values of
128         *        {@code this} track-segment
129         */
130        public Builder toBuilder() {
131                return builder()
132                        .points(_points)
133                        .extensions(_extensions);
134        }
135
136        /**
137         * Return {@code true} if {@code this} track-segment doesn't contain any
138         * track-point.
139         *
140         * @return {@code true} if {@code this} track-segment is empty, {@code false}
141         *         otherwise
142         */
143        public boolean isEmpty() {
144                return _points.isEmpty();
145        }
146
147        /**
148         * Return {@code true} if {@code this} track-segment contains at least one
149         * track-point.
150         *
151         * @since 1.1
152         *
153         * @return {@code true} if {@code this} track-segment is not empty,
154         *         {@code false} otherwise
155         */
156        public boolean nonEmpty() {
157                return !isEmpty();
158        }
159
160        @Override
161        public int hashCode() {
162                return Objects.hashCode(_points);
163        }
164
165        @Override
166        public boolean equals(final Object obj) {
167                return obj == this ||
168                        obj instanceof TrackSegment &&
169                        Objects.equals(((TrackSegment)obj)._points, _points);
170        }
171
172        @Override
173        public String toString() {
174                return format("TrackSegment[points=%s]", _points.size());
175        }
176
177        /**
178         * Builder class for creating immutable {@code TrackSegment} objects.
179         * <p>
180         * Creating a {@code TrackSegment} object with  3 track-points:
181         * <pre>{@code
182         * final TrackSegment segment = TrackSegment.builder()
183         *     .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
184         *     .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))
185         *     .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162))))
186         *     .build();
187         * }</pre>
188         */
189        public static final class Builder implements Filter<WayPoint, TrackSegment> {
190                private final List<WayPoint> _points = new ArrayList<>();
191                private Document _extensions;
192
193                private Builder() {
194                }
195
196                /**
197                 * Set the way-points fo the track segment. The list of way-points may
198                 * be {@code null}.
199                 *
200                 * @param points the track-segment points
201                 * @return {@code this} {@code Builder} for method chaining
202                 * @throws NullPointerException if one of the way-points in the list is
203                 *         {@code null}
204                 */
205                public Builder points(final List<WayPoint> points) {
206                        copyTo(points, _points);
207                        return this;
208                }
209
210                /**
211                 * Add a way-point to the track-segment.
212                 *
213                 * @param point the segment way-point
214                 * @return {@code this} {@code Builder} for method chaining
215                 * @throws NullPointerException if the given {@code href} is {@code null}
216                 */
217                public Builder addPoint(final WayPoint point) {
218                        _points.add(requireNonNull(point));
219                        return this;
220                }
221
222                /**
223                 * Add a way-point to the track-segment, via the given way-point builder.
224                 * <p>
225                 * Creating a {@code TrackSegment} object with  3 track-points:
226                 * <pre>{@code
227                 * final TrackSegment segment = TrackSegment.builder()
228                 *     .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
229                 *     .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))
230                 *     .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162))))
231                 *     .build();
232                 * }</pre>
233                 *
234                 * @param point the segment way-point builder
235                 * @return {@code this} {@code Builder} for method chaining
236                 * @throws NullPointerException if the given {@code href} is {@code null}
237                 */
238                public Builder addPoint(final Consumer<? super WayPoint.Builder> point) {
239                        final WayPoint.Builder builder = WayPoint.builder();
240                        point.accept(builder);
241                        return addPoint(builder.build());
242                }
243
244                /**
245                 * Return the current way-points. The returned list is mutable.
246                 *
247                 * @since 1.1
248                 *
249                 * @return the current, mutable way-point list
250                 */
251                public List<WayPoint> points() {
252                        return new NonNullList<>(_points);
253                }
254
255                /**
256                 * Sets the extensions object, which may be {@code null}. The root
257                 * element of the extensions document must be {@code extensions}.
258                 * <pre>{@code
259                 * <extensions>
260                 *     ...
261                 * </extensions>
262                 * }</pre>
263                 *
264                 * @since 1.5
265                 *
266                 * @param extensions the document
267                 * @return {@code this} {@code Builder} for method chaining
268                 * @throws IllegalArgumentException if the root element is not the
269                 *         an {@code extensions} node
270                 */
271                public Builder extensions(final Document extensions) {
272                        _extensions = XML.checkExtensions(extensions);
273                        return this;
274                }
275
276                /**
277                 * Return the current extensions
278                 *
279                 * @since 1.5
280                 *
281                 * @return the extensions document
282                 */
283                public Optional<Document> extensions() {
284                        return Optional.ofNullable(_extensions);
285                }
286
287                @Override
288                public Builder filter(final Predicate<? super WayPoint> predicate) {
289                        points(_points.stream().filter(predicate).toList());
290                        return this;
291                }
292
293                @Override
294                public Builder map(
295                        final Function<? super WayPoint, ? extends WayPoint> mapper
296                ) {
297                        points(
298                                _points.stream()
299                                        .map(mapper)
300                                        .map(WayPoint.class::cast)
301                                        .toList()
302                        );
303                        return this;
304                }
305
306                @Override
307                public Builder flatMap(
308                        final Function<
309                                ? super WayPoint,
310                                ? extends List<WayPoint>> mapper
311                ) {
312                        points(
313                                _points.stream()
314                                        .flatMap(wp -> mapper.apply(wp).stream())
315                                        .toList()
316                        );
317                        return this;
318                }
319
320                @Override
321                public Builder listMap(
322                        final Function<
323                                ? super List<WayPoint>,
324                                ? extends List<WayPoint>> mapper
325                ) {
326                        points(mapper.apply(_points));
327                        return this;
328                }
329
330                /**
331                 * Create a new track-segment from the current builder state.
332                 *
333                 * @return a new track-segment from the current builder state
334                 */
335                @Override
336                public TrackSegment build() {
337                        return of(_points, _extensions);
338                }
339
340        }
341
342        /**
343         * Create a new track-segment builder.
344         *
345         * @return a new track-segment builder
346         */
347        public static Builder builder() {
348                return new Builder();
349        }
350
351
352        /* *************************************************************************
353         *  Static object creation methods
354         * ************************************************************************/
355
356        /**
357         * Create a new track-segment with the given points.
358         *
359         * @since 1.5
360         *
361         * @param points the points of the track-segment
362         * @param extensions the extensions document
363         * @return a new track-segment with the given points
364         * @throws NullPointerException if the given {@code points} sequence is
365         *        {@code null}
366         */
367        public static TrackSegment of(
368                final List<WayPoint> points,
369                final Document extensions
370        ) {
371                return new TrackSegment(
372                        points,
373                        XML.extensions(XML.clone(extensions))
374                );
375        }
376
377        /**
378         * Create a new track-segment with the given points.
379         *
380         * @param points the points of the track-segment
381         * @return a new track-segment with the given points
382         * @throws NullPointerException if the given {@code points} sequence is
383         *        {@code null}
384         */
385        public static TrackSegment of(final List<WayPoint> points) {
386                return of(points, null);
387        }
388
389
390        /* *************************************************************************
391         *  Java object serialization
392         * ************************************************************************/
393
394        @Serial
395        private Object writeReplace() {
396                return new SerialProxy(SerialProxy.TRACK_SEGMENT, this);
397        }
398
399        @Serial
400        private void readObject(final ObjectInputStream stream)
401                throws InvalidObjectException
402        {
403                throw new InvalidObjectException("Serialization proxy required.");
404        }
405
406        void write(final DataOutput out) throws IOException {
407                IO.writes(_points, WayPoint::write, out);
408                IO.writeNullable(_extensions, IO::write, out);
409        }
410
411        static TrackSegment read(final DataInput in) throws IOException {
412                return new TrackSegment(
413                        IO.reads(WayPoint::read, in),
414                        IO.readNullable(IO::readDoc, in)
415                );
416        }
417
418
419        /* *************************************************************************
420         *  XML stream object serialization
421         * ************************************************************************/
422
423        static XMLWriter<TrackSegment> xmlWriter(
424                final Version version,
425                final Function<? super Number, String> formatter
426        ) {
427                return elem("trkseg",
428                        XMLWriter
429                                .elems(WayPoint.xmlWriter(version,"trkpt", formatter))
430                                .map(ts -> ts._points),
431                        XMLWriter.doc("extensions").map(gpx -> gpx._extensions)
432                );
433        }
434
435        @SuppressWarnings("unchecked")
436        static XMLReader<TrackSegment> xmlReader(
437                final Version version,
438                final Function<? super String, Length> lengthParser
439        ) {
440                return XMLReader.elem(a -> new TrackSegment(
441                                (List<WayPoint>)a[0],
442                                XML.extensions((Document)a[1])
443                        ),
444                        "trkseg",
445                        XMLReader.elems(WayPoint.xmlReader(version,"trkpt", lengthParser)),
446                        XMLReader.doc("extensions")
447                );
448        }
449
450
451
452}