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

geotrellis.raster.Tile.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

import geotrellis.vector.Extent
import geotrellis.raster.summary._

import spire.syntax.cfor._

import java.util.Locale

import math.BigDecimal

import collection.mutable.ArrayBuffer


/**
  * Base trait for a Tile.
  */
trait Tile extends CellGrid with IterableTile with MappableTile[Tile] {

  /**
    * Execute a function at each pixel of a [[Tile]].  Two functions
    * are given: an integer version which is used if the tile is an
    * integer-tile, and the other in the case of a floating-tile.
    *
    * @param  f  A function from Int to Unit
    * @param  g  A function from Double to Unit
    */
  def dualForeach(f: Int => Unit)(g: Double => Unit): Unit =
    if (cellType.isFloatingPoint) foreachDouble(g) else foreach(f)

  /**
    * Conditionally execute (or don't) the given function at each
    * pixel of a [[Tile]], depending on whether that pixel is NODATA or
    * not.  The result of the mapping is returned as a tile.
    *
    * @param  f  A function from Int to Int
    */
  def mapIfSet(f: Int => Int): Tile =
    map { i =>
      if(isNoData(i)) i
      else f(i)
    }

  /**
    * Conditionally execute (or don't) the given function at each
    * pixel of a [[Tile]], depending on whether that pixel is NODATA or
    * not.  The result of the mapping is returned as a tile.
    *
    * @param  f  A function from Double to Double
    */
  def mapIfSetDouble(f: Double => Double): Tile =
    mapDouble { d =>
      if(isNoData(d)) d
      else f(d)
    }

  /**
    * Map one of the two given functions across the [[Tile]] to
    * produce a new one.  One of the functions is from Int to Int, and
    * the other from Double to Double.
    *
    * @param  f  A function from Int to Int
    * @param  g  A function from Double to Double
    */
  def dualMap(f: Int => Int)(g: Double => Double): Tile =
    if (cellType.isFloatingPoint) mapDouble(g) else map(f)

  /**
    * Conditionally map across the [[Tile]] with one of two functions,
    * depending on whether the tile is an integer- or a floating-tile.
    * A pixel is mapped only if it is set.
    *
    * @param  f  A function from Int to Int
    * @param  g  A function from Double to Double
    */
  def dualMapIfSet(f: Int => Int)(g: Double => Double): Tile =
    if (cellType.isFloatingPoint) mapIfSetDouble(g) else mapIfSet(f)

  /**
    * Combine two [[Tile]]s together using one of two given functions.
    * If the union of the types of the two cells is floating-point,
    * then the floating function is used, otherwise the integer
    * function is used.
    *
    * @param  r2  The tile to combine with the present one
    * @param  f   The integer function
    * @param  g   The double function
    */
  def dualCombine(r2: Tile)(f: (Int, Int) => Int)(g: (Double, Double) => Double): Tile =
    if (cellType.union(r2.cellType).isFloatingPoint) combineDouble(r2)(g) else combine(r2)(f)

  /**
    * Create a mutable copy of this tile
    */
  def mutable: MutableArrayTile

  /** Converts the cell type of the tile.
    *
    * @note    This will immediately iterate over the tile and allocate a new
    *          copy of data; this should be a performance consideration.
    */
  def convert(cellType: CellType): Tile

  /**
    * Get value at given coordinates.
    */
  def get(col: Int, row: Int): Int

  /**
    * Get value at given coordinates.
    */
  def getDouble(col: Int, row: Int): Double

  /**
    * Convert the present [[Tile]] to an [[ArrayTile]].
    */
  def toArrayTile(): ArrayTile

  /**
    * Return the data behind this [[Tile]], or a copy, as an Array of
    * integers.
    */
  def toArray(): Array[Int]

  /**
    * Return the data behind this [[Tile]], or a copy, as an Array of
    * doubles.
    */
  def toArrayDouble(): Array[Double]

  /**
    * Return the data behind this [[Tile]], or a copy, as an Array of
    * bytes.
    */
  def toBytes(): Array[Byte]

