/*******************************************************************************
* Copyright (c) 2015 Voyager Search and MITRE
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Apache License, Version 2.0 which
* accompanies this distribution and is available at
* http://www.apache.org/licenses/LICENSE-2.0.txt
******************************************************************************/
package org.locationtech.spatial4j.shape.impl;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.distance.DistanceUtils;
import org.locationtech.spatial4j.shape.BaseShape;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Rectangle;
import org.locationtech.spatial4j.shape.Shape;
import org.locationtech.spatial4j.shape.SpatialRelation;
/**
* A simple Rectangle implementation that also supports a longitudinal
* wrap-around. When minX > maxX, this will assume it is world coordinates that
* cross the date line using degrees. Immutable & threadsafe.
*/
public class RectangleImpl extends BaseShape<SpatialContext> implements Rectangle {
private double minX;
private double maxX;
private double minY;
private double maxY;
/** A simple constructor without normalization / validation. */
public RectangleImpl(double minX, double maxX, double minY, double maxY, SpatialContext ctx) {
super(ctx);
//TODO change to West South East North to be more consistent with OGC?
reset(minX, maxX, minY, maxY);
}
/** A convenience constructor which pulls out the coordinates. */
public RectangleImpl(Point lowerLeft, Point upperRight, SpatialContext ctx) {
this(lowerLeft.getX(), upperRight.getX(),
lowerLeft.getY(), upperRight.getY(), ctx);
}
/** Copy constructor. */
public RectangleImpl(Rectangle r, SpatialContext ctx) {
this(r.getMinX(), r.getMaxX(), r.getMinY(), r.getMaxY(), ctx);
}
@Override
public void reset(double minX, double maxX, double minY, double maxY) {
assert ! isEmpty();
this.minX = minX;
this.maxX = maxX;
this.minY = minY;
this.maxY = maxY;
assert minY <= maxY || Double.isNaN(minY) : "minY, maxY: "+minY+", "+maxY;
}
@Override
public boolean isEmpty() {
return Double.isNaN(minX);
}
@Override
public Rectangle getBuffered(double distance, SpatialContext ctx) {
if (ctx.isGeo()) {
//first check pole touching, triggering a world-wrap rect
if (maxY + distance >= 90) {
return ctx.makeRectangle(-180, 180, Math.max(-90, minY - distance), 90);
} else if (minY - distance <= -90) {
return ctx.makeRectangle(-180, 180, -90, Math.min(90, maxY + distance));
} else {
//doesn't touch pole
double latDistance = distance;
double closestToPoleY = Math.abs(maxY) > Math.abs(minY) ? maxY : minY;
double lonDistance = DistanceUtils.calcBoxByDistFromPt_deltaLonDEG(
closestToPoleY, minX, distance);//lat,lon order
//could still wrap the world though...
if (lonDistance * 2 + getWidth() >= 360)
return ctx.makeRectangle(-180, 180, minY - latDistance, maxY + latDistance);
return ctx.makeRectangle(
DistanceUtils.normLonDEG(minX - lonDistance),
DistanceUtils.normLonDEG(maxX + lonDistance),
minY - latDistance, maxY + latDistance);
}
} else {
Rectangle worldBounds = ctx.getWorldBounds();
double newMinX = Math.max(worldBounds.getMinX(), minX - distance);
double newMaxX = Math.min(worldBounds.getMaxX(), maxX + distance);
double newMinY = Math.max(worldBounds.getMinY(), minY - distance);
double newMaxY = Math.min(worldBounds.getMaxY(), maxY + distance);
return ctx.makeRectangle(newMinX, newMaxX, newMinY, newMaxY);
}
}
@Override
public boolean hasArea() {
return maxX != minX && maxY != minY;
}
@Override
public double getArea(SpatialContext ctx) {
if (ctx == null) {
return getWidth() * getHeight();
} else {
return ctx.getDistCalc().area(this);
}
}
@Override
public boolean getCrossesDateLine() {
return (minX > maxX);
}
@Override
public double getHeight() {
return maxY - minY;
}
@Override
public double getWidth() {
double w = maxX - minX;
if (w < 0) {//only true when minX > maxX (WGS84 assumed)
w += 360;
assert w >= 0;
}
return w;
}
@Override
public double getMaxX() {
return maxX;
}
@Override
public double getMaxY() {
return maxY;
}
@Override
public double getMinX() {
return minX;
}
@Override
public double getMinY() {
return minY;
}
@Override
public Rectangle getBoundingBox() {
return this;
}
@Override
public SpatialRelation relate(Shape other) {
if (isEmpty() || other.isEmpty())
return SpatialRelation.DISJOINT;
if (other instanceof Point) {
return relate((Point) other);
}
if (other instanceof Rectangle) {
return relate((Rectangle) other);
}
return other.relate(this).transpose();
}
public SpatialRelation relate(Point point) {
if (point.getY() > getMaxY() || point.getY() < getMinY())
return SpatialRelation.DISJOINT;
// all the below logic is rather unfortunate but some dateline cases demand it
double minX = this.minX;
double maxX = this.maxX;
double pX = point.getX();
if (ctx.isGeo()) {
//unwrap dateline and normalize +180 to become -180
double rawWidth = maxX - minX;
if (rawWidth < 0) {
maxX = minX + (rawWidth + 360);
}
//shift to potentially overlap
if (pX < minX) {
pX += 360;
} else if (pX > maxX) {
pX -= 360;
} else {
return SpatialRelation.CONTAINS;//short-circuit
}
}
if (pX < minX || pX > maxX)
return SpatialRelation.DISJOINT;
return SpatialRelation.CONTAINS;
}
public SpatialRelation relate(Rectangle rect) {
SpatialRelation yIntersect = relateYRange(rect.getMinY(), rect.getMaxY());
if (yIntersect == SpatialRelation.DISJOINT)
return SpatialRelation.DISJOINT;
SpatialRelation xIntersect = relateXRange(rect.getMinX(), rect.getMaxX());
if (xIntersect == SpatialRelation.DISJOINT)
return SpatialRelation.DISJOINT;
if (xIntersect == yIntersect)//in agreement
return xIntersect;
//if one side is equal, return the other
if (getMinY() == rect.getMinY() && getMaxY() == rect.getMaxY())
return xIntersect;
if (getMinX() == rect.getMinX() && getMaxX() == rect.getMaxX()
|| (ctx.isGeo() && verticalAtDateline(this, rect))) {
return yIntersect;
}
return SpatialRelation.INTERSECTS;
}
//note: if vertical lines at the dateline were normalized (say to -180.0) then this method wouldn't be necessary.
private static boolean verticalAtDateline(RectangleImpl rect1, Rectangle rect2) {
if (rect1.getMinX() == rect1.getMaxX() && rect2.getMinX() == rect2.getMaxX()) {
if (rect1.getMinX() == -180) {
return rect2.getMinX() == +180;
} else if (rect1.getMinX() == +180) {
return rect2.getMinX() == -180;
}
}
return false;
}
//TODO might this utility move to SpatialRelation ?
private static SpatialRelation relate_range(double int_min, double int_max, double ext_min, double ext_max) {
if (ext_min > int_max || ext_max < int_min) {
return SpatialRelation.DISJOINT;
}
if (ext_min >= int_min && ext_max <= int_max) {
return SpatialRelation.CONTAINS;
}
if (ext_min <= int_min && ext_max >= int_max) {
return SpatialRelation.WITHIN;
}
return SpatialRelation.INTERSECTS;
}
@Override
public SpatialRelation relateYRange(double ext_minY, double ext_maxY) {
return relate_range(minY, maxY, ext_minY, ext_maxY);
}
@Override
public SpatialRelation relateXRange(double ext_minX, double ext_maxX) {
//For ext & this we have local minX and maxX variable pairs. We rotate them so that minX <= maxX
double minX = this.minX;
double maxX = this.maxX;
if (ctx.isGeo()) {
//unwrap dateline, plus do world-wrap short circuit
double rawWidth = maxX - minX;
if (rawWidth == 360)
return SpatialRelation.CONTAINS;
if (rawWidth < 0) {
maxX = minX + (rawWidth + 360);
}
double ext_rawWidth = ext_maxX - ext_minX;
if (ext_rawWidth == 360)
return SpatialRelation.WITHIN;
if (ext_rawWidth < 0) {
ext_maxX = ext_minX + (ext_rawWidth + 360);
}
//shift to potentially overlap
if (maxX < ext_minX) {
minX += 360;
maxX += 360;
} else if (ext_maxX < minX) {
ext_minX += 360;
ext_maxX += 360;
}
}
return relate_range(minX, maxX, ext_minX, ext_maxX);
}
@Override
public String toString() {
return "Rect(minX=" + minX + ",maxX=" + maxX + ",minY=" + minY + ",maxY=" + maxY + ")";
}
@Override
public Point getCenter() {
if (Double.isNaN(minX))
return ctx.makePoint(Double.NaN, Double.NaN);
final double y = getHeight() / 2 + minY;
double x = getWidth() / 2 + minX;
if (minX > maxX)//WGS84
x = DistanceUtils.normLonDEG(x);//in case falls outside the standard range
return new PointImpl(x, y, ctx);
}
@Override
public boolean equals(Object obj) {
return equals(this,obj);
}
/**
* All {@link Rectangle} implementations should use this definition of {@link Object#equals(Object)}.
*/
public static boolean equals(Rectangle thiz, Object o) {
assert thiz != null;
if (thiz == o) return true;
if (!(o instanceof Rectangle)) return false;
RectangleImpl rectangle = (RectangleImpl) o;
if (Double.compare(rectangle.getMaxX(), thiz.getMaxX()) != 0) return false;
if (Double.compare(rectangle.getMaxY(), thiz.getMaxY()) != 0) return false;
if (Double.compare(rectangle.getMinX(), thiz.getMinX()) != 0) return false;
if (Double.compare(rectangle.getMinY(), thiz.getMinY()) != 0) return false;
return true;
}
@Override
public int hashCode() {
return hashCode(this);
}
/**
* All {@link Rectangle} implementations should use this definition of {@link Object#hashCode()}.
*/
public static int hashCode(Rectangle thiz) {
int result;
long temp;
temp = thiz.getMinX() != +0.0d ? Double.doubleToLongBits(thiz.getMinX()) : 0L;
result = (int) (temp ^ (temp >>> 32));
temp = thiz.getMaxX() != +0.0d ? Double.doubleToLongBits(thiz.getMaxX()) : 0L;
result = 31 * result + (int) (temp ^ (temp >>> 32));
temp = thiz.getMinY() != +0.0d ? Double.doubleToLongBits(thiz.getMinY()) : 0L;
result = 31 * result + (int) (temp ^ (temp >>> 32));
temp = thiz.getMaxY() != +0.0d ? Double.doubleToLongBits(thiz.getMaxY()) : 0L;
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
}