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