All Downloads are FREE. Search and download functionalities are using the official Maven repository.

geotrellis.vector.Extent.scala Maven / Gradle / Ivy

Go to download

GeoTrellis is an open source geographic data processing engine for high performance applications.

There is a newer version: 0.10.3
Show newest version
package geotrellis.vector

import GeomFactory._
import geotrellis.proj4.CRS

import com.vividsolutions.jts.{geom => jts}

case class ExtentRangeError(msg:String) extends Exception(msg)

object Extent {
  def apply(env: jts.Envelope): Extent =
    Extent(env.getMinX, env.getMinY, env.getMaxX, env.getMaxY)

  /** Create an extent from a string
    *
    * @param s   A string of the form "xmin,ymin,xmax,ymax"
    */
  def fromString(s:String) = {
    val Array(xmin,ymin,xmax,ymax) = s.split(",").map(_.toDouble)
    Extent(xmin,ymin,xmax,ymax)
  }

  implicit def toPolygon(extent: Extent): Polygon =
    extent.toPolygon

  implicit def jts2Extent(env: jts.Envelope): Extent =
    Extent(env)
}

/** A case class for an extent and its corresponding CRS
  *
  * @param extent The Extent which is projected
  * @param crs    The CRS projection of this extent
  */
case class ProjectedExtent(extent: Extent, crs: CRS) {
  def reproject(dest: CRS): Extent =
    extent.reproject(crs, dest)
}

/** ProjectedExtent companion object */
object ProjectedExtent {
  implicit def fromTupleA(tup: (Extent, CRS)):ProjectedExtent = ProjectedExtent(tup._1, tup._2)
  implicit def fromTupleB(tup: (CRS, Extent)):ProjectedExtent = ProjectedExtent(tup._2, tup._1)
}

/** A rectangular region of geographic space
  *
  * @param xmin The minimum x coordinate
  * @param ymin The minimum y coordinate
  * @param xmax The maximum x coordinate
  * @param ymax The maximum y coordinate
  */
