blob: 6145a60c8dae30d464f762d151ecb7b463e7a675 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.referencing.privy;
import java.io.Serializable;
import java.awt.geom.Rectangle2D;
import org.opengis.geometry.Envelope;
import org.opengis.geometry.DirectPosition;
import org.apache.sis.geometry.Envelope2D;
import org.apache.sis.util.privy.Strings;
/**
* Rectangle defines by intervals instead of by a size.
* Instead of ({@code x}, {@code y}, {@code width}, {@code height}) values as do standard Java2D implementations,
* this class contains ({@link #xmin}, {@link #xmax}, {@link #ymin}, {@link #ymax}) values. This choice provides
* three benefits:
*
* <ul>
* <li>Allows this class to work correctly with {@linkplain java.lang.Double#isInfinite() infinite} and
* {@linkplain java.lang.Double#isNaN() NaN} values. By comparison, the (<var>width</var>, <var>height</var>)
* alternative is ambiguous.</li>
* <li>Slightly faster {@code contains(…)} and {@code intersects(…)} methods since there is no addition or
* subtraction to perform.</li>
* <li>Better inter-operability with {@link Envelope2D} when a rectangle spans the anti-meridian.
* This {@code IntervalRectangle} class does not support such envelopes by itself, but it is
* okay to create a rectangle with negative width and gives it in argument to
* {@link Envelope2D#contains(Rectangle2D)} or {@link Envelope2D#intersects(Rectangle2D)} methods.</li>
* </ul>
*
* This class provides the following additional methods which are not defined in {@link Rectangle2D}:
* <ul>
* <li>{@link #containsInclusive(double, double)}</li>
* <li>{@link #distanceSquared(double, double)}</li>
* </ul>
*
* This class does <strong>not</strong> support by itself rectangles crossing the anti-meridian of a geographic CRS.
* However, the {@link #getX()}, {@link #getY()}, {@link #getWidth()} and {@link #getHeight()} methods are defined in
* the straightforward way expected by {@link Envelope2D#intersects(Rectangle2D)} and similar methods for computing
* correct result if the given {@code Rectangle2D} crosses the anti-meridian.
*
* <h2>Internal usage of inheritance</h2>
* This class may also be opportunistically extended by some Apache SIS internal classes that need a rectangle in
* addition of their own information. All {@code Rectangle2D} methods are declared final for reducing the risk of
* confusion with other aspects managed by subclasses. We don't do that in public API because this is not a
* recommended approach, but for Apache SIS private classes this is a way to reduce pressure on garbage collector.
*
* @author Martin Desruisseaux (Geomatys)
*/
@SuppressWarnings("CloneableImplementsClone")
public class IntervalRectangle extends Rectangle2D implements Serializable {
/** For cross-version compatibility. */
private static final long serialVersionUID = -5921513912411186629L;
/** Minimal <var>x</var> coordinate value. */ public double xmin;
/** Minimal <var>y</var> coordinate value. */ public double ymin;
/** Maximal <var>x</var> coordinate value. */ public double xmax;
/** Maximal <var>y</var> coordinate value. */ public double ymax;
/**
* Constructs a default rectangle initialized to {@code (0,0,0,0)}.
*/
public IntervalRectangle() {
}
/**
* Constructs a rectangle initialized to the two first dimensions of the given envelope.
* If the given envelope crosses the anti-meridian, then the new rectangle will span the
* full longitude range (i.e. this constructor does not preserve the convention of using
* negative width for envelopes crossing anti-meridian).
*
* <h4>Design note</h4>
* This constructor expands envelopes that cross the anti-meridian
* because the methods defined in this class are not designed for handling such envelopes.
* If a rectangle with negative width is nevertheless desired for envelope crossing the anti-meridian,
* one can use the following constructor:
*
* {@snippet lang="java" :
* new IntervalRectangle(envelope.getLowerCorner(), envelope.getUpperCorner());
* }
*
* @param envelope the envelope from which to copy the values.
*/
public IntervalRectangle(final Envelope envelope) {
xmin = envelope.getMinimum(0);
xmax = envelope.getMaximum(0);
ymin = envelope.getMinimum(1);
ymax = envelope.getMaximum(1);
}
/**
* Constructs a rectangle initialized to the two first dimensions of the given corners.
* This constructor unconditionally assigns {@code lower} coordinates to {@link #xmin}, {@link #ymin} and
* {@code upper} coordinates to {@link #xmax}, {@link #ymax} regardless of their values; this constructor
* does not verify if {@code lower} coordinates are smaller than {@code upper} coordinates.
* This is sometimes useful for creating a rectangle crossing the anti-meridian,
* even if {@code IntervalRectangle} class does not support such rectangles by itself.
*
* @param lower the limits in the direction of decreasing coordinate values for each dimension.
* @param upper the limits in the direction of increasing coordinate values for each dimension.
*
* @see Envelope#getLowerCorner()
* @see Envelope#getUpperCorner()
*/
public IntervalRectangle(final DirectPosition lower, final DirectPosition upper) {
xmin = lower.getCoordinate(0);
xmax = upper.getCoordinate(0);
ymin = lower.getCoordinate(1);
ymax = upper.getCoordinate(1);
}
/**
* Creates a rectangle using maximal <var>x</var> and <var>y</var> values rather than width and height.
* This constructor avoid the problem of NaN values when extremum are infinite numbers.
*
* @param xmin minimal <var>x</var> coordinate value.
* @param ymin minimal <var>y</var> coordinate value.
* @param xmax maximal <var>x</var> coordinate value.
* @param ymax maximal <var>y</var> coordinate value.
*/
public IntervalRectangle(final double xmin, final double ymin, final double xmax, final double ymax) {
this.xmin = xmin;
this.ymin = ymin;
this.xmax = xmax;
this.ymax = ymax;
}
/**
* Determines whether this rectangle is empty. If this rectangle has at least one
* {@linkplain java.lang.Double#NaN NaN} value, then it is considered empty.
*
* @return {@code true} if this rectangle is empty; {@code false} otherwise.
*/
@Override
public final boolean isEmpty() {
return !(xmin < xmax && ymin < ymax);
}
/**
* Returns the minimal <var>x</var> coordinate value.
*
* @return the minimal <var>x</var> coordinate value.
*/
@Override
public final double getX() {
return xmin;
}
/**
* Returns the minimal <var>y</var> coordinate value.
*
* @return the minimal <var>y</var> coordinate value.
*/
@Override
public final double getY() {
return ymin;
}
/**
* Returns the width of the rectangle. May be negative if the rectangle crosses the anti-meridian.
* This {@code IntervalRectangle} class does not support such envelopes itself, but other classes
* like {@link Envelope2D} will handle correctly the negative width.
*
* @return the width of the rectangle.
*/
@Override
public final double getWidth() {
return xmax - xmin;
}
/**
* Returns the height of the rectangle.
*
* @return the height of the rectangle.
*/
@Override
public final double getHeight() {
return ymax - ymin;
}
/**
* Returns the minimal <var>x</var> coordinate value.
*
* @return the minimal <var>x</var> coordinate value.
*/
@Override
public final double getMinX() {
return xmin;
}
/**
* Returns the minimal <var>y</var> coordinate value.
*
* @return the minimal <var>y</var> coordinate value.
*/
@Override
public final double getMinY() {
return ymin;
}
/**
* Returns the maximal <var>x</var> coordinate value.
*
* @return the maximal <var>x</var> coordinate value.
*/
@Override
public final double getMaxX() {
return xmax;
}
/**
* Returns the maximal <var>y</var> coordinate value.
*
* @return the maximal <var>y</var> coordinate value.
*/
@Override
public final double getMaxY() {
return ymax;
}
/**
* Returns the <var>x</var> coordinate of the center of the rectangle.
*
* @return the median <var>x</var> coordinate value.
*/
@Override
public final double getCenterX() {
return (xmin + xmax) * 0.5;
}
/**
* Returns the <var>y</var> coordinate of the center of the rectangle.
*
* @return the median <var>y</var> coordinate value.
*/
@Override
public final double getCenterY() {
return (ymin + ymax) * 0.5;
}
/**
* Sets the location and size of this rectangle to the specified values.
*
* @param x the <var>x</var> minimal coordinate value.
* @param y the <var>y</var> minimal coordinate value.
* @param width the rectangle width.
* @param height the rectangle height.
*/
@Override
public final void setRect(final double x, final double y, final double width, final double height) {
xmin = x;
ymin = y;
xmax = x + width;
ymax = y + height;
}
/**
* Sets this rectangle to be the same as the specified rectangle.
*
* @param r the rectangle to copy values from.
*/
@Override
public final void setRect(final Rectangle2D r) {
if (r != this) { // Optimization for methods chaining like r.setRect(Shapes.transform(…, r))
xmin = r.getMinX();
ymin = r.getMinY();
xmax = r.getMaxX();
ymax = r.getMaxY();
}
}
/**
* Sets the framing rectangle to the given rectangle. The current implementation delegates
* to {@link #setRect(Rectangle2D)}. This is consistent with the default implementation of
* {@link #setFrame(double, double, double, double)}, which delegates to the corresponding
* method of {@link #setRect(double, double, double, double) setRect}.
*/
@Override
public final void setFrame(final Rectangle2D r) {
setRect(r);
}
/**
* Tests if the interior of this rectangle intersects the interior of a specified set of rectangular coordinates.
* The edges are considered exclusive; this method returns {@code false} if the two rectangles just touch to each
* other.
*
* @param x the <var>x</var> minimal coordinate value.
* @param y the <var>y</var> minimal coordinate value.
* @param width the rectangle width.
* @param height the rectangle height.
* @return {@code true} if this rectangle intersects the interior of the specified set of rectangular coordinates.
*/
@Override
public final boolean intersects(final double x, final double y, final double width, final double height) {
if (!(xmin < xmax && ymin < ymax && width > 0 && height > 0)) {
return false;
} else {
return (x < xmax && y < ymax && x+width > xmin && y+height > ymin);
}
}
/**
* Tests if the interior of this shape intersects the interior of a specified rectangle.
* The edges are considered exclusive; this method returns {@code false} if the two rectangles
* just touch to each other.
*
* @param rect the specified rectangle.
* @return {@code true} if this shape and the specified rectangle intersect each other.
*/
@Override
public final boolean intersects(final Rectangle2D rect) {
if (!(xmin < xmax && ymin < ymax)) {
return false;
} else {
final double xmin2 = rect.getMinX();
final double xmax2 = rect.getMaxX(); if (!(xmax2 > xmin2)) return false;
final double ymin2 = rect.getMinY();
final double ymax2 = rect.getMaxY(); if (!(ymax2 > ymin2)) return false;
return (xmin2 < xmax && ymin2 < ymax && xmax2 > xmin && ymax2 > ymin);
}
}
/**
* Tests if the interior of this rectangle entirely contains the specified set of rectangular coordinates.
*
* @param x the <var>x</var> minimal coordinate value.
* @param y the <var>y</var> minimal coordinate value.
* @param width the rectangle width.
* @param height the rectangle height.
* @return {@code true} if this rectangle entirely contains specified set of rectangular coordinates.
*/
@Override
public final boolean contains(final double x, final double y, final double width, final double height) {
if (!(xmin < xmax && ymin < ymax && width > 0 && height > 0)) {
return false;
} else {
return (x >= xmin && y >= ymin && (x+width) <= xmax && (y+height) <= ymax);
}
}
/**
* Tests if the interior of this shape entirely contains the specified rectangle.
* This methods overrides the default {@link Rectangle2D} implementation in order
* to work correctly with {@linkplain java.lang.Double#POSITIVE_INFINITY infinites}
* and {@linkplain java.lang.Double#NaN NaN} values.
*
* @param rect the specified rectangle.
* @return {@code true} if this shape entirely contains the specified rectangle.
*/
@Override
public final boolean contains(final Rectangle2D rect) {
if (!(xmin < xmax && ymin < ymax)) {
return false;
} else {
final double xmin2 = rect.getMinX();
final double xmax2 = rect.getMaxX(); if (!(xmax2 > xmin2)) return false;
final double ymin2 = rect.getMinY();
final double ymax2 = rect.getMaxY(); if (!(ymax2 > ymin2)) return false;
return (xmin2 >= xmin && ymin2 >= ymin && xmax2 <= xmax && ymax2 <= ymax);
}
}
/**
* Tests if a specified coordinate is inside the boundary of this {@code Rectangle2D}.
* The maximal <var>x</var> and <var>y</var> values (i.e. the right and bottom edges
* of this rectangle) are exclusive.
*
* @param x the <var>x</var> coordinates to test.
* @param y the <var>y</var> coordinates to test.
* @return {@code true} if the specified coordinates are inside the boundary of this rectangle.
*/
@Override
public final boolean contains(final double x, final double y) {
return (x >= xmin && y >= ymin && x < xmax && y < ymax);
}
/**
* Tests if a specified coordinate is inside the boundary of this {@code Rectangle2D}
* with all edges inclusive.
*
* @param x the <var>x</var> coordinates to test.
* @param y the <var>y</var> coordinates to test.
* @return {@code true} if the specified coordinates are inside this rectangle, all edges included.
*/
public final boolean containsInclusive(final double x, final double y) {
return (x >= xmin && x <= xmax && y >= ymin && y <= ymax);
}
/**
* Returns the square of the minimal distance between a point and this rectangle.
* If the point is inside the rectangle or on the edge, then this method returns 0.
*
* @param x the <var>x</var> coordinates to test.
* @param y the <var>y</var> coordinates to test.
* @return square of minimal distance, or 0 if the point is inside this rectangle.
*/
public final double distanceSquared(final double x, final double y) {
int outcode = 0;
double dx = java.lang.Double.POSITIVE_INFINITY;
double dy = java.lang.Double.POSITIVE_INFINITY;
double d;
if ((d = (x - xmax)) >= 0) {dx = d; outcode = 1;}
if ((d = (y - ymax)) >= 0) {dy = d; outcode |= 2;}
if ((d = (xmin - x)) >= 0 && d < dx) {dx = d; outcode |= 1;}
if ((d = (ymin - y)) >= 0 && d < dy) {dy = d; outcode |= 2;}
switch (outcode) {
case 1: return dx*dx; // Only x coordinate is outside.
case 2: return dy*dy; // Only y coordinate is outside.
case 3: return dx*dx + dy*dy; // Both coordinates are outside.
case 0: assert containsInclusive(x, y); return 0; // No coordinate is outside.
default: throw new AssertionError(outcode);
}
/*
* Note: if we want non-squared distance in a future version, we can rely on the fact
* that `dx` and `dy` are guaranteed positives (no need to take the absolute value).
* So we would replace the 3 first cases as below:
*
* case 1: return dx;
* case 2: return dy;
* case 3: return Math.hypot(dx, dy);
*/
}
/**
* Determines where the specified coordinates lie with respect to this rectangle.
* This method computes a binary OR of the appropriate mask values indicating,
* for each side of this {@code Rectangle2D}, whether or not the specified coordinates
* are on the same side of the edge as the rest of this {@code Rectangle2D}.
*
* @return the logical OR of all appropriate out codes.
*
* @see #OUT_LEFT
* @see #OUT_TOP
* @see #OUT_RIGHT
* @see #OUT_BOTTOM
*/
@Override
public final int outcode(final double x, final double y) {
int out = 0;
if (!(xmax > xmin)) out |= OUT_LEFT | OUT_RIGHT;
else if (x < xmin) out |= OUT_LEFT;
else if (x > xmax) out |= OUT_RIGHT;
if (!(ymax > ymin)) out |= OUT_TOP | OUT_BOTTOM;
else if (y < ymin) out |= OUT_TOP;
else if (y > ymax) out |= OUT_BOTTOM;
return out;
}
/**
* Intersects a {@link Rectangle2D} object with this rectangle.
* The resulting* rectangle is the intersection of the two {@code Rectangle2D} objects.
* Invoking this method is equivalent to invoking the following code, except that this
* method behaves correctly with infinite values and {@link Envelope2D} implementation.
*
* {@snippet lang="java" :
* Rectangle2D.intersect(this, rect, this);
* }
*
* @param rect the {@code Rectangle2D} to intersect with this rectangle.
*
* @see #intersect(Rectangle2D, Rectangle2D, Rectangle2D)
* @see #createIntersection(Rectangle2D)
*/
public final void intersect(final Rectangle2D rect) {
double t;
// Must use getMin/Max methods, not getX/Y/Width/Height, for inter-operability with Envelope2D.
if ((t = rect.getMinX()) > xmin) xmin = t;
if ((t = rect.getMaxX()) < xmax) xmax = t;
if ((t = rect.getMinY()) > ymin) ymin = t;
if ((t = rect.getMaxY()) < ymax) ymax = t;
}
/**
* Returns a new {@code Rectangle2D} object representing the intersection of this rectangle with the specified one.
*
* @param rect the {@code Rectangle2D} to be intersected with this rectangle.
* @return the largest {@code Rectangle2D} contained in both the specified rectangle and this one.
*
* @see #intersect(Rectangle2D)
*/
@Override
public final Rectangle2D createIntersection(final Rectangle2D rect) {
final IntervalRectangle r = new IntervalRectangle();
r.xmin = Math.max(xmin, rect.getMinX());
r.ymin = Math.max(ymin, rect.getMinY());
r.xmax = Math.min(xmax, rect.getMaxX());
r.ymax = Math.min(ymax, rect.getMaxY());
return r;
}
/**
* Returns a new {@code Rectangle2D} object representing the
* union of this rectangle with the specified one.
*
* @param rect the {@code Rectangle2D} to be combined with this rectangle.
* @return the smallest {@code Rectangle2D} containing both the specified {@code Rectangle2D} and this one.
*/
@Override
public final Rectangle2D createUnion(final Rectangle2D rect) {
final IntervalRectangle r = new IntervalRectangle();
r.xmin = Math.min(xmin, rect.getMinX());
r.ymin = Math.min(ymin, rect.getMinY());
r.xmax = Math.max(xmax, rect.getMaxX());
r.ymax = Math.max(ymax, rect.getMaxY());
return r;
}
/**
* Adds a point, specified by the arguments {@code x} and {@code y}, to this rectangle.
* The resulting {@code Rectangle2D} is the smallest rectangle that contains both the
* original rectangle and the specified point.
*
* <p>After adding a point, a call to {@code contains} with the added point as an argument
* does not necessarily return {@code true}. The {@code contains} method does not return
* {@code true} for points on the right or bottom edges of a rectangle. Therefore, if the
* added point falls on the left or bottom edge of the enlarged rectangle, {@code contains}
* returns {@code false} for that point.</p>
*
* @param x x coordinate value of the point to add.
* @param y y coordinate value of the point to add.
*/
@Override
public final void add(final double x, final double y) {
if (x < xmin) xmin = x;
if (x > xmax) xmax = x;
if (y < ymin) ymin = y;
if (y > ymax) ymax = y;
}
/**
* Adds a {@code Rectangle2D} object to this rectangle.
* The resulting rectangle is the union of the two {@code Rectangle2D} objects.
*
* @param rect the {@code Rectangle2D} to add to this rectangle.
*/
@Override
public final void add(final Rectangle2D rect) {
double t;
if ((t = rect.getMinX()) < xmin) xmin = t;
if ((t = rect.getMaxX()) > xmax) xmax = t;
if ((t = rect.getMinY()) < ymin) ymin = t;
if ((t = rect.getMaxY()) > ymax) ymax = t;
}
/**
* Returns the {@code String} representation of this {@code Rectangle2D}. The coordinate order is
* (<var>x</var><sub>min</sub>, <var>y</var><sub>min</sub>, <var>x</var><sub>max</sub>, <var>y</var><sub>max</sub>),
* which is consistent with the {@link #IntervalRectangle(double, double, double, double)} constructor
* and with the {@code BBOX} <i>Well Known Text</i> (WKT) syntax.
*
* @return a {@code String} representing this {@code Rectangle2D}.
*/
@Override
public String toString() {
return Strings.toString(getClass(), "xmin", xmin, "ymin", ymin, "xmax", xmax, "ymax", ymax);
}
}