001    // License: GPL. Copyright 2007 by Immanuel Scholz and others
002    package org.openstreetmap.josm.data;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.awt.geom.Rectangle2D;
007    import java.text.DecimalFormat;
008    import java.text.MessageFormat;
009    
010    import org.openstreetmap.josm.data.coor.LatLon;
011    import org.openstreetmap.josm.tools.CheckParameterUtil;
012    
013    /**
014     * This is a simple data class for "rectangular" areas of the world, given in
015     * lat/lon min/max values.  The values are rounded to LatLon.OSM_SERVER_PRECISION
016     *
017     * @author imi
018     */
019    public class Bounds {
020        /**
021         * The minimum and maximum coordinates.
022         */
023        private double minLat, minLon, maxLat, maxLon;
024    
025        public LatLon getMin() {
026            return new LatLon(minLat, minLon);
027        }
028    
029        public LatLon getMax() {
030            return new LatLon(maxLat, maxLon);
031        }
032        
033        public enum ParseMethod {
034            MINLAT_MINLON_MAXLAT_MAXLON,
035            LEFT_BOTTOM_RIGHT_TOP
036        }
037        
038        /**
039         * Construct bounds out of two points
040         */
041        public Bounds(LatLon min, LatLon max) {
042            this(min.lat(), min.lon(), max.lat(), max.lon());
043        }
044    
045        public Bounds(LatLon min, LatLon max, boolean roundToOsmPrecision) {
046            this(min.lat(), min.lon(), max.lat(), max.lon(), roundToOsmPrecision);
047        }
048    
049        public Bounds(LatLon b) {
050            this(b, true);
051        }
052        
053        public Bounds(LatLon b, boolean roundToOsmPrecision) {
054            // Do not call this(b, b) to avoid GPX performance issue (see #7028) until roundToOsmPrecision() is improved
055            if (roundToOsmPrecision) {
056                this.minLat = LatLon.roundToOsmPrecision(b.lat());
057                this.minLon = LatLon.roundToOsmPrecision(b.lon());
058            } else {
059                this.minLat = b.lat();
060                this.minLon = b.lon();
061            }
062            this.maxLat = this.minLat;
063            this.maxLon = this.minLon;
064        }
065    
066        public Bounds(double minlat, double minlon, double maxlat, double maxlon) {
067            this(minlat, minlon, maxlat, maxlon, true);
068        }
069    
070        public Bounds(double minlat, double minlon, double maxlat, double maxlon, boolean roundToOsmPrecision) {
071            if (roundToOsmPrecision) {
072                this.minLat = LatLon.roundToOsmPrecision(minlat);
073                this.minLon = LatLon.roundToOsmPrecision(minlon);
074                this.maxLat = LatLon.roundToOsmPrecision(maxlat);
075                this.maxLon = LatLon.roundToOsmPrecision(maxlon);
076            } else {
077                this.minLat = minlat;
078                this.minLon = minlon;
079                this.maxLat = maxlat;
080                this.maxLon = maxlon;
081            }
082        }
083    
084        public Bounds(double [] coords) {
085            this(coords, true);
086        }
087    
088        public Bounds(double [] coords, boolean roundToOsmPrecision) {
089            CheckParameterUtil.ensureParameterNotNull(coords, "coords");
090            if (coords.length != 4)
091                throw new IllegalArgumentException(MessageFormat.format("Expected array of length 4, got {0}", coords.length));
092            if (roundToOsmPrecision) {
093                this.minLat = LatLon.roundToOsmPrecision(coords[0]);
094                this.minLon = LatLon.roundToOsmPrecision(coords[1]);
095                this.maxLat = LatLon.roundToOsmPrecision(coords[2]);
096                this.maxLon = LatLon.roundToOsmPrecision(coords[3]);
097            } else {
098                this.minLat = coords[0];
099                this.minLon = coords[1];
100                this.maxLat = coords[2];
101                this.maxLon = coords[3];
102            }
103        }
104    
105        public Bounds(String asString, String separator) throws IllegalArgumentException {
106            this(asString, separator, ParseMethod.MINLAT_MINLON_MAXLAT_MAXLON);
107        }
108    
109        public Bounds(String asString, String separator, ParseMethod parseMethod) throws IllegalArgumentException {
110            this(asString, separator, parseMethod, true);
111        }
112    
113        public Bounds(String asString, String separator, ParseMethod parseMethod, boolean roundToOsmPrecision) throws IllegalArgumentException {
114            CheckParameterUtil.ensureParameterNotNull(asString, "asString");
115            String[] components = asString.split(separator);
116            if (components.length != 4)
117                throw new IllegalArgumentException(MessageFormat.format("Exactly four doubles expected in string, got {0}: {1}", components.length, asString));
118            double[] values = new double[4];
119            for (int i=0; i<4; i++) {
120                try {
121                    values[i] = Double.parseDouble(components[i]);
122                } catch(NumberFormatException e) {
123                    throw new IllegalArgumentException(MessageFormat.format("Illegal double value ''{0}''", components[i]));
124                }
125            }
126            
127            switch (parseMethod) {
128                case LEFT_BOTTOM_RIGHT_TOP:
129                    this.minLat = initLat(values[1], roundToOsmPrecision);
130                    this.minLon = initLon(values[0], roundToOsmPrecision);
131                    this.maxLat = initLat(values[3], roundToOsmPrecision);
132                    this.maxLon = initLon(values[2], roundToOsmPrecision);
133                    break;
134                case MINLAT_MINLON_MAXLAT_MAXLON:
135                default:
136                    this.minLat = initLat(values[0], roundToOsmPrecision);
137                    this.minLon = initLon(values[1], roundToOsmPrecision);
138                    this.maxLat = initLat(values[2], roundToOsmPrecision);
139                    this.maxLon = initLon(values[3], roundToOsmPrecision);
140            }
141        }
142        
143        protected static double initLat(double value, boolean roundToOsmPrecision) {
144            if (!LatLon.isValidLat(value))
145                throw new IllegalArgumentException(tr("Illegal latitude value ''{0}''", value));
146            return roundToOsmPrecision ? LatLon.roundToOsmPrecision(value) : value;
147        }
148    
149        protected static double initLon(double value, boolean roundToOsmPrecision) {
150            if (!LatLon.isValidLon(value))
151                throw new IllegalArgumentException(tr("Illegal longitude value ''{0}''", value));
152            return roundToOsmPrecision ? LatLon.roundToOsmPrecision(value) : value;
153        }
154    
155        public Bounds(Bounds other) {
156            this(other.getMin(), other.getMax());
157        }
158    
159        public Bounds(Rectangle2D rect) {
160            this(rect.getMinY(), rect.getMinX(), rect.getMaxY(), rect.getMaxX());
161        }
162    
163        /**
164         * Creates new bounds around a coordinate pair <code>center</code>. The
165         * new bounds shall have an extension in latitude direction of <code>latExtent</code>,
166         * and in longitude direction of <code>lonExtent</code>.
167         *
168         * @param center  the center coordinate pair. Must not be null.
169         * @param latExtent the latitude extent. > 0 required.
170         * @param lonExtent the longitude extent. > 0 required.
171         * @throws IllegalArgumentException thrown if center is null
172         * @throws IllegalArgumentException thrown if latExtent <= 0
173         * @throws IllegalArgumentException thrown if lonExtent <= 0
174         */
175        public Bounds(LatLon center, double latExtent, double lonExtent) {
176            CheckParameterUtil.ensureParameterNotNull(center, "center");
177            if (latExtent <= 0.0)
178                throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0.0 exptected, got {1}", "latExtent", latExtent));
179            if (lonExtent <= 0.0)
180                throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0.0 exptected, got {1}", "lonExtent", lonExtent));
181    
182            this.minLat = LatLon.roundToOsmPrecision(LatLon.toIntervalLat(center.lat() - latExtent / 2));
183            this.minLon = LatLon.roundToOsmPrecision(LatLon.toIntervalLon(center.lon() - lonExtent / 2));
184            this.maxLat = LatLon.roundToOsmPrecision(LatLon.toIntervalLat(center.lat() + latExtent / 2));
185            this.maxLon = LatLon.roundToOsmPrecision(LatLon.toIntervalLon(center.lon() + lonExtent / 2));
186        }
187    
188        @Override public String toString() {
189            return "Bounds["+minLat+","+minLon+","+maxLat+","+maxLon+"]";
190        }
191    
192        public String toShortString(DecimalFormat format) {
193            return
194            format.format(minLat) + " "
195            + format.format(minLon) + " / "
196            + format.format(maxLat) + " "
197            + format.format(maxLon);
198        }
199    
200        /**
201         * @return Center of the bounding box.
202         */
203        public LatLon getCenter()
204        {
205            if (crosses180thMeridian()) {
206                LatLon result = new LatLon(minLat, minLon-360.0).getCenter(getMax());
207                if (result.lon() < -180.0) {
208                    result.setLocation(result.lon()+360.0, result.lat());
209                }
210                return result;
211            } else {
212                return getMin().getCenter(getMax());
213            }
214        }
215    
216        /**
217         * Extend the bounds if necessary to include the given point.
218         */
219        public void extend(LatLon ll) {
220            if (ll.lat() < minLat) {
221                minLat = LatLon.roundToOsmPrecision(ll.lat());
222            }
223            if (ll.lat() > maxLat) {
224                maxLat = LatLon.roundToOsmPrecision(ll.lat());
225            }
226            if (crosses180thMeridian()) {
227                if (ll.lon() > maxLon && ll.lon() < minLon) {
228                    if (Math.abs(ll.lon() - minLon) <= Math.abs(ll.lon() - maxLon)) {
229                        minLon = LatLon.roundToOsmPrecision(ll.lon());
230                    } else {
231                        maxLon = LatLon.roundToOsmPrecision(ll.lon());
232                    }
233                }
234            } else {
235                if (ll.lon() < minLon) {
236                    minLon = LatLon.roundToOsmPrecision(ll.lon());
237                }
238                if (ll.lon() > maxLon) {
239                    maxLon = LatLon.roundToOsmPrecision(ll.lon());
240                }
241            }
242        }
243    
244        public void extend(Bounds b) {
245            extend(b.getMin());
246            extend(b.getMax());
247        }
248        
249        /**
250         * Is the given point within this bounds?
251         */
252        public boolean contains(LatLon ll) {
253            if (ll.lat() < minLat || ll.lat() > maxLat)
254                return false;
255            if (crosses180thMeridian()) {
256                if (ll.lon() > maxLon && ll.lon() < minLon)
257                    return false;
258            } else {
259                if (ll.lon() < minLon || ll.lon() > maxLon)
260                    return false;
261            }
262            return true;
263        }
264    
265        private static boolean intersectsLonCrossing(Bounds crossing, Bounds notCrossing) {
266            return notCrossing.minLon <= crossing.maxLon || notCrossing.maxLon >= crossing.minLon;
267        }
268        
269        /**
270         * The two bounds intersect? Compared to java Shape.intersects, if does not use
271         * the interior but the closure. (">=" instead of ">")
272         */
273        public boolean intersects(Bounds b) {
274            if (b.maxLat < minLat || b.minLat > maxLat)
275                return false;
276            
277            if (crosses180thMeridian() && !b.crosses180thMeridian()) {
278                return intersectsLonCrossing(this, b);
279            } else if (!crosses180thMeridian() && b.crosses180thMeridian()) {
280                return intersectsLonCrossing(b, this);
281            } else if (crosses180thMeridian() && b.crosses180thMeridian()) {
282                return true;
283            } else {
284                return b.maxLon >= minLon && b.minLon <= maxLon;
285            }
286        }
287    
288        /**
289         * Determines if this Bounds object crosses the 180th Meridian.
290         * See http://wiki.openstreetmap.org/wiki/180th_meridian
291         * @return true if this Bounds object crosses the 180th Meridian.
292         */
293        public boolean crosses180thMeridian() {
294            return this.minLon > this.maxLon;
295        }
296        
297        /**
298         * Converts the lat/lon bounding box to an object of type Rectangle2D.Double
299         * @return the bounding box to Rectangle2D.Double
300         */
301        public Rectangle2D.Double asRect() {
302            double w = maxLon-minLon + (crosses180thMeridian() ? 360.0 : 0.0);
303            return new Rectangle2D.Double(minLon, minLat, w, maxLat-minLat);
304        }
305    
306        public double getArea() {
307            double w = maxLon-minLon + (crosses180thMeridian() ? 360.0 : 0.0);
308            return w * (maxLat - minLat);
309        }
310    
311        public String encodeAsString(String separator) {
312            StringBuffer sb = new StringBuffer();
313            sb.append(minLat).append(separator).append(minLon)
314            .append(separator).append(maxLat).append(separator)
315            .append(maxLon);
316            return sb.toString();
317        }
318    
319        /**
320         * <p>Replies true, if this bounds are <em>collapsed</em>, i.e. if the min
321         * and the max corner are equal.</p>
322         * 
323         * @return true, if this bounds are <em>collapsed</em>
324         */
325        public boolean isCollapsed() {
326            return getMin().equals(getMax());
327        }
328    
329        public boolean isOutOfTheWorld() {
330            return
331            minLat < -90 || minLat > 90 ||
332            maxLat < -90 || maxLat > 90 ||
333            minLon < -180 || minLon > 180 ||
334            maxLon < -180 || maxLon > 180;
335        }
336    
337        public void normalize() {
338            minLat = LatLon.toIntervalLat(minLat);
339            maxLat = LatLon.toIntervalLat(maxLat);
340            minLon = LatLon.toIntervalLon(minLon);
341            maxLon = LatLon.toIntervalLon(maxLon);
342        }
343    
344        @Override
345        public int hashCode() {
346            final int prime = 31;
347            int result = 1;
348            long temp;
349            temp = Double.doubleToLongBits(maxLat);
350            result = prime * result + (int) (temp ^ (temp >>> 32));
351            temp = Double.doubleToLongBits(maxLon);
352            result = prime * result + (int) (temp ^ (temp >>> 32));
353            temp = Double.doubleToLongBits(minLat);
354            result = prime * result + (int) (temp ^ (temp >>> 32));
355            temp = Double.doubleToLongBits(minLon);
356            result = prime * result + (int) (temp ^ (temp >>> 32));
357            return result;
358        }
359    
360        @Override
361        public boolean equals(Object obj) {
362            if (this == obj)
363                return true;
364            if (obj == null)
365                return false;
366            if (getClass() != obj.getClass())
367                return false;
368            Bounds other = (Bounds) obj;
369            if (Double.doubleToLongBits(maxLat) != Double.doubleToLongBits(other.maxLat))
370                return false;
371            if (Double.doubleToLongBits(maxLon) != Double.doubleToLongBits(other.maxLon))
372                return false;
373            if (Double.doubleToLongBits(minLat) != Double.doubleToLongBits(other.minLat))
374                return false;
375            if (Double.doubleToLongBits(minLon) != Double.doubleToLongBits(other.minLon))
376                return false;
377            return true;
378        }
379    }