Hello all,

I received a question about the rasterization code (Features to Grid
Coverage) and upgraded the code to Geotools 2.5.2.  I've attached the
main class (FeatureRasterizer.java) and a test class
(TestFeatureRasterizer.java). 

Here are the geotools jars needed to run this:

geoapi-2.2-M1.jar
 gt-api-2.5.2.jar
 gt-coverage-2.5.2.jar
 gt-epsg-wkt-2.5.2.jar
 gt-main-2.5.2.jar
 gt-metadata-2.5.2.jar
 gt-referencing-2.5.2.jar
 gt-render-2.5.2.jar
 gt-shapefile-2.5.2.jar
 jai_codec-1.1.3.jar
 jai_imageio-1.1.jar
jsr-275-1.0-beta-2.jar
 jts-1.9.jar
vecmath-1.3.1.jar


Steve

Steve Ansari said the following on 4/1/2008 4:46 PM:
> Thanks Ferdinando!  It looks great.
>
> Steve
>
>
> Ferdinando Villa said the following on 4/1/2008 4:18 PM:
>> Actually Steve's code works beautifully and does not seem to require much
>> modernization! I haven't tested it with a subsampled envelope yet, but it
>> spits out 4000x2000 rasters from big shapefiles in seconds. I only commented
>> out some (not all) debug printouts and added a function to produce a
>> GridCoverage2D directly:
>>
>>     public GridCoverage2D rasterize(String name, FeatureCollection fc,
>> String attributeName, ReferencedEnvelope env)
>>
>> you can pass null as the envelope and it will use the boundaries of the
>> feature collection. I also internalized the FeatureRasterizerException so
>> the code is now self-contained as long as geotools is in the classpath. It
>> does not use any deprecated API - that's the extent of my modernization
>> capabilities :)
>>
>> If I find bugs along the way I'll try to fix them - progress can be followed
>> at
>> http://ecoinformatics.uvm.edu/crucible/browse/ThinklabGeospacePlugin/org/int
>> egratedmodelling/geospace/gis/FeatureRasterizer.java.
>>
>> Bottom line: Thanks Steve!
>>
>> Cheers,
>> ferdinando
>>
>>
>> -----Original Message-----
>> From: Jody Garnett [mailto:[email protected]] 
>> Sent: Tuesday, April 01, 2008 3:28 PM
>> To: Ferdinando Villa
>> Cc: 'Steve Ansari'; [email protected]
>> Subject: Re: [Geotools-gt2-users] Vector to Raster conversion
>>
>> Hey guys; we are setting up a "Process" module in geotools (see this 
>> weeks IRC). After you do your "testing and modernize" do you want to 
>> send it our way? It woudl be great to have examples of different kinds 
>> of processes to make sure the API works out okay.
>> Jody
>>   
>> ------------------------------------------------------------------------
>>
>> -------------------------------------------------------------------------
>> Check out the new SourceForge.net Marketplace.
>> It's the best place to buy or sell services for
>> just about anything Open Source.
>> http://ad.doubleclick.net/clk;164216239;13503038;w?http://sf.net/marketplace
>> ------------------------------------------------------------------------
>>
>> _______________________________________________
>> Geotools-gt2-users mailing list
>> [email protected]
>> https://lists.sourceforge.net/lists/listinfo/geotools-gt2-users
>>   
>
> -- 
> Steve Ansari
> Physical Scientist
> NOAA's National Climatic Data Center
> Veach-Baley Federal Building
> 151 Patton Avenue
> Asheville, NC 28801
> Ph: 828-271-4611
> Fax: 828-271-4022 

-- 
Steve Ansari
Physical Scientist
NOAA's National Climatic Data Center
Veach-Baley Federal Building
151 Patton Avenue
Asheville, NC 28801
Ph: 828-271-4611
Fax: 828-271-4328

package gov.noaa.ncdc.geotools;

/**
 * NOAA's National Climatic Data Center
 * NOAA/NESDIS/NCDC
 * 151 Patton Ave, Asheville, NC  28801
 * 
 * THIS SOFTWARE AND ITS DOCUMENTATION ARE CONSIDERED TO BE IN THE 
 * PUBLIC DOMAIN AND THUS ARE AVAILABLE FOR UNRESTRICTED PUBLIC USE.  
 * THEY ARE FURNISHED "AS IS." THE AUTHORS, THE UNITED STATES GOVERNMENT, ITS
 * INSTRUMENTALITIES, OFFICERS, EMPLOYEES, AND AGENTS MAKE NO WARRANTY,
 * EXPRESS OR IMPLIED, AS TO THE USEFULNESS OF THE SOFTWARE AND
 * DOCUMENTATION FOR ANY PURPOSE. THEY ASSUME NO RESPONSIBILITY (1)
 * FOR THE USE OF THE SOFTWARE AND DOCUMENTATION; OR (2) TO PROVIDE
 * TECHNICAL SUPPORT TO USERS.
 */

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.WritableRaster;
import java.nio.ByteBuffer;

