/******************************************************************************* * 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; } }