package mil.nga.giat.geowave.adapter.raster.adapter.merge.nodata; import java.awt.image.Raster; import java.awt.image.WritableRaster; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import mil.nga.giat.geowave.adapter.raster.adapter.merge.nodata.NoDataMetadata.SampleIndex; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; public class NoDataMetadataFactory { private static class NoDataSummary { private final Set<SampleIndex> indices; private final double[][] usedNoDataValues; public NoDataSummary( final Set<SampleIndex> indices, final double[][] usedNoDataValues ) { this.indices = indices; this.usedNoDataValues = usedNoDataValues; } } private static final int MAX_LIST_NO_DATA = 20; public static NoDataMetadata createMetadata( final double[][] allNoDataValues, final Geometry shape, final Raster data ) { final NoDataSummary noDataSummary = getNoDataSummary( allNoDataValues, shape, data); return createMetadata( noDataSummary, new Geometry[] { shape }, data.getWidth(), data.getHeight()); } public static NoDataMetadata mergeMetadata( final NoDataMetadata noDataMetadata1, final WritableRaster raster1, final NoDataMetadata noDataMetadata2, final WritableRaster raster2 ) { if ((noDataMetadata1 == null) || (noDataMetadata2 == null)) { // this implies that there is no nodata values in one of the rasters // so there is no nodata values in the merge return null; } final Set<SampleIndex> noDataIndices1 = noDataMetadata1.getNoDataIndices(); final Set<SampleIndex> noDataIndices2 = noDataMetadata2.getNoDataIndices(); if ((noDataIndices1 != null) && (noDataIndices2 != null)) { // simple case, just take the intersection of the sets noDataIndices2.retainAll(noDataIndices1); return new NoDataBySampleIndex( noDataIndices2); } else if (noDataIndices1 != null) { // just determine which of the no data indices are covered by the // second set of metadata and remove them return mergeMetadataBySummary( noDataIndices1, noDataMetadata2, raster2); } else if (noDataIndices2 != null) { // just determine which of the no data indices are covered by the // first set of metadata and remove them return mergeMetadataBySummary( noDataIndices2, noDataMetadata1, raster1); } else if ((noDataMetadata1 instanceof NoDataByFilter) && (noDataMetadata2 instanceof NoDataByFilter)) { final NoDataByFilter noDataByFilter1 = ((NoDataByFilter) noDataMetadata1); final NoDataByFilter noDataByFilter2 = ((NoDataByFilter) noDataMetadata2); final double[][] noDataPerBand1 = noDataByFilter1.getNoDataPerBand(); final double[][] noDataPerBand2 = noDataByFilter2.getNoDataPerBand(); // union the no data values from each filter final int numBands = Math.min( noDataPerBand1.length, noDataPerBand2.length); final double[][] allNoDataValues = new double[numBands][]; for (int b = 0; b < numBands; b++) { final Set<Double> noDataValuesInBand = new HashSet<Double>(); if (noDataPerBand1[b] != null) { for (final double noDataValue : noDataPerBand1[b]) { noDataValuesInBand.add(noDataValue); } } if (noDataPerBand2[b] != null) { for (final double noDataValue : noDataPerBand2[b]) { noDataValuesInBand.add(noDataValue); } } allNoDataValues[b] = new double[noDataValuesInBand.size()]; int i = 0; final Iterator<Double> it = noDataValuesInBand.iterator(); while (it.hasNext()) { allNoDataValues[b][i++] = it.next(); } } return mergeMetadataBySummary( allNoDataValues, noDataByFilter1, raster1, noDataByFilter2, raster2); } else { // this should never happen because the only implementations of // metadata are by index or by filter but just in case iteratively // go through every sample, determine if its covered by the first or // the second set of metadata and use the indices return exhaustiveMergeMetadata( noDataMetadata1, raster1, noDataMetadata2, raster2); } } private static NoDataMetadata createMetadata( final NoDataSummary noDataSummary, final Geometry[] shapes, final int width, final int height ) { if (noDataSummary.indices.size() > MAX_LIST_NO_DATA) { Geometry finalShape; if ((shapes == null) || (shapes.length == 0)) { finalShape = null; } else { finalShape = shapes[0]; if ((shapes.length > 1) && (finalShape != null)) { for (int i = 1; i < shapes.length; i++) { if (shapes[i] == null) { finalShape = null; break; } else { finalShape = finalShape.union(shapes[i]); } } } } if ((finalShape != null) && finalShape.covers(new GeometryFactory().toGeometry(new Envelope( 0, width, 0, height)))) { // if the coverage of this geometric union ever gets to the // point that it fully covers the raster, stop storing it and // just set the geometry to null finalShape = null; } return new NoDataByFilter( finalShape, noDataSummary.usedNoDataValues); } else if (!noDataSummary.indices.isEmpty()) { // just go through every raster sample and determine whether it // qualifies as null data return new NoDataBySampleIndex( noDataSummary.indices); } else { // the "no data" samples in the dataset must be 0, so just return // null for the metadata return null; } } private static NoDataMetadata mergeMetadataBySummary( final Set<SampleIndex> noDataIndices, final NoDataMetadata noDataMetadata, final WritableRaster raster ) { final Iterator<SampleIndex> indices = noDataIndices.iterator(); while (indices.hasNext()) { final SampleIndex index = indices.next(); if (!noDataMetadata.isNoData( index, raster.getSampleDouble( index.getX(), index.getY(), index.getBand()))) { indices.remove(); } } return new NoDataBySampleIndex( noDataIndices); } private static NoDataMetadata exhaustiveMergeMetadata( final NoDataMetadata noDataMetadata1, final WritableRaster raster1, final NoDataMetadata noDataMetadata2, final WritableRaster raster2 ) { final int width = Math.min( raster1.getWidth(), raster2.getWidth()); final int height = Math.min( raster1.getHeight(), raster2.getHeight()); final int numBands = Math.min( raster1.getNumBands(), raster2.getNumBands()); final Set<SampleIndex> indices = new HashSet<SampleIndex>(); for (int b = 0; b < numBands; b++) { for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { final SampleIndex index = new SampleIndex( x, y, b); if (noDataMetadata1.isNoData( index, raster1.getSampleDouble( x, y, b)) && noDataMetadata2.isNoData( index, raster2.getSampleDouble( x, y, b))) { indices.add(index); } } } } return new NoDataBySampleIndex( indices); } private static NoDataMetadata mergeMetadataBySummary( final double[][] allNoDataValues, final NoDataByFilter noDataMetadata1, final WritableRaster raster1, final NoDataByFilter noDataMetadata2, final WritableRaster raster2 ) { final NoDataSummary noDataSummary = getNoDataSummary( allNoDataValues, noDataMetadata1, raster1, noDataMetadata2, raster2); return createMetadata( noDataSummary, new Geometry[] { noDataMetadata1.getShape(), noDataMetadata2.getShape() }, raster2.getWidth(), // both rasters better be the same // dimensions raster2.getHeight()); } private static NoDataSummary getNoDataSummary( final double[][] allNoDataValues, final NoDataByFilter noDataMetadata1, final WritableRaster raster1, final NoDataByFilter noDataMetadata2, final WritableRaster raster2 ) { final int width = Math.min( raster1.getWidth(), raster2.getWidth()); final int height = Math.min( raster1.getHeight(), raster2.getHeight()); final int numBands = Math.min( raster1.getNumBands(), raster2.getNumBands()); return getNoDataSummary( allNoDataValues, new MultiShape( new Geometry[] { noDataMetadata1.getShape(), noDataMetadata2.getShape() }), new MultiRaster( new Raster[] { raster1, raster2 }), width, height, numBands); } private static NoDataSummary getNoDataSummary( final double[][] allNoDataValues, final Geometry shape, final Raster data ) { return getNoDataSummary( allNoDataValues, new SingleShape( shape), new SingleRaster( data), data.getWidth(), data.getHeight(), data.getNumBands()); } private static NoDataSummary getNoDataSummary( final double[][] allNoDataValues, final NoDataByCoordinate shape, final NoDataBySample data, final int width, final int height, final int numBands ) { final Set<Double>[] noDataValuesPerBand; boolean skipNoData; final Set<SampleIndex> indices = new HashSet<SampleIndex>(); if (allNoDataValues == null) { skipNoData = true; noDataValuesPerBand = null; if (shape == null) { return new NoDataSummary( indices, new double[][] {}); } } else { noDataValuesPerBand = new Set[numBands]; for (int b = 0; b < numBands; b++) { noDataValuesPerBand[b] = new HashSet<Double>(); } skipNoData = false; } for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { if (shape.isNoData( x, y)) { for (int b = 0; b < numBands; b++) { indices.add(new SampleIndex( x, y, b)); } // this will ignore the no data values for this x,y // which should be fine because the shape will // always classify this x,y as "no data" } else if (!skipNoData) { for (int b = 0; b < numBands; b++) { if (allNoDataValues[b] == null) { continue; } else { final double[] samples = data.getSampleValues( x, y, b); for (int i = 0; i < allNoDataValues[b].length; i++) { // if a single sample is not a "no data" value // then it is valid boolean noData = true; for (final double sample : samples) { // we wrap it with Object equality to make // sure we generically catch special // cases, such as NaN and positive and // negative infinite if (!new Double( sample).equals(allNoDataValues[b][i])) { noData = false; break; } } if (noData) { indices.add(new SampleIndex( x, y, b)); if (noDataValuesPerBand != null && noDataValuesPerBand[b] != null) { noDataValuesPerBand[b].add(allNoDataValues[b][i]); } } } } } } } } final double[][] usedNoDataValues; if (!skipNoData && noDataValuesPerBand != null) { usedNoDataValues = new double[noDataValuesPerBand.length][]; for (int b = 0; b < noDataValuesPerBand.length; b++) { usedNoDataValues[b] = new double[noDataValuesPerBand[b].size()]; int i = 0; final Iterator<Double> noDataValues = noDataValuesPerBand[b].iterator(); while (noDataValues.hasNext()) { usedNoDataValues[b][i++] = noDataValues.next(); } } } else { usedNoDataValues = new double[][] {}; } return new NoDataSummary( indices, usedNoDataValues); } private static interface NoDataByCoordinate { public boolean isNoData( int x, int y ); } private static interface NoDataBySample { public double[] getSampleValues( int x, int y, int b ); } private static class SingleShape implements NoDataByCoordinate { private final Geometry shape; public SingleShape( final Geometry shape ) { this.shape = shape; } @Override public boolean isNoData( final int x, final int y ) { return ((shape != null) && !shape.intersects(new GeometryFactory().createPoint(new Coordinate( x, y)))); } } private static class MultiShape implements NoDataByCoordinate { private final Geometry[] shapes; private boolean acceptNone = false; public MultiShape( final Geometry[] shapes ) { this.shapes = shapes; if ((shapes == null) || (shapes.length == 0)) { acceptNone = true; } else { for (final Geometry shape : shapes) { if (shape == null) { acceptNone = true; } } } } @Override public boolean isNoData( final int x, final int y ) { if (!acceptNone) { for (final Geometry shape : shapes) { // if any one intersects the point than it is not "no data" // based on shape if (shape.intersects(new GeometryFactory().createPoint(new Coordinate( x, y)))) { return false; } } return true; } return false; } } private static class SingleRaster implements NoDataBySample { private final Raster raster; public SingleRaster( final Raster raster ) { this.raster = raster; } @Override public double[] getSampleValues( final int x, final int y, final int b ) { return new double[] { raster.getSampleDouble( x, y, b) }; } } private static class MultiRaster implements NoDataBySample { private final Raster[] rasters; public MultiRaster( final Raster[] rasters ) { this.rasters = rasters; } @Override public double[] getSampleValues( final int x, final int y, final int b ) { final double[] samples = new double[rasters.length]; for (int i = 0; i < rasters.length; i++) { samples[i] = rasters[i].getSampleDouble( x, y, b); } return samples; } } }