/* (c) 2014-2015 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wcs2_0.response;
import it.geosolutions.jaiext.range.NoDataContainer;
import java.awt.image.DataBuffer;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.measure.unit.Unit;
import javax.measure.unit.UnitFormat;
import javax.media.jai.PlanarImage;
import javax.media.jai.iterator.RectIter;
import javax.media.jai.iterator.RectIterFactory;
import org.geoserver.catalog.CoverageDimensionInfo;
import org.geoserver.catalog.DimensionInfo;
import org.geoserver.catalog.DimensionPresentation;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.wcs2_0.GetCoverage;
import org.geoserver.wcs2_0.exception.WCS20Exception;
import org.geoserver.wcs2_0.util.EnvelopeAxesLabelsMapper;
import org.geotools.coverage.GridSampleDimension;
import org.geotools.coverage.TypeMap;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridEnvelope2D;
import org.geotools.coverage.grid.GridGeometry2D;
import org.geotools.coverage.grid.io.GridCoverage2DReader;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.referencing.CRS;
import org.geotools.referencing.CRS.AxisOrder;
import org.geotools.referencing.operation.transform.AffineTransform2D;
import org.geotools.resources.coverage.CoverageUtilities;
import org.geotools.util.DateRange;
import org.geotools.util.NumberRange;
import org.geotools.util.Utilities;
import org.geotools.xml.transform.TransformerBase;
import org.geotools.xml.transform.Translator;
import org.opengis.coverage.SampleDimension;
import org.opengis.coverage.SampleDimensionType;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.metadata.spatial.PixelOrientation;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.crs.GeographicCRS;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.operation.MathTransform2D;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.helpers.AttributesImpl;
import org.xml.sax.helpers.NamespaceSupport;
/**
* Internal Base {@link GMLTransformer} for DescribeCoverage and GMLCoverageEncoding
*
* @author Simone Giannecchini, GeoSolutions SAS
*
*/
class GMLTransformer extends TransformerBase {
protected final EnvelopeAxesLabelsMapper envelopeDimensionsMapper;
protected FileReference fileReference;
protected String mimeType;
public GMLTransformer(EnvelopeAxesLabelsMapper envelopeDimensionsMapper) {
this.envelopeDimensionsMapper=envelopeDimensionsMapper;
}
public void setFileReference(FileReference fileReference) {
this.fileReference = fileReference;
}
/**
* Set of custom TAGs for Metadata elements
*/
static class TAG {
private final static String RANGE = "wcsgs:Range";
private final static String INTERVAL_START = "wcsgs:start";
private final static String INTERVAL_END = "wcsgs:end";
private final static String INTERVAL_PERIOD = "wcsgs:Interval";
private final static String SINGLE_VALUE = "wcsgs:SingleValue";
private static final String ADDITIONAL_DIMENSION = "wcsgs:DimensionDomain";
private static final String TIME_DOMAIN = "wcsgs:TimeDomain";
private static final String ELEVATION_DOMAIN = "wcsgs:ElevationDomain";
}
class GMLTranslator extends TranslatorSupport {
protected List<WCS20CoverageMetadataProvider> extensions;
private WCS20CoverageMetadataProvider.Translator translator = new WCS20CoverageMetadataProvider.Translator() {
@Override
public void start(String element, Attributes attributes) {
GMLTranslator.this.start(element, attributes);
}
@Override
public void start(String element) {
GMLTranslator.this.start(element);
}
@Override
public void end(String element) {
GMLTranslator.this.end(element);
}
@Override
public void chars(String text) {
GMLTranslator.this.chars(text);
}
};
protected TranslatorHelper helper = new TranslatorHelper();
public GMLTranslator(ContentHandler contentHandler) {
super(contentHandler, null, null);
this.extensions = GeoServerExtensions.extensions(WCS20CoverageMetadataProvider.class);
}
@Override
public void encode(Object o) throws IllegalArgumentException {
// register namespaces provided by extended capabilities
NamespaceSupport namespaces = getNamespaceSupport();
namespaces.declarePrefix("wcscrs", "http://www.opengis.net/wcs/service-extension/crs/1.0");
namespaces.declarePrefix("int", "http://www.opengis.net/WCS_service-extension_interpolation/1.0");
namespaces.declarePrefix("gml", "http://www.opengis.net/gml/3.2");
namespaces.declarePrefix("gmlcov", "http://www.opengis.net/gmlcov/1.0");
namespaces.declarePrefix("swe", "http://www.opengis.net/swe/2.0");
namespaces.declarePrefix("xlink", "http://www.w3.org/1999/xlink");
namespaces.declarePrefix("xsi", "http://www.w3.org/2001/XMLSchema-instance");
for (WCS20CoverageMetadataProvider cp : extensions) {
cp.registerNamespaces(namespaces);
}
// is this a GridCoverage?
if (!(o instanceof GridCoverage2D)) {
throw new IllegalArgumentException("Provided object is not a GridCoverage2D:"
+ (o != null ? o.getClass().toString() : "null"));
}
final GridCoverage2D gc2d = (GridCoverage2D) o;
// we are going to use this name as an ID
final String gcName = gc2d.getName().toString(Locale.getDefault());
// get the crs and look for an EPSG code
final CoordinateReferenceSystem crs = gc2d.getCoordinateReferenceSystem2D();
List<String> axesNames = GMLTransformer.this.envelopeDimensionsMapper.getAxesNames(
gc2d.getEnvelope2D(), true);
// lookup EPSG code
Integer EPSGCode = null;
try {
EPSGCode = CRS.lookupEpsgCode(crs, false);
} catch (FactoryException e) {
throw new IllegalStateException("Unable to lookup epsg code for this CRS:" + crs, e);
}
if (EPSGCode == null) {
throw new IllegalStateException("Unable to lookup epsg code for this CRS:" + crs);
}
final String srsName = GetCoverage.SRS_STARTER + EPSGCode;
// handle axes swap for geographic crs
final boolean axisSwap = !CRS.getAxisOrder(crs).equals(AxisOrder.EAST_NORTH);
final AttributesImpl attributes = new AttributesImpl();
helper.registerNamespaces(getNamespaceSupport(), attributes);
// using Name as the ID
attributes.addAttribute("", "gml:id", "gml:id", "",
gc2d.getName().toString(Locale.getDefault()));
start("gml:RectifiedGridCoverage", attributes);
// handle domain
final StringBuilder builder = new StringBuilder();
for (String axisName : axesNames) {
builder.append(axisName).append(" ");
}
String axesLabel = builder.substring(0, builder.length() - 1);
try {
GeneralEnvelope envelope = new GeneralEnvelope(gc2d.getEnvelope());
handleBoundedBy(envelope, axisSwap, srsName, axesLabel, null);
} catch (IOException ex) {
throw new WCS20Exception(ex);
}
// handle domain
builder.setLength(0);
axesNames = GMLTransformer.this.envelopeDimensionsMapper.getAxesNames(
gc2d.getEnvelope2D(), false);
for (String axisName : axesNames) {
builder.append(axisName).append(" ");
}
axesLabel = builder.substring(0, builder.length() - 1);
handleDomainSet(gc2d.getGridGeometry(), gc2d.getDimension(), gcName, srsName, axisSwap);
// handle rangetype
handleRangeType(gc2d);
// handle coverage function
final GridEnvelope2D ge2D = gc2d.getGridGeometry().getGridRange2D();
handleCoverageFunction(ge2D, axisSwap);
// handle range
handleRange(gc2d);
// handle metadata OPTIONAL
try {
handleMetadata(null, null);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
end("gml:RectifiedGridCoverage");
}
/**
* Encode the coverage function or better the GridFunction as per clause 19.3.12 of GML 3.2.1 which helps us with indicating in which way we
* traverse the data.
*
* <p>
* Notice that we use the axisOrder to actually <strong>always</strong> encode data il easting,northing, hence in case of a northing,easting
* crs we use a reversed order to indicate that we always walk on the raster columns first.
*
* <p>
* In cases where the coordinates increases in the opposite order ho our walk the offsetVectors of the RectifiedGrid will do the rest.
*
* @param gc2d
* @param axisSwap
*/
public void handleCoverageFunction(GridEnvelope2D gridRange, boolean axisSwap) {
start("gml:coverageFunction");
start("gml:GridFunction");
// build the fragment
final AttributesImpl gridAttrs = new AttributesImpl();
gridAttrs.addAttribute("", "axisOrder", "axisOrder", "", axisSwap ? "+2 +1" : "+1 +2");
element("gml:sequenceRule", "Linear", gridAttrs); // minOccurs 0, default Linear
element("gml:startPoint", gridRange.x + " " + gridRange.y); // we start at minx, miny (this is optional though)
end("gml:GridFunction");
end("gml:coverageFunction");
}
/**
* Encoding eventual metadata that come along with this coverage
*
* <pre>
* {@code
* <gmlcov:metadata>
* <gmlcov:Extension>
* <myNS:metadata>Some metadata ...</myNS:metadata>
* </gmlcov:Extension>
* </gmlcov:metadata>
* }
* </pre>
*
* @param context Can be either a {@link GridCoverage2DReader} or a {@link GridCoverage2D}, depending
* on how the method is invoked
* @throws IOException
*/
public void handleMetadata(Object context, WCSDimensionsHelper dimensionsHelper) throws IOException {
start("gmlcov:metadata");
start("gmlcov:Extension");
if (dimensionsHelper != null) {
// handle time if necessary
handleTimeMetadata(dimensionsHelper);
// handle elevation if necessary
handleElevationMetadata(dimensionsHelper);
// handle additional dimensions if necessary
handleAdditionalDimensionMetadata(dimensionsHelper);
}
for (WCS20CoverageMetadataProvider extension : extensions) {
extension.encode(translator, context);
}
end("gmlcov:Extension");
end("gmlcov:metadata");
}
/**
* Look for additional dimensions in the dimensionsHelper and put additional domains to the metadata
* @param helper
* @throws IOException
*/
private void handleAdditionalDimensionMetadata(final WCSDimensionsHelper helper) throws IOException {
Utilities.ensureNonNull("helper", helper);
final Map<String, DimensionInfo> additionalDimensions = helper.getAdditionalDimensions();
final Set<String> dimensionsName = additionalDimensions.keySet();
final Iterator<String> dimensionsIterator = dimensionsName.iterator();
while (dimensionsIterator.hasNext()) {
final String dimensionName = dimensionsIterator.next();
final DimensionInfo customDimension = additionalDimensions.get(dimensionName);
if (customDimension != null) {
setAdditionalDimensionMetadata(dimensionName, customDimension, helper);
}
}
}
/**
* Set additional dimension metadata to the DescribeCoverage element
* @param name the custom dimension name
* @param dimension the custom dimension related {@link DimensionInfo} instance
* @param helper the {@link WCSDimensionsHelper} instance to be used to parse domains
* @throws IOException
*/
private void setAdditionalDimensionMetadata(final String name,
final DimensionInfo dimension, WCSDimensionsHelper helper)
throws IOException {
Utilities.ensureNonNull("helper", helper);
final String startTag = initStartMetadataTag(TAG.ADDITIONAL_DIMENSION, name, dimension, helper);
start(startTag);
// Custom dimension only supports List presentation
final List<String> domain = helper.getDomain(name);
// TODO: check if we are in the list of instants case, or in the list of periods case
// list case
int i = 0;
for (String item : domain) {
Date date = WCSDimensionsValueParser.parseAsDate(item);
if (date != null) {
final String dimensionId = helper.getCoverageId() + "_dd_" + i;
encodeDate(date, helper, dimensionId);
continue;
}
Double number = WCSDimensionsValueParser.parseAsDouble(item);
if (number != null ) {
element(TAG.SINGLE_VALUE, item.toString());
continue;
}
NumberRange<Double> range = WCSDimensionsValueParser.parseAsDoubleRange(item);
if (range != null ) {
encodeInterval(range.getMinValue().toString(), range.getMaxValue()
.toString(), null, null);
continue;
}
//TODO: Add support for date Ranges
if (item instanceof String) {
element(TAG.SINGLE_VALUE, item.toString());
}
// else if (item instanceof DateRange) {
// final String dimensionId = helper.getCoverageId() + "_dd_" + i;
// encodeDateRange((DateRange) item, helper, dimensionId);
// }
//TODO: Add more cases
i++;
}
end(TAG.ADDITIONAL_DIMENSION);
}
/**
* Initialize the metadata start tag for a custom dimension, setting dimension name,
* checking for UOM, defaultValue, ...
*
* @param dimensionTag the TAG referring to type of dimension (Time, Elevation, Additional ,...)
* @param name the name of the custom dimension
* @param dimension the custom dimension {@link DimensionInfo} instance
* @param helper the {@link WCSDimensionsHelper} instance used to parse default values
*
* @throws IOException
*/
private String initStartMetadataTag(final String dimensionTag, final String name, final DimensionInfo dimension,
final WCSDimensionsHelper helper) throws IOException {
final String uom = dimension.getUnitSymbol();
String defaultValue = null;
String prolog = null;
if (dimensionTag.equals(TAG.ADDITIONAL_DIMENSION)) {
prolog = TAG.ADDITIONAL_DIMENSION + " name = \"" + name + "\"";
defaultValue = helper.getDefaultValue(name);
} else if (dimensionTag.equals(TAG.ELEVATION_DOMAIN)) {
prolog = TAG.ELEVATION_DOMAIN;
defaultValue = helper.getBeginElevation();
} else if (dimensionTag.equals(TAG.TIME_DOMAIN)) {
prolog = TAG.TIME_DOMAIN;
defaultValue = helper.getEndTime();
}
return prolog + (uom != null ? (" uom=\"" + uom + "\"") : "")
+ (defaultValue != null ? (" default=\"" + defaultValue + "\"") : "");
}
/**
* Set the timeDomain metadata in case the dimensionsHelper instance has a timeDimension
*
* @param helper
* @throws IOException
*/
private void handleTimeMetadata(WCSDimensionsHelper helper) throws IOException {
Utilities.ensureNonNull("helper", helper);
final DimensionInfo timeDimension = helper.getTimeDimension();
if (timeDimension != null) {
start(initStartMetadataTag(TAG.TIME_DOMAIN, null, timeDimension, helper));
final DimensionPresentation presentation = timeDimension.getPresentation();
final String id = helper.getCoverageId();
switch(presentation) {
case CONTINUOUS_INTERVAL:
encodeTimePeriod(helper.getBeginTime(), helper.getEndTime(), id + "_tp_0", null, null);
break;
case DISCRETE_INTERVAL:
encodeTimePeriod(helper.getBeginTime(), helper.getEndTime(), id + "_tp_0",
helper.getTimeResolutionUnit(), helper.getTimeResolutionValue());
break;
default:
// TODO: check if we are in the list of instants case, or in the list of periods case
// list case
final TreeSet<Object> domain = helper.getTimeDomain();
int i = 0;
for (Object item : domain) {
// gml:id is mandatory for time instant...
if(item instanceof Date) {
encodeDate((Date) item, helper, id + "_td_" + i);
} else if(item instanceof DateRange) {
encodeDateRange((DateRange) item, helper, id + "_td_" + i);
}
i++;
}
break;
}
end(TAG.TIME_DOMAIN);
}
}
/**
* Set the elevationDomain metadata in case the dimensionsHelper instance has an elevationDimension
*
* @param helper
* @throws IOException
*/
private void handleElevationMetadata(WCSDimensionsHelper helper) throws IOException {
// Null check has been performed in advance
final DimensionInfo elevationDimension = helper.getElevationDimension();
if (elevationDimension != null) {
start(initStartMetadataTag(TAG.ELEVATION_DOMAIN, null, elevationDimension, helper));
final DimensionPresentation presentation = elevationDimension.getPresentation();
switch(presentation) {
// Where _er_ means elevation range
case CONTINUOUS_INTERVAL:
encodeInterval(helper.getBeginElevation(), helper.getEndElevation(), null, null);
break;
case DISCRETE_INTERVAL:
encodeInterval(helper.getBeginElevation(), helper.getEndElevation(),
helper.getElevationResolutionUnit(), helper.getElevationResolutionValue());
break;
default:
// TODO: check if we are in the list of instants case, or in the list of periods case
// list case
final TreeSet<Object> domain = helper.getElevationDomain();
for (Object item : domain) {
if (item instanceof Number) {
element(TAG.SINGLE_VALUE, item.toString());
} else if(item instanceof NumberRange) {
NumberRange range = (NumberRange) item;
encodeInterval(range.getMinValue().toString(), range.getMaxValue().toString(), null, null);
}
}
break;
}
end(TAG.ELEVATION_DOMAIN);
}
}
/**
* Encode a DateRange item as a GML TimePeriod
* @param range
* @param helper
* @param id
*/
private void encodeDateRange(final DateRange range, final WCSDimensionsHelper helper, final String id) {
encodeTimePeriod(helper.format(range.getMinValue()), helper.format(range.getMaxValue()),
id, null, null);
}
/**
* Encode a Date item as a GML TimeInstant
* @param item
* @param helper
* @param id
*/
private void encodeDate(final Date item, final WCSDimensionsHelper helper, final String id) {
final AttributesImpl atts = new AttributesImpl();
atts.addAttribute("", "gml:id", "gml:id", "", id);
start("gml:TimeInstant", atts);
element("gml:timePosition", helper.format(item));
end("gml:TimeInstant");
}
/**
* Encode a GML time period
*
* @param beginPosition
* @param endPosition
* @param timePeriodId
* @param intervalUnit
* @param intervalValue
*/
public void encodeTimePeriod(String beginPosition, String endPosition, String timePeriodId, String intervalUnit, Long intervalValue) {
AttributesImpl atts = new AttributesImpl();
atts.addAttribute("", "gml:id", "gml:id", "", timePeriodId);
start("gml:TimePeriod", atts);
element("gml:beginPosition", beginPosition);
element("gml:endPosition", endPosition);
if (intervalUnit != null && intervalValue != null) {
atts = new AttributesImpl();
atts.addAttribute("", "unit", "unit", "", intervalUnit);
element("gml:TimeInterval", intervalValue.toString(), atts);
}
end("gml:TimePeriod");
}
/**
* Encode Interval
*
* @param beginPosition
* @param endPosition
* @param dimensionId
*/
public void encodeInterval(String beginPosition, String endPosition,
String intervalUnit, Double intervalValue) {
AttributesImpl atts = new AttributesImpl();
start(TAG.RANGE, atts);
element(TAG.INTERVAL_START, beginPosition);
element(TAG.INTERVAL_END, endPosition);
if (intervalUnit != null && intervalValue != null) {
atts = new AttributesImpl();
atts.addAttribute("", "unit", "unit", "", intervalUnit);
element(TAG.INTERVAL_PERIOD, intervalValue.toString(), atts);
}
end(TAG.RANGE);
}
/**
* Encodes the boundedBy element
*
* e.g.:
*
* <pre>
* {@code
* <gml:boundedBy>
* <gml:Envelope srsName="http://www.opengis.net/def/crs/EPSG/0/4326" axisLabels="Lat Long" uomLabels="deg deg" srsDimension="2">
* <gml:lowerCorner>1 1</gml:lowerCorner>
* <gml:upperCorner>5 3</gml:upperCorner>
* </gml:Envelope>
* </gml:boundedBy>
* }
* </pre>
* @param ci
* @param gc2d
* @param ePSGCode
* @param axisSwap
* @param srsName
* @param axesNames
* @param axisLabels
* @throws IOException
*/
public void handleBoundedBy(final GeneralEnvelope envelope, boolean axisSwap, String srsName, String axisLabels, WCSDimensionsHelper dimensionHelper) throws IOException {
final CoordinateReferenceSystem crs = envelope.getCoordinateReferenceSystem();
final CoordinateSystem cs = crs.getCoordinateSystem();
// TODO time
String uomLabels = extractUoM(crs, cs.getAxis(axisSwap ? 1 : 0).getUnit()) + " "
+ extractUoM(crs, cs.getAxis(axisSwap ? 0 : 1).getUnit());
// time and elevation dimensions management
boolean hasElevation = false;
boolean hasTime = false;
if (dimensionHelper != null) {
if (dimensionHelper.getElevationDimension() != null) {
uomLabels = uomLabels + " m"; //TODO: Check elevation uom
hasElevation = true;
}
if (dimensionHelper.getTimeDimension() != null) {
uomLabels = uomLabels + " s";
hasTime = true;
}
}
final int srsDimension = cs.getDimension() + (hasElevation ? 1 : 0);
// Setting up envelope bounds (including elevation)
final String lower = new StringBuilder()
.append(envelope.getLowerCorner().getOrdinate(axisSwap ? 1 : 0)).append(" ")
.append(envelope.getLowerCorner().getOrdinate(axisSwap ? 0 : 1))
.append(hasElevation ? " " + dimensionHelper.getBeginElevation() : "")
.toString();
final String upper = new StringBuilder()
.append(envelope.getUpperCorner().getOrdinate(axisSwap ? 1 : 0)).append(" ")
.append(envelope.getUpperCorner().getOrdinate(axisSwap ? 0 : 1))
.append(hasElevation ? " " + dimensionHelper.getEndElevation() : "")
.toString();
// build the fragment
final AttributesImpl envelopeAttrs = new AttributesImpl();
envelopeAttrs.addAttribute("", "srsName", "srsName", "", srsName);
envelopeAttrs.addAttribute("", "axisLabels", "axisLabels", "", axisLabels);
envelopeAttrs.addAttribute("", "uomLabels", "uomLabels", "", uomLabels);
envelopeAttrs.addAttribute("", "srsDimension", "srsDimension", "",
String.valueOf(srsDimension));
start("gml:boundedBy");
String envelopeName;
if (dimensionHelper != null && (hasTime || hasElevation)) {
envelopeName = "gml:EnvelopeWithTimePeriod";
} else {
envelopeName = "gml:Envelope";
}
start(envelopeName, envelopeAttrs);
element("gml:lowerCorner", lower);
element("gml:upperCorner", upper);
if (dimensionHelper != null && hasTime) {
element("gml:beginPosition", dimensionHelper.getBeginTime());
element("gml:endPosition", dimensionHelper.getEndTime());
}
end(envelopeName);
end("gml:boundedBy");
}
/**
* Returns a beautiful String representation for the provided {@link Unit}
*
* @param crs
* @param uom
*
*/
public String extractUoM (CoordinateReferenceSystem crs, Unit<?> uom) {
// special handling for Degrees
if (crs instanceof GeographicCRS) {
return "Deg";
}
return UnitFormat.getInstance().format(uom);
}
/**
* Encodes the Range as per the GML spec of the provided {@link GridCoverage2D}
*
* @param gc2d the {@link GridCoverage2D} for which to encode the Range.
*/
public void handleRange(GridCoverage2D gc2d) {
// preamble
start("gml:rangeSet");
if(fileReference != null) {
encodeFileReference(fileReference);
} else {
encodeAsDataBlocks(gc2d);
}
end("gml:rangeSet");
}
private void encodeFileReference(FileReference fileReference) {
start("gml:File");
final AttributesImpl atts = new AttributesImpl();
atts.addAttribute("", "xlink:arcrole", "xlink:arcrole", "", "fileReference");
atts.addAttribute("", "xlink:href", "xlink:href", "", "cid:" + fileReference.getReference());
atts.addAttribute("", "xlink:role", "xlink:role", "", fileReference.getConformanceClass());
element("gml:rangeParameters", "", atts);
element("gml:fileReference", "cid:" + fileReference.getReference());
element("gml:fileStructure", "");
element("gml:mimeType", fileReference.getMimeType());
end("gml:File");
}
private void encodeAsDataBlocks(GridCoverage2D gc2d) {
start("gml:DataBlock");
start("gml:rangeParameters");
end("gml:rangeParameters");
start("tupleList");
// walk through the coverage and spit it out!
final RenderedImage raster = gc2d.getRenderedImage();
final int numBands = raster.getSampleModel().getNumBands();
final int dataType = raster.getSampleModel().getDataType();
final double[] valuesD = new double[numBands];
final int[] valuesI = new int[numBands];
RectIter iterator = RectIterFactory.create(raster, PlanarImage
.wrapRenderedImage(raster).getBounds());
iterator.startLines();
while (!iterator.finishedLines()) {
iterator.startPixels();
while (!iterator.finishedPixels()) {
switch (dataType) {
case DataBuffer.TYPE_BYTE:
case DataBuffer.TYPE_INT:
case DataBuffer.TYPE_SHORT:
case DataBuffer.TYPE_USHORT:
iterator.getPixel(valuesI);
for (int i = 0; i < numBands; i++) {
// spit out
chars(String.valueOf(valuesI[i]));
if (i + 1 < numBands) {
chars(",");
}
}
break;
case DataBuffer.TYPE_DOUBLE:
case DataBuffer.TYPE_FLOAT:
iterator.getPixel(valuesD);
for (int i = 0; i < numBands; i++) {
// spit out
chars(String.valueOf(valuesD[i]));
if (i + 1 < numBands) {
chars(",");
}
}
break;
default:
break;
}
// space as sample separator
chars(" ");
iterator.nextPixel();
}
iterator.nextLine();
chars("\n");
}
end("tupleList");
end("gml:DataBlock");
}
/**
* Encodes the RangeType as per the GML spec of the provided {@link GridCoverage2D}
*
* e.g.:
*
* <pre>
* {@code
* <gmlcov:rangeType>
* <swe:DataRecord>
* <swe:field name="singleBand">
* <swe:Quantity definition="http://www.opengis.net/def/property/OGC/0/Radiance">
* <gml:description>Panchromatic Channel</gml:description>
* <gml:name>single band</gml:name>
* <swe:uom code="W/cm2"/>
* <swe:constraint>
* <swe:AllowedValues>
* <swe:interval>0 255</swe:interval>
* <swe:significantFigures>3</swe:significantFigures>
* </swe:AllowedValues>
* </swe:constraint>
* </swe:Quantity>
* </swe:field>
* </swe:DataRecord>
* </gmlcov:rangeType>
* }
* </pre>
*
* @param gc2d the {@link GridCoverage2D} for which to encode the RangeType.
*/
public void handleRangeType(GridCoverage2D gc2d) {
start("gml:rangeType");
start("swe:DataRecord");
// get bands
final SampleDimension[] bands = gc2d.getSampleDimensions();
// handle bands
for (SampleDimension sd : bands) {
final AttributesImpl fieldAttr = new AttributesImpl();
fieldAttr.addAttribute("", "name", "name", "", sd.getDescription().toString()); // TODO NCNAME? TODO Use Band[i] convention?
start("swe:field", fieldAttr);
start("swe:Quantity");
// Description
start("swe:description");
chars(sd.getDescription().toString());// TODO can we make up something better??
end("swe:description");
// UoM
final AttributesImpl uomAttr = new AttributesImpl();
final Unit<?> uom = sd.getUnits();
uomAttr.addAttribute("", "code", "code", "", uom == null ? "W.m-2.Sr-1"
: UnitFormat.getInstance().format(uom));
start("swe:uom", uomAttr);
end("swe:uom");
// constraint on values
start("swe:constraint");
start("swe:AllowedValues");
handleSampleDimensionRange(sd);// TODO make this generic
end("swe:AllowedValues");
end("swe:constraint");
// nil values
handleSampleDimensionNilValues(gc2d, sd.getNoDataValues());
end("swe:Quantity");
end("swe:field");
}
end("swe:DataRecord");
end("gml:rangeType");
}
/**
* @param sd
*/
public void handleSampleDimensionNilValues(GridCoverage2D gc2d, GridSampleDimension sd) {
handleSampleDimensionNilValues(gc2d, sd != null ? sd.getNoDataValues() : null);
}
public void handleSampleDimensionNilValues(GridCoverage2D gc2d, double[] nodataValues) {
start("swe:nilValues");
start("swe:NilValues");
if (nodataValues != null && nodataValues.length > 0) {
for (double nodata : nodataValues) {
final AttributesImpl nodataAttr = new AttributesImpl();
nodataAttr.addAttribute("", "reason", "reason", "",
"http://www.opengis.net/def/nil/OGC/0/unknown");
element("swe:nilValue", String.valueOf(nodata), nodataAttr);
}
} else if (gc2d != null) {
// do we have already a a NO_DATA value at hand?
NoDataContainer noDataProperty = CoverageUtilities.getNoDataProperty(gc2d);
if (noDataProperty != null) {
String nodata = Double.valueOf(noDataProperty.getAsSingleValue()).toString(); // TODO test me
final AttributesImpl nodataAttr = new AttributesImpl();
nodataAttr.addAttribute("", "reason", "reason", "",
"http://www.opengis.net/def/nil/OGC/0/unknown");
element("swe:nilValue", nodata, nodataAttr);
} else {
// let's suggest some meaningful value from the data type of the underlying image
Number nodata = CoverageUtilities.suggestNoDataValue(gc2d.getRenderedImage()
.getSampleModel().getDataType());
final AttributesImpl nodataAttr = new AttributesImpl();
nodataAttr.addAttribute("", "reason", "reason", "",
"http://www.opengis.net/def/nil/OGC/0/unknown");
element("swe:nilValue", nodata.toString(), nodataAttr);
}
}
end("swe:NilValues");
end("swe:nilValues");
}
/**
* Tries to encode a meaningful range for a {@link SampleDimension}.
*
* @param sd the {@link CoverageDimensionInfo} to encode a meaningful range for.
*/
public void handleSampleDimensionRange(CoverageDimensionInfo sd) {
if (!setRange(sd.getRange())) {
SampleDimensionType sdType = sd.getDimensionType();
handleSampleDimensionType(sdType);
}
}
private void handleSampleDimensionType(SampleDimensionType sdType) {
// old data dirs upgrading will have this empty
if(sdType == null) {
// pick the one with the largest domain and be done with it
sdType = SampleDimensionType.REAL_64BITS;
}
final NumberRange<? extends Number> indicativeRange = TypeMap.getRange(sdType);
setRange(indicativeRange);
}
/**
* Encode the interval range
* @param range
*/
private boolean setRange(NumberRange<? extends Number> range) {
if (range != null && !Double.isInfinite(range.getMaximum()) && !Double.isInfinite(range.getMinimum())) {
start("swe:interval");
chars(range.getMinValue() + " " + range.getMaxValue());
end("swe:interval");
return true;
}
return false;
}
/**
* Tries to encode a meaningful range for a {@link SampleDimension}.
*
* @param sd the {@link SampleDimension} to encode a meaningful range for.
*/
public void handleSampleDimensionRange(SampleDimension sd) {
// look for ranges on the sample dimension
boolean setRange = false;
if (sd instanceof GridSampleDimension) {
GridSampleDimension gridSd = ((GridSampleDimension) sd);
setRange = setRange(gridSd.getRange());
}
if (!setRange) {
// fallback on sampleDimensionType
SampleDimensionType sdType = sd.getSampleDimensionType();
handleSampleDimensionType(sdType);
}
}
/**
* Encodes the DomainSet as per the GML spec of the provided {@link GridCoverage2D}
*
* e.g.:
*
* <pre>
* {@code
* <gml:domainSet>
* <gml:Grid gml:id="gr0001_C0001" dimension="2">
* <gml:limits>
* <gml:GridEnvelope>
* <gml:low>1 1</gml:low>
* <gml:high>5 3</gml:high>
* </gml:GridEnvelope>
* </gml:limits>
* <gml:axisLabels>Lat Long</gml:axisLabels>
* </gml:Grid>
* </gml:domainSet>
* }
* </pre>
*
*
* @param gc2d the {@link GridCoverage2D} for which to encode the DomainSet.
* @param srsName
* @param axesSwap
*/
public void handleDomainSet(GridGeometry2D gg2D, int gridDimension, String gcName, String srsName,
boolean axesSwap) {
// setup vars
final String gridId = "grid00__" + gcName;
// Grid Envelope
final GridEnvelope gridEnvelope = gg2D.getGridRange();
final StringBuilder lowSb = new StringBuilder();
for (int i : gridEnvelope.getLow().getCoordinateValues()) {
lowSb.append(i).append(' ');
}
final StringBuilder highSb = new StringBuilder();
for (int i : gridEnvelope.getHigh().getCoordinateValues()) {
highSb.append(i).append(' ');
}
// build the fragment
final AttributesImpl gridAttrs = new AttributesImpl();
gridAttrs.addAttribute("", "gml:id", "gml:id", "", gridId);
gridAttrs.addAttribute("", "dimension", "dimension", "", String.valueOf(gridDimension));
start("gml:domainSet");
start("gml:RectifiedGrid", gridAttrs);
start("gml:limits");
// GridEnvelope
start("gml:GridEnvelope");
element("gml:low", lowSb.toString().trim());
element("gml:high", highSb.toString().trim());
end("gml:GridEnvelope");
end("gml:limits");
// Axis Label
element("gml:axisLabels", "i j");
final MathTransform2D transform = gg2D.getGridToCRS2D(PixelOrientation.CENTER);
if (!(transform instanceof AffineTransform2D)) {
throw new IllegalStateException("Invalid grid to worl provided:"
+ transform.toString());
}
final AffineTransform2D g2W = (AffineTransform2D) transform;
// Origin
// we use ULC as per our G2W transformation
final AttributesImpl pointAttr = new AttributesImpl();
pointAttr.addAttribute("", "gml:id", "gml:id", "", "p00_" + gcName);
pointAttr.addAttribute("", "srsName", "srsName", "", srsName);
start("gml:origin");
start("gml:Point", pointAttr);
element("gml:pos",
axesSwap ? g2W.getTranslateY() + " " + g2W.getTranslateX() : g2W
.getTranslateX() + " " + g2W.getTranslateY());
end("gml:Point");
end("gml:origin");
// Offsets
final AttributesImpl offsetAttr = new AttributesImpl();
offsetAttr.addAttribute("", "srsName", "srsName", "", srsName);
// notice the orientation of the transformation I create. The origin of the coordinates
// in this grid is not at UPPER LEFT like in our grid to world but at LOWER LEFT !!!
element("gml:offsetVector",
Double.valueOf(axesSwap ? g2W.getShearX() : g2W.getScaleX()) + " "
+ Double.valueOf(axesSwap ? g2W.getScaleX() : g2W.getShearX()),
offsetAttr);
element("gml:offsetVector",
Double.valueOf(axesSwap ? g2W.getScaleY() : g2W.getShearY()) + " "
+ Double.valueOf(axesSwap ? g2W.getShearY() : g2W.getScaleY()),
offsetAttr);
end("gml:RectifiedGrid");
end("gml:domainSet");
}
}
@Override
public Translator createTranslator(ContentHandler handler) {
return new GMLTranslator(handler);
}
}