001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.geom.Rectangle2D; 007import java.text.DecimalFormat; 008import java.text.MessageFormat; 009import java.util.Objects; 010 011import org.openstreetmap.josm.data.coor.LatLon; 012import org.openstreetmap.josm.data.osm.BBox; 013import org.openstreetmap.josm.tools.CheckParameterUtil; 014 015/** 016 * This is a simple data class for "rectangular" areas of the world, given in 017 * lat/lon min/max values. The values are rounded to LatLon.OSM_SERVER_PRECISION 018 * 019 * @author imi 020 */ 021public class Bounds { 022 /** 023 * The minimum and maximum coordinates. 024 */ 025 private double minLat, minLon, maxLat, maxLon; 026 027 public LatLon getMin() { 028 return new LatLon(minLat, minLon); 029 } 030 031 /** 032 * Returns min latitude of bounds. Efficient shortcut for {@code getMin().lat()}. 033 * 034 * @return min latitude of bounds. 035 * @since 6203 036 */ 037 public double getMinLat() { 038 return minLat; 039 } 040 041 /** 042 * Returns min longitude of bounds. Efficient shortcut for {@code getMin().lon()}. 043 * 044 * @return min longitude of bounds. 045 * @since 6203 046 */ 047 public double getMinLon() { 048 return minLon; 049 } 050 051 public LatLon getMax() { 052 return new LatLon(maxLat, maxLon); 053 } 054 055 /** 056 * Returns max latitude of bounds. Efficient shortcut for {@code getMax().lat()}. 057 * 058 * @return max latitude of bounds. 059 * @since 6203 060 */ 061 public double getMaxLat() { 062 return maxLat; 063 } 064 065 /** 066 * Returns max longitude of bounds. Efficient shortcut for {@code getMax().lon()}. 067 * 068 * @return max longitude of bounds. 069 * @since 6203 070 */ 071 public double getMaxLon() { 072 return maxLon; 073 } 074 075 public enum ParseMethod { 076 MINLAT_MINLON_MAXLAT_MAXLON, 077 LEFT_BOTTOM_RIGHT_TOP 078 } 079 080 /** 081 * Construct bounds out of two points. Coords will be rounded. 082 * @param min min lat/lon 083 * @param max max lat/lon 084 */ 085 public Bounds(LatLon min, LatLon max) { 086 this(min.lat(), min.lon(), max.lat(), max.lon()); 087 } 088 089 public Bounds(LatLon min, LatLon max, boolean roundToOsmPrecision) { 090 this(min.lat(), min.lon(), max.lat(), max.lon(), roundToOsmPrecision); 091 } 092 093 /** 094 * Constructs bounds out a single point. 095 * @param b lat/lon 096 */ 097 public Bounds(LatLon b) { 098 this(b, true); 099 } 100 101 /** 102 * Single point Bounds defined by lat/lon {@code b}. 103 * Coordinates will be rounded to osm precision if {@code roundToOsmPrecision} is true. 104 * 105 * @param b lat/lon of given point. 106 * @param roundToOsmPrecision defines if lat/lon will be rounded. 107 */ 108 public Bounds(LatLon b, boolean roundToOsmPrecision) { 109 this(b.lat(), b.lon(), roundToOsmPrecision); 110 } 111 112 /** 113 * Single point Bounds defined by point [lat,lon]. 114 * Coordinates will be rounded to osm precision if {@code roundToOsmPrecision} is true. 115 * 116 * @param lat latitude of given point. 117 * @param lon longitude of given point. 118 * @param roundToOsmPrecision defines if lat/lon will be rounded. 119 * @since 6203 120 */ 121 public Bounds(double lat, double lon, boolean roundToOsmPrecision) { 122 // Do not call this(b, b) to avoid GPX performance issue (see #7028) until roundToOsmPrecision() is improved 123 if (roundToOsmPrecision) { 124 this.minLat = LatLon.roundToOsmPrecision(lat); 125 this.minLon = LatLon.roundToOsmPrecision(lon); 126 } else { 127 this.minLat = lat; 128 this.minLon = lon; 129 } 130 this.maxLat = this.minLat; 131 this.maxLon = this.minLon; 132 } 133 134 public Bounds(double minlat, double minlon, double maxlat, double maxlon) { 135 this(minlat, minlon, maxlat, maxlon, true); 136 } 137 138 public Bounds(double minlat, double minlon, double maxlat, double maxlon, boolean roundToOsmPrecision) { 139 if (roundToOsmPrecision) { 140 this.minLat = LatLon.roundToOsmPrecision(minlat); 141 this.minLon = LatLon.roundToOsmPrecision(minlon); 142 this.maxLat = LatLon.roundToOsmPrecision(maxlat); 143 this.maxLon = LatLon.roundToOsmPrecision(maxlon); 144 } else { 145 this.minLat = minlat; 146 this.minLon = minlon; 147 this.maxLat = maxlat; 148 this.maxLon = maxlon; 149 } 150 } 151 152 public Bounds(double[] coords) { 153 this(coords, true); 154 } 155 156 public Bounds(double[] coords, boolean roundToOsmPrecision) { 157 CheckParameterUtil.ensureParameterNotNull(coords, "coords"); 158 if (coords.length != 4) 159 throw new IllegalArgumentException(MessageFormat.format("Expected array of length 4, got {0}", coords.length)); 160 if (roundToOsmPrecision) { 161 this.minLat = LatLon.roundToOsmPrecision(coords[0]); 162 this.minLon = LatLon.roundToOsmPrecision(coords[1]); 163 this.maxLat = LatLon.roundToOsmPrecision(coords[2]); 164 this.maxLon = LatLon.roundToOsmPrecision(coords[3]); 165 } else { 166 this.minLat = coords[0]; 167 this.minLon = coords[1]; 168 this.maxLat = coords[2]; 169 this.maxLon = coords[3]; 170 } 171 } 172 173 public Bounds(String asString, String separator) { 174 this(asString, separator, ParseMethod.MINLAT_MINLON_MAXLAT_MAXLON); 175 } 176 177 public Bounds(String asString, String separator, ParseMethod parseMethod) { 178 this(asString, separator, parseMethod, true); 179 } 180 181 public Bounds(String asString, String separator, ParseMethod parseMethod, boolean roundToOsmPrecision) { 182 CheckParameterUtil.ensureParameterNotNull(asString, "asString"); 183 String[] components = asString.split(separator); 184 if (components.length != 4) 185 throw new IllegalArgumentException( 186 MessageFormat.format("Exactly four doubles expected in string, got {0}: {1}", components.length, asString)); 187 double[] values = new double[4]; 188 for (int i = 0; i < 4; i++) { 189 try { 190 values[i] = Double.parseDouble(components[i]); 191 } catch (NumberFormatException e) { 192 throw new IllegalArgumentException(MessageFormat.format("Illegal double value ''{0}''", components[i]), e); 193 } 194 } 195 196 switch (parseMethod) { 197 case LEFT_BOTTOM_RIGHT_TOP: 198 this.minLat = initLat(values[1], roundToOsmPrecision); 199 this.minLon = initLon(values[0], roundToOsmPrecision); 200 this.maxLat = initLat(values[3], roundToOsmPrecision); 201 this.maxLon = initLon(values[2], roundToOsmPrecision); 202 break; 203 case MINLAT_MINLON_MAXLAT_MAXLON: 204 default: 205 this.minLat = initLat(values[0], roundToOsmPrecision); 206 this.minLon = initLon(values[1], roundToOsmPrecision); 207 this.maxLat = initLat(values[2], roundToOsmPrecision); 208 this.maxLon = initLon(values[3], roundToOsmPrecision); 209 } 210 } 211 212 protected static double initLat(double value, boolean roundToOsmPrecision) { 213 if (!LatLon.isValidLat(value)) 214 throw new IllegalArgumentException(tr("Illegal latitude value ''{0}''", value)); 215 return roundToOsmPrecision ? LatLon.roundToOsmPrecision(value) : value; 216 } 217 218 protected static double initLon(double value, boolean roundToOsmPrecision) { 219 if (!LatLon.isValidLon(value)) 220 throw new IllegalArgumentException(tr("Illegal longitude value ''{0}''", value)); 221 return roundToOsmPrecision ? LatLon.roundToOsmPrecision(value) : value; 222 } 223 224 /** 225 * Creates new {@code Bounds} from an existing one. 226 * @param other The bounds to copy 227 */ 228 public Bounds(final Bounds other) { 229 this(other.minLat, other.minLon, other.maxLat, other.maxLon); 230 } 231 232 public Bounds(Rectangle2D rect) { 233 this(rect.getMinY(), rect.getMinX(), rect.getMaxY(), rect.getMaxX()); 234 } 235 236 /** 237 * Creates new bounds around a coordinate pair <code>center</code>. The 238 * new bounds shall have an extension in latitude direction of <code>latExtent</code>, 239 * and in longitude direction of <code>lonExtent</code>. 240 * 241 * @param center the center coordinate pair. Must not be null. 242 * @param latExtent the latitude extent. > 0 required. 243 * @param lonExtent the longitude extent. > 0 required. 244 * @throws IllegalArgumentException if center is null 245 * @throws IllegalArgumentException if latExtent <= 0 246 * @throws IllegalArgumentException if lonExtent <= 0 247 */ 248 public Bounds(LatLon center, double latExtent, double lonExtent) { 249 CheckParameterUtil.ensureParameterNotNull(center, "center"); 250 if (latExtent <= 0.0) 251 throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0.0 expected, got {1}", "latExtent", latExtent)); 252 if (lonExtent <= 0.0) 253 throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0.0 expected, got {1}", "lonExtent", lonExtent)); 254 255 this.minLat = LatLon.roundToOsmPrecision(LatLon.toIntervalLat(center.lat() - latExtent / 2)); 256 this.minLon = LatLon.roundToOsmPrecision(LatLon.toIntervalLon(center.lon() - lonExtent / 2)); 257 this.maxLat = LatLon.roundToOsmPrecision(LatLon.toIntervalLat(center.lat() + latExtent / 2)); 258 this.maxLon = LatLon.roundToOsmPrecision(LatLon.toIntervalLon(center.lon() + lonExtent / 2)); 259 } 260 261 /** 262 * Creates BBox with same coordinates. 263 * 264 * @return BBox with same coordinates. 265 * @since 6203 266 */ 267 public BBox toBBox() { 268 return new BBox(minLon, minLat, maxLon, maxLat); 269 } 270 271 @Override 272 public String toString() { 273 return "Bounds["+minLat+','+minLon+','+maxLat+','+maxLon+']'; 274 } 275 276 public String toShortString(DecimalFormat format) { 277 return format.format(minLat) + ' ' 278 + format.format(minLon) + " / " 279 + format.format(maxLat) + ' ' 280 + format.format(maxLon); 281 } 282 283 /** 284 * @return Center of the bounding box. 285 */ 286 public LatLon getCenter() { 287 if (crosses180thMeridian()) { 288 double lat = (minLat + maxLat) / 2; 289 double lon = (minLon + maxLon - 360.0) / 2; 290 if (lon < -180.0) { 291 lon += 360.0; 292 } 293 return new LatLon(lat, lon); 294 } else { 295 return new LatLon((minLat + maxLat) / 2, (minLon + maxLon) / 2); 296 } 297 } 298 299 /** 300 * Extend the bounds if necessary to include the given point. 301 * @param ll The point to include into these bounds 302 */ 303 public void extend(LatLon ll) { 304 extend(ll.lat(), ll.lon()); 305 } 306 307 /** 308 * Extend the bounds if necessary to include the given point [lat,lon]. 309 * Good to use if you know coordinates to avoid creation of LatLon object. 310 * @param lat Latitude of point to include into these bounds 311 * @param lon Longitude of point to include into these bounds 312 * @since 6203 313 */ 314 public void extend(final double lat, final double lon) { 315 if (lat < minLat) { 316 minLat = LatLon.roundToOsmPrecision(lat); 317 } 318 if (lat > maxLat) { 319 maxLat = LatLon.roundToOsmPrecision(lat); 320 } 321 if (crosses180thMeridian()) { 322 if (lon > maxLon && lon < minLon) { 323 if (Math.abs(lon - minLon) <= Math.abs(lon - maxLon)) { 324 minLon = LatLon.roundToOsmPrecision(lon); 325 } else { 326 maxLon = LatLon.roundToOsmPrecision(lon); 327 } 328 } 329 } else { 330 if (lon < minLon) { 331 minLon = LatLon.roundToOsmPrecision(lon); 332 } 333 if (lon > maxLon) { 334 maxLon = LatLon.roundToOsmPrecision(lon); 335 } 336 } 337 } 338 339 public void extend(Bounds b) { 340 extend(b.minLat, b.minLon); 341 extend(b.maxLat, b.maxLon); 342 } 343 344 /** 345 * Determines if the given point {@code ll} is within these bounds. 346 * @param ll The lat/lon to check 347 * @return {@code true} if {@code ll} is within these bounds, {@code false} otherwise 348 */ 349 public boolean contains(LatLon ll) { 350 if (ll.lat() < minLat || ll.lat() > maxLat) 351 return false; 352 if (crosses180thMeridian()) { 353 if (ll.lon() > maxLon && ll.lon() < minLon) 354 return false; 355 } else { 356 if (ll.lon() < minLon || ll.lon() > maxLon) 357 return false; 358 } 359 return true; 360 } 361 362 private static boolean intersectsLonCrossing(Bounds crossing, Bounds notCrossing) { 363 return notCrossing.minLon <= crossing.maxLon || notCrossing.maxLon >= crossing.minLon; 364 } 365 366 /** 367 * The two bounds intersect? Compared to java Shape.intersects, if does not use 368 * the interior but the closure. (">=" instead of ">") 369 * @param b other bounds 370 * @return {@code true} if the two bounds intersect 371 */ 372 public boolean intersects(Bounds b) { 373 if (b.maxLat < minLat || b.minLat > maxLat) 374 return false; 375 376 if (crosses180thMeridian() && !b.crosses180thMeridian()) { 377 return intersectsLonCrossing(this, b); 378 } else if (!crosses180thMeridian() && b.crosses180thMeridian()) { 379 return intersectsLonCrossing(b, this); 380 } else if (crosses180thMeridian() && b.crosses180thMeridian()) { 381 return true; 382 } else { 383 return b.maxLon >= minLon && b.minLon <= maxLon; 384 } 385 } 386 387 /** 388 * Determines if this Bounds object crosses the 180th Meridian. 389 * See http://wiki.openstreetmap.org/wiki/180th_meridian 390 * @return true if this Bounds object crosses the 180th Meridian. 391 */ 392 public boolean crosses180thMeridian() { 393 return this.minLon > this.maxLon; 394 } 395 396 /** 397 * Converts the lat/lon bounding box to an object of type Rectangle2D.Double 398 * @return the bounding box to Rectangle2D.Double 399 */ 400 public Rectangle2D.Double asRect() { 401 double w = maxLon-minLon + (crosses180thMeridian() ? 360.0 : 0.0); 402 return new Rectangle2D.Double(minLon, minLat, w, maxLat-minLat); 403 } 404 405 public double getArea() { 406 double w = maxLon-minLon + (crosses180thMeridian() ? 360.0 : 0.0); 407 return w * (maxLat - minLat); 408 } 409 410 public String encodeAsString(String separator) { 411 StringBuilder sb = new StringBuilder(); 412 sb.append(minLat).append(separator).append(minLon) 413 .append(separator).append(maxLat).append(separator) 414 .append(maxLon); 415 return sb.toString(); 416 } 417 418 /** 419 * <p>Replies true, if this bounds are <em>collapsed</em>, i.e. if the min 420 * and the max corner are equal.</p> 421 * 422 * @return true, if this bounds are <em>collapsed</em> 423 */ 424 public boolean isCollapsed() { 425 return Double.doubleToLongBits(minLat) == Double.doubleToLongBits(maxLat) 426 && Double.doubleToLongBits(minLon) == Double.doubleToLongBits(maxLon); 427 } 428 429 public boolean isOutOfTheWorld() { 430 return 431 minLat < -90 || minLat > 90 || 432 maxLat < -90 || maxLat > 90 || 433 minLon < -180 || minLon > 180 || 434 maxLon < -180 || maxLon > 180; 435 } 436 437 public void normalize() { 438 minLat = LatLon.toIntervalLat(minLat); 439 maxLat = LatLon.toIntervalLat(maxLat); 440 minLon = LatLon.toIntervalLon(minLon); 441 maxLon = LatLon.toIntervalLon(maxLon); 442 } 443 444 @Override 445 public int hashCode() { 446 return Objects.hash(minLat, minLon, maxLat, maxLon); 447 } 448 449 @Override 450 public boolean equals(Object obj) { 451 if (this == obj) return true; 452 if (obj == null || getClass() != obj.getClass()) return false; 453 Bounds bounds = (Bounds) obj; 454 return Double.compare(bounds.minLat, minLat) == 0 && 455 Double.compare(bounds.minLon, minLon) == 0 && 456 Double.compare(bounds.maxLat, maxLat) == 0 && 457 Double.compare(bounds.maxLon, maxLon) == 0; 458 } 459}