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