RectangleImpl.java

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