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

geotrellis.raster.mapalgebra.focal.Cursor.scala Maven / Gradle / Ivy

/*
 * Copyright (c) 2014 Azavea.
 * 
 * Licensed 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 geotrellis.raster.mapalgebra.focal

import geotrellis.raster._

import scala.math.{min, max}

sealed trait Movement { val isVertical: Boolean }

/**
 * Movements used to move a [[Cursor]] around, and to track it's movements.
 */
object Movement { 
  val Up = new Movement { val isVertical = true }
  val Down = new Movement { val isVertical = true }
  val Left = new Movement { val isVertical = false }
  val Right = new Movement { val isVertical = false }
  val NoMovement = new Movement { val isVertical = false }
}

import Movement._


object Cursor {
  /** Creates a cursor from a raster and a neighborhood
   *
   * This will create a [[Cursor]] based on the raster and neighborhood extent,
   * and will apply the neighborhood mask if it has one.
   *
   * @param    r          Tile the [[Cursor]] will be for.
   * @param    n          Neighborhood this [[Cursor]] will be based on,
   *                      for extent and mask.
   * @param analysisArea  Analysis area
   */
  def apply(r: Tile, n: Neighborhood, analysisArea: GridBounds): Cursor = {
    val result = new Cursor(r, analysisArea, n.extent)
    if(n.hasMask) { result.setMask(n.mask) }
    result
  }

  def apply(r: Tile, n: Neighborhood): Cursor =  apply(r, n, GridBounds(r))

  def apply(r: Tile, extent: Int): Cursor = new Cursor(r, GridBounds(r), extent)
}

/**
 * Represents a cursor that can be used to iterate over cells within a focal
 * neighborhood.
 *
 * @param      r                     Tile that this cursor runs over
 * @param      analysisArea          Analysis area
 * @param      extent                The distance from the focus that the
 *                                   bounding box of this cursor extends.
 *                                   e.g. if the bounding box is 3x3, then
 *                                   the distance from center is 1.
 */
class Cursor(r: Tile, analysisArea: GridBounds, val extent: Int) {
  private val rows = r.rows
  private val cols = r.cols

  // How many columns from the left/top of the input raster does the analysis area begin?
  val analysisOffsetCols = analysisArea.colMin
  val analysisOffsetRows = analysisArea.rowMin

  private val d = 2 * extent + 1

  private var mask: CursorMask = null
  private var hasMask = false

  // Values to track the bound of the cursor
  private var _colmin = 0
  private var _colmax = 0
  private var _rowmin = 0
  private var _rowmax = 0

  protected def colmin = _colmin
  protected def colmax = _colmax
  protected def rowmin = _rowmin
  protected def rowmax = _rowmax


  // Values to track added\removed values
  private var addedCol = 0
  private var removedCol = 0

  private var addedRow = 0
  private var removedRow = 0

  var movement = NoMovement

  // Values to track the focus of the cursor
  private var _col = 0
  private var _row = 0

  protected def focusCol = _col
  protected def focusRow = _row

  /** Indicates whether or not this cursor has been moved and is tracking state between
   *  the previous position and the current position */
  def isReset = movement == NoMovement

  /** 
   *  Cursor column relative to the analysis area.
   *
   *  For example, if the analysis area starts at col 2 and the focusX is currently 3,
   *  then the col should be 1. 
   */
  def col = _col - analysisOffsetCols 

  /** Cursor row relative to the analysis area */
  def row = _row - analysisOffsetRows

  /**
   * Centers the cursor on a cell of the raster.
   * Added\Removed cells are not kept track of between centering moves,
   * and centering the cursor resets the state.
   *
   * @param   col    Column of raster to center on.
   * @param   row    Row of raster to center on.
   */
  def centerOn(col: Int, row: Int) = { 
    movement = NoMovement
    _col = col
    _row = row

    _colmin = max(0, _col - extent)
    _colmax = min(cols - 1, _col + extent)
    _rowmin = max(0, _row - extent)
    _rowmax = min(rows - 1, _row + extent)
  }

