/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2008, 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.
*
* NOTICE REGARDING STATUS AS PUBLIC DOMAIN WORK AND ABSENCE OF ANY WARRANTIES
*
* The work (source code) was prepared by an officer or employee of the
* United States Government as part of that person's official duties, thus
* it is a "work of the U.S. Government," which is in the public domain and
* not elegible for copyright protection. See, 17 U.S.C. ยง 105. No warranty
* of any kind is given regarding the work.
*/
package org.geotools.process.raster;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.media.jai.RasterFactory;
import javax.media.jai.TiledImage;
import org.geotools.coverage.grid.GridCoordinates2D;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridCoverageFactory;
import org.geotools.coverage.grid.GridEnvelope2D;
import org.geotools.coverage.grid.GridGeometry2D;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.filter.text.cql2.CQLException;
import org.geotools.filter.text.ecql.ECQL;
import org.geotools.geometry.DirectPosition2D;
import org.geotools.geometry.jts.Geometries;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.process.feature.AbstractFeatureCollectionProcess;
import org.geotools.process.feature.AbstractFeatureCollectionProcessFactory;
import org.geotools.referencing.CRS;
import org.geotools.util.NullProgressListener;
import org.geotools.util.SimpleInternationalString;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.filter.expression.Expression;
import org.opengis.geometry.Envelope;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.TransformException;
import org.opengis.util.ProgressListener;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
/**
* A Process to rasterize vector features in an input FeatureCollection.
* <p>
* A feature attribute is specified from which to extract the numeric
* values that will be written to the output grid coverage.
* At present only int or float values are written to the output grid
* coverage. If the attribute is of type Long it will be coerced to
* int values and a warning will be logged. Similarly if the attribute
* is of type Double it will be coerced to float and a warning logged.
*
* @author Steve Ansari, NOAA
* @author Michael Bedward
*
*
* @source $URL$
*/
public class VectorToRasterProcess extends AbstractFeatureCollectionProcess {
private static final int COORD_GRID_CHUNK_SIZE = 1000;
private static enum TransferType {
INTEGRAL,
FLOAT;
}
private TransferType transferType;
private static enum ValueSource {
PROPERTY_NAME,
EXPRESSION;
}
private ValueSource valueSource;
GridCoverage2D result;
private Number minAttValue;
private Number maxAttValue;
private float nodataValue;
private ReferencedEnvelope extent;
private GridGeometry2D gridGeom;
private Geometry extentGeometry;
private int[] coordGridX = new int[COORD_GRID_CHUNK_SIZE];
private int[] coordGridY = new int[COORD_GRID_CHUNK_SIZE];
// private double cellsize;
TiledImage image;
Graphics2D graphics;
/**
* Constructor
*
* @param factory
*/
public VectorToRasterProcess(VectorToRasterFactory factory) {
super(factory);
}
/**
* A static helper method that can be called directy to run the process.
* <p>
* The process interface is useful for advertising functionality to
* dynamic applications, but for 'hands on' coding this method is much more
* convenient than working via the {@linkplain org.geotools.process.Process#execute }.
*
* @param features the feature collection to be (wholly or partially) rasterized
*
* @param attribute source of values for the output grid: either a
* {@code String} for the name of a numeric feature property or
* an {@code org.opengis.filter.expression.Expression} that
* evaluates to a numeric value
*
* @param gridWidthInCells width (cell) of the output raster
*
* @param gridHeightInCells height (cell) of the output raster
*
* @param bounds bounds (world coordinates) of the output raster
*
* @param covName a name for the output raster
*
* @param monitor an optional {@code ProgressListener} (may be {@code null}
*
* @return a new grid coverage
*
* @throws org.geotools.process.raster.VectorToRasterException
*/
public static GridCoverage2D process(
SimpleFeatureCollection features,
Object attribute,
Dimension gridDim,
Envelope bounds,
String covName,
ProgressListener monitor) throws VectorToRasterException {
VectorToRasterFactory factory = new VectorToRasterFactory();
VectorToRasterProcess process = factory.create();
return process.convert(features, attribute, gridDim, bounds, covName, monitor);
}
/**
* Retrieves the input parameters from the supplied Map, conducts some basic checking,
* and then carries out the vector to raster conversion.
*
* @param input
* input parameters from those defined in {@linkplain VectorToRasterFactory}
*
* @param monitor
* a ProgressListener object, or null if monitoring is not required
*
* @return a Map of result objects
*
* @throws org.geotools.process.raster.VectorToRasterException if unable to
* rasterize the features as requested
*
* @see VectorToRasterFactory#getResultInfo(java.util.Map)
*/
public Map<String, Object> execute( Map<String, Object> input, ProgressListener monitor )
throws VectorToRasterException {
SimpleFeatureCollection features = (SimpleFeatureCollection)
input.get(AbstractFeatureCollectionProcessFactory.FEATURES.key);
String attributeStr = (String) input.get(VectorToRasterFactory.ATTRIBUTE.key);
Expression attribute = null;
try {
attribute = ECQL.toExpression(attributeStr);
} catch(CQLException e) {
throw new VectorToRasterException(e);
}
int w = (Integer) input.get(VectorToRasterFactory.RASTER_WIDTH.key);
int h = (Integer) input.get(VectorToRasterFactory.RASTER_HEIGHT.key);
Dimension gridDim = new Dimension(w, h);
ReferencedEnvelope env = (ReferencedEnvelope) input.get(VectorToRasterFactory.BOUNDS.key);
String title = (String) input.get(VectorToRasterFactory.TITLE.key);
if(title == null) {
title = "raster";
}
GridCoverage2D cov = convert(features, attribute, gridDim, env, title, monitor);
Map<String, Object> results = new HashMap<String, Object>();
results.put(VectorToRasterFactory.RESULT.key, cov);
return results;
}
/**
* This method is called by {@linkplain #execute} to rasterize an individual feature.
*
* @param feature
* the feature to be rasterized
*
* @param input
* the intput parameters (ignored in this implementation)
*
* @throws java.lang.Exception
*/
@Override
protected void processFeature(SimpleFeature feature, Map<String, Object> input) throws Exception {
Object attribute = input.get(VectorToRasterFactory.ATTRIBUTE.key);
Geometry geometry = (Geometry) feature.getDefaultGeometry();
if (geometry.intersects(extentGeometry)) {
Number value = getFeatureValue(feature, attribute);
switch (transferType) {
case FLOAT:
if (minAttValue == null) {
minAttValue = maxAttValue = Float.valueOf(value.floatValue());
} else if (Float.compare(value.floatValue(), minAttValue.floatValue()) < 0) {
minAttValue = value.floatValue();
} else if (Float.compare(value.floatValue(), maxAttValue.floatValue()) > 0) {
maxAttValue = value.floatValue();
}
break;
case INTEGRAL:
if (minAttValue == null) {
minAttValue = maxAttValue = Integer.valueOf(value.intValue());
} else if (value.intValue() < minAttValue.intValue()) {
minAttValue = value.intValue();
} else if (value.intValue() > maxAttValue.intValue()) {
maxAttValue = value.intValue();
}
break;
}
graphics.setColor(valueToColor(value));
Geometries geomType = Geometries.get(geometry);
switch (geomType) {
case MULTIPOLYGON:
case MULTILINESTRING:
case MULTIPOINT:
final int numGeom = geometry.getNumGeometries();
for (int i = 0; i < numGeom; i++) {
Geometry geomN = geometry.getGeometryN(i);
drawGeometry(Geometries.get(geomN), geomN);
}
break;
case POLYGON:
case LINESTRING:
case POINT:
drawGeometry(geomType, geometry);
default:
throw new UnsupportedOperationException(
"Unsupported geometry type: " + geomType.getName());
}
}
}
private Number getFeatureValue(SimpleFeature feature, Object attribute) {
Class<? extends Number> rtnType = transferType == TransferType.FLOAT ? Float.class : Integer.class;
if (valueSource == ValueSource.PROPERTY_NAME) {
return rtnType.cast(feature.getAttribute((String)attribute));
} else {
return ((Expression)attribute).evaluate(feature, rtnType);
}
}
private GridCoverage2D convert(
SimpleFeatureCollection features,
Object attribute,
Dimension gridDim,
Envelope bounds,
String covName,
ProgressListener monitor)
throws VectorToRasterException {
if ( monitor == null ) {
monitor = new NullProgressListener();
}
initialize( features, bounds, attribute, gridDim );
Map<String, Object> params = new HashMap<String, Object>();
params.put(VectorToRasterFactory.ATTRIBUTE.key, attribute);
monitor.setTask(new SimpleInternationalString("Rasterizing features..."));
float scale = 100.0f / features.size();
monitor.started();
SimpleFeatureIterator fi = features.features();
try {
int counter = 0;
while( fi.hasNext() ) {
try {
processFeature(fi.next(), params);
}
catch( Exception e ) {
monitor.exceptionOccurred( e );
}
monitor.progress( scale * counter++);
}
}
finally {
features.close( fi );
}
monitor.complete();
flattenImage();
GridCoverageFactory gcf = new GridCoverageFactory();
return gcf.create(covName, image, extent);
}
private void initialize(SimpleFeatureCollection features,
Envelope bounds, Object attribute, Dimension gridDim ) throws VectorToRasterException {
// check the attribute argument
if (attribute instanceof String) {
String propName = (String) attribute;
AttributeDescriptor attDesc = features.getSchema().getDescriptor(propName);
if (attDesc == null) {
throw new VectorToRasterException(propName + " not found");
}
Class<?> attClass = attDesc.getType().getBinding();
if (!Number.class.isAssignableFrom(attClass)) {
throw new VectorToRasterException(propName + " is not numeric");
}
if (Float.class.isAssignableFrom(attClass)) {
transferType = TransferType.FLOAT;
} else if (Double.class.isAssignableFrom(attClass)) {
transferType = TransferType.FLOAT;
Logger.getLogger(VectorToRasterProcess.class.getName()).log(Level.WARNING, "coercing double feature values to float raster values");
} else if (Long.class.isAssignableFrom(attClass)) {
transferType = TransferType.INTEGRAL;
Logger.getLogger(VectorToRasterProcess.class.getName()).log(Level.WARNING, "coercing long feature values to int raster values");
} else {
transferType = TransferType.INTEGRAL;
}
valueSource = ValueSource.PROPERTY_NAME;
} else if (attribute instanceof Expression) {
valueSource = ValueSource.EXPRESSION;
} else {
throw new VectorToRasterException(
"value attribute must be a feature property name" +
"or an org.opengis.filter.expression.Expression object");
}
minAttValue = maxAttValue = null;
setBounds( features, bounds, gridDim );
createImage( gridDim );
gridGeom = new GridGeometry2D(
new GridEnvelope2D(0, 0, gridDim.width, gridDim.height),
extent);
}
/**
*
* @param env
* @throws org.geotools.process.raster.VectorToRasterException
*/
private void setBounds( SimpleFeatureCollection features,
Envelope bounds, Dimension gridDim ) throws VectorToRasterException {
ReferencedEnvelope featureBounds = features.getBounds();
if (bounds != null) {
ReferencedEnvelope inputBounds = new ReferencedEnvelope(bounds);
CoordinateReferenceSystem featuresCRS = featureBounds.getCoordinateReferenceSystem();
CoordinateReferenceSystem envCRS = bounds.getCoordinateReferenceSystem();
ReferencedEnvelope trEnv;
if (!CRS.equalsIgnoreMetadata(envCRS, featuresCRS)) {
try {
trEnv = inputBounds.transform(featuresCRS, true);
} catch (Exception tex) {
throw new VectorToRasterException(tex);
}
} else {
trEnv = inputBounds;
}
// If the provided bounds cover the feature bounds, use them
if (trEnv.covers(features.getBounds())) {
extent = trEnv;
} else {
// If the provided bounds partially overlap the feature bounds
// use the intersection
com.vividsolutions.jts.geom.Envelope common = trEnv.intersection(features.getBounds());
if (common == null || common.isNull()) {
throw new VectorToRasterException(
"Features do not lie within the requested rasterizing bounds");
}
extent = new ReferencedEnvelope(common, featuresCRS);
}
} else {
// No bounds provided - use feature bounds
extent = featureBounds;
}
GeometryFactory gf = new GeometryFactory();
extentGeometry = gf.toGeometry(extent);
}
/**
* Create the tiled image and the associated graphics object that we will be used to
* draw the vector features into a raster.
* <p>
* Note, the graphics objects will be an
* instance of TiledImageGraphics which is a sub-class of Graphics2D.
*/
private void createImage( Dimension gridDim ) {
ColorModel cm = ColorModel.getRGBdefault();
SampleModel sm = cm.createCompatibleSampleModel(gridDim.width, gridDim.height);
image = new TiledImage(0, 0, gridDim.width, gridDim.height, 0, 0, sm, cm);
graphics = image.createGraphics();
graphics.setPaintMode();
graphics.setComposite(AlphaComposite.Src);
}
/**
* Takes the 4-band ARGB image that we have been drawing into and
* converts it to a single-band image.
*
* @todo There is probably a much easier / faster way to do this that
* still takes advantage of image tiling (?)
*/
private void flattenImage() {
if (transferType == TransferType.FLOAT) {
flattenImageToFloat();
} else {
flattenImageToInt();
}
}
/**
* Takes the 4-band ARGB image that we have been drawing into and
* converts it to a single-band int image.
*/
private void flattenImageToInt() {
int numXTiles = image.getNumXTiles();
int numYTiles = image.getNumYTiles();
SampleModel sm = RasterFactory.createPixelInterleavedSampleModel(
DataBuffer.TYPE_INT, image.getWidth(), image.getHeight(), 1);
TiledImage destImage = new TiledImage(0, 0, image.getWidth(), image.getHeight(),
0, 0, sm, new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY),
false, false, Transparency.OPAQUE, DataBuffer.TYPE_INT));
for (int yt = 0; yt < numYTiles; yt++) {
for (int xt = 0; xt < numXTiles; xt++) {
Raster srcTile = image.getTile(xt, yt);
WritableRaster destTile = destImage.getWritableTile(xt, yt);
int[] data = new int[srcTile.getDataBuffer().getSize()];
srcTile.getDataElements(srcTile.getMinX(), srcTile.getMinY(),
srcTile.getWidth(), srcTile.getHeight(), data);
Rectangle bounds = destTile.getBounds();
destTile.setPixels(bounds.x, bounds.y, bounds.width, bounds.height, data);
destImage.releaseWritableTile(xt, yt);
}
}
image = destImage;
}
/**
* Takes the 4-band ARGB image that we have been drawing into and
* converts it to a single-band float image
*/
private void flattenImageToFloat() {
int numXTiles = image.getNumXTiles();
int numYTiles = image.getNumYTiles();
SampleModel sm = RasterFactory.createPixelInterleavedSampleModel(DataBuffer.TYPE_FLOAT, image.getWidth(), image.getHeight(), 1);
TiledImage destImage = new TiledImage(0, 0, image.getWidth(), image.getHeight(), 0, 0, sm,
new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY),
false, false, Transparency.OPAQUE, DataBuffer.TYPE_FLOAT));
for (int yt = 0; yt < numYTiles; yt++) {
for (int xt = 0; xt < numXTiles; xt++) {
Raster srcTile = image.getTile(xt, yt);
WritableRaster destTile = destImage.getWritableTile(xt, yt);
int[] data = new int[srcTile.getDataBuffer().getSize()];
srcTile.getDataElements(srcTile.getMinX(), srcTile.getMinY(), data);
Rectangle bounds = destTile.getBounds();
int k = 0;
for (int dy = bounds.y, drow = 0; drow < bounds.height; dy++, drow++) {
for (int dx = bounds.x, dcol = 0; dcol < bounds.width; dx++, dcol++) {
destTile.setSample(dx, dy, 0, Float.intBitsToFloat(data[k]));
}
}
destImage.releaseWritableTile(xt, yt);
}
}
image = destImage;
}
private void drawGeometry(Geometries geomType, Geometry geometry) {
Coordinate[] coords = geometry.getCoordinates();
// enlarge if needed
if (coords.length > coordGridX.length) {
int n = coords.length / COORD_GRID_CHUNK_SIZE + 1;
coordGridX = new int[n * COORD_GRID_CHUNK_SIZE];
coordGridY = new int[n * COORD_GRID_CHUNK_SIZE];
}
// Go through coordinate array in order received
DirectPosition2D worldPos = new DirectPosition2D();
try {
for (int n = 0; n < coords.length; n++) {
worldPos.setLocation(coords[n].x, coords[n].y);
GridCoordinates2D gridPos = gridGeom.worldToGrid(worldPos);
coordGridX[n] = gridPos.x;
coordGridY[n] = gridPos.y;
}
} catch (TransformException ex) {
throw new RuntimeException(ex);
}
switch (geomType) {
case POLYGON:
graphics.fillPolygon(coordGridX, coordGridY, coords.length);
break;
case LINESTRING: // includes LinearRing
graphics.drawPolyline(coordGridX, coordGridY, coords.length);
break;
case POINT:
graphics.fillRect(coordGridX[0], coordGridY[0], 1, 1);
break;
default:
throw new IllegalArgumentException(
"Invalid geometry type: " + geomType.getName());
}
}
/**
* Encode a value as a Color. The value will be Integer or Float.
* @param value the value to enclode
* @return the resulting sRGB Color
*/
private Color valueToColor(Number value) {
int intBits;
if (transferType == TransferType.FLOAT) {
intBits = Float.floatToIntBits(value.floatValue());
} else {
intBits = value.intValue();
}
return new Color(intBits, true);
}
}