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. &gt; 0 required.
243     * @param lonExtent the longitude extent. &gt; 0 required.
244     * @throws IllegalArgumentException if center is null
245     * @throws IllegalArgumentException if latExtent &lt;= 0
246     * @throws IllegalArgumentException if lonExtent &lt;= 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. ("&gt;=" instead of "&gt;")
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}