  /**
   * Move the cursor one cell space in a horizontal
   * of vertical direction. The cursor will keep track
   * of what cells became added by this move (covered by the cursor
   * or unmasked), and what cells became removed by this move
   * (no longer covered by the cursor or masked when previously unmasked).
   * The cursor will only keep the state of one move, so if two moves
   * are done in a row, the state of the first move is forgotten. Only
   * the difference between the cursor and it's most recent previous position
   * are accounted for.
   *
   * param     m     Movement enum that represents moving the cursor
   *                 Up, Down, Left or Right.
   */
  def move(m: Movement) = {
    movement = m
    m match {
      case Up => 
        addedRow = _rowmin - 1
        removedRow = _row + extent
        _row -= 1
      case Down =>
        addedRow = _rowmax + 1
        removedRow = _row - extent
        _row += 1
      case Left =>
        addedCol = _colmin - 1
        removedCol = _col + extent
        _col -= 1
      case Right =>
        addedCol = _colmax + 1
        removedCol = _col - extent
        _col += 1
      case _ => 
    }

    _colmin = max(0, _col - extent)
    _colmax = min(cols - 1, _col + extent)
    _rowmin = max(0, _row - extent)
    _rowmax = min(rows - 1, _row + extent)
  }

  /** Sets the mask for this cursor.
   *
   * @param     f    Function that takes a col and row of the neighborhood coordinates
   *                 and returns true if that cell should be masked.
   *                 The neighborhood coordinates are the size of the cursor's
   *                 bounding box, with (0, 0) being the top right corner.
   */
  def setMask(f: (Int, Int) => Boolean) = {
    hasMask = true
    mask = new CursorMask(d, f)
  }

  /** A [[CellSet]] reperesenting all unmasked cells that are within the cursor bounds. */
  val allCells = new CellSet {
    def foreach(f: (Int, Int)=>Unit) = Cursor.this.foreach(f)
  }

  /** A [[CellSet]] reperesenting unmasked cells currently within the cursor bounds,
   *  that were added by the previous cursor movement. If the cursor has not been moved
   *  (i.e. if isReset == true) then addedCells represents the same thing
   *  as allCells.
   */
  val addedCells = new CellSet {
    def foreach(f: (Int, Int)=>Unit) = { Cursor.this.foreachAdded(f) }
  }

  /** A [[CellSet]] reperesenting cells that were moved outside the cursor bounds,
   *  or unmasked cells that were masked, by the previous cursor movement.
   *  If the cursor has not been moved this will be a no-op.
   */
  val removedCells = new CellSet {
    def foreach(f: (Int, Int)=>Unit) = Cursor.this.foreachRemoved(f)
  }

  /**
   * Iterates over all cell values of the raster which
   * are covered by the cursor and not masked.
   *
   * @param     f         Function that receives from each cell
   *                      it's col and row coordinates and it's value.
   */
  protected def foreach(f: (Int, Int)=>Unit): Unit = {
    if(!hasMask) {
      var y = _rowmin
      var x = 0
      while(y <= _rowmax) {
        x = _colmin
        while(x <= _colmax) {
          f(x, y)
          x += 1
        }
        y += 1
      }
    } else {
      var y = 0
      while(y < d) {
        mask.foreachX(y) { x =>
          val xTile = x + (_col - extent)
          val yTile = y + (_row - extent)
          if(_colmin <= xTile && xTile <= _colmax && _rowmin <= yTile && yTile <= _rowmax) {
            f(xTile, yTile)
          }
        }
        y += 1
      }
    }
  }

