source: josm/trunk/src/org/openstreetmap/josm/data/Bounds.java

Last change on this file was 19307, checked in by taylor.smock, 17 months ago

Fix most new PMD issues

It would be better to use the newer switch syntax introduced in Java 14 (JEP 361),
but we currently target Java 11+. When we move to Java 17, this should be
reverted and the newer switch syntax should be used.

  • Property svn:eol-style set to native
File size: 20.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.geom.Rectangle2D;
7import java.text.DecimalFormat;
8import java.text.MessageFormat;
9import java.util.Arrays;
10
11import org.openstreetmap.josm.data.coor.ILatLon;
12import org.openstreetmap.josm.data.coor.LatLon;
13import org.openstreetmap.josm.data.osm.BBox;
14import org.openstreetmap.josm.tools.CheckParameterUtil;
15
16/**
17 * This is a simple data class for "rectangular" areas of the world, given in
18 * lat/lon min/max values. The values are {@linkplain LatLon#roundToOsmPrecision(double) rounded to server precision}
19 *
20 * @author imi
21 *
22 * @see BBox to represent invalid areas.
23 */
24public class Bounds implements IBounds {
25 /**
26 * The minimum and maximum coordinates.
27 */
28 private double minLat, minLon, maxLat, maxLon;
29
30 /**
31 * Gets the point that has both the minimal lat and lon coordinate
32 * @return The point
33 */
34 @Override
35 public LatLon getMin() {
36 return new LatLon(minLat, minLon);
37 }
38
39 /**
40 * Returns min latitude of bounds. Efficient shortcut for {@code getMin().lat()}.
41 *
42 * @return min latitude of bounds.
43 * @since 6203
44 */
45 @Override
46 public double getMinLat() {
47 return minLat;
48 }
49
50 /**
51 * Returns min longitude of bounds. Efficient shortcut for {@code getMin().lon()}.
52 *
53 * @return min longitude of bounds.
54 * @since 6203
55 */
56 @Override
57 public double getMinLon() {
58 return minLon;
59 }
60
61 /**
62 * Gets the point that has both the maximum lat and lon coordinate
63 * @return The point
64 */
65 @Override
66 public LatLon getMax() {
67 return new LatLon(maxLat, maxLon);
68 }
69
70 /**
71 * Returns max latitude of bounds. Efficient shortcut for {@code getMax().lat()}.
72 *
73 * @return max latitude of bounds.
74 * @since 6203
75 */
76 @Override
77 public double getMaxLat() {
78 return maxLat;
79 }
80
81 /**
82 * Returns max longitude of bounds. Efficient shortcut for {@code getMax().lon()}.
83 *
84 * @return max longitude of bounds.
85 * @since 6203
86 */
87 @Override
88 public double getMaxLon() {
89 return maxLon;
90 }
91
92 /**
93 * The method used by the {@link Bounds#Bounds(String, String, ParseMethod)} constructor
94 */
95 public enum ParseMethod {
96 /**
97 * Order: minlat, minlon, maxlat, maxlon
98 */
99 MINLAT_MINLON_MAXLAT_MAXLON,
100 /**
101 * Order: left, bottom, right, top
102 */
103 LEFT_BOTTOM_RIGHT_TOP
104 }
105
106 /**
107 * Construct bounds out of two points. Coords will be rounded.
108 * @param min min lat/lon
109 * @param max max lat/lon
110 */
111 public Bounds(LatLon min, LatLon max) {
112 this(min.lat(), min.lon(), max.lat(), max.lon());
113 }
114
115 /**
116 * Constructs bounds out of two points.
117 * @param min min lat/lon
118 * @param max max lat/lon
119 * @param roundToOsmPrecision defines if lat/lon will be rounded
120 */
121 public Bounds(LatLon min, LatLon max, boolean roundToOsmPrecision) {
122 this(min.lat(), min.lon(), max.lat(), max.lon(), roundToOsmPrecision);
123 }
124
125 /**
126 * Constructs bounds out a single point. Coords will be rounded.
127 * @param b lat/lon
128 */
129 public Bounds(LatLon b) {
130 this(b, true);
131 }
132
133 /**
134 * Single point Bounds defined by lat/lon {@code b}.
135 * Coordinates will be rounded to osm precision if {@code roundToOsmPrecision} is true.
136 *
137 * @param b lat/lon of given point.
138 * @param roundToOsmPrecision defines if lat/lon will be rounded.
139 */
140 public Bounds(LatLon b, boolean roundToOsmPrecision) {
141 this(b.lat(), b.lon(), roundToOsmPrecision);
142 }
143
144 /**
145 * Single point Bounds defined by point [lat,lon].
146 * Coordinates will be rounded to osm precision if {@code roundToOsmPrecision} is true.
147 *
148 * @param lat latitude of given point.
149 * @param lon longitude of given point.
150 * @param roundToOsmPrecision defines if lat/lon will be rounded.
151 * @since 6203
152 */
153 public Bounds(double lat, double lon, boolean roundToOsmPrecision) {
154 // Do not call this(b, b) to avoid GPX performance issue (see #7028) until roundToOsmPrecision() is improved
155 if (roundToOsmPrecision) {
156 this.minLat = LatLon.roundToOsmPrecision(lat);
157 this.minLon = LatLon.roundToOsmPrecision(lon);
158 } else {
159 this.minLat = lat;
160 this.minLon = lon;
161 }
162 this.maxLat = this.minLat;
163 this.maxLon = this.minLon;
164 }
165
166 /**
167 * Constructs bounds out of two points. Coords will be rounded.
168 * @param minLat min lat
169 * @param minLon min lon
170 * @param maxLat max lat
171 * @param maxLon max lon
172 */
173 public Bounds(double minLat, double minLon, double maxLat, double maxLon) {
174 this(minLat, minLon, maxLat, maxLon, true);
175 }
176
177 /**
178 * Constructs bounds out of two points.
179 * @param minLat min lat
180 * @param minLon min lon
181 * @param maxLat max lat
182 * @param maxLon max lon
183 * @param roundToOsmPrecision defines if lat/lon will be rounded
184 */
185 public Bounds(double minLat, double minLon, double maxLat, double maxLon, boolean roundToOsmPrecision) {
186 if (roundToOsmPrecision) {
187 this.minLat = LatLon.roundToOsmPrecision(minLat);
188 this.minLon = LatLon.roundToOsmPrecision(minLon);
189 this.maxLat = LatLon.roundToOsmPrecision(maxLat);
190 this.maxLon = LatLon.roundToOsmPrecision(maxLon);
191 } else {
192 this.minLat = minLat;
193 this.minLon = minLon;
194 this.maxLat = maxLat;
195 this.maxLon = maxLon;
196 }
197 }
198
199 /**
200 * Constructs bounds out of two points. Coords will be rounded.
201 * @param coords exactly 4 values: min lat, min lon, max lat, max lon
202 * @throws IllegalArgumentException if coords does not contain 4 double values
203 */
204 public Bounds(double... coords) {
205 this(coords, true);
206 }
207
208 /**
209 * Constructs bounds out of two points.
210 * @param coords exactly 4 values: min lat, min lon, max lat, max lon
211 * @param roundToOsmPrecision defines if lat/lon will be rounded
212 * @throws IllegalArgumentException if coords does not contain 4 double values
213 */
214 public Bounds(double[] coords, boolean roundToOsmPrecision) {
215 CheckParameterUtil.ensureParameterNotNull(coords, "coords");
216 if (coords.length != 4)
217 throw new IllegalArgumentException(MessageFormat.format("Expected array of length 4, got {0}", coords.length));
218 if (roundToOsmPrecision) {
219 this.minLat = LatLon.roundToOsmPrecision(coords[0]);
220 this.minLon = LatLon.roundToOsmPrecision(coords[1]);
221 this.maxLat = LatLon.roundToOsmPrecision(coords[2]);
222 this.maxLon = LatLon.roundToOsmPrecision(coords[3]);
223 } else {
224 this.minLat = coords[0];
225 this.minLon = coords[1];
226 this.maxLat = coords[2];
227 this.maxLon = coords[3];
228 }
229 }
230
231 /**
232 * Parse the bounds in order {@link ParseMethod#MINLAT_MINLON_MAXLAT_MAXLON}
233 * @param asString The string
234 * @param separator The separation regex
235 */
236 public Bounds(String asString, String separator) {
237 this(asString, separator, ParseMethod.MINLAT_MINLON_MAXLAT_MAXLON);
238 }
239
240 /**
241 * Parse the bounds from a given string and round to OSM precision
242 * @param asString The string
243 * @param separator The separation regex
244 * @param parseMethod The order of the numbers
245 */
246 public Bounds(String asString, String separator, ParseMethod parseMethod) {
247 this(asString, separator, parseMethod, true);
248 }
249
250 /**
251 * Parse the bounds from a given string
252 * @param asString The string
253 * @param separator The separation regex
254 * @param parseMethod The order of the numbers
255 * @param roundToOsmPrecision Whether to round to OSM precision
256 */
257 public Bounds(String asString, String separator, ParseMethod parseMethod, boolean roundToOsmPrecision) {
258 CheckParameterUtil.ensureParameterNotNull(asString, "asString");
259 String[] components = asString.split(separator, -1);
260 if (components.length != 4)
261 throw new IllegalArgumentException(
262 MessageFormat.format("Exactly four doubles expected in string, got {0}: {1}", components.length, asString));
263 double[] values = new double[4];
264 for (int i = 0; i < 4; i++) {
265 try {
266 values[i] = Double.parseDouble(components[i]);
267 } catch (NumberFormatException e) {
268 throw new IllegalArgumentException(MessageFormat.format("Illegal double value ''{0}''", components[i]), e);
269 }
270 }
271
272 switch (parseMethod) {
273 case LEFT_BOTTOM_RIGHT_TOP:
274 this.minLat = initLat(values[1], roundToOsmPrecision);
275 this.minLon = initLon(values[0], roundToOsmPrecision);
276 this.maxLat = initLat(values[3], roundToOsmPrecision);
277 this.maxLon = initLon(values[2], roundToOsmPrecision);
278 break;
279 case MINLAT_MINLON_MAXLAT_MAXLON:
280 this.minLat = initLat(values[0], roundToOsmPrecision);
281 this.minLon = initLon(values[1], roundToOsmPrecision);
282 this.maxLat = initLat(values[2], roundToOsmPrecision);
283 this.maxLon = initLon(values[3], roundToOsmPrecision);
284 }
285 }
286
287 protected static double initLat(double value, boolean roundToOsmPrecision) {
288 if (!LatLon.isValidLat(value))
289 throw new IllegalArgumentException(tr("Illegal latitude value ''{0}''", value));
290 return roundToOsmPrecision ? LatLon.roundToOsmPrecision(value) : value;
291 }
292
293 protected static double initLon(double value, boolean roundToOsmPrecision) {
294 if (!LatLon.isValidLon(value))
295 throw new IllegalArgumentException(tr("Illegal longitude value ''{0}''", value));
296 return roundToOsmPrecision ? LatLon.roundToOsmPrecision(value) : value;
297 }
298
299 /**
300 * Creates new {@code Bounds} from an existing one.
301 * @param other The bounds to copy
302 */
303 public Bounds(final Bounds other) {
304 this(other.minLat, other.minLon, other.maxLat, other.maxLon);
305 }
306
307 /**
308 * Creates new {@code Bounds} from a rectangle.
309 * @param rect The rectangle
310 */
311 public Bounds(Rectangle2D rect) {
312 this(rect.getMinY(), rect.getMinX(), rect.getMaxY(), rect.getMaxX());
313 }
314
315 /**
316 * Creates new bounds around a coordinate pair <code>center</code>. The
317 * new bounds shall have an extension in latitude direction of <code>latExtent</code>,
318 * and in longitude direction of <code>lonExtent</code>.
319 *
320 * @param center the center coordinate pair. Must not be null.
321 * @param latExtent the latitude extent. &gt; 0 required.
322 * @param lonExtent the longitude extent. &gt; 0 required.
323 * @throws IllegalArgumentException if center is null
324 * @throws IllegalArgumentException if latExtent &lt;= 0
325 * @throws IllegalArgumentException if lonExtent &lt;= 0
326 */
327 public Bounds(LatLon center, double latExtent, double lonExtent) {
328 CheckParameterUtil.ensureParameterNotNull(center, "center");
329 if (latExtent <= 0.0)
330 throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0.0 expected, got {1}", "latExtent", latExtent));
331 if (lonExtent <= 0.0)
332 throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0.0 expected, got {1}", "lonExtent", lonExtent));
333
334 this.minLat = LatLon.roundToOsmPrecision(LatLon.toIntervalLat(center.lat() - latExtent / 2));
335 this.minLon = LatLon.roundToOsmPrecision(LatLon.toIntervalLon(center.lon() - lonExtent / 2));
336 this.maxLat = LatLon.roundToOsmPrecision(LatLon.toIntervalLat(center.lat() + latExtent / 2));
337 this.maxLon = LatLon.roundToOsmPrecision(LatLon.toIntervalLon(center.lon() + lonExtent / 2));
338 }
339
340 /**
341 * Creates BBox with same coordinates.
342 *
343 * @return BBox with same coordinates.
344 * @since 6203
345 */
346 public BBox toBBox() {
347 return new BBox(minLon, minLat, maxLon, maxLat);
348 }
349
350 @Override
351 public String toString() {
352 return "Bounds["+minLat+','+minLon+','+maxLat+','+maxLon+']';
353 }
354
355 /**
356 * Converts this bounds to a human readable short string
357 * @param format The number format to use
358 * @return The string
359 */
360 public String toShortString(DecimalFormat format) {
361 return format.format(minLat) + ' '
362 + format.format(minLon) + " / "
363 + format.format(maxLat) + ' '
364 + format.format(maxLon);
365 }
366
367 /**
368 * Returns center of the bounding box.
369 * @return Center of the bounding box.
370 */
371 @Override
372 public LatLon getCenter() {
373 if (crosses180thMeridian()) {
374 double lat = (minLat + maxLat) / 2;
375 double lon = (minLon + maxLon - 360.0) / 2;
376 if (lon < -180.0) {
377 lon += 360.0;
378 }
379 return new LatLon(lat, lon);
380 } else {
381 return new LatLon((minLat + maxLat) / 2, (minLon + maxLon) / 2);
382 }
383 }
384
385 /**
386 * Extend the bounds if necessary to include the given point.
387 * @param ll The point to include into these bounds
388 */
389 public void extend(LatLon ll) {
390 extend(ll.lat(), ll.lon());
391 }
392
393 /**
394 * Extend the bounds if necessary to include the given point [lat,lon].
395 * Good to use if you know coordinates to avoid creation of LatLon object.
396 * @param lat Latitude of point to include into these bounds
397 * @param lon Longitude of point to include into these bounds
398 * @since 6203
399 */
400 public void extend(final double lat, final double lon) {
401 if (lat < minLat) {
402 minLat = LatLon.roundToOsmPrecision(lat);
403 }
404 if (lat > maxLat) {
405 maxLat = LatLon.roundToOsmPrecision(lat);
406 }
407 if (crosses180thMeridian()) {
408 if (lon > maxLon && lon < minLon) {
409 if (Math.abs(lon - minLon) <= Math.abs(lon - maxLon)) {
410 minLon = LatLon.roundToOsmPrecision(lon);
411 } else {
412 maxLon = LatLon.roundToOsmPrecision(lon);
413 }
414 }
415 } else {
416 if (lon < minLon) {
417 minLon = LatLon.roundToOsmPrecision(lon);
418 }
419 if (lon > maxLon) {
420 maxLon = LatLon.roundToOsmPrecision(lon);
421 }
422 }
423 }
424
425 /**
426 * Extends this bounds to enclose an other bounding box
427 * @param b The other bounds to enclose
428 */
429 public void extend(Bounds b) {
430 extend(b.minLat, b.minLon);
431 extend(b.maxLat, b.maxLon);
432 }
433
434 /**
435 * Determines if the given point {@code ll} is within these bounds.
436 * <p>
437 * Points with unknown coordinates are always outside the coordinates.
438 * @param ll The lat/lon to check
439 * @return {@code true} if {@code ll} is within these bounds, {@code false} otherwise
440 */
441 public boolean contains(LatLon ll) {
442 // binary compatibility
443 return contains((ILatLon) ll);
444 }
445
446 /**
447 * Determines if the given point {@code ll} is within these bounds.
448 * <p>
449 * Points with unknown coordinates are always outside the coordinates.
450 * @param ll The lat/lon to check
451 * @return {@code true} if {@code ll} is within these bounds, {@code false} otherwise
452 * @since 12161
453 */
454 @Override
455 public boolean contains(ILatLon ll) {
456 if (!ll.isLatLonKnown()) {
457 return false;
458 }
459 if (ll.lat() < minLat || ll.lat() > maxLat)
460 return false;
461 if (crosses180thMeridian()) {
462 if (ll.lon() > maxLon && ll.lon() < minLon)
463 return false;
464 } else {
465 if (ll.lon() < minLon || ll.lon() > maxLon)
466 return false;
467 }
468 return true;
469 }
470
471 private static boolean intersectsLonCrossing(IBounds crossing, IBounds notCrossing) {
472 return notCrossing.getMinLon() <= crossing.getMaxLon() || notCrossing.getMaxLon() >= crossing.getMinLon();
473 }
474
475 /**
476 * The two bounds intersect? Compared to java Shape.intersects, if does not use
477 * the interior but the closure. ("&gt;=" instead of "&gt;")
478 * @param b other bounds
479 * @return {@code true} if the two bounds intersect
480 */
481 public boolean intersects(Bounds b) {
482 return intersects((IBounds) b);
483 }
484
485 @Override
486 public boolean intersects(IBounds b) {
487 if (b.getMaxLat() < minLat || b.getMinLat() > maxLat)
488 return false;
489
490 if (crosses180thMeridian() && !b.crosses180thMeridian()) {
491 return intersectsLonCrossing(this, b);
492 } else if (!crosses180thMeridian() && b.crosses180thMeridian()) {
493 return intersectsLonCrossing(b, this);
494 } else if (crosses180thMeridian() && b.crosses180thMeridian()) {
495 return true;
496 } else {
497 return b.getMaxLon() >= minLon && b.getMinLon() <= maxLon;
498 }
499 }
500
501 /**
502 * Determines if this Bounds object crosses the 180th Meridian.
503 * See <a href="http://wiki.openstreetmap.org/wiki/180th_meridian">180th Meridian</a>.
504 * @return true if this Bounds object crosses the 180th Meridian.
505 */
506 @Override
507 public boolean crosses180thMeridian() {
508 return this.minLon > this.maxLon;
509 }
510
511 /**
512 * Converts the lat/lon bounding box to an object of type Rectangle2D.Double
513 * @return the bounding box to Rectangle2D.Double
514 */
515 public Rectangle2D.Double asRect() {
516 return new Rectangle2D.Double(minLon, minLat, getWidth(), getHeight());
517 }
518
519 /**
520 * Returns the bounds width.
521 * @return the bounds width
522 * @since 14521
523 */
524 @Override
525 public double getHeight() {
526 return maxLat-minLat;
527 }
528
529 /**
530 * Returns the bounds width.
531 * @return the bounds width
532 * @since 14521
533 */
534 @Override
535 public double getWidth() {
536 return maxLon-minLon + (crosses180thMeridian() ? 360.0 : 0.0);
537 }
538
539 /**
540 * Gets the area of this bounds (in lat/lon space)
541 * @return The area
542 */
543 @Override
544 public double getArea() {
545 return getWidth() * getHeight();
546 }
547
548 /**
549 * Encodes this as a string so that it may be parsed using the {@link ParseMethod#MINLAT_MINLON_MAXLAT_MAXLON} order
550 * @param separator The separator
551 * @return The string encoded bounds
552 */
553 public String encodeAsString(String separator) {
554 return minLat + separator + minLon + separator +
555 maxLat + separator + maxLon;
556 }
557
558 /**
559 * <p>Replies true, if this bounds are <em>collapsed</em>, i.e. if the min
560 * and the max corner are equal.</p>
561 *
562 * @return true, if this bounds are <em>collapsed</em>
563 */
564 public boolean isCollapsed() {
565 return Double.doubleToLongBits(minLat) == Double.doubleToLongBits(maxLat)
566 && Double.doubleToLongBits(minLon) == Double.doubleToLongBits(maxLon);
567 }
568
569 /**
570 * Determines if these bounds are out of the world.
571 * @return true if lat outside of range [-90,90] or lon outside of range [-180,180]
572 */
573 public boolean isOutOfTheWorld() {
574 return
575 !LatLon.isValidLat(minLat) ||
576 !LatLon.isValidLat(maxLat) ||
577 !LatLon.isValidLon(minLon) ||
578 !LatLon.isValidLon(maxLon);
579 }
580
581 /**
582 * Clamp the bounds to be inside the world.
583 */
584 public void normalize() {
585 minLat = LatLon.toIntervalLat(minLat);
586 maxLat = LatLon.toIntervalLat(maxLat);
587 minLon = LatLon.toIntervalLon(minLon);
588 maxLon = LatLon.toIntervalLon(maxLon);
589 }
590
591 @Override
592 public int hashCode() {
593 return Arrays.hashCode(new double[] {minLat, minLon, maxLat, maxLon});
594 }
595
596 @Override
597 public boolean equals(Object obj) {
598 if (this == obj) return true;
599 if (obj == null || getClass() != obj.getClass()) return false;
600 Bounds bounds = (Bounds) obj;
601 return Double.compare(bounds.minLat, minLat) == 0 &&
602 Double.compare(bounds.minLon, minLon) == 0 &&
603 Double.compare(bounds.maxLat, maxLat) == 0 &&
604 Double.compare(bounds.maxLon, maxLon) == 0;
605 }
606}
Note: See TracBrowser for help on using the repository browser.