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