Bounds.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.lang.Math.max;
023 import static java.lang.Math.min;
024 import static java.lang.String.format;
025 import static java.util.Objects.hash;
026 import static java.util.Objects.requireNonNull;
027 
028 import java.io.DataInput;
029 import java.io.DataOutput;
030 import java.io.IOException;
031 import java.io.InvalidObjectException;
032 import java.io.ObjectInputStream;
033 import java.io.Serial;
034 import java.io.Serializable;
035 import java.util.Objects;
036 import java.util.function.Function;
037 import java.util.stream.Collector;
038 
039 /**
040  * Two lat/lon pairs defining the extent of an element.
041  *
042  @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
043  @version 1.6
044  @since 1.0
045  */
046 public final class Bounds implements Serializable {
047 
048     @Serial
049     private static final long serialVersionUID = 2L;
050 
051     private final Latitude _minLatitude;
052     private final Longitude _minLongitude;
053     private final Latitude _maxLatitude;
054     private final Longitude _maxLongitude;
055 
056     /**
057      * Create a new {@code Bounds} object with the given extent.
058      *
059      @param minLatitude the minimum latitude
060      @param minLongitude the minimum longitude
061      @param maxLatitude the maximum latitude
062      @param maxLongitude the maximum longitude
063      @throws NullPointerException if one of the arguments is {@code null}
064      */
065     private Bounds(
066         final Latitude minLatitude,
067         final Longitude minLongitude,
068         final Latitude maxLatitude,
069         final Longitude maxLongitude
070     ) {
071         _minLatitude = requireNonNull(minLatitude);
072         _minLongitude = requireNonNull(minLongitude);
073         _maxLatitude = requireNonNull(maxLatitude);
074         _maxLongitude = requireNonNull(maxLongitude);
075     }
076 
077     /**
078      * Return the minimum latitude.
079      *
080      @return the minimum latitude
081      */
082     public Latitude getMinLatitude() {
083         return _minLatitude;
084     }
085 
086     /**
087      * Return the minimum longitude.
088      *
089      @return the minimum longitude
090      */
091     public Longitude getMinLongitude() {
092         return _minLongitude;
093     }
094 
095     /**
096      * Return the maximum latitude.
097      *
098      @return the maximum latitude
099      */
100     public Latitude getMaxLatitude() {
101         return _maxLatitude;
102     }
103 
104     /**
105      * Return the maximum longitude
106      *
107      @return the maximum longitude
108      */
109     public Longitude getMaxLongitude() {
110         return _maxLongitude;
111     }
112 
113     @Override
114     public int hashCode() {
115         return hash(_minLatitude, _minLongitude, _maxLatitude, _maxLongitude);
116     }
117 
118     @Override
119     public boolean equals(final Object obj) {
120         return obj == this ||
121             obj instanceof  Bounds bounds &&
122             Objects.equals(bounds._minLatitude, _minLatitude&&
123             Objects.equals(bounds._minLongitude, _minLongitude&&
124             Objects.equals(bounds._maxLatitude, _maxLatitude&&
125             Objects.equals(bounds._maxLongitude, _maxLongitude);
126     }
127 
128     @Override
129     public String toString() {
130         return format(
131             "[%s, %s][%s, %s]",
132             _minLatitude,
133             _minLongitude,
134             _maxLatitude,
135             _maxLongitude
136         );
137     }
138 
139     /**
140      * Return a collector which calculates the bounds of a given way-point
141      * stream. The following example shows how to calculate the bounds of all
142      * track-points of a given GPX object.
143      *
144      <pre>{@code
145      * final Bounds bounds = gpx.tracks()
146      *     .flatMap(Track::segments)
147      *     .flatMap(TrackSegment::points)
148      *     .collect(Bounds.toBounds());
149      * }</pre>
150      *
151      * If the collecting way-point stream is empty, the collected {@code Bounds}
152      * object is {@code null}.
153      *
154      @since 1.6
155      *
156      @param <P> The actual point type
157      @return a new bounds collector
158      */
159     public static <P extends Point> Collector<P, ?, Bounds> toBounds() {
160         return Collector.of(
161             () -> {
162                 final double[] a = new double[4];
163                 a[0= Double.MAX_VALUE;
164                 a[1= Double.MAX_VALUE;
165                 a[2= -Double.MAX_VALUE;
166                 a[3= -Double.MAX_VALUE;
167                 return a;
168             },
169             (a, b-> {
170                 a[0= min(b.getLatitude().doubleValue(), a[0]);
171                 a[1= min(b.getLongitude().doubleValue(), a[1]);
172                 a[2= max(b.getLatitude().doubleValue(), a[2]);
173                 a[3= max(b.getLongitude().doubleValue(), a[3]);
174             },
175             (a, b-> {
176                 a[0= min(a[0], b[0]);
177                 a[1= min(a[1], b[1]);
178                 a[2= max(a[2], b[2]);
179                 a[3= max(a[3], b[3]);
180                 return a;
181             },
182             a -> a[0== Double.MAX_VALUE
183                 null
184                 : Bounds.of(a[0], a[1], a[2], a[3])
185         );
186     }
187 
188     /* *************************************************************************
189      *  Static object creation methods
190      * ************************************************************************/
191 
192     /**
193      * Create a new {@code Bounds} object with the given extent.
194      *
195      @param minLatitude the minimum latitude
196      @param minLongitude the minimum longitude
197      @param maxLatitude the maximum latitude
198      @param maxLongitude the maximum longitude
199      @return a new {@code Bounds} object with the given extent
200      @throws NullPointerException if one of the arguments is {@code null}
201      */
202     public static Bounds of(
203         final Latitude minLatitude,
204         final Longitude minLongitude,
205         final Latitude maxLatitude,
206         final Longitude maxLongitude
207     ) {
208         return new Bounds(minLatitude, minLongitude, maxLatitude, maxLongitude);
209     }
210 
211     /**
212      * Create a new {@code Bounds} object with the given extent.
213      *
214      @param minLatitudeDegree the minimum latitude
215      @param minLongitudeDegree the minimum longitude
216      @param maxLatitudeDegree the maximum latitude
217      @param maxLongitudeDegree the maximum longitude
218      @return a new {@code Bounds} object with the given extent
219      @throws IllegalArgumentException if the latitude values are not within
220      *         the range of {@code [-90..90]}
221      @throws IllegalArgumentException if the longitudes value are not within
222      *         the range of {@code [-180..180]}
223      */
224     public static Bounds of(
225         final double minLatitudeDegree,
226         final double minLongitudeDegree,
227         final double maxLatitudeDegree,
228         final double maxLongitudeDegree
229     ) {
230         return new Bounds(
231             Latitude.ofDegrees(minLatitudeDegree),
232             Longitude.ofDegrees(minLongitudeDegree),
233             Latitude.ofDegrees(maxLatitudeDegree),
234             Longitude.ofDegrees(maxLongitudeDegree)
235         );
236     }
237 
238     /* *************************************************************************
239      *  Java object serialization
240      * ************************************************************************/
241 
242     @Serial
243     private Object writeReplace() {
244         return new SerialProxy(SerialProxy.BOUNDS, this);
245     }
246 
247     @Serial
248     private void readObject(final ObjectInputStream stream)
249         throws InvalidObjectException
250     {
251         throw new InvalidObjectException("Serialization proxy required.");
252     }
253 
254     void write(final DataOutput outthrows IOException {
255         out.writeDouble(_minLatitude.toDegrees());
256         out.writeDouble(_minLongitude.toDegrees());
257         out.writeDouble(_maxLatitude.toDegrees());
258         out.writeDouble(_maxLongitude.toDegrees());
259     }
260 
261     static Bounds read(final DataInput inthrows IOException {
262         return Bounds.of(
263             in.readDouble(), in.readDouble(),
264             in.readDouble(), in.readDouble()
265         );
266     }
267 
268     /* *************************************************************************
269      *  XML stream object serialization
270      * ************************************************************************/
271 
272     static XMLWriter<Bounds>
273     writer(final Function<? super Number, String> formatter) {
274         return XMLWriter.elem("bounds",
275             XMLWriter.attr("minlat").map(b -> formatter.apply(b.getMinLatitude())),
276             XMLWriter.attr("minlon").map(b -> formatter.apply(b.getMinLongitude())),
277             XMLWriter.attr("maxlat").map(b -> formatter.apply(b.getMaxLatitude())),
278             XMLWriter.attr("maxlon").map(b -> formatter.apply(b.getMaxLongitude()))
279         );
280     }
281 
282     static final XMLReader<Bounds> READER = XMLReader.elem(
283         v -> Bounds.of(
284             (Latitude)v[0](Longitude)v[1],
285             (Latitude)v[2](Longitude)v[3]
286         ),
287         "bounds",
288         XMLReader.attr("minlat").map(Latitude::parse),
289         XMLReader.attr("minlon").map(Longitude::parse),
290         XMLReader.attr("maxlat").map(Latitude::parse),
291         XMLReader.attr("maxlon").map(Longitude::parse)
292     );
293 
294 }