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 out) throws 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 in) throws 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 }
|