/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2014, 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 static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.awt.image.Raster; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.data.DataStoreFactorySpi; import org.geotools.data.DataUtilities; import org.geotools.data.DefaultTransaction; import org.geotools.data.FeatureSource; import org.geotools.data.FeatureStore; import org.geotools.data.Transaction; import org.geotools.data.collection.ListFeatureCollection; import org.geotools.data.shapefile.ShapefileDataStore; import org.geotools.data.shapefile.ShapefileDataStoreFactory; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.feature.FeatureIterator; import org.geotools.feature.simple.SimpleFeatureBuilder; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.gce.geotiff.GeoTiffReader; import org.geotools.resources.image.ImageUtilities; import org.geotools.test.TestData; import org.geotools.util.Range; import org.geotools.util.Utilities; import org.junit.Before; import org.junit.Test; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.referencing.crs.CoordinateReferenceSystem; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.Polygon; import com.vividsolutions.jts.io.InStream; import com.vividsolutions.jts.io.InputStreamInStream; import com.vividsolutions.jts.io.OutStream; import com.vividsolutions.jts.io.OutputStreamOutStream; import com.vividsolutions.jts.io.ParseException; import com.vividsolutions.jts.io.WKBReader; import com.vividsolutions.jts.io.WKBWriter; import com.vividsolutions.jts.io.WKTReader; import com.vividsolutions.jts.io.WKTWriter; /** * Tests for the raster to vector FootprintExtractionProcess. * * @author Daniele Romagnoli, GeoSolutions SAS * */ public class FootprintExtractionProcessTest { private static final String THE_GEOM = "the_geom"; private static final String CREATE_SPATIAL_INDEX = "create spatial index"; private static final double TOLERANCE = 1.0e-12; private FootprintExtractionProcess process; /** A reference geometry being extracted from the cloud file by excluding only BLACK pixels */ private Geometry referenceGeometry; private File cloudFile; private File islandFile; static enum WritingFormat { WKB { @Override void write(Geometry geometry, File outputFile, CoordinateReferenceSystem crs) throws IOException { final WKBWriter wkbWriter = new WKBWriter(2); final OutputStream outputStream = new FileOutputStream(outputFile); final BufferedOutputStream bufferedStream = new BufferedOutputStream(outputStream); final OutStream outStream = new OutputStreamOutStream(bufferedStream); try { wkbWriter.write(geometry, outStream); } finally { IOUtils.closeQuietly(bufferedStream); } } }, WKT { @Override void write(Geometry geometry, File outputFile, CoordinateReferenceSystem crs) throws IOException { final WKTWriter wktWriter = new WKTWriter(2); final StringWriter wkt = new StringWriter(); BufferedWriter bufferedWriter = new BufferedWriter(wkt); try { wktWriter.write(geometry, bufferedWriter); } finally { IOUtils.closeQuietly(bufferedWriter); } // write to file if (outputFile != null) { bufferedWriter = new BufferedWriter(new FileWriter(outputFile)); try { bufferedWriter.write(wkt.toString()); } finally { IOUtils.closeQuietly(bufferedWriter); } } } }, SHAPEFILE { @Override void write(Geometry geometry, File outputFile, CoordinateReferenceSystem crs) throws IOException { // create feature type final SimpleFeatureTypeBuilder featureTypeBuilder = new SimpleFeatureTypeBuilder(); featureTypeBuilder.setName("raster2vector"); featureTypeBuilder.setCRS(crs); featureTypeBuilder.add(THE_GEOM, Polygon.class); featureTypeBuilder.add("cat", Integer.class); SimpleFeatureType featureType = featureTypeBuilder.buildFeatureType(); // Preparing the collection final ListFeatureCollection collection = new ListFeatureCollection(featureType); final String typeName = featureType.getTypeName(); // Creating the feature final SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(featureType); final Object[] values = new Object[] { geometry, 0 }; featureBuilder.addAll(values); final SimpleFeature feature = featureBuilder.buildFeature(typeName + '.' + 0); // adding the feature to the collection collection.add(feature); // create shapefile final DataStoreFactorySpi factory = new ShapefileDataStoreFactory(); // Preparing creation param final Map<String, Serializable> params = new HashMap<String, Serializable>(); params.put(CREATE_SPATIAL_INDEX, Boolean.TRUE); params.put("url", DataUtilities.fileToURL(outputFile)); final ShapefileDataStore ds = (ShapefileDataStore) factory .createNewDataStore(params); ds.createSchema(featureType); if (crs != null) { ds.forceSchemaCRS(crs); } // Write the features to the shapefile Transaction transaction = new DefaultTransaction("create"); FeatureSource source = ds.getFeatureSource(ds.getTypeNames()[0]); if (source instanceof FeatureStore) { FeatureStore store = (FeatureStore) source; store.setTransaction(transaction); try { store.addFeatures(collection); transaction.commit(); } catch (Exception e) { transaction.rollback(); } finally { try { transaction.close(); } catch (IOException ioe) { } } } ds.dispose(); } }; abstract void write(Geometry geometry, File outputFile, CoordinateReferenceSystem crs) throws IOException; } @Before public void setup() throws IOException, ParseException { process = new FootprintExtractionProcess(); cloudFile = TestData.file(this, "cloud.tif"); islandFile = TestData.file(this, "island.tif"); final File geometryFile = TestData.file(this, "cloud.wkt"); referenceGeometry = wktRead(geometryFile); } private static Geometry wktRead(File geometryFile) throws FileNotFoundException, ParseException { FileReader fileReader = null; try { WKTReader wktReader = new WKTReader(); fileReader = new FileReader(geometryFile); return wktReader.read(fileReader); } finally { IOUtils.closeQuietly(fileReader); } } private static Geometry wkbRead(File geometryFile) throws ParseException, IOException { BufferedInputStream bufferedStream = null; try { WKBReader wkbReader = new WKBReader(); final InputStream inputStream = new FileInputStream(geometryFile); bufferedStream = new BufferedInputStream(inputStream); final InStream inStream = new InputStreamInStream(bufferedStream); return wkbReader.read(inStream); } finally { IOUtils.closeQuietly(bufferedStream); } } @Test public void cloudExtractionTest() throws Exception { GeoTiffReader reader = null; FeatureIterator<SimpleFeature> iter = null; GridCoverage2D cov = null; try { reader = new GeoTiffReader(cloudFile); cov = reader.read(null); SimpleFeatureCollection fc = process.execute(cov, null, 10d, false, null, true, true, null, null); assertEquals(1, fc.size()); iter = fc.features(); SimpleFeature feature = iter.next(); MultiPolygon geometry = (MultiPolygon) feature.getDefaultGeometry(); assertTrue(referenceGeometry.equalsExact(geometry, TOLERANCE)); } finally { if (iter != null) { iter.close(); } if (reader != null) { try { reader.dispose(); } catch (Throwable t) { } } if (cov != null) { try { cov.dispose(true); } catch (Throwable t) { } } } } @Test public void cloudExtractionSimplified() throws Exception { GeoTiffReader reader = null; FeatureIterator<SimpleFeature> iter = null; GridCoverage2D cov = null; try { reader = new GeoTiffReader(cloudFile); cov = reader.read(null); SimpleFeatureCollection fc = process.execute(cov, null, 10d, true, 4d, true, true, null, null); assertEquals(2, fc.size()); iter = fc.features(); // Getting the main Footprint SimpleFeature feature = iter.next(); Geometry geometry = (Geometry) feature.getDefaultGeometry(); double fullArea = geometry.getArea(); // Getting to the simplified Footprint feature = (SimpleFeature) iter.next(); geometry = (Geometry) feature.getDefaultGeometry(); double simplifiedArea = geometry.getArea(); // area are different and polygons are different too assertTrue(Math.abs(simplifiedArea - fullArea) > 0); assertFalse(referenceGeometry.equalsExact(geometry, TOLERANCE)); } finally { if (iter != null) { iter.close(); } if (reader != null) { try { reader.dispose(); } catch (Throwable t) { } } if (cov != null) { try { cov.dispose(true); } catch (Throwable t) { } } } } @Test public void cloudExtractionNoRemoveCollinear() throws Exception { GeoTiffReader reader = null; FeatureIterator<SimpleFeature> iter = null; GridCoverage2D cov = null; try { reader = new GeoTiffReader(cloudFile); cov = reader.read(null); SimpleFeatureCollection fc = process.execute(cov, null, 10d, false, null, false, true, null, null); iter = fc.features(); SimpleFeature feature = iter.next(); Geometry geometry = (Geometry) feature.getDefaultGeometry(); final int removeCollinearLength = referenceGeometry.getGeometryN(0).getCoordinates().length; assertEquals(133, removeCollinearLength); // The computed polygon should have more vertices due to collinear point not being removed final int length = geometry.getGeometryN(0).getCoordinates().length; assertTrue(length > removeCollinearLength); assertFalse(referenceGeometry.equalsExact(geometry, TOLERANCE)); } finally { if (iter != null) { iter.close(); } if (reader != null) { try { reader.dispose(); } catch (Throwable t) { } } if (cov != null) { try { cov.dispose(true); } catch (Throwable t) { } } } } @Test public void cloudExtractionWithoutDarkPixels() throws Exception { GeoTiffReader reader = null; FeatureIterator<SimpleFeature> iter = null; GridCoverage2D cov = null; try { reader = new GeoTiffReader(cloudFile); cov = reader.read(null); // Exclude pixels with luminance less than 20. final int referenceLuminance = 10; List<Range<Integer>> exclusionRanges = Collections.singletonList(new Range<Integer>(Integer.class, 0, referenceLuminance)); SimpleFeatureCollection fc = process.execute(cov, exclusionRanges, 10d, false, null, true, true, null, null); iter = fc.features(); SimpleFeature feature = iter.next(); Geometry geometry = (Geometry) feature.getDefaultGeometry(); Raster raster = cov.getRenderedImage().getData(); int[] darkPixel = new int[3]; // These positions identify a couple of dark pixels of the cloud edge raster.getPixel(9, 13, darkPixel); double luminance = ImageUtilities.RGB_TO_GRAY_MATRIX[0][0] * darkPixel[0] + ImageUtilities.RGB_TO_GRAY_MATRIX[0][1] * darkPixel[1] + ImageUtilities.RGB_TO_GRAY_MATRIX[0][2] * darkPixel[2]; assertTrue(luminance < referenceLuminance); raster.getPixel(15, 7, darkPixel); luminance = ImageUtilities.RGB_TO_GRAY_MATRIX[0][0] * darkPixel[0] + ImageUtilities.RGB_TO_GRAY_MATRIX[0][1] * darkPixel[1] + ImageUtilities.RGB_TO_GRAY_MATRIX[0][2] * darkPixel[2]; assertTrue(luminance < referenceLuminance); // The computed polygon should have different shape due to dark pixels being excluded assertFalse(referenceGeometry.equalsExact(geometry, TOLERANCE)); } finally { if (iter != null) { iter.close(); } if (reader != null) { try { reader.dispose(); } catch (Throwable t) { } } if (cov != null) { try { cov.dispose(true); } catch (Throwable t) { } } } } @Test public void islandPolygonExtractionWithoutDarkPixelsAndWhiteClouds() throws Exception { GeoTiffReader reader = null; FeatureIterator<SimpleFeature> iter = null; GridCoverage2D cov = null; try { reader = new GeoTiffReader(islandFile); cov = reader.read(null); // Test removing black areas and clouds List<Range<Integer>> exclusionRanges = new ArrayList<Range<Integer>>(); exclusionRanges.add(new Range<Integer>(Integer.class, 0, 10)); exclusionRanges.add(new Range<Integer>(Integer.class, 253, 255)); SimpleFeatureCollection fc = process.execute(cov, exclusionRanges, 10d, false, null, true, true, null, null); iter = fc.features(); SimpleFeature feature = iter.next(); Geometry geometry = (Geometry) feature.getDefaultGeometry(); Geometry islandWkt = wktRead(TestData.file(this, "island.wkt")); assertTrue(islandWkt.equalsExact(geometry, TOLERANCE)); } finally { if (iter != null) { iter.close(); } if (reader != null) { try { reader.dispose(); } catch (Throwable t) { } } if (cov != null) { try { cov.dispose(true); } catch (Throwable t) { } } } } @Test public void valuesEqualityTest() throws Exception { double p1 = 0.2; double p2 = 2d / 10; double p3 = 0.21; assertTrue(MarchingSquaresVectorizer.areEqual(p1, p2)); assertFalse(MarchingSquaresVectorizer.areEqual(p1, p3)); } @Test public void cloudExtractionWriteToDisk() throws Exception { GeoTiffReader reader = null; FeatureIterator<SimpleFeature> iter = null; GridCoverage2D cov = null; try { reader = new GeoTiffReader(cloudFile); cov = reader.read(null); SimpleFeatureCollection fc = process.execute(cov, null, 10d, false, null, true, true, null, null); assertEquals(1, fc.size()); iter = fc.features(); SimpleFeature feature = iter.next(); Geometry geometry = (Geometry) feature.getDefaultGeometry(); assertTrue(referenceGeometry.equalsExact(geometry, TOLERANCE)); final File wktFile = TestData.temp(this, "cloudWkt.wkt"); final File wkbFile = TestData.temp(this, "cloudWkb.wkb"); final File shapeFile = TestData.temp(this, "cloudShape.shp"); CoordinateReferenceSystem crs = reader.getCoordinateReferenceSystem(); // Write geometries writeGeometry(WritingFormat.WKT, geometry, wktFile, crs); writeGeometry(WritingFormat.WKB, geometry, wkbFile, crs); writeGeometry(WritingFormat.SHAPEFILE, geometry, shapeFile, crs); // read geometries back assertTrue(referenceGeometry.equalsExact(wktRead(wktFile), TOLERANCE)); assertTrue(referenceGeometry.equalsExact(wkbRead(wkbFile), TOLERANCE)); } finally { if (iter != null) { iter.close(); } if (reader != null) { try { reader.dispose(); } catch (Throwable t) { } } if (cov != null) { try { cov.dispose(true); } catch (Throwable t) { } } } } public static void writeGeometry(final WritingFormat writingFormat, final Geometry geometry, final File outputFile, final CoordinateReferenceSystem crs) throws IOException { Utilities.ensureNonNull("writingFormat", writingFormat); Utilities.ensureNonNull("geometry", geometry); if ((outputFile != null) && outputFile.exists()) { FileUtils.deleteQuietly(outputFile); } writingFormat.write(geometry, outputFile, crs); } }