From 1672326917ac0bc5a78d889b4407f431d1866132 Mon Sep 17 00:00:00 2001 From: "Simeon H.K. fitch" Date: Thu, 7 Sep 2017 18:54:21 -0400 Subject: [PATCH] Cleaned up no-data handling and fleshed out tests. Signed-off-by: Simeon H.K. fitch --- .../render/ascii/RenderAsciiTests.scala | 89 +++++++++++++++++-- .../raster/render/AsciiRenderMethods.scala | 4 +- .../raster/render/ascii/AsciiArtEncoder.scala | 40 ++++++--- .../raster/render/ascii/NumericEncoder.scala | 45 +++++----- 4 files changed, 138 insertions(+), 40 deletions(-) diff --git a/raster-test/src/test/scala/geotrellis/raster/render/ascii/RenderAsciiTests.scala b/raster-test/src/test/scala/geotrellis/raster/render/ascii/RenderAsciiTests.scala index e639bf1294..c9932ee9ce 100644 --- a/raster-test/src/test/scala/geotrellis/raster/render/ascii/RenderAsciiTests.scala +++ b/raster-test/src/test/scala/geotrellis/raster/render/ascii/RenderAsciiTests.scala @@ -1,16 +1,33 @@ +/* + * Copyright 2017 Astraea, Inc. + * + * 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.render.ascii -import geotrellis.raster.render.ascii.AsciiArtEncoder.Settings -import geotrellis.raster.testkit.{RasterMatchers, TileBuilders} +import geotrellis.raster.io.geotiff.{GeoTiffTestUtils, SinglebandGeoTiff} +import geotrellis.raster.render.ascii.AsciiArtEncoder.Palette +import geotrellis.raster.testkit.TileBuilders import org.scalatest.{FunSuite, Inspectors, Matchers} /** + * Simple tests to check basic ASCII art rendering. * * @since 9/6/17 */ class RenderAsciiTests extends FunSuite with Matchers with TileBuilders with Inspectors { - test("generate ASCII art from tile") { + test("generate ASCII art from graident tile") { val palettes = Seq( AsciiArtEncoder.Palette.WIDE, AsciiArtEncoder.Palette.NARROW, @@ -21,9 +38,69 @@ class RenderAsciiTests extends FunSuite with Matchers with TileBuilders with Ins ) forEvery(palettes) { palette ⇒ val sample = createConsecutiveTile(palette.length) - println("-" * 80) - println(sample.renderAscii(Settings(palette))) - println("-" * 80) + val render = sample.renderAscii(palette) + forAll(palette.values) { c ⇒ + assert(render.contains(c)) + } } } + + test("generate ASCII art from image")(new GeoTiffTestUtils { + val tiff = SinglebandGeoTiff(geoTiffPath("alaska-polar-3572.tif")) + // format: off + val expected = + """∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘████ ∘∘∘∘∘∘∘∘∘∘∘∘ + |∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘█████ ████ ∘∘∘∘∘∘∘∘∘∘∘ + |∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘████████████▜ █▜ ∘∘∘∘∘∘∘∘∘∘∘ + |∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘█████▖▖▖▖█████▖▖ █ ∘∘∘∘∘∘∘∘∘∘ + |∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘██████████▜▖ ▖██▖█▖ ▖██ ∘∘∘∘∘∘∘∘∘∘ + |∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘████████████▜▖▖▖▖▖███▖▖▖▖▖ █▖ █ ∘∘∘∘∘∘∘∘∘ + |∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘██████████████████████▖ █████ ▜█ ██ ∘∘∘∘∘∘∘∘∘ + |∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘████████████████████▜▜███▖▜█████ ∘∘∘∘∘∘∘∘ + |∘∘∘∘∘∘∘∘∘∘∘∘∘████████████████████████▜█▜███████ ∘∘∘∘∘∘∘∘ + |∘∘∘∘∘∘∘∘████████████████████████████▜▖▖██████▖ ∘∘∘∘∘∘∘ + |∘∘∘∘██████████████████████████████▖▖▖ ▜▜▖▖ ▖ ▖ ▖ ∘∘∘∘∘∘∘ + |█████████████████████████████▜██▜▖▜▜▖ ███ ∘∘∘∘∘∘ + |████████████████████████████▜█▖█▜▚▜▜▖▖ ▖ ▖ ▖▖ ▖ ∘∘∘∘∘∘ + |██████████████████████████████▜▚▜▚▜█▖▖▖▖▖▖▖▖▖ ▖▖▖▖▖▖▖▖▖▖ ▖▖ ▖▖ ∘∘∘∘∘ + |∘█████████████████████████████▜▚▚▚▚▖▖▚▚▚▚▖▖█▖ ▖▖▖▖▚█▜█▜▜▖▖ ▖ ∘∘∘∘∘ + |∘████████████▜█████████████████▚▚▜▜▚▜▜▜▖▚▚▖▖▖▖▚▚▚█████████▜▖▖▖▖▖ ∘∘∘∘ + |∘█████████████▖████████████████▚▚▜▜▜▚▚▚▚▚▚▚▚▚▖▜████████████████▖▖ ▖ ▖ ∘∘∘∘ + |∘∘█████████████████████████████▚▚▚▜▚▚▚▚▜▜▜▜▜▚▚████████████████▖▖▖ ▖▖▖▖▖▖ ∘∘∘ + |∘∘███████████████████████████▜█▚▚▚▚▚▜▜▜▜▜▜▜▜▜▚█████████▜▜▜▜▖▖▖▖▖▖ ▖▖█▜▜█▖ ∘∘∘ + |∘∘▖▖████████████████████████▚▚▜▜▚▚▚▚▜▜▜▜▜▜▜▜▜▜▜█████▜▜▜▜▚▚▚▚▚▚▚▖▖▚▚▖▜██▖▖ ▖ ∘∘∘ + |∘∘∘█████████████████████████▜▚▚▚▚▚▚▚▜▜▜▜▜▜▜▜▜▚██▜▜██▜▜▜▜▜▜▜█▖▚▚▖▖██████▖▖▖▖ ∘∘ + |∘∘∘▖███████████████████████▚▚▚▚▜▚▚▚▚▜▜▜▜▜▜▜▜▚▚▚███▚▜▜▜▜▜█████▜▚▚▚██████▖▜█▜▜ ∘∘ + |∘∘∘█████████████████████████▚▜▚▚▚▚▚▚▜▜▜▜▜▚▚▚▚▚▚▜▜▜██▚▜██▚▜▜█▜▜▜████████████▖ ∘∘ + |∘∘∘∘ ▖██████████████████████▚▜▜▚▚▚▚▚▚▚▚▚▚▖▚▚▚▚▚▚▚▚▚▜█▚█▚▜▜▜▚▚▚█████████████▖ ∘ + |∘∘∘∘ ▖████████▖▖████████████▜▚▜▚▚▚▚▚▚▚▜▚▖▚▚▚▚▚▚▚▚▜▚▜▚▜▜▚▖▖▜▜▜▜▚▜▚███████████ ∘ + |∘∘∘∘∘ ▖███████▜▚▚▖▖██████████▜▜▚▜▚▚▜▚▚▚▜▜▚▚▚▜▜▜█▚▚▚▚▚▚██▜█▚▜▜▚▚▚▚▚▚▜████████▜ ∘ + |∘∘∘∘∘ ▖▖▖▖█▜▖▖▖▖▖█████████▚▜▜▜▜▚▚▚▚▚▚▚▜▜▜▚▜█▜████▚▜████▚▚▜▜▚▚█████████ ██▜ ▜ + |∘∘∘∘∘ ▜▖▖▜▜▖▖▖▖▖▖▖▖▖███████████▚▚▚▜▚▚▚▚▚▚▚▜▜▜▚█▚▚██▜██████▜▚▚▚█████████▖█▜█████ + |∘∘∘∘∘∘ ▖█▖▖▖▖▖▖▖ ▖▖▖▖▖▖█████████▜▜▚▚▚▚▖▚▚▚▚▚▚▚▚▜████████████████████▖▖▖██▖█████ + |∘∘∘∘∘∘ ▖ ▖▜████▜█████████▜▚▚███████▜█████▜██████████████████▖█▜▖████∘∘∘ + |∘∘∘∘∘∘∘ ▖███▜█▖ ▖████▚▖██▚▚▜███████████████▚████████▜▖█████████∘∘∘∘∘∘∘ + |∘∘∘∘∘∘∘ ▖▖▖▚▖▜▜███████████████████████████████∘∘∘∘∘∘∘∘∘∘∘ + |∘∘∘∘∘∘∘∘ ▖▖▖▖▜████████████████████████████∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘ + |∘∘∘∘∘∘∘∘ ▖▖▖▖██████▜████████████████∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘ + |∘∘∘∘∘∘∘∘∘ ▖▖▖▖▜██▖ ▖▖▜▜█████∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘ + |∘∘∘∘∘∘∘∘∘∘ ▖▜▖ ▖ ∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘ + |∘∘∘∘∘∘∘∘∘∘ ∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘ + |∘∘∘∘∘∘∘∘∘∘∘ ▖ ∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘ + |∘∘∘∘∘∘∘∘∘∘∘ ∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘ + |∘∘∘∘∘∘∘∘∘∘∘∘ ∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘∘""" + .stripMargin + // format: on + val scaled = tiff.tile.resample(80, 40) + val palette = Palette.STIPLED + val render = scaled.renderAscii(palette) + + println(render) + + forEvery(render.toCharArray.distinct.filter(_ != '\n'))(c ⇒ + assert(c == palette.nodata || palette.values.contains(c)) + ) + + assert(render === expected) + }) } diff --git a/raster/src/main/scala/geotrellis/raster/render/AsciiRenderMethods.scala b/raster/src/main/scala/geotrellis/raster/render/AsciiRenderMethods.scala index 245755523e..c507749bf0 100644 --- a/raster/src/main/scala/geotrellis/raster/render/AsciiRenderMethods.scala +++ b/raster/src/main/scala/geotrellis/raster/render/AsciiRenderMethods.scala @@ -27,8 +27,8 @@ import geotrellis.util.MethodExtensions */ trait AsciiRenderMethods extends MethodExtensions[Tile] { - def renderAscii(settings: AsciiArtEncoder.Settings = AsciiArtEncoder.Settings()): String = - AsciiArtEncoder.encode(self, settings) + def renderAscii(palette: AsciiArtEncoder.Palette = AsciiArtEncoder.Palette.WIDE): String = + AsciiArtEncoder.encode(self, palette) /** * Return ASCII representation of the tile, emitting a string matrix of diff --git a/raster/src/main/scala/geotrellis/raster/render/ascii/AsciiArtEncoder.scala b/raster/src/main/scala/geotrellis/raster/render/ascii/AsciiArtEncoder.scala index 7acef9a7e1..680289b21d 100644 --- a/raster/src/main/scala/geotrellis/raster/render/ascii/AsciiArtEncoder.scala +++ b/raster/src/main/scala/geotrellis/raster/render/ascii/AsciiArtEncoder.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2017 Astraea, Inc. + * + * 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.render.ascii import geotrellis.raster.Tile @@ -12,21 +28,25 @@ import spire.syntax.cfor.cfor */ object AsciiArtEncoder { - def encode(tile: Tile, settings: Settings): String = { - val palette = settings.palette + /** Convert the given tile into an ASCII art string using the given palette of + * character codes as fill values. */ + def encode(tile: Tile, palette: AsciiArtEncoder.Palette): String = { val palSize = palette.length + val opts = ColorMap.Options.DEFAULT + .copy(noDataColor = palette.nodata) val colorMap = if(tile.cellType.isFloatingPoint) { val hist = tile.histogramDouble(palSize) - ColorMap.fromQuantileBreaks(hist, palette.colorRamp) + ColorMap.fromQuantileBreaks(hist, palette.colorRamp, opts) } else { val hist = tile.histogram - ColorMap.fromQuantileBreaks(hist, palette.colorRamp) + ColorMap.fromQuantileBreaks(hist, palette.colorRamp, opts) } val intEncoded = colorMap.render(tile) val sb = new StringBuilder + cfor(0)(_ < tile.rows, _ + 1) { row => cfor(0)(_ < tile.cols, _ + 1) { col => val p = intEncoded.get(col, row) @@ -34,24 +54,22 @@ object AsciiArtEncoder { } sb += '\n' } - //sb.dropRight(1) - sb.toString + sb.dropRight(1).toString } - case class Settings(palette: Palette = Palette.WIDE) - - case class Palette(values: Array[Char]) { + case class Palette(values: Array[Char], nodata: Char = '∘') { def colorRamp: ColorRamp = ColorRamp(values.map(_.toInt)) def length: Int = values.length } object Palette { - def apply(str: String): Palette = apply(str.toCharArray) val WIDE = Palette(" .'`^\",:;Il!i><~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$") val NARROW = Palette( " .:-=+*#%@") val HATCHING = Palette(" ...,,;;---===+++xxxXX##") val FILLED = Palette(" ▤▦▩█") val STIPLED = Palette(" ▖▚▜█") - val BINARY = Palette(" #") + val BINARY = Palette(" ■") + + def apply(str: String): Palette = apply(str.toCharArray) } } diff --git a/raster/src/main/scala/geotrellis/raster/render/ascii/NumericEncoder.scala b/raster/src/main/scala/geotrellis/raster/render/ascii/NumericEncoder.scala index 8e67acf41e..8fdca4b1cf 100644 --- a/raster/src/main/scala/geotrellis/raster/render/ascii/NumericEncoder.scala +++ b/raster/src/main/scala/geotrellis/raster/render/ascii/NumericEncoder.scala @@ -1,5 +1,5 @@ /* - * Copyright 2017 Azavea + * Copyright 2016-2017 Azavea * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -72,7 +72,28 @@ case object NumericEncoder { formatTileString(buff, max, tile.cols, tile.rows) } - /** Helper. */ + /** + * Returns an ASCII string representation of a subset of the tile, with cell + * values encoded as hexadecimal integers. + */ + def rangeEncode(tile: Tile, colMin: Int, colMax: Int, rowMin: Int, rowMax: Int): String = { + val buff = ArrayBuffer[String]() + + for (row <- rowMin to rowMax) { + for (col <- colMin to colMax) { + val z = tile.get(row, col) + if (isNoData(z)) { + buff += ".." + } else { + buff += "%02X".formatLocal(Locale.ENGLISH, z) + } + } + buff += "\n" + } + buff.toString + } + + /** Stringization helper. */ private def formatTileString(buff: ArrayBuffer[String], cols: Int, rows: Int, maxSize: Int) = { val sb = new StringBuilder val limit = math.max(6, maxSize) @@ -88,23 +109,5 @@ case object NumericEncoder { sb.toString } - /** - * Returns an ASCII string representation of a subset of the tile, with cell - * values encoded as hexadecimal integers. - */ - def rangeEncode(tile: Tile, colMin: Int, colMax: Int, rowMin: Int, rowMax: Int): String = { - var s = "" - for (row <- rowMin to rowMax) { - for (col <- colMin to colMax) { - val z = tile.get(row, col) - if (isNoData(z)) { - s += ".." - } else { - s += "%02X".formatLocal(Locale.ENGLISH, z) - } - } - s += "\n" - } - s - } + }