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