/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2016, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.gce.imagemosaic.egr;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.IndexColorModel;
import java.awt.image.MultiPixelPackedSampleModel;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.media.jai.PlanarImage;
import javax.media.jai.RasterFactory;
import javax.media.jai.iterator.RandomIter;
import javax.media.jai.iterator.WritableRandomIter;
import org.geotools.geometry.jts.JTS;
import org.geotools.util.logging.Logging;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Polygon;
import org.geotools.util.SoftValueHashMap;
import it.geosolutions.jaiext.iterators.RandomIterFactory;
/**
* A tile of the whole grid space.
*
* The related Raster is lazily allocated, so to skip allocation for fully covered tiles that do not need to be drawn.
*
* @author Emanuele Tajariol <etj at geo-solutions.it>
*/
class Tile {
private static final Logger LOGGER = Logging.getLogger(Tile.class);
private static final boolean antiAliasing = false;
private static final byte FF = (byte) 0xff;
static Map<String, WritableRaster> solidRasterCache = new SoftValueHashMap<>();
private static final ColorModel BINARY_COLOR_MODEL = new IndexColorModel(1, 2,
new byte[] { 0, FF }, new byte[] { 0, FF }, new byte[] { 0, FF });
// the sample model used for internal "full size" tiles
private static MultiPixelPackedSampleModel DEFAULT_PACKED_SAMPLE_MODEL = new MultiPixelPackedSampleModel(
DataBuffer.TYPE_BYTE, ROIExcessGranuleRemover.DEFAULT_TILE_SIZE,
ROIExcessGranuleRemover.DEFAULT_TILE_SIZE, 1);
// used for border tiles
static Map<String, MultiPixelPackedSampleModel> mpSampleModelCache = new SoftValueHashMap<>();
/**
* Width in pixels of this tile.
*/
private final int tileWidth;
/**
* Height in pixels of this tile.
*/
private final int tileHeight;
/**
* Standard width in pixels of the tiles of this tileset. Tiles in last row or last column may have different size than other tiles. We need the
* standard size for computing the grid translation when drawing geometries.
*/
private final int stdTileWidth;
/**
* Standard height in pixels of the tiles of this tileset.
*/
private final int stdTileHeight;
private final int col;
private final int row;
private Polygon tileBBox;
private long coverageCount;
// Lazy stuff
private WritableRaster raster;
private BufferedImage bi;
private Graphics2D graphics;
private BitSet rowFull;
private Rectangle tileArea;
public Tile(int tileWidth, int tileHeight, int col, int row, AffineTransform w2s) {
this(tileWidth, tileHeight, col, row, w2s, tileWidth, tileHeight);
}
public Tile(int tileWidth, int tileHeight, int col, int row, AffineTransform w2s,
int stdTileWidth, int stdTileHeight) {
this.tileWidth = tileWidth;
this.tileHeight = tileHeight;
this.col = col;
this.row = row;
this.stdTileWidth = stdTileWidth;
this.stdTileHeight = stdTileHeight;
this.rowFull = new BitSet(tileHeight);
tileArea = new Rectangle(col * stdTileWidth, row * stdTileHeight, tileWidth, tileHeight);
try {
Envelope e = RendererUtilities.createMapEnvelope(tileArea, w2s);
tileBBox = JTS.toGeometry(e);
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("TileBBox: " + tileBBox);
}
} catch (NoninvertibleTransformException ex) {
LOGGER.log(Level.SEVERE, "Error creating tile", ex);
tileBBox = null;
}
}
public int getCol() {
return col;
}
public int getRow() {
return row;
}
public long getCoverageCount() {
return coverageCount;
}
public boolean isFullyCovered() {
return (tileWidth * tileHeight) == coverageCount;
}
/**
* @return the bbox of this tile in world coordinates
*/
public Polygon getTileBBox() {
return tileBBox;
}
private void initGraphics(boolean inverted) {
initRaster(inverted);
// lazily allocate graphics
if (graphics == null) {
Color drawColor = inverted ? Color.BLACK : Color.WHITE;
bi = new BufferedImage(BINARY_COLOR_MODEL, raster, false, null);
bi.setAccelerationPriority(0);
graphics = bi.createGraphics();
final int offset = antiAliasing ? 2 : 0;
graphics.setClip(-offset, -offset, tileWidth + (offset * 2), tileHeight + (offset * 2));
graphics.translate(-this.col * stdTileWidth, -this.row * stdTileHeight);
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antiAliasing
? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
graphics.setColor(drawColor);
}
}
private void initRaster(boolean inverted) {
// lazily allocate raster
if (raster == null) {
allocateRaster(inverted);
}
}
/**
* Set the coverageCount according to the current count of pixels set to 1.
*
* @return true if count changed.
* @throws IllegalStateException if raster has not been initialized
*/
public boolean refreshCoverageCount() throws IllegalStateException {
if (raster == null) {
throw new IllegalStateException("Raster not initialized");
}
long cnt = 0;
raster.getSampleModel();
int scanlineStride = ((MultiPixelPackedSampleModel) raster.getSampleModel())
.getScanlineStride();
DataBufferByte data = (DataBufferByte) raster.getDataBuffer();
byte[] bytes = data.getData();
int pos = 0;
for (int row = 0; row < tileHeight; row++) {
if (rowFull.get(row)) {
cnt += tileWidth;
pos += scanlineStride;
} else {
int rowCnt = 0;
for (int col = 0; col < scanlineStride; col++) {
byte b = bytes[pos++];
int count = Integer.bitCount(0xFF & b);
rowCnt += count;
}
cnt += rowCnt;
if (rowCnt >= tileWidth) {
rowFull.set(row);
}
}
}
if (coverageCount == cnt) {
return false;
} else {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Updating count for " + this + " to " + cnt);
}
coverageCount = cnt;
return true;
}
}
/**
* Draws a binary image already in raster space. Updates the coverage count as a side effect, so no need to call {@link #refreshCoverageCount()}
* after it
*
* @param roiImage
* @return True if at least one pixel has been added
*/
public boolean draw(PlanarImage binaryImage) {
initRaster(false);
final Rectangle tileBounds = raster.getBounds();
final Rectangle imageBounds = binaryImage.getBounds();
final Rectangle overlapArea = imageBounds.intersection(tileBounds);
if (overlapArea.isEmpty()) {
return false;
}
RandomIter sourceIter = RandomIterFactory.create(binaryImage, overlapArea, true, true);
WritableRandomIter rasterIter = RandomIterFactory.createWritable(raster, overlapArea);
boolean added = false;
final int maxCol = overlapArea.x + overlapArea.width;
final int maxRow = overlapArea.y + overlapArea.height;
for (int row = overlapArea.y; row < maxRow; row++) {
for (int col = overlapArea.x; col < maxCol; col++) {
int maskValue = sourceIter.getSample(col, row, 0);
int rasValue = rasterIter.getSample(col, row, 0);
if (maskValue == 1 && rasValue == 0) {
rasterIter.setSample(col, row, 0, 1);
coverageCount++;
added = true;
}
}
}
return added;
}
/**
* Sets raster and sampleModel
*/
private void allocateRaster(boolean inverted) {
final int value = inverted ? 1 : 0;
WritableRaster result;
if ((tileWidth != tileHeight) || (value == 0)) {
result = buildSolidRaster(tileWidth, tileHeight, value);
} else {
Raster template = getSolidRaster(tileWidth, tileHeight, value);
result = template.createCompatibleWritableRaster();
byte[] src = ((DataBufferByte) template.getDataBuffer()).getData();
byte[] dst = ((DataBufferByte) result.getDataBuffer()).getData();
System.arraycopy(src, 0, dst, 0, src.length);
}
raster = result;
}
Raster getSolidRaster(int tileWidth, int tileHeight, int value) {
String key = tileWidth + "x" + tileHeight + '_' + value;
WritableRaster result = solidRasterCache.get(key);
if (result == null) {
result = buildSolidRaster(tileWidth, tileHeight, value);
solidRasterCache.put(key, result);
}
return result;
}
private WritableRaster buildSolidRaster(int tileWidth, int tileHeight, int value) {
SampleModel sampleModel = getMPSampleModel(tileWidth, tileHeight);
// build the raster
WritableRaster newRaster = RasterFactory.createWritableRaster(sampleModel,
new java.awt.Point(0, 0));
// sanity checks
int dataType = sampleModel.getTransferType();
int numBands = sampleModel.getNumBands();
if (dataType != DataBuffer.TYPE_BYTE) {
throw new IllegalArgumentException(
"The code works only if the sample model data type is BYTE");
}
if (numBands != 1) {
throw new IllegalArgumentException("The code works only for single band rasters!");
}
if (value != 0) {
// flood fill
int w = sampleModel.getWidth();
int h = sampleModel.getHeight();
int[] data = new int[w * h];
Arrays.fill(data, value);
newRaster.setSamples(0, 0, w, h, 0, data);
}
return newRaster;
}
private SampleModel getMPSampleModel(int tileWidth, int tileHeight) {
SampleModel sampleModel;
if ((tileWidth == ROIExcessGranuleRemover.DEFAULT_TILE_SIZE)
&& (tileHeight == ROIExcessGranuleRemover.DEFAULT_TILE_SIZE)) {
sampleModel = DEFAULT_PACKED_SAMPLE_MODEL;
} else {
String key = tileWidth + "x" + tileHeight;
sampleModel = mpSampleModelCache.get(key);
if (sampleModel == null) {
sampleModel = new MultiPixelPackedSampleModel(DataBuffer.TYPE_BYTE, tileWidth,
tileHeight, 1);
mpSampleModelCache.put(key, (MultiPixelPackedSampleModel) sampleModel);
}
}
return sampleModel;
}
public void draw(Shape projectedShape) {
draw(projectedShape, false);
}
public void draw(Shape projectedShape, boolean inverted) {
initGraphics(inverted);
Color drawColor = inverted ? Color.BLACK : Color.WHITE;
graphics.setColor(drawColor);
graphics.fill(projectedShape);
}
public void draw(/* another Raster here I suppose? Maybe a tile, maybe mis-aligned with this one */) {
initRaster(false);
// flip bits here, if the tile is aligned we can do int math, otherwise bit by bit...
}
public void dispose() {
if (graphics != null) {
graphics.dispose();
}
bi = null;
raster = null;
}
@Override
public String toString() {
return "Tile[" + col + 'x' + row + ':' + coverageCount + ']';
}
WritableRaster getRaster() {
return raster;
}
public Rectangle getTileArea() {
return tileArea;
}
}