Filters.java
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  */
020 package io.jenetics.jpx;
021 
022 import static java.time.ZoneOffset.UTC;
023 import static java.util.stream.Collectors.groupingBy;
024 
025 import java.time.LocalDate;
026 import java.util.List;
027 import java.util.Map;
028 import java.util.Map.Entry;
029 import java.util.stream.Stream;
030 
031 /**
032  * Some commonly usable way-point filter methods.
033  *
034  @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
035  @version 1.1
036  @since 1.1
037  */
038 public final class Filters {
039 
040     private Filters() {
041     }
042 
043     /**
044      * Merges the given segments into one segment containing all way-points. The
045      * order of the way-points is preserved.
046      <pre>{@code
047      * final GPX merged = gpx.toBuilder()
048      *     .trackFilter()
049      *         .map(track -> track.toBuilder()
050      *             .listMap(Filters::mergeSegments)
051      *             .filter(TrackSegment::nonEmpty)
052      *             .build())
053      *         .build()
054      *     .build();
055      * }</pre>
056      *
057      @param segments the segment list to merge
058      @return a list with one segment, containing all way-points of the original
059      *         segments
060      @throws NullPointerException if the given segment list is {@code null}
061      */
062     public static List<TrackSegment> mergeSegments(
063         final List<TrackSegment> segments
064     ) {
065         final List<WayPoint> points = segments.stream()
066             .flatMap(TrackSegment::points)
067             .toList();
068 
069         return List.of(TrackSegment.of(points));
070     }
071 
072     /**
073      * Merges the given tracks into one track containing all segments. The order
074      * of the segments is preserved.
075      <pre>{@code
076      * final GPX merged = gpx.toBuilder()
077      *     .trackFilter()
078      *         .listMap(Filters::mergeTracks)
079      *         .filter(Track::nonEmpty)
080      *         .build())
081      *     .build();
082      * }</pre>
083      *
084      @param tracks the track list to merge
085      @return a list with one track, containing all segments
086      @throws NullPointerException if the given track list is {@code null}
087      */
088     public static List<Track> mergeTracks(final List<Track> tracks) {
089         final List<TrackSegment> segments = tracks.stream()
090             .flatMap(Track::segments)
091             .toList();
092 
093         return tracks.isEmpty()
094             ? List.of()
095             : List.of(
096                 tracks.get(0).toBuilder()
097                     .segments(segments)
098                     .build()
099                 );
100     }
101 
102     /**
103      * Merges all way-points of all segments of the given track list into one
104      * track with one segment, containing all way-points. The order of the
105      * way-points is preserved.
106      <pre>{@code
107      * final GPX merged = gpx.toBuilder()
108      *     .trackFilter()
109      *         .listMap(Filters::fullyMergeTracks)
110      *         .build())
111      *     .build();
112      * }</pre>
113      *
114      *
115      @param tracks the track list to merge
116      @return a list with one track, containing one segment with all way-points
117      @throws NullPointerException if the given track list is {@code null}
118      */
119     public static List<Track> fullyMergeTracks(final List<Track> tracks) {
120         final List<WayPoint> points = tracks.stream()
121             .flatMap(Track::segments)
122             .flatMap(TrackSegment::points)
123             .toList();
124 
125         return tracks.isEmpty()
126             ? List.of()
127             : List.of(
128                 tracks.get(0).toBuilder()
129                     .segments(List.of(TrackSegment.of(points)))
130                     .build()
131                 );
132     }
133 
134     /**
135      * Return a new {@code GPX} object with all <i>empty</i> elements (tracks,
136      * track-segments, routes and metadata) removed. This method can be used
137      * to clean up the GPX object before writing it to file.
138      <pre>{@code
139      * final GPX gpx = ...;
140      * final GPX.write(Filters.nonEmptyGPX(gpx), "tracks.gpx", "    ");
141      * }</pre>
142      *
143      @param gpx the GPX object to clean up
144      @return a new {@code GPX} object with all <i>empty</i> elements removed
145      @throws NullPointerException if the given {@code gpx} object is
146      *         {@code null}
147      */
148     public static GPX nonEmptyGPX(final GPX gpx) {
149         return gpx.toBuilder()
150             .routeFilter()
151                 .listMap(Filters::nonEmptyRoutes)
152                 .build()
153             .trackFilter()
154                 .listMap(Filters::nonEmptyTracks)
155                 .build()
156             .metadata(gpx.getMetadata()
157                         .filter(Metadata::nonEmpty)
158                         .orElse(null))
159             .build();
160     }
161 
162     /**
163      * Return a new route list with all <i>empty</i> routes removed.
164      *
165      @param routes the route list to clean up
166      @return a new route list with all <i>empty</i> routes removed
167      @throws NullPointerException if the given argument is {@code null}
168      */
169     public static List<Route> nonEmptyRoutes(final List<Route> routes) {
170         return routes.stream()
171             .filter(Route::nonEmpty)
172             .toList();
173     }
174 
175     /**
176      * Return a new track list with all <i>empty</i> tracks removed.
177      *
178      @param tracks the track list to clean up
179      @return a new track list with all <i>empty</i> tracks removed
180      @throws NullPointerException if the given argument is {@code null}
181      */
182     public static List<Track> nonEmptyTracks(final List<Track> tracks) {
183         return tracks.stream()
184             .map(track -> track.toBuilder()
185                 .listMap(Filters::nonEmptySegments)
186                 .build())
187             .filter(Track::nonEmpty)
188             .toList();
189     }
190 
191     /**
192      * Return a new segment list with all <i>empty</i> segments removed.
193      *
194      @param segments the segment list to clean up
195      @return  a new segment list with all <i>empty</i> segments removed
196      @throws NullPointerException if the given argument is {@code null}
197      */
198     public static List<TrackSegment> nonEmptySegments(
199         final List<TrackSegment> segments
200     ) {
201         return segments.stream()
202             .filter(TrackSegment::nonEmpty)
203             .toList();
204     }
205 
206     static List<Track> splitByDay(final Track track) {
207         return splitWayPointsByDay(
208             track.segments()
209                 .flatMap(TrackSegment::points))
210             .stream()
211             .map(TrackSegment::of)
212             .map(segment -> Track.builder().addSegment(segment).build())
213             .toList();
214     }
215 
216     private static List<List<WayPoint>> splitWayPointsByDay(
217         final Stream<WayPoint> points
218     ) {
219         final Map<LocalDate, List<WayPoint>> parts = points
220             .collect(groupingBy(wp -> wp.getTime()
221                 .map(i -> LocalDate.ofInstant(i, UTC))
222                 .orElse(LocalDate.MIN)));
223 
224         return parts.entrySet().stream()
225             .sorted(Entry.comparingByKey())
226             .map(Entry::getValue)
227             .toList();
228     }
229 
230     static List<TrackSegment> splitByDay(final TrackSegment segment) {
231         return splitWayPointsByDay(segment.points()).stream()
232             .map(TrackSegment::of)
233             .toList();
234     }
235 
236 }