import javax.media.jai.RasterFactory;

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.opengis.feature.Feature;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
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;

/**
 *  Rasterize features onto a WritableRaster object using Java 2D 
Graphics/BufferedImage.
 *
 * @author     steve.ansari
 * @author Ferdinando Villa
 * @created    March 20, 2008
 */
public class FeatureRasterizer {

    public class FeatureRasterizerException extends Exception {
           
        /**
         * Constructor with message argument.
         *
         * @param message Reason for the exception being thrown
         */
        public FeatureRasterizerException(String message) {
            super(message);
        }       
     }

    private int height;
    private int width;
    private double noDataValue;
    private WritableRaster raster = null;   
    private BufferedImage bimage = null;
    private Graphics2D graphics = null;

    private java.awt.geom.Rectangle2D.Double bounds;
    private double cellsize;
    private double minAttValue = 999999999;
    private double maxAttValue = -999999999;

    // Declare these as global
    private int[] coordGridX = new int[3500];
    private int[] coordGridY = new int[3500];
    private float value;

    private boolean emptyGrid = false;

    private Geometry extentGeometry;
    private GeometryFactory geoFactory = new GeometryFactory();
    private String attributeName = "value";

    public static GridCoverageFactory rasterFactory = new GridCoverageFactory();
    
    private double xInterval;
    private double yInterval;  

    // Any change in height, width or no_data values will cause 
    // the raster to 'reset' at the next call to .rasterize(...)
    private boolean resetRaster = false;

    /**
     *Constructor for the FeatureRasterizer object
     *
     * @exception  FeatureRasterizerException  Description of the Exception
     */
    public FeatureRasterizer() {
        this(800, 800, -999.0f);
    }

    /**
     * Constructor for the FeatureRasterizer object - will use default 800x800 
raster
     *
     * @param  noData                         No Data value for raster
     * @exception  FeatureRasterizerException  Description of the Exception
     */
    public FeatureRasterizer(float noData) {
        this(800, 800, noData);
    }

    /**
     * Constructor for the FeatureRasterizer object.  No Data value defaults to 
-999.0
     *
     * @param  height                         Height of raster (number of grid 
cells)
     * @param  width                          Width of raster (number of grid 
cells)
     */
    public FeatureRasterizer(int height, int width) {
        this(height, width, -999.0f);
    }

    /**
     * Constructor for the FeatureRasterizer object
     *
     * @param  height                         Height of raster (number of grid 
cells)
     * @param  width                          Width of raster (number of grid 
cells)
     * @param  noData                         No Data value for raster
     */
    public FeatureRasterizer(int height, int width, float noData) {
        this.height = height;
        this.width = width;
        this.noDataValue = noData;

        raster = RasterFactory.createBandedRaster(DataBuffer.TYPE_FLOAT,
                width, height, 1, null);
        bimage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        bimage.setAccelerationPriority(1.0f);

        GraphicsEnvironment ge = 
GraphicsEnvironment.getLocalGraphicsEnvironment();
//      System.out.println("IMAGE ACCELERATED? 
"+bimage.getCapabilities(ge.getDefaultScreenDevice().getDefaultConfiguration()).isAccelerated());
        graphics = bimage.createGraphics();
        graphics.setPaintMode();
        graphics.setComposite(AlphaComposite.Src);


    }
    
    public GridCoverage2D rasterize(String name, FeatureCollection fc, String 
attributeName, ReferencedEnvelope env) throws FeatureRasterizerException {
        
        if (raster == null) {
        
            WritableRaster raster = 
                RasterFactory.createBandedRaster(
                    DataBuffer.TYPE_FLOAT, 
                    this.width, 
                    this.height, 
                    1, 
                    null);
        
            setWritableRaster(raster);
        } 

        clearRaster();
        
        if (env == null) {
                    
            rasterize(fc, attributeName);

            /*
             * Use full envelope from feature collection
             * TODO check if we need to use a buffer like in Steve's code above
             */
            env = fc.getBounds();

        } else {
            
            /*
             * TODO check if we need to use a buffer like in Steve's code above
             */
             java.awt.geom.Rectangle2D.Double box =
                   new java.awt.geom.Rectangle2D.Double(
                           env.getMinX(),
                           env.getMinY(), 
                           env.getWidth(), 
                           env.getHeight());
             
             rasterize(fc, box, attributeName);
        }
        
        GridCoverage2D coverage = 
            rasterFactory.create(name, raster, env);
        
        return coverage;
    }