  /**
    * Execute the given function at each pixel of the present
    * [[Tile]].
    */
  def foreach(f: Int=>Unit): Unit

  /**
    * Execute the given function at each pixel of the present
    * [[Tile]].
    */
  def foreachDouble(f: Double=>Unit): Unit

  /**
    * Map the given function across the present [[Tile]].  The result
    * is another Tile.
    */
  def map(f: Int => Int): Tile

  /**
    * Combine the given [[Tile]] with the present one using the given
    * function.
    */
  def combine(r2: Tile)(f: (Int, Int) => Int): Tile

  /**
    * Map the given function across the present [[Tile]].  The result
    * is another Tile.
    */
  def mapDouble(f: Double => Double): Tile

  /**
    * Combine the given [[Tile]] with the present one using the given
    * function.
    */
  def combineDouble(r2: Tile)(f: (Double, Double) => Double): Tile

  def isNoDataTile: Boolean = {
    var (c, r) = (0, 0)
    while (r < rows) {
      while(c < cols) {
        if(cellType.isFloatingPoint) { if (isData(getDouble(c, r))) return false }
        else { if(isData(get(c, r))) return false }
        c += 1
      }
      c = 0; r += 1
    }

    true
  }

  /**
    * Normalizes the values of this raster, given the current min and
    * max, to a new min and max.
    *
    * @param oldMin  Old minimum value
    * @param oldMax  Old maximum value
    * @param newMin  New minimum value
    * @param newMax  New maximum value
    */
  def normalize(oldMin: Int, oldMax: Int, newMin: Int, newMax: Int): Tile = {
    val dnew = newMax - newMin
    val dold = oldMax - oldMin
    if(dold <= 0 || dnew <= 0) { sys.error(s"Invalid parameters: $oldMin, $oldMax, $newMin, $newMax") }
    mapIfSet(z => ( ((z - oldMin) * dnew) / dold ) + newMin)
  }

  /**
    * Normalizes the values of this raster, given the current min and
    * max, to a new min and max.
    *
    * @param oldMin  Old minimum value
    * @param oldMax  Old maximum value
    * @param newMin  New minimum value
    * @param newMax  New maximum value
    */
  def normalize(oldMin: Double, oldMax: Double, newMin: Double, newMax: Double): Tile = {
    val dnew = newMax - newMin
    val dold = oldMax - oldMin
    if(dold <= 0 || dnew <= 0) { sys.error(s"Invalid parameters: $oldMin, $oldMax, $newMin, $newMax") }
    mapIfSetDouble(z => ( ((z - oldMin) * dnew) / dold ) + newMin)
  }

  /**
    * Rescale the values in this [[Tile]] so that they are between the
    * two given values.
    */
  def rescale(newMin: Int, newMax: Int): Tile = {
    val (min, max) = findMinMax
    normalize(min, max, newMin, newMax)
  }

  /**
    * Rescale the values in this [[Tile]] so that they are between the
    * two given values.
    */
  def rescale(newMin: Double, newMax: Double): Tile = {
    val (min, max) = findMinMaxDouble
    normalize(min, max, newMin, newMax)
  }

  /**
    * Reduce the resolution of the present [[Tile]] to the given
    * number of columns and rows.  A new Tile is returned.
    *
    * @param  newCols  The number of columns in the new Tile
    * @param  newRows  The number of rows in the new Tile
    */
  def downsample(newCols: Int, newRows: Int)(f: CellSet => Int): Tile = {
    val colsPerBlock = math.ceil(cols / newCols.toDouble).toInt
    val rowsPerBlock = math.ceil(rows / newRows.toDouble).toInt

    val tile = ArrayTile.empty(cellType, newCols, newRows)

    trait DownsampleCellSet extends CellSet { def focusOn(col: Int, row: Int): Unit }

    val cellSet =
      new DownsampleCellSet {
        private var focusCol = 0
        private var focusRow = 0

        def focusOn(col: Int, row: Int) = {
          focusCol = col
          focusRow = row
        }

        def foreach(f: (Int, Int)=>Unit): Unit = {
          var col = 0
          while(col < colsPerBlock) {
            var row = 0
            while(row < rowsPerBlock) {
              f(focusCol * colsPerBlock + col, focusRow * rowsPerBlock + row)
              row += 1
            }
            col += 1
          }
        }
      }

    var col = 0
    while(col < newCols) {
      var row = 0
      while(row < newRows) {
        cellSet.focusOn(col, row)
        tile.set(col, row, f(cellSet))
        row += 1
      }
      col += 1
    }
    tile
  }

