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