/* * 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 com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.LinearRing; import com.vividsolutions.jts.geom.MultiLineString; import com.vividsolutions.jts.geom.MultiPoint; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.image.ColorModel; 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.GridCoverage2D; import org.geotools.coverage.grid.GridCoverageFactory; import org.geotools.feature.FeatureCollection; import org.geotools.feature.FeatureIterator; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.process.Process; import org.geotools.process.feature.AbstractFeatureCollectionProcess; import org.geotools.process.feature.AbstractFeatureCollectionProcessFactory; import org.geotools.referencing.CRS; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.util.ProgressListener; import org.geotools.util.NullProgressListener; import org.geotools.util.SimpleInternationalString; import org.opengis.geometry.Envelope; /** * 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; GridCoverage2D result; private Number minAttValue; private Number maxAttValue; private float nodataValue; private ReferencedEnvelope extent; 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 attributeName a numeric feature attribute to provide raster cell values * @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( FeatureCollection<SimpleFeatureType, SimpleFeature> features, String attributeName, Dimension gridDim, Envelope bounds, String covName, ProgressListener monitor) throws VectorToRasterException { VectorToRasterFactory factory = new VectorToRasterFactory(); VectorToRasterProcess process = factory.create(); return process.convert(features, attributeName, 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 { FeatureCollection<SimpleFeatureType, SimpleFeature> features = (FeatureCollection<SimpleFeatureType, SimpleFeature>) input.get(AbstractFeatureCollectionProcessFactory.FEATURES.key); String attributeName = (String) input.get(VectorToRasterFactory.ATTRIBUTE.key); Dimension gridDim = (Dimension) input.get(VectorToRasterFactory.GRID_DIM.key); ReferencedEnvelope env = (ReferencedEnvelope) input.get(VectorToRasterFactory.BOUNDS.key); String title = (String) input.get(VectorToRasterFactory.TITLE.key); GridCoverage2D cov = convert(features, attributeName, 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 { String attName = (String) input.get(VectorToRasterFactory.ATTRIBUTE.key); Geometry geometry = (Geometry) feature.getDefaultGeometry(); if (geometry.intersects(extentGeometry)) { Number value = null; switch (transferType) { case FLOAT: value = Float.valueOf(feature.getAttribute(attName).toString()); 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: value = Integer.valueOf(feature.getAttribute(attName).toString()); 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)); if (geometry.getClass().equals(MultiPolygon.class)) { MultiPolygon mp = (MultiPolygon) geometry; for (int n = 0; n < mp.getNumGeometries(); n++) { drawGeometry(mp.getGeometryN(n)); } } else if (geometry.getClass().equals(MultiLineString.class)) { MultiLineString mp = (MultiLineString) geometry; for (int n = 0; n < mp.getNumGeometries(); n++) { drawGeometry(mp.getGeometryN(n)); } } else if (geometry.getClass().equals(MultiPoint.class)) { MultiPoint mp = (MultiPoint) geometry; for (int n = 0; n < mp.getNumGeometries(); n++) { drawGeometry(mp.getGeometryN(n)); } } else { drawGeometry(geometry); } } } private GridCoverage2D convert( FeatureCollection<SimpleFeatureType, SimpleFeature> features, String attributeName, Dimension gridDim, Envelope bounds, String covName, ProgressListener monitor) throws VectorToRasterException { if ( monitor == null ) { monitor = new NullProgressListener(); } initialize( features, bounds, attributeName, gridDim ); Map<String, Object> params = new HashMap<String, Object>(); params.put(VectorToRasterFactory.ATTRIBUTE.key, attributeName); monitor.setTask(new SimpleInternationalString("Rasterizing features...")); float scale = 100.0f / features.size(); monitor.started(); FeatureIterator<SimpleFeature> 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(FeatureCollection<SimpleFeatureType, SimpleFeature> features, Envelope bounds, String attributeName, Dimension gridDim ) throws VectorToRasterException { // check that the attribute exists and is numeric AttributeDescriptor attDesc = features.getSchema().getDescriptor(attributeName); if (attDesc == null) { throw new VectorToRasterException(attributeName + " not found"); } Class<?> attClass = attDesc.getType().getBinding(); if (!Number.class.isAssignableFrom(attClass)) { throw new VectorToRasterException(attributeName + " 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; } minAttValue = maxAttValue = null; setBounds( features, bounds, gridDim ); createImage( gridDim ); } /** * * @param env * @throws org.geotools.process.raster.VectorToRasterException */ private void setBounds( FeatureCollection<SimpleFeatureType, SimpleFeature> 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; } 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 { /* * The bounds arg was null - interpreted as set bounds to * those of the FeatureCollection */ extent = featureBounds; } GeometryFactory gf = new GeometryFactory(); extentGeometry = gf.toGeometry(extent); double xInterval = extent.getWidth() / gridDim.getWidth(); double yInterval = extent.getHeight() / gridDim.getHeight(); cellsize = Math.max(xInterval, yInterval); } /** * 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, null); 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, null); 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(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 (clockwise) for (int n = 0; n < coords.length; n++) { coordGridX[n] = (int) (((coords[n].x - extent.getMinX()) / cellsize)); coordGridY[n] = (int) (((coords[n].y - extent.getMinY()) / cellsize)); coordGridY[n] = image.getHeight() - coordGridY[n]; } if (geometry.getClass().equals(Polygon.class)) { graphics.fillPolygon(coordGridX, coordGridY, coords.length); } else if (geometry.getClass().equals(LinearRing.class)) { graphics.drawPolyline(coordGridX, coordGridY, coords.length); } else if (geometry.getClass().equals(LineString.class)) { graphics.drawPolyline(coordGridX, coordGridY, coords.length); } else if (geometry.getClass().equals(Point.class)) { graphics.drawPolyline(coordGridX, coordGridY, coords.length); } } /** * 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); } }