    /**
     *  Gets the raster attribute of the FeatureRasterizer object
     *  Processes data from the FeatureCollection and approximates onto a 
Raster Grid.
     *
     * @param  fc                             Feature Collection with features 
to rasterize.
     * @param  attributeName                  Name of attribute from feature 
collection to provide as the cell value.
     * @exception  FeatureRasterizerException  An error when rasterizing the 
data
     */
    public void rasterize(FeatureCollection fc, String attributeName)
    throws FeatureRasterizerException {

        // calculate variable resolution bounds that fit around feature 
collection

        double edgeBuffer = 0.001;
        double x = fc.getBounds().getMinX() - edgeBuffer;
        double y = fc.getBounds().getMinY() - edgeBuffer;
        double width = fc.getBounds().getWidth() + edgeBuffer * 2;
        double height = fc.getBounds().getHeight() + edgeBuffer * 2;
        java.awt.geom.Rectangle2D.Double bounds = new 
java.awt.geom.Rectangle2D.Double(x, y, width, height);
        
        System.out.println("BOUNDS: "+bounds);
        System.out.println("FCBNDS: "+fc.getBounds());

        
        rasterize(fc, bounds, attributeName);
        
    }

    /**
     *  Gets the raster attribute of the FeatureRasterizer object
     *  Processes data from the FeatureCollection and approximates onto a 
Raster Grid.
     *
     * @param  fc                             Description of the Parameter
     * @param  bounds                         Description of the Parameter
     * @param  attributeName                  Name of attribute from feature 
collection to provide as the cell value.
     * @exception  FeatureRasterizerException  An error when rasterizing the 
data
     */
    public void rasterize(FeatureCollection fc, 
java.awt.geom.Rectangle2D.Double bounds, String attributeName)
        throws FeatureRasterizerException {

        this.attributeName = attributeName;
        
        // Check if we need to change the underlying raster
        if (resetRaster) {
            raster = RasterFactory.createBandedRaster(DataBuffer.TYPE_FLOAT,
                    width, height, 1, null);

            bimage = new BufferedImage(width, height, 
BufferedImage.TYPE_INT_ARGB);
            bimage.setAccelerationPriority(1.0f);
            GraphicsEnvironment ge = 
GraphicsEnvironment.getLocalGraphicsEnvironment();
//          System.out.println("IMAGE ACCELERATED? 
"+bimage.getCapabilities(ge.getDefaultScreenDevice().getDefaultConfiguration()).isAccelerated());
            graphics = bimage.createGraphics();
            graphics.setPaintMode();
            graphics.setComposite(AlphaComposite.Src);


            resetRaster = false;

            //System.out.println("---------------- RESETING FeatureRasterizer 
WritableRaster OBJECT -------------------- ");
        }
        // initialize raster to NoData value
        clearRaster();
        setBounds(bounds);

        // TODO - change method calls to account for a switch to control if 
rasterizer should work if vis bounds > feature bounds


        // All the data should start in the lower left corner.  Don't export 
what we don't need.
        double ratio = bounds.height / bounds.width;
        int ncols;
        int nrows;
        if (ratio < 1) {
            // wider than tall
            nrows = (int) (ratio * height);
            ncols = width;
        }
        else {
            nrows = height;
            ncols = (int) (height / ratio);
        }

        //System.out.println("1 --- WIDTH: " + ncols + "   HEIGHT: " + nrows);

        FeatureIterator fci = fc.features();
        Feature feature;

        while (fci.hasNext()) {


            feature = fci.next();

            addFeature(feature);

        }
        close();
    }

    /**
     * Implementation the StreamingProcess interface.  Rasterize a single 
feature and 
     * update current WriteableRaster using the current settings.
     * 
     * @param  feature     The feature to rasterize and add to current 
WritableRaster
     */   
    public void addFeature(Feature feature) {

        //System.out.println("rasterizer - processing feature: "+ attributeName 
+ " -- " + feature);
        