  /**
    * Return tuple of highest and lowest value in raster.
    *
    * @note Currently does not support double valued raster data types
    *       (FloatConstantNoDataCellType,
    *       DoubleConstantNoDataCellType). Calling findMinMax on
    *       rasters of those types will give the integer min and max
    *       of the rounded values of their cells.
    */
  def findMinMax: (Int, Int) = {
    var zmin = Int.MaxValue
    var zmax = Int.MinValue

    foreach {
      z => if (isData(z)) {
        zmin = math.min(zmin, z)
        zmax = math.max(zmax, z)
      }
    }

    if(zmin == Int.MaxValue) { zmin = NODATA }
    (zmin, zmax)
  }

  /**
    * Return tuple of highest and lowest value in raster.
    */
  def findMinMaxDouble: (Double, Double) = {
    var zmin = Double.NaN
    var zmax = Double.NaN

    foreachDouble {
      z => if (isData(z)) {
        if(isNoData(zmin)) {
          zmin = z
          zmax = z
        } else {
          zmin = math.min(zmin, z)
          zmax = math.max(zmax, z)
        }
      }
    }

    (zmin, zmax)
  }

  /**
    * Return ascii art of this raster.
    */
  def asciiDraw(): String = {
    val buff = ArrayBuffer[String]()
    var max = 0
    cfor(0)(_ < rows, _ + 1) { row =>
      cfor(0)(_ < cols, _ + 1) { col =>
        val v = get(col, row)
        val s = if (isNoData(v)) "ND" else s"$v"
        max = math.max(max, s.size)
        buff += s
      }
    }

    createAsciiTileString(buff.toArray, max)
  }

  /**
    * Return ascii art of this raster. The single int parameter
    * indicates the number of significant digits to be printed.
    */
  def asciiDrawDouble(significantDigits: Int = Int.MaxValue): String = {
    val buff = ArrayBuffer[String]()
    val mc = new java.math.MathContext(significantDigits)
    var max = 0
    cfor(0)(_ < rows, _ + 1) { row =>
      cfor(0)(_ < cols, _ + 1) { col =>
        val v = getDouble(col, row)
        val s = if (isNoData(v)) "ND" else {
          val s = s"$v"
          if (s.size > significantDigits) BigDecimal(s).round(mc).toString
          else s
        }

        max = math.max(s.size, max)
        buff += s
      }
    }

    createAsciiTileString(buff.toArray, max)
  }

  private def createAsciiTileString(buff: Array[String], maxSize: Int) = {
    val sb = new StringBuilder
    val limit = math.max(6, maxSize)
    cfor(0)(_ < rows, _ + 1) { row =>
      cfor(0)(_ < cols, _ + 1) { col =>
        val s = buff(row * cols + col)
        val pad = " " * math.max(limit - s.length, 1)
        sb.append(s"$pad$s")
      }

      sb += '\n'
    }
    sb += '\n'
    sb.toString
  }

  /**
    * Return ascii art of a range from this raster.
    */
  def asciiDrawRange(colMin: Int, colMax: Int, rowMin: Int, rowMax: Int): String = {
    var s = ""
    for (row <- rowMin to rowMax) {
      for (col <- colMin to colMax) {
        val z = this.get(row, col)
        if (isNoData(z)) {
          s += ".."
        } else {
          s += "%02X".formatLocal(Locale.ENGLISH, z)
        }
      }
      s += "\n"
    }
    s
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy