/*******************************************************************************
* Copyright 2013 Geoscience Australia
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package au.gov.ga.earthsci.model.core.raster;
import static au.gov.ga.earthsci.common.buffer.BufferUtil.getValue;
import static au.gov.ga.earthsci.common.buffer.BufferUtil.skipValues;
import static au.gov.ga.earthsci.core.raster.GDALRasterUtil.getBufferType;
import java.awt.Color;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.UUID;
import org.gdal.gdal.Band;
import org.gdal.gdal.Dataset;
import org.gdal.osr.CoordinateTransformation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import au.gov.ga.earthsci.common.buffer.BufferType;
import au.gov.ga.earthsci.common.buffer.BufferUtil;
import au.gov.ga.earthsci.common.color.ColorMap;
import au.gov.ga.earthsci.common.color.ColorMap.InterpolationMode;
import au.gov.ga.earthsci.common.color.ColorType;
import au.gov.ga.earthsci.common.spatial.SpatialReferences;
import au.gov.ga.earthsci.common.util.Validate;
import au.gov.ga.earthsci.model.IModel;
import au.gov.ga.earthsci.model.bounds.BoundingBox;
import au.gov.ga.earthsci.model.data.IModelData;
import au.gov.ga.earthsci.model.data.ModelDataBuilder;
import au.gov.ga.earthsci.model.geometry.BasicColouredMeshGeometry;
import au.gov.ga.earthsci.model.geometry.FaceType;
import au.gov.ga.earthsci.model.geometry.ModelGeometryStatistics;
import au.gov.ga.earthsci.model.render.RendererCreatorRegistry;
import au.gov.ga.earthsci.worldwind.common.util.CoordinateTransformationUtil;
import au.gov.ga.earthsci.worldwind.common.util.Util;
/**
* A factory class used to create {@link IModel} instances from GDAL raster
* datasets according to a set of parameters.
*
* @author James Navin (james.navin@ga.gov.au)
*/
public class GDALRasterModelFactory
{
private static final int VERTEX_GROUP_SIZE = 3;
private static final int RGBA_GROUP_SIZE = 4;
private static final Color DEFAULT_NODATA_COLOR = new Color(0, 0, 0, 0);
private static final Logger logger = LoggerFactory.getLogger(GDALRasterModelFactory.class);
/**
* Create a new {@link GDALRasterModel} from the provided GDAL dataset and
* parameters
*/
public static GDALRasterModel createModel(Dataset ds, GDALRasterModelParameters parameters) throws Exception
{
Validate.notNull(ds, "A GDAL dataset is required"); //$NON-NLS-1$
Validate.notNull(parameters, "Model parameters are required"); //$NON-NLS-1$
ModelGeometryStatistics stats = new ModelGeometryStatistics();
BasicColouredMeshGeometry geometry =
new BasicColouredMeshGeometry(UUID.randomUUID().toString(),
ds.GetDescription(), ds.GetDescription());
addVerticesAndNodata(geometry, ds, parameters, stats);
addVertexColours(geometry, ds, parameters, stats);
addEdges(geometry, ds, parameters, stats);
geometry.setBoundingVolume(new BoundingBox(stats.getMinLon(), stats.getMaxLon(),
stats.getMinLat(), stats.getMaxLat(),
stats.getMinElevation(), stats.getMaxElevation()));
geometry.setRenderer(RendererCreatorRegistry.getDefaultCreator(geometry).createRenderer(geometry));
return new GDALRasterModel(null, geometry, ds, parameters,
parameters.getModelName(),
parameters.getModelDescription());
}
private GDALRasterModelFactory()
{
};
/**
* Read vertice data from the given raster dataset using the provided
* parameters, and store calculated statistics about the mesh in the
* provided object for later use.
*/
private static void addVerticesAndNodata(BasicColouredMeshGeometry geometry, Dataset ds,
GDALRasterModelParameters parameters,
ModelGeometryStatistics stats)
{
Band band = ds.GetRasterBand(parameters.getElevationBandIndex());
int rasterXSize = band.getXSize();
int rasterYSize = band.getYSize();
ByteBuffer buffer = readRasterBuffer(band, rasterXSize, rasterYSize);
// Transform pixel coords -> source coordinate system coords
double[] geoTransform = ds.GetGeoTransform();
// Transform source coordinate system -> WGS84
CoordinateTransformation coordinateTransformation = getCoordinateTransform(parameters);
BufferType sourceBufferType = getBufferType(band);
double elevationOffset = getOffset(band, parameters);
double elevationScale = getScale(band, parameters);
double[] transformedCoords = new double[2];
double[] projectedCoords = new double[VERTEX_GROUP_SIZE];
int stride = parameters.getNormalisedSubsample();
ByteBuffer vertexBuffer = allocateVerticesBuffer(rasterXSize, rasterYSize, stride);
Double nodata = getNodata(band);
Double scaledNodata = nodata;
if (nodata != null)
{
scaledNodata = toElevation(elevationOffset, elevationScale, nodata, nodata);
}
for (int y = 0; y < rasterYSize; y += stride)
{
for (int x = 0; x < rasterXSize; x += stride)
{
double datasetValue = getValue(buffer, sourceBufferType).doubleValue();
double elevation = toElevation(elevationOffset, elevationScale, datasetValue, nodata);
transformCoordinates(geoTransform, x, y, transformedCoords);
projectCoordinates(coordinateTransformation,
transformedCoords[0],
transformedCoords[1],
elevation,
projectedCoords);
vertexBuffer.putFloat((float) projectedCoords[0])
.putFloat((float) projectedCoords[1])
.putFloat((float) projectedCoords[2]);
if (!isNoData(scaledNodata, elevation))
{
stats.updateStats(projectedCoords[1], projectedCoords[0], projectedCoords[2]);
}
int step = Math.min(stride, rasterXSize - x) - 1; // Skip stride values, or move to the end of the column
skipValues(step, buffer, sourceBufferType);
}
int step = Math.min(stride, rasterYSize - y) - 1;
skipValues(rasterXSize * step, buffer, sourceBufferType);
}
// TODO Move name/description to constant somewhere for reuse as standard name
IModelData vertices = ModelDataBuilder.createFromBuffer(vertexBuffer)
.ofType(BufferType.FLOAT)
.withNodata(scaledNodata == null ? null : scaledNodata.floatValue())
.named("Vertices")
.describedAs("Vertices")
.withGroupSize(3)
.build();
logger.debug("Loaded vertices: {}", vertices); //$NON-NLS-1$
logger.debug("Vertex stats: {}", stats); //$NON-NLS-1$
geometry.setVertices(vertices);
geometry.setUseZMasking(nodata != null);
}
/**
* Create and return a vertex colour data object containing RGBA values for
* each vertex based on a color map contained in the provided parameters.
* <p/>
* If no colour map is found, will return <code>null</code>.
*/
private static void addVertexColours(BasicColouredMeshGeometry geometry, Dataset ds,
GDALRasterModelParameters parameters,
ModelGeometryStatistics stats)
{
ColorMap map = parameters.getColorMap();
if (map == null)
{
return;
}
// TODO: Generalise this to support all map types
if (map.isPercentageBased() && map.getMode() != InterpolationMode.EXACT_MATCH)
{
geometry.setColorMap(map);
return;
}
IModelData vertices = geometry.getVertices();
int numVertices = vertices.getNumberOfGroups();
ByteBuffer coloursBuffer = allocateVertexColourBuffer(numVertices);
ByteBuffer verticesBuffer = vertices.getSource();
float[] rgba = new float[4];
for (int i = 0; i < numVertices; i++)
{
BufferUtil.skipValues(2, verticesBuffer, vertices.getBufferType());
float elevation = BufferUtil.getValue(verticesBuffer, vertices.getBufferType()).floatValue();
Color color;
if (vertices.getNoDataValue() != null && isNoData((Float) vertices.getNoDataValue(), elevation))
{
color = map.getNodataColour();
if (color == null)
{
color = DEFAULT_NODATA_COLOR;
}
}
else
{
color = map.getColor(elevation, stats.getMinElevation(), stats.getMaxElevation());
}
color.getRGBComponents(rgba);
coloursBuffer.putFloat(rgba[0]);
coloursBuffer.putFloat(rgba[1]);
coloursBuffer.putFloat(rgba[2]);
coloursBuffer.putFloat(rgba[3]);
}
IModelData vertexColours = ModelDataBuilder.createFromBuffer(coloursBuffer)
.ofType(BufferType.FLOAT)
.withGroupSize(RGBA_GROUP_SIZE)
.named("Vertex Colours")
.describedAs("Vertex Colours")
.build();
geometry.setVertexColour(vertexColours);
geometry.setColourType(ColorType.RGBA);
}
/**
* Create and return edge indices data from the provided dataset parameters
* and vertices.
* <p/>
* The returned edges are intended for use with the
* {@link FaceType#TRIANGLE_STRIP} type.
*/
private static void addEdges(BasicColouredMeshGeometry geometry, Dataset ds,
GDALRasterModelParameters parameters,
ModelGeometryStatistics stats)
{
Band band = ds.GetRasterBand(parameters.getElevationBandIndex());
int rasterXSize = band.getXSize();
int rasterYSize = band.getYSize();
int subsample = parameters.getNormalisedSubsample();
int numColumns = subsample(rasterXSize, subsample);
int numRows = subsample(rasterYSize, subsample);
ByteBuffer edgesBuffer = allocateEdgesBuffer(rasterXSize, rasterYSize, subsample);
for (int y = 0; y < numRows - 1; y++)
{
for (int x = 0; x < numColumns; x++)
{
int top = y * numColumns + x;
int bottom = top + numColumns;
edgesBuffer.putInt(top);
edgesBuffer.putInt(bottom);
if (x == numColumns - 1 && y < numRows - 2)
{
// Put two empty triangles to reset back to the next strip
edgesBuffer.putInt(bottom);
edgesBuffer.putInt(bottom);
int nextTop = (y + 1) * numColumns;
edgesBuffer.putInt(nextTop);
edgesBuffer.putInt(nextTop);
}
}
}
// Edges are full, so make sure limit is set appropriately
edgesBuffer.limit(edgesBuffer.position());
// TODO Move name/description to constant somewhere for reuse as standard name
IModelData edges = ModelDataBuilder.createFromBuffer(edgesBuffer)
.ofType(BufferType.INT)
.named("Edges")
.describedAs("Edges")
.withGroupSize(1)
.build();
geometry.setEdgeIndices(edges);
geometry.setFaceType(FaceType.TRIANGLE_STRIP);
}
private static ByteBuffer allocateVerticesBuffer(int rasterXSize, int rasterYSize, int subsample)
{
int numColumns = subsample(rasterXSize, subsample);
int numRows = subsample(rasterYSize, subsample);
int numVerts = numColumns * numRows;
ByteBuffer vertices = allocateBuffer(numVerts * VERTEX_GROUP_SIZE * BufferType.FLOAT.getNumberOfBytes());
return vertices;
}
private static ByteBuffer allocateVertexColourBuffer(int numVertices)
{
ByteBuffer colours = allocateBuffer(numVertices * RGBA_GROUP_SIZE * BufferType.FLOAT.getNumberOfBytes());
return colours;
}
private static ByteBuffer allocateEdgesBuffer(int rasterXSize, int rasterYSize, int subsample)
{
int numColumns = subsample(rasterXSize, subsample);
int numRows = subsample(rasterYSize, subsample);
// Each row has 2 indices for each vertex, plus 4 terminating indices (2 @ start and end)
// The exception is first and last row, which have only 1 index per vertex and no terminating indices
int numIndices = (2 * numColumns * (numRows - 1)) + 4 * (numRows - 2);
ByteBuffer edges = allocateBuffer(numIndices * BufferType.INT.getNumberOfBytes());
return edges;
}
private static CoordinateTransformation getCoordinateTransform(GDALRasterModelParameters parameters)
{
String sourceProjection = parameters.getSourceProjection();
if (Util.isBlank(sourceProjection))
{
logger.info("No source projection found. Assuming WGS84."); //$NON-NLS-1$
return new CoordinateTransformation(SpatialReferences.WGS84, SpatialReferences.WGS84);
}
CoordinateTransformation coordinateTransformation =
CoordinateTransformationUtil.getTransformationToWGS84(sourceProjection);
return coordinateTransformation;
}
private static ByteBuffer readRasterBuffer(Band band, int columns, int rows)
{
ByteBuffer buffer = band.ReadRaster_Direct(0, 0, columns, rows, band.getDataType());
buffer.order(ByteOrder.nativeOrder()); // @see Band.ReadRaster_Direct
buffer.rewind();
return buffer;
}
private static Double getNodata(Band band)
{
Double[] nodatas = new Double[1];
band.GetNoDataValue(nodatas);
Double nodata = nodatas[0];
return nodata;
}
private static double toElevation(double elevationOffset, double elevationScale, double datasetValue, Double nodata)
{
// if (isNoData(nodata, datasetValue))
// {
// return nodata;
// }
return elevationOffset + (elevationScale * datasetValue);
}
/**
* @return The data scale for the provided band
*/
private static double getScale(Band band, GDALRasterModelParameters parameters)
{
if (parameters.getScaleFactor() != null)
{
return parameters.getScaleFactor();
}
Double[] vals = new Double[1];
band.GetScale(vals);
return vals[0] != null ? vals[0] : 1.0;
}
/**
* @return The data offset for the provided band
*/
private static double getOffset(Band band, GDALRasterModelParameters parameters)
{
if (parameters.getOffset() != null)
{
return parameters.getOffset();
}
Double[] vals = new Double[1];
band.GetOffset(vals);
return vals[0] != null ? vals[0] : 0.0;
}
/**
* Transform the dataset pixel coordinates [Xs,Ys] into coordinates in the
* dataset's source projection [Xp,Yp]
*
* @param geoTransform
* The geo transform coefficients
* @param x
* Pixel X coordinate
* @param y
* Pixel Y coordinate
*
* @return The provided coordinates transformed into the source projection
* [Xp,Yp]
*
* @see Dataset#GetGeoTransform()
*/
private static double[] transformCoordinates(double[] geoTransform, double x, double y, double[] out)
{
double Xp = geoTransform[0] + x * geoTransform[1] + y * geoTransform[2];
double Yp = geoTransform[3] + x * geoTransform[4] + y * geoTransform[5];
if (out == null)
{
out = new double[2];
}
out[0] = Xp;
out[1] = Yp;
return out;
}
/**
* Project the provided x,y,z coordinates from the source SRS using the
* provided coordinate transformation
*
* @param ct
* The coordinate transformation to use for transforming the
* coordinates
* @param x
* The x coordinate in source SRS
* @param y
* The y coordinate in source SRS
* @param elevation
* The elevation in source SRS
* @param out
* A 3-element array to hold the projected coordinates
*
* @return The projected coordinates contained in {@code out}
*/
private static double[] projectCoordinates(CoordinateTransformation ct, double x, double y, double elevation,
double[] out)
{
if (out == null)
{
out = new double[3];
}
if (ct == null)
{
out[0] = x;
out[1] = y;
out[2] = elevation;
}
ct.TransformPoint(out, x, y, elevation);
return out;
}
/**
* @return <code>true</code> if any value in the provided values is NODATA
*/
private static boolean isNoData(Double nodata, double... values)
{
if (nodata == null)
{
return false;
}
for (double f : values)
{
if (f == nodata)
{
return true;
}
}
return false;
}
/**
* @return <code>true</code> if any value in the provided values is NODATA
*/
private static boolean isNoData(Float nodata, float... values)
{
if (nodata == null)
{
return false;
}
for (float f : values)
{
if (f == nodata)
{
return true;
}
}
return false;
}
/**
* @return <code>true</code> if any value in the provided values is NaN
*/
private static boolean isNaN(double... values)
{
for (double f : values)
{
if (Double.isNaN(f))
{
return true;
}
}
return false;
}
private static int subsample(int original, int subsample)
{
return (original + subsample - 1) / subsample;
}
private static ByteBuffer allocateBuffer(int size)
{
ByteBuffer result = ByteBuffer.allocate(size);
result.order(ByteOrder.nativeOrder());
return result;
}
}