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