PageRenderTime 28ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/geomesa-raster/src/main/scala/org/locationtech/geomesa/raster/util/RasterUtils.scala

https://gitlab.com/zachcoyle/geomesa
Scala | 227 lines | 191 code | 22 blank | 14 comment | 33 complexity | cb7462a4907d4539993e3bd4243b66d2 MD5 | raw file
  1. /***********************************************************************
  2. * Copyright (c) 2013-2016 Commonwealth Computer Research, Inc.
  3. * All rights reserved. This program and the accompanying materials
  4. * are made available under the terms of the Apache License, Version 2.0
  5. * which accompanies this distribution and is available at
  6. * http://www.opensource.org/licenses/apache2.0.php.
  7. *************************************************************************/
  8. package org.locationtech.geomesa.raster.util
  9. import java.awt.RenderingHints
  10. import java.awt.image.{BufferedImage, Raster => JRaster, RenderedImage}
  11. import java.io.{ByteArrayInputStream, ByteArrayOutputStream, ObjectInputStream, ObjectOutputStream}
  12. import java.util.{Hashtable => JHashtable}
  13. import javax.media.jai.remote.SerializableRenderedImage
  14. import org.geotools.coverage.grid.GridGeometry2D
  15. import org.geotools.geometry.jts.ReferencedEnvelope
  16. import org.geotools.referencing.crs.DefaultGeographicCRS
  17. import org.locationtech.geomesa.raster.data.Raster
  18. import org.opengis.geometry.Envelope
  19. import scala.reflect.runtime.universe._
  20. object RasterUtils {
  21. val defaultBufferedImage = new BufferedImage(5, 5, BufferedImage.TYPE_INT_RGB)
  22. object IngestRasterParams {
  23. val ACCUMULO_INSTANCE = "geomesa-tools.ingestraster.instance"
  24. val ZOOKEEPERS = "geomesa-tools.ingestraster.zookeepers"
  25. val ACCUMULO_MOCK = "geomesa-tools.ingestraster.useMock"
  26. val ACCUMULO_USER = "geomesa-tools.ingestraster.user"
  27. val ACCUMULO_PASSWORD = "geomesa-tools.ingestraster.password"
  28. val AUTHORIZATIONS = "geomesa-tools.ingestraster.authorizations"
  29. val VISIBILITIES = "geomesa-tools.ingestraster.visibilities"
  30. val FILE_PATH = "geomesa-tools.ingestraster.path"
  31. val FORMAT = "geomesa-tools.ingestraster.format"
  32. val TIME = "geomesa-tools.ingestraster.time"
  33. val TABLE = "geomesa-tools.ingestraster.table"
  34. val WRITE_MEMORY = "geomesa-tools.ingestraster.write.memory"
  35. val WRITE_THREADS = "geomesa-tools.ingestraster.write.threads"
  36. val QUERY_THREADS = "geomesa-tools.ingestraster.query.threads"
  37. val PARLEVEL = "geomesa-tools.ingestraster.parallel.level"
  38. val IS_TEST_INGEST = "geomesa.tools.ingestraster.is-test-ingest"
  39. }
  40. def imageSerialize(image: RenderedImage): Array[Byte] = {
  41. val buffer: ByteArrayOutputStream = new ByteArrayOutputStream
  42. val out: ObjectOutputStream = new ObjectOutputStream(buffer)
  43. val serializableImage = new SerializableRenderedImage(image, true)
  44. try {
  45. out.writeObject(serializableImage)
  46. } finally {
  47. out.close
  48. }
  49. buffer.toByteArray
  50. }
  51. def imageDeserialize(imageBytes: Array[Byte]): RenderedImage = {
  52. val in: ObjectInputStream = new ObjectInputStream(new ByteArrayInputStream(imageBytes))
  53. var read: RenderedImage = null
  54. try {
  55. read = in.readObject.asInstanceOf[RenderedImage]
  56. } finally {
  57. in.close
  58. }
  59. read
  60. }
  61. def allocateBufferedImage(width: Int, height: Int, chunk: RenderedImage): BufferedImage = {
  62. val properties = new JHashtable[String, Object]
  63. if (chunk.getPropertyNames != null) {
  64. chunk.getPropertyNames.foreach(name => properties.put(name, chunk.getProperty(name)))
  65. }
  66. val colorModel = chunk.getColorModel
  67. val alphaPremultiplied = colorModel.isAlphaPremultiplied
  68. val sampleModel = chunk.getSampleModel.createCompatibleSampleModel(width, height)
  69. val emptyRaster = JRaster.createWritableRaster(sampleModel, null)
  70. new BufferedImage(colorModel, emptyRaster, alphaPremultiplied, properties)
  71. }
  72. def allocateBufferedImage(width: Int, height: Int, buf: BufferedImage): BufferedImage = {
  73. val colorModel = buf.getColorModel
  74. val alphaPremultiplied = colorModel.isAlphaPremultiplied
  75. val emptyRaster = colorModel.createCompatibleWritableRaster(width, height)
  76. new BufferedImage(colorModel, emptyRaster, alphaPremultiplied, null)
  77. }
  78. def renderedImageToBufferedImage(r: RenderedImage): BufferedImage = {
  79. val properties = new JHashtable[String, Object]
  80. if (r.getPropertyNames != null) {
  81. r.getPropertyNames.foreach(name => properties.put(name, r.getProperty(name)))
  82. }
  83. val colorModel = r.getColorModel
  84. val alphaPremultiplied = colorModel.isAlphaPremultiplied
  85. val sampleModel = r.getSampleModel
  86. new BufferedImage(colorModel, r.copyData(null), alphaPremultiplied, properties)
  87. }
  88. def writeToMosaic(mosaic: BufferedImage, raster: Raster, env: Envelope, resX: Double, resY: Double) = {
  89. val croppedRaster = cropRaster(raster, env)
  90. croppedRaster.foreach{ cropped =>
  91. val rasterEnv = raster.referencedEnvelope.intersection(envelopeToReferencedEnvelope(env))
  92. val originX = Math.floor((rasterEnv.getMinX - env.getMinimum(0)) / resX).toInt
  93. val originY = Math.floor((env.getMaximum(1) - rasterEnv.getMaxY) / resY).toInt
  94. mosaic.getRaster.setRect(originX, originY, cropped.getData)
  95. }
  96. }
  97. // TODO: refactor https://geomesa.atlassian.net/browse/GEOMESA-869. Probably move this to a new object also...
  98. def mosaicChunks(chunks: Iterator[Raster], queryWidth: Int, queryHeight: Int, queryEnv: Envelope): (BufferedImage, Int) = {
  99. if (chunks.isEmpty) {
  100. (null, 0)
  101. } else {
  102. val firstRaster = chunks.next()
  103. if (!chunks.hasNext) {
  104. val croppedRaster = cropRaster(firstRaster, queryEnv)
  105. croppedRaster match {
  106. case None => (null, 1)
  107. case Some(buf) => (scaleBufferedImage(queryWidth, queryHeight, buf), 1)
  108. }
  109. } else {
  110. val accumuloRasterXRes = firstRaster.referencedEnvelope.getSpan(0) / firstRaster.chunk.getWidth
  111. val accumuloRasterYRes = firstRaster.referencedEnvelope.getSpan(1) / firstRaster.chunk.getHeight
  112. //TODO: check for corner cases: https://geomesa.atlassian.net/browse/GEOMESA-758
  113. val mosaicX = Math.round(queryEnv.getSpan(0) / accumuloRasterXRes).toInt
  114. val mosaicY = Math.round(queryEnv.getSpan(1) / accumuloRasterYRes).toInt
  115. if (mosaicX <= 0 || mosaicY <= 0) {
  116. (null, 1)
  117. } else {
  118. var count = 1
  119. val mosaic = allocateBufferedImage(mosaicX, mosaicY, firstRaster.chunk)
  120. writeToMosaic(mosaic, firstRaster, queryEnv, accumuloRasterXRes, accumuloRasterYRes)
  121. while (chunks.hasNext) {
  122. writeToMosaic(mosaic, chunks.next(), queryEnv, accumuloRasterXRes, accumuloRasterYRes)
  123. count += 1
  124. }
  125. (scaleBufferedImage(queryWidth, queryHeight, mosaic), count)
  126. }
  127. }
  128. }
  129. }
  130. def scaleBufferedImage(newWidth: Int, newHeight: Int, image: BufferedImage): BufferedImage = {
  131. if (image.getWidth == newWidth && image.getHeight == newHeight) {
  132. image
  133. } else {
  134. if (newWidth < 1 || newHeight < 1) null
  135. else {
  136. val result = allocateBufferedImage(newWidth, newHeight, image)
  137. val resGraphics = result.createGraphics()
  138. //TODO scaling can fail with floating points, very small rasters, etc: https://geomesa.atlassian.net/browse/GEOMESA-869
  139. resGraphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR)
  140. resGraphics.drawImage(image, 0, 0, newWidth, newHeight, null)
  141. resGraphics.dispose()
  142. result
  143. }
  144. }
  145. }
  146. def cropRaster(raster: Raster, cropEnv: Envelope): Option[BufferedImage] = {
  147. val rasterEnv = raster.referencedEnvelope
  148. val intersection = rasterEnv.intersection(envelopeToReferencedEnvelope(cropEnv))
  149. if (intersection.equals(rasterEnv)) {
  150. // If the intersection of the crop envelope and tile is equal to the tile envelope we are done.
  151. Some(renderedImageToBufferedImage(raster.chunk))
  152. } else {
  153. // Check the area of the intersection to ensure it is at least 1x1 pixels
  154. val chunkWidth = raster.chunk.getWidth
  155. val chunkHeight = raster.chunk.getHeight
  156. val chunkXRes = rasterEnv.getWidth / chunkWidth
  157. val chunkYRes = rasterEnv.getHeight / chunkHeight
  158. //TODO: check for corner cases: https://geomesa.atlassian.net/browse/GEOMESA-758
  159. val widthPixels = Math.round(intersection.getWidth / chunkXRes)
  160. val heightPixels = Math.round(intersection.getHeight / chunkYRes)
  161. if (widthPixels > 0 && heightPixels > 0) {
  162. // Now that we know the area is at least 1x1 perform the cropping operation
  163. val uLX = Math.max(Math.floor((intersection.getMinX - rasterEnv.getMinimum(0)) / chunkXRes).toInt, 0)
  164. val uLY = Math.max(Math.floor((rasterEnv.getMaximum(1) - intersection.getMaxY) / chunkYRes).toInt, 0)
  165. val tempWidth = Math.max(Math.ceil(intersection.getWidth / chunkXRes).toInt, 0)
  166. val finalWidth = if (tempWidth + uLX > chunkWidth) chunkWidth - uLX else tempWidth
  167. val tempHeight = Math.max(Math.ceil(intersection.getHeight / chunkYRes).toInt, 0)
  168. val finalHeight = if (tempHeight + uLY > chunkHeight) chunkHeight - uLY else tempHeight
  169. val bufferedChunk = renderedImageToBufferedImage(raster.chunk)
  170. Some(bufferedChunk.getSubimage(uLX, uLY, finalWidth, finalHeight))
  171. } else None
  172. }
  173. }
  174. def envelopeToReferencedEnvelope(e: Envelope): ReferencedEnvelope = {
  175. new ReferencedEnvelope(e.getMinimum(0),
  176. e.getMaximum(0),
  177. e.getMinimum(1),
  178. e.getMaximum(1),
  179. DefaultGeographicCRS.WGS84)
  180. }
  181. def getNewImage[T: TypeTag](w: Int, h: Int, fill: Array[T],
  182. imageType: Int = BufferedImage.TYPE_BYTE_GRAY): BufferedImage = {
  183. val image = new BufferedImage(w, h, imageType)
  184. val wr = image.getRaster
  185. val setPixel: (Int, Int) => Unit = typeOf[T] match {
  186. case t if t =:= typeOf[Int] =>
  187. (i, j) => wr.setPixel(j, i, fill.asInstanceOf[Array[Int]])
  188. case t if t =:= typeOf[Float] =>
  189. (i, j) => wr.setPixel(j, i, fill.asInstanceOf[Array[Float]])
  190. case t if t =:= typeOf[Double] =>
  191. (i, j) => wr.setPixel(j, i, fill.asInstanceOf[Array[Double]])
  192. case _ =>
  193. throw new IllegalArgumentException(s"Error, cannot handle Arrays of type: ${typeOf[T]}")
  194. }
  195. for (i <- 0 until h; j <- 0 until w) { setPixel(i, j) }
  196. image
  197. }
  198. case class sharedRasterParams(gg: GridGeometry2D, envelope: Envelope) {
  199. val width = gg.getGridRange2D.getWidth
  200. val height = gg.getGridRange2D.getHeight
  201. val resX = (envelope.getMaximum(0) - envelope.getMinimum(0)) / width
  202. val resY = (envelope.getMaximum(1) - envelope.getMinimum(1)) / height
  203. val suggestedQueryResolution = math.min(resX, resY)
  204. }
  205. }