        try {

            value = 
Float.parseFloat(feature.getProperty(attributeName).getValue().toString());     
          

            if (value > maxAttValue) { maxAttValue = value; }
            if (value < minAttValue) { minAttValue = value; }

        } catch (Exception e) {         
            e.printStackTrace();            
            System.err.println("THE FEATURE COULD NOT BE RASTERIZED BASED ON 
THE '"+attributeName+
                    "' ATTRIBUTE VALUE OF 
'"+feature.getProperty(attributeName).getValue().toString()+"'");           
            return;         
        }

        int rgbVal = floatBitsToInt(value);

        graphics.setColor(new Color(rgbVal, true));

        // Extract polygon and rasterize!
        Geometry geometry = 
(Geometry)feature.getDefaultGeometryProperty().getValue();
        if (geometry.intersects(extentGeometry)) {

            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);
            }
        }
    }

    /**
     * Implementation the StreamingProcess interface - this copies values from 
BufferedImage RGB to WritableRaster of floats.
     */
    public void close() {
        for (int i = 0; i < width; i++) {
            for (int j = 0; j < height; j++) {
                double val = Float.intBitsToFloat(bimage.getRGB(i, j));
                raster.setSample(i, j, 0, val);
            }
        }
    }

    private void drawGeometry(Geometry geometry) {

        Coordinate[] coords = geometry.getCoordinates();

        // enlarge if needed
        if (coords.length > coordGridX.length) {
            coordGridX = new int[coords.length];
            coordGridY = new int[coords.length];
        }

        // Clear Array
        for (int i = 0; i < coords.length; i++) {
            coordGridX[i] = -1;
        }
        for (int i = 0; i < coords.length; i++) {
            coordGridY[i] = -1;
        }

        // Go through coordinate array in order received (clockwise)
        for (int n = 0; n < coords.length; n++) {
            coordGridX[n] = (int) (((coords[n].x - bounds.x) / xInterval));
            coordGridY[n] = (int) (((coords[n].y - bounds.y) / yInterval));
            coordGridY[n] = bimage.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);
        }
    }

    /**
     *  Gets the emptyGrid attribute of the FeatureRasterizer object
     *
     * @return    The emptyGrid value
     */
    public boolean isEmptyGrid() {
        return emptyGrid;
    }


    /**
     *  Gets the writableRaster attribute of the FeatureRasterizer object
     *
     * @return    The writableRaster value
     */
    public WritableRaster getWritableRaster() {
        return raster;
    }

    /**
     *  Sets the writableRaster attribute of the FeatureRasterizer object
     *
     * @param  raster  The new writableRaster value
     */
    public void setWritableRaster(WritableRaster raster) {
        this.raster = raster;
    }

    /**
     *  Gets the bounds attribute of the FeatureRasterizer object
     *
     * @return    The bounds value
     */
    public java.awt.geom.Rectangle2D.Double getBounds() {
        return bounds;
    }

    /**
     *  Sets the bounds for the Rasterizer
     *
     * @return    The bounds value
     */
    public void setBounds(java.awt.geom.Rectangle2D.Double bounds) {
        this.bounds = bounds;

        xInterval = bounds.width / (double) width;
        yInterval = bounds.height / (double) height;

        System.out.println("xInterval: " + xInterval + "  yInterval: " + 
yInterval);

        if (xInterval > yInterval) {
            yInterval = xInterval;
        }
        if (yInterval > xInterval) {
            xInterval = yInterval;
        }

        cellsize = yInterval;

        // Clip geometries to the provided bounds      
        // Create extent geometry  
        Envelope env = new Envelope(
                bounds.getX(), 
                bounds.getX() + bounds.getWidth(),
                bounds.getY(),
                bounds.getY() + bounds.getHeight()
        );
        extentGeometry = geoFactory.toGeometry(env);
    }

    /**
     *  Sets the entire raster to NoData
     */
    public void clearRaster() {

//      System.out.println("CLEARING RASTER");      
        minAttValue = 999999999;
        maxAttValue = -999999999;

        // initialize raster to NoData value
        for (int i = 0; i < width; i++) {
            for (int j = 0; j < height; j++) {
                raster.setSample(i, j, 0, noDataValue);
                bimage.setRGB(i, j, floatBitsToInt((float)noDataValue));
            }
        }
    }

    /**
     *  Get the current attribute to use as the grid cell values.
     */
    public String getAttName() {
        return attributeName;
    }

    /**
     *  Sets the current attribute to use as the grid cell values.
     */
    public void setAttName(String attName) {
        this.attributeName = attName;
    }

    /**
     *  Gets the cellsize attribute of the FeatureRasterizer object
     *
     * @return    The cellsize value
     */
    public double getCellsize() {
        return cellsize;
    }


    public double getNoDataValue() {
        return noDataValue;
    }

    public void setNoDataValue(double noData) {
        if (noData != noDataValue) {
            resetRaster = true;
        }
        this.noDataValue = noData;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        if (height != height) {
            resetRaster = true;
        }
        this.height = height;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        if (width != width) {
            resetRaster = true;
        }
        this.width = width;
    }

    public double getMinAttValue() {
        return minAttValue;
    }

    public double getMaxAttValue() {
        return maxAttValue;
    }

    private static int floatBitsToInt(float f) {
        ByteBuffer conv = ByteBuffer.allocate(4);
        conv.putFloat(0, f);
        return conv.getInt(0);
    }

    public String toString() {
        return "FEATURE RASTERIZER: WIDTH="+width+" , HEIGHT="+height+" , 
NODATA="+noDataValue;
    }

}


package gov.noaa.ncdc.geotools;

import gov.noaa.ncdc.geotools.FeatureRasterizerSteve.FeatureRasterizerException;

import java.awt.geom.Rectangle2D;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.NoSuchElementException;

import javax.swing.JFrame;

import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridCoverageFactory;
import org.geotools.data.FeatureSource;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.CRS;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.NoSuchAuthorityCodeException;

import com.sun.media.jai.widget.DisplayJAI;

public class TestFeatureRasterizer {

    
    public static void main(String[] args) {
        try {
            testRasterizer();
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    
    public static void testRasterizer() throws IOException, 
NoSuchElementException, FeatureRasterizerException, 
MismatchedDimensionException, NoSuchAuthorityCodeException, FactoryException {
        
        
        URL url = new File("H:\\ESRI\\usa\\counties.shp").toURI().toURL();
        ShapefileDataStore ds = new ShapefileDataStore(url);
        FeatureSource fs = ds.getFeatureSource("counties");

        
        FeatureRasterizerSteve rasterizer = new FeatureRasterizerSteve(800, 
800, -999.0f);
        rasterizer.setAttName("POP2000");

//        Envelope env = fs.getBounds();
//        Rectangle2D.Double bounds = new Rectangle2D.Double(env.getMinX(), 
env.getMinY(), env.getWidth(), env.getHeight());
        Rectangle2D.Double bounds = new Rectangle2D.Double(-120.0, 20.0, 40.0, 
20.0);
        
        rasterizer.setBounds(bounds);
        
        
        
        FeatureIterator fi = fs.getFeatures().features();
        while (fi.hasNext()) {
            rasterizer.addFeature(fi.next());
        }
        rasterizer.close();
        
        
        GridCoverageFactory gcFactory = new GridCoverageFactory();
        GridCoverage2D gc = gcFactory.create("TEST1", 
rasterizer.getWritableRaster(), 
                new ReferencedEnvelope(bounds.getMinX(), bounds.getMaxX(), 
bounds.getMinY(), bounds.getMaxY(), 
                        CRS.decode("EPSG:4326")));
        DisplayJAI display = new DisplayJAI(gc.getRenderedImage());
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        frame.getContentPane().add(display);
        frame.pack();
        frame.setVisible(true);
        
        
        
        
        FeatureCollection fc = fs.getFeatures();
        System.out.println("POP2001 feature collection size: "+fc.size());
        rasterizer.rasterize(fc, "POP2001");
        
        GridCoverage2D gc2 = gcFactory.create("TEST1", 
rasterizer.getWritableRaster(),
                new ReferencedEnvelope(bounds.getMinX(), bounds.getMaxX(), 
bounds.getMinY(), bounds.getMaxY(), 
                CRS.decode("EPSG:4326")));
        DisplayJAI display2 = new DisplayJAI(gc2.getRenderedImage());
        JFrame frame2 = new JFrame();
        frame2.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        frame2.getContentPane().add(display2);
        frame2.pack();
        frame2.setVisible(true);


    }
}
------------------------------------------------------------------------------
This SF.net email is sponsored by:
SourcForge Community
SourceForge wants to tell your story.
http://p.sf.net/sfu/sf-spreadtheword
_______________________________________________
Geotools-gt2-users mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/geotools-gt2-users

Reply via email to