/*
* 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.Shape;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
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.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.media.jai.PlanarImage;
import javax.media.jai.ROI;
import javax.media.jai.ROIShape;
import javax.media.jai.RasterFactory;
import org.geotools.geometry.jts.JTS;
import org.geotools.geometry.jts.LiteShape;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.util.logging.Logging;
import it.geosolutions.jaiext.vectorbin.ROIGeometry;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.geom.prep.PreparedGeometry;
import com.vividsolutions.jts.geom.prep.PreparedGeometryFactory;
/**
* Creates a quantized binary representation of a Bounding Box.
* <p/>
* <br/>
* The BBOx is represented by a grid of pixels, which are then split in a set of tiles. <br/>
* <p/>
* By {@link #add(com.vividsolutions.jts.geom.Geometry) adding} Geometries to the Binarizator, they will be rendered on the grid.<br/>
* <p/>
* You can check if the grid has been fully drawn using the {@link #isComplete() } method.<br/>
* <p/>
* When a Tile has been completely drawn, it is removed from the list.
*
* @author Emanuele Tajariol <etj at geo-solutions dot it>
*/
class Binarizator {
private static final Logger LOGGER = Logging.getLogger(Binarizator.class);
private final AffineTransform w2gTransform;
private final int tileWidth, tileHeight;
// only used for debug
private final int origW, origH;
/**
* Tiles not yet fully covered by the input geometries
*/
private List<Tile> activeTiles;
Binarizator(Polygon bbox, int pxWidth, int pxHeight, int tileWidth, int tileHeight) {
this.tileWidth = tileWidth;
this.tileHeight = tileHeight;
this.origW = pxWidth;
this.origH = pxHeight;
final ReferencedEnvelope env = JTS.toEnvelope(bbox);
w2gTransform = RendererUtilities.worldToScreenTransform(env,
new Rectangle(pxWidth, pxHeight));
if (w2gTransform == null) {
LOGGER.info("Null transformer, possible bad bbox requested " + env);
activeTiles = Collections.emptyList();
} else {
createTiles(pxWidth, pxHeight, tileWidth, tileHeight, w2gTransform);
}
}
private void createTiles(int w, int h, int tw, int th, AffineTransform w2s) {
int colnum = w / tw;
int rownum = h / th;
// create main tiles list
activeTiles = new LinkedList<>();
for (int col = 0; col < colnum; col++) {
for (int row = 0; row < rownum; row++) {
Tile tile = new Tile(tileWidth, tileHeight, col, row, w2s);
activeTiles.add(tile);
}
}
// add smaller last row and column if needed
if ((w % tw) != 0) {
for (int row = 0; row < rownum; row++) {
Tile tile = new Tile(w % tw, tileHeight, colnum, row, w2s, tileWidth, tileHeight);
activeTiles.add(tile);
}
}
if ((h % th) != 0) {
for (int col = 0; col < colnum; col++) {
Tile tile = new Tile(tileWidth, h % th, col, rownum, w2s, tileWidth, tileHeight);
activeTiles.add(tile);
}
}
// add the rightmost lower cut tile
if (((w % tw) != 0) && ((h % th) != 0)) {
Tile tile = new Tile(w % tw, h % th, colnum, rownum, w2s, tileWidth, tileHeight);
activeTiles.add(tile);
}
}
/**
* Tells if the grid has been fully drawn.
*/
public boolean isComplete() {
return activeTiles.isEmpty();
}
/**
* Adds a ROI to the binarizator. This assumes the operation is already fully working in raster space, in other words, the world to grid
* transformation is the identity
*
* @param roi
* @return
*/
public boolean add(ROI roi) {
// do we need to transform it?
// roi.transform(at)
// look for intercepted tiles
// if roi is shape or polygon base go the vector path?
// avoid using the raster path if possible, the vector one has
// some useful optimizations and we'll avod generating the raster version
// of
if (roi instanceof ROIGeometry) {
Geometry geometry = ((ROIGeometry) roi).getAsGeometry();
return add(geometry);
} else if (roi instanceof ROIShape) {
Shape shape = ((ROIShape) roi).getAsShape();
Geometry geometry = JTS.toGeometry(shape);
return add(geometry);
}
// ok, fully raster addition then
final PlanarImage roiImage = roi.getAsImage();
final Rectangle roiBounds = roiImage.getBounds();
boolean added = false;
for (Iterator<Tile> it = activeTiles.iterator(); it.hasNext();) {
Tile tile = it.next();
Rectangle tileBounds = tile.getTileArea();
if (tileBounds.intersects(roiBounds)) {
if (tile.draw(roiImage)) {
added = true;
if (tile.isFullyCovered()) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Removing covered tile " + tile + " (" + activeTiles.size()
+ " left)");
}
it.remove();
tile.dispose();
}
}
} else {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Skipping tile " + tile);
}
}
}
return added;
}
/**
* Render a Geometry on the grid.
*
* @param geometry the geometry to add
* @return true if the added geometry turned on at least one pixel
*/
public boolean add(Geometry geometry) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Adding geometry " + geometry);
}
PreparedGeometry pg = PreparedGeometryFactory.prepare(geometry);
// transform the JTS object into AWT, and project on grid space
LiteShape shape = new LiteShape(geometry, null, false);
Shape projectedShape = w2gTransform.createTransformedShape(shape);
boolean added = false;
for (Iterator<Tile> it = activeTiles.iterator(); it.hasNext();) {
Tile tile = it.next();
Polygon tileBBox = tile.getTileBBox();
Envelope geometryEnvelope = geometry.getEnvelopeInternal();
if (geometryEnvelope.intersects(tileBBox.getEnvelopeInternal())
&& pg.intersects(tileBBox)) // geometry partially covers the tile: draw it
{
if (pg.contains(tileBBox)) // geometry fully covers the tile?
{
added = true;
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Removing fully covered tile " + tile + " ("
+ activeTiles.size() + " left)");
}
it.remove();
tile.dispose();
} else {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("drawing " + pg + " on " + tile + ' ' + tileBBox);
}
tile.draw(projectedShape);
if (tile.refreshCoverageCount()) {
added = true;
if (tile.isFullyCovered()) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Removing covered tile " + tile + " ("
+ activeTiles.size() + " left)");
}
it.remove();
tile.dispose();
}
}
}
} else {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Skipping tile " + tile);
}
}
}
return added;
}
List<Tile> getActiveTiles() {
return activeTiles;
}
BufferedImage getDebugImage() {
// "The code works only if the sample model data type is BYTE");
SampleModel sampleModel = new MultiPixelPackedSampleModel(DataBuffer.TYPE_BYTE, origW,
origH, 1);
// build the raster
WritableRaster mainRaster = RasterFactory.createWritableRaster(sampleModel,
new java.awt.Point(0, 0));
// fill with 0 the whole raster
int[] data = new int[origW * origH];
Arrays.fill(data, 0);
mainRaster.setSamples(0, 0, origW, origH, 0, data);
// Draw white crosses as palceholder for all the tiles.
// Existing tiles will overwrite the cross.
int colnum = origW / tileWidth;
int rownum = origH / tileHeight;
final byte[] x00FF = { 0, (byte) 0xff };
final ColorModel binaryCM = new IndexColorModel(1, 2, x00FF, x00FF, x00FF);
BufferedImage mainBI = new BufferedImage(binaryCM, mainRaster, false, null);
Graphics2D graphics = mainBI.createGraphics();
for (int col = 0; col <= colnum; col++) {
for (int row = 0; row <= rownum; row++) {
graphics.setColor(Color.WHITE);
graphics.drawRect(col * tileWidth, row * tileHeight, tileWidth - 1, tileHeight - 1);
drawChecks(graphics, col, row);
graphics.setColor(Color.BLACK);
}
}
// todo add crossed for last row an col if they exist
// Draw the tiles
for (Tile tile : activeTiles) {
Raster tileRaster = tile.getRaster();
int col = tile.getCol();
int row = tile.getRow();
if (tileRaster != null) {
mainRaster.setDataElements(col * tileWidth, row * tileHeight, tileRaster);
} else {
// tile exists, but no raster yet: not still drawn, so basically
// it's all black:
graphics.setColor(Color.BLACK);
graphics.fillRect(col * tileWidth, row * tileHeight, tileWidth - 1, tileHeight - 1);
graphics.setColor(Color.WHITE);
drawCross(graphics, col, row);
}
}
// for (int col = 0; col <= colnum; col++)
// {
// for (int row = 0; row <= rownum; row++)
// {
// graphics.setColor(Color.WHITE);
// graphics.drawString(col + "x" + row, (int) ((col + 0.5) * tileWidth), (int) ((row - 0.5) * tileWidth));
// }
// }
return mainBI;
}
private void drawCross(Graphics2D graphics, int col, int row) {
graphics.drawRect(col * tileWidth, row * tileHeight, tileWidth - 1, tileHeight - 1);
graphics.drawLine(col * tileWidth, row * tileHeight, (col * tileWidth) + tileWidth - 1,
(row * tileHeight) + tileHeight - 1);
graphics.drawLine((col * tileWidth) + tileWidth - 1, row * tileWidth, col * tileWidth,
(row * tileHeight) + tileHeight - 1);
}
private void drawChecks(Graphics2D graphics, int col, int row) {
graphics.setColor(Color.WHITE);
for (int y = 0; y < tileHeight; y++) {
for (int x = y % 2; x < tileWidth; x += 2) {
graphics.drawRect((col * tileWidth) + x, (row * tileWidth) + y, 0, 0);
}
}
}
}