  /**
   * Iterates over all cell values of the raster which
   * are covered by the cursor and not masked, that were exposed
   * as part of the last move of the cursor.
   *
   * For instance, if move(Movement.Up) is called, then there will
   * potentially be a new row that is now covered by the cursor,
   * which are now covered. These values will be included for the
   * iterations of this function, as well any previously masked
   * cell values that were unmasked as part of the move.
   *
   * @param     f         Function that receives from each cell it's
   *                      col and row coordinates and it's value.
   */
  protected def foreachAdded(f: (Int, Int)=>Unit): Unit = {
    if(movement == NoMovement) {
      foreach(f) 
    } else if (movement.isVertical) {
      if(0 <= addedRow && addedRow < rows) {
        if(!hasMask) {
          var x = _colmin
          while(x <= _colmax) {
            f(x, addedRow)
            x += 1
          }
        } else {
          mask.foreachX(addedRow - (_row - extent)) { x =>
            val xTile = x + (_col - extent)
            if(0 <= xTile && xTile <= cols) {
              f(xTile, addedRow)
            }
          }
        }
      }        
    } else { // Horizontal
      if(0 <= addedCol && addedCol < cols) {
        if(!hasMask) {
          var y = _rowmin
          while(y <= _rowmax) {
            f(addedCol, y)
            y += 1
          }
        } else {
          if(movement == Left) {
            mask.foreachWestColumn { y =>
              val yTile = y + (_row - extent)
              if(0 <= yTile && yTile < rows) {
                f(addedCol, yTile)
              }
            }
          } else { // Right
            mask.foreachEastColumn { y =>
              val yTile = y + (_row - extent)
              if(0 <= yTile && yTile < rows) {
                f(addedCol, yTile)
              }
            }
          }
        }
      }        
    }

    if(hasMask) {
      mask.foreachUnmasked(movement) { (x, y) =>
        val xTile = x + (_col - extent)
        val yTile = y + (_row - extent)
        if(0 <= xTile && xTile < cols && 0 <= yTile && yTile < rows) {
          f(xTile, yTile)
        }
      }
    }
  }

  /**
   * Iterates over all cell values of the raster which
   * are no longer covered by the cursor 
   * as part of the last move last move of the cursor.
   *
   * For instance, if move(Movement.Up) is called, then there will
   * potentially be a new row at the bottom of the cursor that is now
   * uncovered by the cursor. These values will be included for the
   * iterations of this function, as well any previously unmasked
   * cell values that were masked as part of the move.
   *
   * @param     f         Function that receives from each cell it's
   *                      col and row coordinates and it's value.
   */
  protected def foreachRemoved(f: (Int, Int)=>Unit): Unit = {
    if(movement == NoMovement) { return }

    if(movement.isVertical) {
      if(0 <= removedRow && removedRow < rows) {
        if(!hasMask) {
          var x = _colmin
          while(x <= _colmax) {
            f(x, removedRow)
            x += 1
          }
        } else {
          if(movement == Up) {
            mask.foreachX(d - 1) { x =>
              val xTile = x + (_col - extent)
              if(0 <= xTile && xTile < cols) {
                f(xTile, removedRow)
              }
            }
          }
          else { // Down
            mask.foreachX(0) { x =>
              val xTile = x + (_col - extent)
              if(0 <= xTile && xTile < cols) {
                f(xTile, removedRow)
              }
            }
          }
        }
      }
    } else { // Horizontal
      if(0 <= removedCol && removedCol < cols) {
        if(!hasMask) {
          var y = _rowmin
          while(y <= _rowmax) {
            f(removedCol, y)
            y += 1
          }
        } else {
          if(movement == Left) {
            mask.foreachEastColumn { y =>
              val yTile = y + (_row - extent)
              if(0 <= yTile && yTile < rows) {
                f(removedCol, yTile)
              }
            }
          } else { //Right
            mask.foreachWestColumn { y =>
              val yTile = y + (_row - extent)
              if(0 <= yTile && yTile < rows) {
                f(removedCol, yTile)
              }
            }
          }
        }
      }
    }

    if(hasMask) {
      mask.foreachMasked(movement) { (x, y) =>
        val xTile = x + (_col - extent)
        val yTile = y + (_row - extent)
        if(0 <= xTile && xTile < cols && 0 <= yTile && yTile < rows) {
          f(xTile, yTile)
        }
      }
    }
  }

  def asciiDraw: String = {
    val sb = new StringBuilder
    var row = 0
    allCells.foreach { (cl, rw) =>
      if(row != rw) { sb.append("\n") ; row += 1 }
      val s = r.get(cl, rw).toString
      val pad = " " * math.max(6 - s.size, 0)
      sb.append(s"$pad$s")
    }
    sb.toString
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy