/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2011, 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.process.raster;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.imageio.ImageIO;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.Polygon;
import org.jaitools.numeric.Range;
import org.geotools.coverage.CoverageFactoryFinder;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridCoverageFactory;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.feature.FeatureIterator;
import org.geotools.feature.NameImpl;
import org.geotools.geometry.jts.JTS;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.process.Process;
import org.geotools.process.ProcessFactory;
import org.geotools.process.Processors;
import org.geotools.process.raster.PolygonExtractionProcess;
import org.opengis.feature.simple.SimpleFeature;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Tests for the raster to vector PolygonExtractionProcess.
*
* @author Michael Bedward
* @since 8.0
*
* @source $URL$
* @version $Id$
*/
public class PolygonExtractionProcessTest {
private static final double TOL = 1.0e-6;
private static final GridCoverageFactory covFactory = CoverageFactoryFinder.getGridCoverageFactory(null);
private PolygonExtractionProcess process;
@Before
public void setup() {
process = new PolygonExtractionProcess();
}
@Test
public void simpleSmallCoverage() throws Exception {
GridCoverage2D cov = buildSmallCoverage();
final int perimeters[] = { 4, 16, 4 };
final int areas[] = {1, 7, 1};
int band = 0;
Set<Double> outsideValues = Collections.singleton(0D);
SimpleFeatureCollection fc = process.execute(cov, 0, Boolean.TRUE, null, null, null, null);
assertEquals(3, fc.size());
FeatureIterator iter = fc.features();
try {
while (iter.hasNext()) {
SimpleFeature feature = (SimpleFeature) iter.next();
Polygon poly = (Polygon) feature.getDefaultGeometry();
int value = ((Number) feature.getAttribute("value")).intValue();
assertEquals(perimeters[value - 1], (int) (poly.getBoundary().getLength() + 0.5));
assertEquals(areas[value - 1], (int) (poly.getArea() + 0.5));
}
} finally {
iter.close();
}
}
private GridCoverage2D buildSmallCoverage() {
// small raster with 3 non-zero regions
final float[][] DATA = {
{2, 2, 0, 3},
{0, 2, 0, 0},
{0, 2, 2, 2},
{1, 0, 0, 2}
};
GridCoverage2D cov = covFactory.create(
"coverage",
DATA,
new ReferencedEnvelope(0, DATA[0].length, 0, DATA.length, null));
return cov;
}
@Test
public void checkThatHolesArePresentInPolygons() throws Exception {
final float[][] DATA = {
{1, 1, 1, 1, 0, 1, 1, 1, 1},
{1, 0, 0, 1, 0, 1, 0, 0, 1},
{1, 0, 0, 1, 0, 1, 0, 0, 1},
{1, 1, 1, 1, 0, 1, 1, 1, 1}
};
final int NUM_POLYS = 2;
GridCoverage2D cov = covFactory.create(
"coverage",
DATA,
new ReferencedEnvelope(0, DATA[0].length, 0, DATA.length, null));
SimpleFeatureCollection fc = process.execute(cov, 0, Boolean.TRUE, null, null, null, null);
assertEquals(NUM_POLYS, fc.size());
SimpleFeatureIterator iter = fc.features();
try {
while (iter.hasNext()) {
Polygon poly = (Polygon) iter.next().getDefaultGeometry();
assertEquals(1, poly.getNumInteriorRing());
}
} finally {
iter.close();
}
}
@Test
public void treatZeroAsDataValue() throws Exception {
final float[][] DATA = {
{1, 1, 1, 1, 0, 1, 1, 1, 1},
{1, 0, 0, 1, 0, 1, 0, 0, 1},
{1, 0, 0, 1, 0, 1, 0, 0, 1},
{1, 1, 1, 1, 0, 1, 1, 1, 1}
};
final int NUM_POLYS = 5;
GridCoverage2D cov = covFactory.create(
"coverage",
DATA,
new ReferencedEnvelope(0, DATA[0].length, 0, DATA.length, null));
Number[] noDataValues = { -1 };
SimpleFeatureCollection fc = process.execute(cov, 0, Boolean.TRUE, null, Arrays.asList(noDataValues), null, null);
assertEquals(NUM_POLYS, fc.size());
}
@Test
public void ignoreInteriorBoundariesBetweenRegions() throws Exception {
final float[][] DATA = {
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 1, 1, 1, 2, 2, 2, 3, 3, 0},
{0, 1, 1, 1, 2, 2, 2, 3, 3, 0},
{0, 1, 1, 1, 2, 2, 2, 3, 3, 0},
{0, 4, 4, 5, 5, 5, 6, 6, 6, 0},
{0, 4, 4, 5, 5, 5, 6, 6, 6, 0},
{0, 4, 4, 5, 5, 5, 6, 6, 6, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
};
final int width = DATA[0].length;
final int height = DATA.length;
GridCoverage2D cov = covFactory.create(
"coverage",
DATA,
new ReferencedEnvelope(0, width, 0, height, null));
SimpleFeatureCollection fc = process.execute(cov, 0, Boolean.FALSE, null, null, null, null);
assertEquals(1, fc.size());
Geometry geom = (Geometry) fc.features().next().getDefaultGeometry();
Envelope env = geom.getEnvelopeInternal();
assertEquals(new Envelope(1, width-1, 1, height-1), env);
}
/**
* This test works with a simple L-shape raster pattern which was causing
* the original raster to vector code to fail.
*/
@Test
public void singleLShapedRegion() throws Exception {
final float[][] DATA = {
{0, 0, 0, 0, 0},
{0, 1, 0, 0, 0},
{0, 1, 1, 1, 0},
{0, 0, 0, 0, 0}
};
GridCoverage2D cov = covFactory.create(
"coverage",
DATA,
new ReferencedEnvelope(0, DATA[0].length, 0, DATA.length, null));
Set<Double> outsideValues = Collections.singleton(0D);
SimpleFeatureCollection fc = process.execute(cov, 0, Boolean.TRUE, null, null, null, null);
assertEquals(1, fc.size());
SimpleFeature feature = fc.features().next();
int value = ((Number) feature.getAttribute("value")).intValue();
assertEquals(1, value);
}
/**
* This test uses an image that caused the original raster to vector code
* to fail. It is kept here to guard against regression.
*/
@Test
public void extractPolygonsFromViewshedRaster() throws Exception {
final double ROUND_OFF_TOLERANCE = 1.0e-4D;
URL url = getClass().getResource("viewshed.tif");
BufferedImage img = ImageIO.read(url);
Rectangle bounds = new Rectangle(img.getMinX(), img.getMinY(), img.getWidth(), img.getHeight());
ReferencedEnvelope env = new ReferencedEnvelope(bounds, null);
GridCoverage2D cov = covFactory.create("coverage", img, env);
final int OUTSIDE = -1;
List<Number> noDataValues = new ArrayList<Number>();
noDataValues.add(OUTSIDE);
SimpleFeatureCollection fc = process.execute(
cov, 0, Boolean.TRUE, null, noDataValues, null, null);
// validate geometries and sum areas
SimpleFeatureIterator iter = fc.features();
Map<Integer, Double> areas = new HashMap<Integer, Double>();
try {
while (iter.hasNext()) {
SimpleFeature feature = iter.next();
Geometry geom = (Geometry) feature.getDefaultGeometry();
assertTrue(geom.isValid());
int value = ((Number) feature.getAttribute("value")).intValue();
if (value != OUTSIDE) {
Double sum = areas.get(value);
if (sum == null) {
sum = 0.0d;
}
sum += geom.getArea();
areas.put(value, sum);
}
}
} finally {
iter.close();
}
// compare summed areas to image data
Map<Integer, Double> imgAreas = new HashMap<Integer, Double>();
Raster tile = img.getTile(0, 0);
for (int y = img.getMinY(), ny = 0; ny < img.getHeight(); y++, ny++) {
for (int x = img.getMinX(), nx = 0; nx < img.getWidth(); x++, nx++) {
double gridvalue = tile.getSampleDouble(x, y, 0);
if (Math.abs(gridvalue - OUTSIDE) < TOL) {
Double sum = areas.get((int)gridvalue);
if (sum == null) {
sum = 1.0D;
} else {
sum += 1.0D;
}
areas.put((int)gridvalue, sum);
}
}
}
for (Integer i : imgAreas.keySet()) {
double ratio = areas.get(i) / imgAreas.get(i);
assertTrue(Math.abs(1.0D - ratio) < ROUND_OFF_TOLERANCE);
}
}
@Test
public void classificationRanges() {
// Test data contains values 1 - 9 but we will only define
// classification ranges for 1 - 4 and 5 - 8. As a reasult,
// we expect the region with value == 9 to be absent from
// the returned polygons.
final float[][] DATA = {
{1, 1, 3, 3, 5, 5, 7, 7},
{1, 1, 3, 3, 5, 5, 7, 7},
{2, 2, 4, 4, 6, 6, 8, 8},
{2, 2, 4, 4, 6, 6, 8, 8},
{9, 9, 9, 9, 9, 9, 9, 9}
};
final int width = DATA[0].length;
final int height = DATA.length;
GridCoverage2D cov = covFactory.create(
"coverage",
DATA,
new ReferencedEnvelope(0, width, 0, height, null));
List<Range> classificationRanges = new ArrayList<Range>();
Range<Integer> r1 = Range.create(1, true, 4, true);
Range<Integer> r2 = Range.create(5, true, 8, true);
classificationRanges.add(r1);
classificationRanges.add(r2);
SimpleFeatureCollection fc = process.execute(
cov, 0, Boolean.TRUE, null, null, classificationRanges, null);
assertEquals(2, fc.size());
// Expected result is 2 polygons, each with area == 16.0
SimpleFeatureIterator iter = fc.features();
List<Integer> expectedValues = new ArrayList<Integer>();
expectedValues.add(1);
expectedValues.add(2);
try {
while (iter.hasNext()) {
SimpleFeature feature = iter.next();
Integer value = ((Number) feature.getAttribute("value")).intValue();
assertTrue(expectedValues.remove(value));
Polygon poly = (Polygon) feature.getDefaultGeometry();
assertEquals(16.0, poly.getArea(), TOL);
}
} finally {
iter.close();
}
}
/**
* Creates an ROI having the same bounds as the input grid coverage
* to check that the whole coverage is included in vectorizing.
*
* This test fails at the moment because the top and right edges
* (in world space) of the coverage are treated as not included by
* the ROI.
*/
@Ignore("See GEOT-3861")
@Test
public void useCoverageBoundsAsROI() {
final float[][] DATA = {
{1, 1, 1, 1},
{1, 1, 1, 1},
{1, 1, 1, 1},
{1, 1, 1, 1}
};
final int width = DATA[0].length;
final int height = DATA.length;
final double cellSize = 1000;
final double minX = 10000;
final double minY = 5000;
final double maxX = minX + width * cellSize;
final double maxY = minY + height * cellSize;
final ReferencedEnvelope dataEnv = new ReferencedEnvelope(minX, maxX, minY, maxY, null);
GridCoverage2D cov = covFactory.create("coverage", DATA, dataEnv);
/*
* Create rectangular polygon with the same envelope as the
* grid coverage to use as an ROI
*/
Polygon roiGeom = JTS.toGeometry(dataEnv);
/*
* Vectorize the coverage and check that we get a single polygon
* having the same bounds as the input coverage
*/
SimpleFeatureCollection fc =
process.execute(cov, 0, Boolean.TRUE, roiGeom, null, null, null);
assertEquals(1, fc.size());
SimpleFeature feature = fc.features().next();
assertEquals(1, ((Number) feature.getAttribute("value")).intValue());
ReferencedEnvelope polyEnv = JTS.toEnvelope((Geometry) feature.getDefaultGeometry());
assertTrue("Expected " + dataEnv + " but got " + polyEnv,
dataEnv.boundsEquals2D(polyEnv, TOL));
}
@Ignore("See GEOT-3861")
@Test
public void useROIToExcludeLeftAndRightImageCols() {
final float[][] DATA = {
{0, 0, 0, 0, 0, 0, 0, 0},
{1, 1, 2, 2, 2, 2, 3, 3},
{1, 1, 2, 2, 2, 2, 3, 3},
{1, 1, 2, 2, 2, 2, 3, 3},
{1, 1, 2, 2, 2, 2, 3, 3},
{1, 1, 2, 2, 2, 2, 3, 3},
{0, 0, 0, 0, 0, 0, 0, 0}
};
final int width = DATA[0].length;
final int height = DATA.length;
final double cellSize = 1000;
final double minX = 10000;
final double minY = 5000;
final double maxX = minX + width * cellSize;
final double maxY = minY + height * cellSize;
final ReferencedEnvelope dataEnv = new ReferencedEnvelope(minX, maxX, minY, maxY, null);
GridCoverage2D cov = covFactory.create("coverage", DATA, dataEnv);
// Create an ROI that cuts off the left and right-most pixels
ReferencedEnvelope processEnv = new ReferencedEnvelope(
minX + cellSize, maxX - cellSize, minY, maxY, null);
Polygon roiGeometry = JTS.toGeometry(processEnv);
SimpleFeatureCollection fc = process.execute(
cov, 0, Boolean.TRUE, roiGeometry, null, null, null);
// Expected result is 3 polygons:
// value == 1, area = 5 cells
// value == 2, area = 20 cells
// value == 3, area = 5 cells
assertEquals(3, fc.size());
final double cellArea = cellSize * cellSize;
final double[] areas = { 5 * cellArea, 20 * cellArea, 5 * cellArea };
List<Integer> expectedValues = new ArrayList<Integer>();
expectedValues.addAll(Arrays.asList(1, 2, 3));
SimpleFeatureIterator iter = fc.features();
try {
while (iter.hasNext()) {
SimpleFeature feature = iter.next();
Integer value = ((Number) feature.getAttribute("value")).intValue();
System.out.println(value);
assertTrue(expectedValues.remove(value));
Polygon poly = (Polygon) feature.getDefaultGeometry();
System.out.println(poly.toText());
assertEquals(areas[value - 1], poly.getArea(), TOL);
}
} finally {
iter.close();
}
}
@Test
public void testCallViaProcessFactory() {
Process process = Processors.createProcess(new NameImpl("ras", "PolygonExtraction"));
assertNotNull(process);
Map<String, Object> inputs = new HashMap<String, Object>();
inputs.put("data", buildSmallCoverage());
inputs.put("ranges", Arrays.asList(new Range<Integer>(0, true, 1, true), new Range<Integer>(2, true, 3, true)));
Map<String, Object> results = process.execute(inputs, null);
Object result = results.get("result");
assertTrue(result instanceof SimpleFeatureCollection);
SimpleFeatureCollection fc = (SimpleFeatureCollection) result;
assertEquals(4, fc.size());
}
}