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

Last change on this file since 11117 was 10910, checked in by Don-vip, 8 years ago

improve javadoc, unit tests, reduce visibility of some public fields

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