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 }