case class Extent(xmin: Double, ymin: Double, xmax: Double, ymax: Double) {

  // Validation: Do not accept extents min values greater than max values.
  if (xmin > xmax) { throw ExtentRangeError(s"Invalid Extent: xmin must be less than xmax (xmin=$xmin, xmax=$xmax)") }
  if (ymin > ymax) { throw ExtentRangeError(s"Invalid Extent: ymin must be less than ymax (ymin=$ymin, ymax=$ymax)") }

  def jtsEnvelope: com.vividsolutions.jts.geom.Envelope =
    new com.vividsolutions.jts.geom.Envelope(xmin, xmax, ymin, ymax)

  val width: Double = xmax - xmin
  val height: Double = ymax - ymin

  def min: Point = Point(xmin, ymin)
  def max: Point = Point(xmax, ymax)

  /** The SW corner (xmin, ymin) as a Point. */
  def southWest: Point = Point(xmin, ymin)

  /** The SE corner (xmax, ymin) as a Point. */
  def southEast: Point = Point(xmax, ymin)

  /** The NE corner (xmax, ymax) as a Point. */
  def northEast: Point = Point(xmax, ymax)

  /** The NW corner (xmin, ymax) as a Point. */
  def northWest: Point = Point(xmin, ymax)

  /** The area of this extent */
  def area: Double = width * height

  /** The minimum between the height and width of this extent */
  def minExtent: Double = if(width < height) width else height

  /** The maximum between the height and width of this extent */
  def maxExtent: Double = if(width > height) width else height

  /** Predicate for whether this extent has 0 area */
  def isEmpty: Boolean = area == 0

  /** The centroid of this extent */
  def center: Point =
    Point((xmin + xmax) / 2.0, (ymin + ymax) / 2.0)

  /** Predicate for whether this extent intersects the interior of another */
  def interiorIntersects(other: Extent): Boolean =
    !(other.xmax <= xmin ||
      other.xmin >= xmax) &&
    !(other.ymax <= ymin ||
      other.ymin >= ymax)

  /** Predicate for whether this extent intersects another */
  def intersects(other: Extent): Boolean =
    !(other.xmax < xmin ||
      other.xmin > xmax) &&
    !(other.ymax < ymin ||
      other.ymin > ymax)

  /** Predicate for whether this extent intersects another */
  def intersects(p: Point): Boolean =
    intersects(p.x, p.y)

  /** Predicate for whether this extent intersects the specified point */
  def intersects(x: Double, y: Double): Boolean =
    x >= xmin && x <= xmax && y >= ymin && y <= ymax

  /** Empty extent contains nothing, though non empty extent contains iteslf */
  def contains(other: Extent): Boolean = {
    if(xmin == 0 && xmax == 0 && ymin == 0 && ymax == 0) false
    else
      other.xmin >= xmin &&
      other.ymin >= ymin &&
      other.xmax <= xmax &&
      other.ymax <= ymax
  }

  /** Tests if the given point lies in or on the envelope.
    *
    * @note This is the same definition as the SFS contains,
    *       which is unlike the JTS Envelope.contains, which would include the
    *       envelope boundary.
    */
  def contains(p: Point): Boolean =
    contains(p.x, p.y)

  /** Tests if the given point lies in or on the envelope.
    *
    * @note This is the same definition as the SFS contains,
    *       which is unlike the JTS Envelope.contains, which would include the
    *       envelope boundary.
    */
  def contains(x: Double, y: Double): Boolean =
    x > xmin && x < xmax && y > ymin && y < ymax

  /** Predicate for whether this extent covers another */
  def covers(other: Extent): Boolean =
    contains(other)

  /** Predicate for whether this extent covers a point */
  def covers(p: Point): Boolean =
    covers(p.x, p.y)

  /** Predicate for whether this extent covers a point */
  def covers(x: Double, y: Double): Boolean =
    intersects(x, y)

  /** Distance from another extent */
  def distance(other: Extent): Double =
    if(intersects(other)) 0
    else {
      val dx =
        if(xmax < other.xmin)
          other.xmin - xmax
        else if(xmin > other.xmax)
          xmin - other.xmax
        else
          0.0

        val dy =
          if(ymax < other.ymin)
            other.ymin - ymax
          else if(ymin > other.ymax)
            ymin - other.ymax
          else
            0.0

        // if either is zero, the envelopes overlap either vertically or horizontally
        if(dx == 0.0)
          dy
        else if(dy == 0.0)
          dx
        else
          math.sqrt(dx * dx + dy * dy)
    }

  /** Create an optional extent which represents the intersection with a provided extent */
  def intersection(other: Extent): Option[Extent] = {
    val xminNew = if(xmin > other.xmin) xmin else other.xmin
    val yminNew = if(ymin > other.ymin) ymin else other.ymin
    val xmaxNew = if(xmax < other.xmax) xmax else other.xmax
    val ymaxNew = if(ymax < other.ymax) ymax else other.ymax

    if(xminNew <= xmaxNew && yminNew <= ymaxNew) {
      Some(Extent(xminNew, yminNew, xmaxNew, ymaxNew))
    } else { None }
  }

  /** Create an optional extent which represents the intersection with a provided extent */
  def &(other: Extent): Option[Extent] =
    intersection(other)

  /** Create a new extent using a buffer around this extent */
  def buffer(d: Double): Extent =
    Extent(xmin - d, ymin - d, xmax + d, ymax + d)

  /** Orders two bounding boxes by their (geographically) lower-left corner. The bounding box
    * that is further south (or west in the case of a tie) comes first.
    *
    * If the lower-left corners are the same, the upper-right corners are
    * compared. This is mostly to assure that 0 is only returned when the
    * extents are equal.
    *
    * Return type signals:
    *
    *   -1 this bounding box comes first
    *    0 the bounding boxes have the same lower-left corner
    *    1 the other bounding box comes first
    */
  def compare(other: Extent): Int = {
    var cmp = ymin compare other.ymin
    if (cmp != 0) return cmp

    cmp = xmin compare other.xmin
    if (cmp != 0) return cmp

    cmp = ymax compare other.ymax
    if (cmp != 0) return cmp

    xmax compare other.xmax
  }

  /** Return the smallest extent that contains this extent and the provided extent. */
  def combine(other:Extent): Extent =
    Extent(
      if(xmin < other.xmin) xmin else other.xmin,
      if(ymin < other.ymin) ymin else other.ymin,
      if(xmax > other.xmax) xmax else other.xmax,
      if(ymax > other.ymax) ymax else other.ymax
    )

  /** Return the smallest extent that contains this extent and the provided extent. */
  def expandToInclude(other: Extent): Extent =
    combine(other)

  /** Return the smallest extent that contains this extent and the provided point. */
  def expandToInclude(p: Point): Extent =
    expandToInclude(p.x, p.y)

  /** Return the smallest extent that contains this extent and the provided point. */
  def expandToInclude(x: Double, y: Double): Extent =
    Extent(
      if(xmin < x) xmin else x,
      if(ymin < y) ymin else y,
      if(xmax > x) xmax else x,
      if(ymax > y) ymax else y
    )

  /** Return an extent of this extent expanded by the provided distance on all sides */
  def expandBy(distance: Double): Extent =
    expandBy(distance, distance)


  /** Return an extent of this extent expanded by the provided x and y distances */
  def expandBy(deltaX: Double, deltaY: Double): Extent =
    Extent(
      xmin - deltaX,
      ymin - deltaY,
      xmax + deltaX,
      ymax + deltaY
    )

  /** Return this extent moved x and y amounts */
  def translate(deltaX: Double, deltaY: Double): Extent =
    Extent(
      xmin + deltaX,
      ymin + deltaY,
      xmax + deltaX,
      ymin + deltaY
    )

  /** Return this extent as a polygon */
  def toPolygon(): Polygon =
    Polygon( Line((xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin), (xmin, ymin)) )

  /** Equality check against this extent
    *
    * @note only returns true given another extent
    */
  override
  def equals(o: Any): Boolean =
    o match {
      case other: Extent =>
        xmin == other.xmin && ymin == other.ymin &&
        xmax == other.xmax && ymax == other.ymax
      case _ => false
  }

  override
  def hashCode(): Int = (xmin, ymin, xmax, ymax).hashCode

  override def toString = s"Extent($xmin, $ymin, $xmax, $ymax)"
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy