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