/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved
* (c) 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 java.awt.geom.AffineTransform;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.opengis.wcs20.DimensionSliceType;
import net.opengis.wcs20.DimensionSubsetType;
import net.opengis.wcs20.DimensionTrimType;
import net.opengis.wcs20.GetCoverageType;
import org.eclipse.emf.common.util.EList;
import org.geoserver.catalog.CoverageInfo;
import org.geoserver.catalog.DimensionInfo;
import org.geoserver.catalog.ResourceInfo;
import org.geoserver.catalog.util.ReaderDimensionsAccessor;
import org.geoserver.wcs2_0.GetCoverage;
import org.geoserver.wcs2_0.GridCoverageRequest;
import org.geoserver.wcs2_0.WCSEnvelope;
import org.geoserver.wcs2_0.exception.WCS20Exception;
import org.geoserver.wcs2_0.response.DimensionBean.DimensionType;
import org.geoserver.wcs2_0.util.EnvelopeAxesLabelsMapper;
import org.geoserver.wcs2_0.util.RequestUtils;
import org.geotools.coverage.grid.io.DimensionDescriptor;
import org.geotools.coverage.grid.io.GranuleSource;
import org.geotools.coverage.grid.io.GridCoverage2DReader;
import org.geotools.coverage.grid.io.StructuredGridCoverage2DReader;
import org.geotools.data.Query;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.geometry.jts.JTS;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.CRS;
import org.geotools.renderer.crs.ProjectionHandler;
import org.geotools.renderer.crs.ProjectionHandlerFinder;
import org.geotools.util.DateRange;
import org.geotools.util.NumberRange;
import org.geotools.util.Utilities;
import org.geotools.util.logging.Logging;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.expression.Literal;
import org.opengis.filter.expression.PropertyName;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.TransformException;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.Polygon;
/**
* Provides support to deal with dimensions slicing, trimming, values conversions and default values computations
*
* TODO: Port timeSubset and elevationSubset code here too
* @author Daniele Romagnoli - GeoSolutions
*/
public class WCSDimensionsSubsetHelper {
public static final Set<String> TIME_NAMES = new HashSet<String>();
public static final Set<String> ELEVATION_NAMES = new HashSet<String>();
private final static Logger LOGGER = Logging.getLogger(WCSDimensionsHelper.class);
private GetCoverageType request;
private Map<String, DimensionInfo> enabledDimensions;
private ReaderDimensionsAccessor accessor;
private DimensionInfo timeDimension;
private DimensionInfo elevationDimension;
private CoordinateReferenceSystem subsettingCRS;
private WCSEnvelope requestedEnvelope;
private GridCoverage2DReader reader;
private EnvelopeAxesLabelsMapper envelopeDimensionsMapper;
private CoverageInfo coverageInfo;
private GridCoverageRequest gridCoverageRequest;
private FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2();
private final static WCSDimensionsValueParser PARSER = new WCSDimensionsValueParser();
public void setGridCoverageRequest(GridCoverageRequest gridCoverageRequest) {
this.gridCoverageRequest = gridCoverageRequest;
}
public GridCoverageRequest getGridCoverageRequest() {
return gridCoverageRequest;
}
public CoverageInfo getCoverageInfo() {
return coverageInfo;
}
public void setCoverageInfo(CoverageInfo coverageInfo) {
this.coverageInfo = coverageInfo;
}
static {
TIME_NAMES.add("t");
TIME_NAMES.add("time");
TIME_NAMES.add("temporal");
TIME_NAMES.add("phenomenontime");
ELEVATION_NAMES.add("elevation");
}
public WCSDimensionsSubsetHelper(GridCoverage2DReader reader, GetCoverageType request, CoverageInfo ci,
CoordinateReferenceSystem subsettingCRS, EnvelopeAxesLabelsMapper envelopeDimensionsMapper) throws IOException {
this.request = request;
this.coverageInfo = ci;
// Note that dimensions will be returned if existing and enabled too
this.enabledDimensions = WCSDimensionsHelper.getDimensionsFromMetadata(ci.getMetadata());
this.subsettingCRS = subsettingCRS;
this.reader = reader;
this.envelopeDimensionsMapper = envelopeDimensionsMapper;
timeDimension = enabledDimensions.get(ResourceInfo.TIME);
elevationDimension = enabledDimensions.get(ResourceInfo.ELEVATION);
if (timeDimension != null || elevationDimension != null || !enabledDimensions.isEmpty()) {
accessor = new ReaderDimensionsAccessor(reader);
}
}
/**
* Extracts the simplified dimension name, throws exception if the dimension name is empty
* @param dim
*
*/
public static String getDimensionName(DimensionSubsetType dim) {
// get basic information
String dimension = dim.getDimension();
// remove common prefixes
if (dimension.startsWith("http://www.opengis.net/def/axis/OGC/0/")) {
dimension = dimension.substring("http://www.opengis.net/def/axis/OGC/0/".length());
} else if (dimension.startsWith("http://opengis.net/def/axis/OGC/0/")) {
dimension = dimension.substring("http://opengis.net/def/axis/OGC/0/".length());
} else if (dimension.startsWith("http://opengis.net/def/crs/ISO/2004/")) {
dimension = dimension.substring("http://opengis.net/def/crs/ISO/2004/".length());
}
// checks
// TODO synonyms on axes labels
if (dimension == null || dimension.length() <= 0) {
throw new WCS20Exception("Empty/invalid axis label provided: " + dim.getDimension(),
WCS20Exception.WCS20ExceptionCode.InvalidAxisLabel, "subset");
}
return dimension;
}
/**
* This method is responsible for extracting the subsettingEvelope from the
* incoming request.
*
* @param reader
* @param request
* @param subsettingCRS
*
*/
private WCSEnvelope extractSubsettingEnvelope() {
//default envelope in subsettingCRS
final CoordinateReferenceSystem sourceCRS = reader.getCoordinateReferenceSystem();
WCSEnvelope sourceEnvelopeInSubsettingCRS = new WCSEnvelope(reader.getOriginalEnvelope());
if (!(subsettingCRS == null || CRS.equalsIgnoreMetadata(subsettingCRS, sourceCRS))) {
// reproject source envelope to subsetting crs for initialization
try {
sourceEnvelopeInSubsettingCRS = new WCSEnvelope(CRS.transform(
reader.getOriginalEnvelope(), subsettingCRS));
} catch (Exception e) {
try {
// see if we can get a valid restricted area using the projection handlers
ProjectionHandler handler = ProjectionHandlerFinder.getHandler(
new ReferencedEnvelope(0, 1, 0, 1, subsettingCRS), sourceCRS, true);
if (handler != null) {
ReferencedEnvelope validArea = handler.getValidAreaBounds();
Envelope intersection = validArea.intersection(ReferencedEnvelope
.reference(reader.getOriginalEnvelope()));
ReferencedEnvelope re = new ReferencedEnvelope(intersection, sourceCRS);
sourceEnvelopeInSubsettingCRS = new WCSEnvelope(re.transform(subsettingCRS,
true));
} else {
throw new WCS20Exception("Unable to initialize subsetting envelope",
WCS20Exception.WCS20ExceptionCode.SubsettingCrsNotSupported,
subsettingCRS.toWKT(), e); // TODO extract code
}
} catch (Exception e2) {
throw new WCS20Exception("Unable to initialize subsetting envelope",
WCS20Exception.WCS20ExceptionCode.SubsettingCrsNotSupported,
subsettingCRS.toWKT(), e2); // TODO extract code
}
}
}
// check if we have to subset, if not let's send back the basic coverage
final EList<DimensionSubsetType> requestedDimensions = request.getDimensionSubset();
if(requestedDimensions==null||requestedDimensions.size()<=0){
return sourceEnvelopeInSubsettingCRS;
}
int maxDimensions = 2 + enabledDimensions.size();
if(requestedDimensions.size() > maxDimensions){
throw new WCS20Exception(
"Invalid number of dimensions",
WCS20Exception.WCS20ExceptionCode.InvalidSubsetting,Integer.toString(requestedDimensions.size()));
}
// put aside the dimensions that we have for double checking
final List<String> axesNames = envelopeDimensionsMapper.getAxesNames(sourceEnvelopeInSubsettingCRS, true);
final List<String> foundDimensions= new ArrayList<String>();
// === parse dimensions
// the subsetting envelope is initialized with the source envelope in subsetting CRS
WCSEnvelope subsettingEnvelope = new WCSEnvelope(sourceEnvelopeInSubsettingCRS);
Set<String> dimensionKeys = enabledDimensions.keySet();
for (DimensionSubsetType dim : requestedDimensions){
String dimension = WCSDimensionsSubsetHelper.getDimensionName(dim);
// skip time support
if (WCSDimensionsSubsetHelper.TIME_NAMES.contains(dimension.toLowerCase())) {
if (dimensionKeys.contains(ResourceInfo.TIME)) {
// fine, we'll parse it later
continue;
} else {
throw new WCS20Exception("Invalid axis label provided: " + dimension,
WCS20Exception.WCS20ExceptionCode.InvalidAxisLabel, null);
}
}
if (WCSDimensionsSubsetHelper.ELEVATION_NAMES.contains(dimension.toLowerCase())) {
if (dimensionKeys.contains(ResourceInfo.ELEVATION)) {
// fine, we'll parse it later
continue;
} else {
throw new WCS20Exception("Invalid axis label provided: " + dimension,
WCS20Exception.WCS20ExceptionCode.InvalidAxisLabel, null);
}
}
boolean isCustomDimension = false;
for (String dimensionKey : dimensionKeys){
if (dimensionKey.equalsIgnoreCase(dimension)) {
isCustomDimension = true;
break;
}
}
if (isCustomDimension) {
continue;
}
if(!axesNames.contains(dimension)) {
throw new WCS20Exception("Invalid axis label provided: " + dimension,
WCS20Exception.WCS20ExceptionCode.InvalidAxisLabel,
dimension == null ? "Null" : dimension);
}
// did we already do something with this dimension?
if(foundDimensions.contains(dimension)){
throw new WCS20Exception("Axis label already used during subsetting",WCS20Exception.WCS20ExceptionCode.InvalidAxisLabel,dimension);
}
foundDimensions.add(dimension);
// now decide what to do
// final String CRS= dim.getCRS();// TODO HOW DO WE USE THIS???
if(dim instanceof DimensionTrimType){
// TRIMMING
final DimensionTrimType trim = (DimensionTrimType) dim;
final double low = Double.parseDouble(trim.getTrimLow());
final double high = Double.parseDouble(trim.getTrimHigh());
final int axisIndex = envelopeDimensionsMapper.getAxisIndex(sourceEnvelopeInSubsettingCRS, dimension);
if (axisIndex < 0) {
throw new WCS20Exception("Invalid axis provided",WCS20Exception.WCS20ExceptionCode.InvalidAxisLabel,dimension);
}
// low > high && not dateline wrapping?
if (low > high && !subsettingEnvelope.isLongitude(axisIndex)) {
throw new WCS20Exception("Low greater than High",
WCS20Exception.WCS20ExceptionCode.InvalidSubsetting, trim.getTrimLow());
}
// notice how we choose the order of the axes
subsettingEnvelope.setRange(axisIndex, low, high);
} else if (dim instanceof DimensionSliceType) {
// SLICING
final DimensionSliceType slicing= (DimensionSliceType) dim;
final String slicePointS = slicing.getSlicePoint();
final double slicePoint=Double.parseDouble(slicePointS);
final int axisIndex=envelopeDimensionsMapper.getAxisIndex(sourceEnvelopeInSubsettingCRS, dimension);
if (axisIndex < 0) {
throw new WCS20Exception("Invalid axis provided",WCS20Exception.WCS20ExceptionCode.InvalidAxisLabel,dimension);
}
// notice how we choose the order of the axes
AffineTransform affineTransform = RequestUtils.getAffineTransform(reader.getOriginalGridToWorld(PixelInCell.CELL_CENTER));
final double scale = axisIndex == 0 ? affineTransform.getScaleX() : -affineTransform.getScaleY();
subsettingEnvelope.setRange(axisIndex, slicePoint, slicePoint + scale);
// slice point outside coverage
if (sourceEnvelopeInSubsettingCRS.getMinimum(axisIndex) > slicePoint || slicePoint > sourceEnvelopeInSubsettingCRS.getMaximum(axisIndex)){
throw new WCS20Exception(
"SlicePoint outside coverage envelope",
WCS20Exception.WCS20ExceptionCode.InvalidSubsetting,
slicePointS);
}
} else {
throw new WCS20Exception(
"Invalid element found while attempting to parse dimension subsetting request",
WCS20Exception.WCS20ExceptionCode.InvalidSubsetting,
dim.getClass().toString());
}
}
// make sure we have not been requested to subset outside of the source CRS
requestedEnvelope = new WCSEnvelope(subsettingEnvelope);
subsettingEnvelope.intersect(new GeneralEnvelope(sourceEnvelopeInSubsettingCRS));
if (subsettingEnvelope.isEmpty()) {
throw new WCS20Exception("Empty intersection after subsetting",
WCS20Exception.WCS20ExceptionCode.InvalidSubsetting, "");// TODO spit our
// envelope trimmed
}
// return the subsetting envelope in the CRS it was specified into, to
// allow projection handlers to handle dateline crossing
return subsettingEnvelope;
//
// intersect with original envelope to make sure the subsetting is valid
//
// GeneralEnvelope sourceEnvelope = reader.getOriginalEnvelope();
// reproject envelope to native crs for cropping
// try {
// if(!CRS.equalsIgnoreMetadata(subsettingEnvelope.getCoordinateReferenceSystem(),
// reader.getOriginalEnvelope())){
// // look for transform
// if (!CRS.equalsIgnoreMetadata(subsettingCRS, sourceCRS)) {
// final GeneralEnvelope subsettingEnvelopeInSourceCRS = CRS.transform(
// subsettingEnvelope, sourceCRS);
//
// // intersect
// subsettingEnvelopeInSourceCRS.intersect(sourceEnvelope);
//
// // provided trim extent does not intersect coverage envelope
// if (subsettingEnvelopeInSourceCRS.isEmpty()) {
// throw new WCS20Exception(
// "Empty intersection after subsetting",
// WCS20Exception.WCS20ExceptionCode.InvalidSubsetting,"");// TODO spit our envelope trimmed
// }
// return new WCSEnvelope(subsettingEnvelopeInSourceCRS);
// }
// }
// // we are reprojecting
// subsettingEnvelope.intersect(sourceEnvelope);
//
// // provided trim extent does not intersect coverage envelope
// if(subsettingEnvelope.isEmpty()){
// throw new WCS20Exception(
// "Empty intersection after subsetting",
// WCS20Exception.WCS20ExceptionCode.InvalidSubsetting,"");// TODO spit our envelope trimmed
// }
// return new WCSEnvelope(subsettingEnvelope);
// } catch (TransformException e) {
// final WCS20Exception exception= new WCS20Exception(
// "Unable to initialize subsetting envelope",
// WCS20Exception.WCS20ExceptionCode.SubsettingCrsNotSupported,
// subsettingCRS.toWKT()); // TODO extract code
// exception.initCause(e);
// throw exception;
// }
}
/**
* Parses a date range out of the dimension subsetting directives
* @param accessor
* @param request
* @param timeDimension
*
* @throws IOException
*/
private DateRange extractTemporalSubset() throws IOException {
DateRange timeSubset = null;
if (timeDimension != null) {
for (DimensionSubsetType dim : request.getDimensionSubset()) {
String dimension = WCSDimensionsSubsetHelper.getDimensionName(dim);
// only care for time dimensions
if (!TIME_NAMES.contains(dimension.toLowerCase())) {
continue;
}
// did we parse the range already?
if(timeSubset != null) {
throw new WCS20Exception("Time dimension trimming/slicing specified twice in the request",
WCS20Exception.WCS20ExceptionCode.InvalidSubsetting, "subset");
}
// now decide what to do
if (dim instanceof DimensionTrimType) {
// TRIMMING
final DimensionTrimType trim = (DimensionTrimType) dim;
final Date low = PARSER.parseDateTime(trim.getTrimLow());
final Date high = PARSER.parseDateTime(trim.getTrimHigh());
// low > high???
if (low.compareTo(high) > 0) {
throw new WCS20Exception("Low greater than High: " + trim.getTrimLow()
+ ", " + trim.getTrimHigh(),
WCS20Exception.WCS20ExceptionCode.InvalidSubsetting, "subset");
}
timeSubset = new DateRange(low, high);
} else if (dim instanceof DimensionSliceType) {
// SLICING
final DimensionSliceType slicing = (DimensionSliceType) dim;
final String slicePointS = slicing.getSlicePoint();
final Date slicePoint = PARSER.parseDateTime(slicePointS);
timeSubset = new DateRange(slicePoint, slicePoint);
} else {
throw new WCS20Exception(
"Invalid element found while attempting to parse dimension subsetting request: " + dim.getClass()
.toString(), WCS20Exception.WCS20ExceptionCode.InvalidSubsetting, "subset");
}
}
// right now we don't support trimming
// TODO: revisit when we have some multidimensional output support
if(!(reader instanceof StructuredGridCoverage2DReader) && timeSubset != null && !timeSubset.getMinValue().equals(timeSubset.getMaxValue())) {
throw new WCS20Exception("Trimming on time is not supported at the moment on not StructuredGridCoverage2DReaders, only slicing is");
}
// apply nearest neighbor matching on time
if (timeSubset != null && timeSubset.getMinValue().equals(timeSubset.getMaxValue())) {
timeSubset = interpolateTime(timeSubset, accessor);
}
}
return timeSubset;
}
/**
* Nearest interpolation against time
* @param timeSubset
* @param accessor
*
* @throws IOException
*/
private DateRange interpolateTime(DateRange timeSubset, ReaderDimensionsAccessor accessor) throws IOException {
TreeSet<Object> domain = accessor.getTimeDomain();
Date slicePoint = timeSubset.getMinValue();
if(!domainContainsPoint(slicePoint, domain)) {
// look for the closest time
Date previous = null;
Date newSlicePoint = null;
// for NN matching we don't need the ranges, NN against their extrema will be fine
TreeSet<Date> domainDates = getDomainDates(domain);
for (Date curr : domainDates) {
if(curr.compareTo(slicePoint) > 0) {
if(previous == null) {
newSlicePoint = curr;
break;
} else {
long diffPrevious = slicePoint.getTime() - previous.getTime();
long diffCurr = curr.getTime() - slicePoint.getTime();
if(diffCurr > diffPrevious) {
newSlicePoint = curr;
break;
} else {
newSlicePoint = previous;
break;
}
}
} else {
previous = curr;
}
}
if(newSlicePoint == null) {
newSlicePoint = previous;
}
timeSubset = new DateRange(newSlicePoint, newSlicePoint);
}
return timeSubset;
}
/**
* Get the domain set as a set of dates.
* @param domain
*
*/
private TreeSet<Date> getDomainDates(TreeSet<Object> domain) {
TreeSet<Date> results = new TreeSet<Date>();
for (Object item : domain) {
if(item instanceof Date) {
Date date = (Date) item;
results.add(date);
} else if(item instanceof DateRange) {
DateRange range = (DateRange) item;
results.add(range.getMinValue());
results.add(range.getMaxValue());
}
}
return results;
}
public WCSEnvelope getRequestedEnvelope() {
return requestedEnvelope;
}
/**
* Check whether the provided domain contains the specified slicePoint.
*
* @param slicePoint the point to be checked (a Date or a Number)
* @param domain the domain to be scan for containment.
*
*/
private boolean domainContainsPoint(final Object slicePoint, final TreeSet<Object> domain) {
// cannot use this...
// if(domain.contains(slicePoint)) {
// return true;
// }
// check date ranges for containment
if (slicePoint instanceof Date) {
Date sliceDate = (Date) slicePoint;
for (Object curr : domain) {
if(curr instanceof Date) {
Date date = (Date) curr;
int result = date.compareTo(sliceDate);
if(result > 0) {
return false;
} else if(result == 0) {
return true;
}
} else if(curr instanceof DateRange) {
DateRange range = (DateRange) curr;
if(range.contains(sliceDate)) {
return true;
} else if(range.getMaxValue().compareTo(sliceDate) < 0) {
return false;
}
}
}
} else if (slicePoint instanceof Number) {
//TODO: Should we check for other data types?
Number sliceNumber = (Number) slicePoint;
for (Object curr : domain) {
if(curr instanceof Number) {
Double num = (Double) curr;
int result = num.compareTo((Double)sliceNumber);
if( result > 0) {
return false;
} else if(result == 0) {
return true;
}
} else if(curr instanceof NumberRange) {
NumberRange range = (NumberRange) curr;
if(range.contains(sliceNumber)) {
return true;
} else if(range.getMaxValue().compareTo(sliceNumber) < 0) {
return false;
}
}
}
}
return false;
}
/**
* Parses a number range out of the dimension subsetting directives
* @param accessor
* @param request
* @param elevationDimension
*
* @throws IOException
*/
private NumberRange extractElevationSubset() throws IOException {
NumberRange elevationSubset = null;
if (elevationDimension != null) {
for (DimensionSubsetType dim : request.getDimensionSubset()) {
String dimension = WCSDimensionsSubsetHelper.getDimensionName(dim);
// only care for elevation dimensions
if (!WCSDimensionsSubsetHelper.ELEVATION_NAMES.contains(dimension.toLowerCase())) {
continue;
}
// did we parse the range already?
if (elevationSubset != null) {
throw new WCS20Exception("Elevation dimension trimming/slicing specified twice in the request",
WCS20Exception.WCS20ExceptionCode.InvalidSubsetting, "subset");
}
// now decide what to do
if (dim instanceof DimensionTrimType) {
// TRIMMING
final DimensionTrimType trim = (DimensionTrimType) dim;
final Double low = PARSER.parseDouble(trim.getTrimLow());
final Double high = PARSER.parseDouble(trim.getTrimHigh());
// low > high???
if (low > high) {
throw new WCS20Exception("Low greater than High: " + trim.getTrimLow()
+ ", " + trim.getTrimHigh(),
WCS20Exception.WCS20ExceptionCode.InvalidSubsetting, "subset");
}
elevationSubset = new NumberRange<Double>(Double.class, low, high);
} else if (dim instanceof DimensionSliceType) {
// SLICING
final DimensionSliceType slicing = (DimensionSliceType) dim;
final String slicePointS = slicing.getSlicePoint();
final Double slicePoint = PARSER.parseDouble(slicePointS);
elevationSubset = new NumberRange<Double>(Double.class, slicePoint, slicePoint);
} else {
throw new WCS20Exception(
"Invalid element found while attempting to parse dimension subsetting request: " + dim.getClass()
.toString(), WCS20Exception.WCS20ExceptionCode.InvalidSubsetting, "subset");
}
}
// right now we don't support trimming
// TODO: revisit when we have some multidimensional output support
if (!(reader instanceof StructuredGridCoverage2DReader) && elevationSubset != null && !elevationSubset.getMinValue().equals(elevationSubset.getMaxValue())) {
throw new WCS20Exception("Trimming on elevation is not supported at the moment on not StructuredGridCoverage2DReaders, only slicing is");
}
// apply nearest neighbor matching on elevation
if (elevationSubset != null && elevationSubset.getMinValue().equals(elevationSubset.getMaxValue())) {
interpolateElevation (elevationSubset, accessor);
}
}
return elevationSubset;
}
/**
* Nearest interpolation on elevation
* @param elevationSubset
* @param accessor
*
* @throws IOException
*/
private NumberRange interpolateElevation(NumberRange elevationSubset, ReaderDimensionsAccessor accessor) throws IOException {
TreeSet<Object> domain = accessor.getElevationDomain();
Double slicePoint = elevationSubset.getMinimum();
if (!domainContainsPoint(slicePoint, domain)) {
// look for the closest elevation
Double previous = null;
Double newSlicePoint = null;
// for NN matching we don't need the range, NN against their extrema will be fine
TreeSet<Double> domainDates = PARSER.getDomainNumber(domain);
for (Double curr : domainDates) {
if (curr.compareTo(slicePoint) > 0) {
if (previous == null) {
newSlicePoint = curr;
break;
} else {
double diffPrevious = slicePoint - previous;
double diffCurr = curr - slicePoint;
if (diffCurr > diffPrevious) {
newSlicePoint = curr;
break;
} else {
newSlicePoint = previous;
break;
}
}
} else {
previous = curr;
}
}
if (newSlicePoint == null) {
newSlicePoint = previous;
}
elevationSubset = new NumberRange<Double>(Double.class, newSlicePoint, newSlicePoint);
}
return elevationSubset;
}
/**
* Extract custom dimension subset from the current helper
*
* @param accessor
* @param request
* @param enabledDimensions
* @param timeDimension
*
* @throws IOException
*/
private Map<String, List<Object>> extractDimensionsSubset() throws IOException {
Map<String, List<Object>> dimensionSubset = new HashMap<String, List<Object>>();
if (enabledDimensions != null && !enabledDimensions.isEmpty()) {
Set<String> dimensionKeys = enabledDimensions.keySet();
for (DimensionSubsetType dim : request.getDimensionSubset()) {
String dimension = getDimensionName(dim);
if (WCSDimensionsSubsetHelper.ELEVATION_NAMES.contains(dimension.toLowerCase()) || WCSDimensionsSubsetHelper.TIME_NAMES.contains(dimension.toLowerCase())) {
continue;
}
// only care for custom dimensions
if (dimensionKeys.contains(dimension)) {
List<Object> selectedValues = new ArrayList<Object>();
// now decide what to do
if (dim instanceof DimensionTrimType) {
// TRIMMING
final DimensionTrimType trim = (DimensionTrimType) dim;
setSubsetRangeValue(dimension, trim.getTrimLow(), trim.getTrimHigh(), selectedValues);
} else
if (dim instanceof DimensionSliceType) {
// SLICING
final DimensionSliceType slicing = (DimensionSliceType) dim;
setSubsetValue(dimension, slicing.getSlicePoint(), selectedValues);
} else {
throw new WCS20Exception(
"Invalid element found while attempting to parse dimension subsetting request: "
+ dim.getClass().toString(),
WCS20Exception.WCS20ExceptionCode.InvalidSubsetting, "subset");
}
// TODO: Deal with default values
dimensionSubset.put(dimension, selectedValues);
}
}
}
return dimensionSubset;
}
/**
* Set the trim value as proper object (by checking whether the domainDatatype metadata exists or by try multiple parsing until one is
* successfull)
*
* @param dimensionName the name of the dimension to be set
* @param slicing
* @param selectedValues
* @throws IOException
*/
private void setSubsetRangeValue(String dimensionName, String low, String high, List<Object> selectedValues) throws IOException {
boolean sliceSet = false;
String domainDatatype = accessor.getDomainDatatype(dimensionName);
if (domainDatatype != null) {
PARSER.setRangeValues(low, high, selectedValues, domainDatatype);
} else {
// Try with recursive settings
// Try setting the value as a time
if (!sliceSet) {
sliceSet = PARSER.setAsDateRange(low, high, selectedValues);
}
// Try setting the value as an integer
if (!sliceSet) {
sliceSet = PARSER.setAsIntegerRange(low, high, selectedValues);
}
// Try setting the value as a double
if (!sliceSet) {
sliceSet = PARSER.setAsDoubleRange(low, high, selectedValues);
}
if (!sliceSet) {
// Setting it as a String
selectedValues.add(low + "/" + high); //Check That
}
}
}
/**
* Set the slice value as proper object (by checking whether the domainDatatype metadata exists or by try multiple parsing until one is
* successfull)
*
* @param dimensionName the name of the dimension to be set
* @param slicing
* @param selectedValues
* @throws IOException
*/
private void setSubsetValue(String dimensionName, String slicePoint, List<Object> selectedValues) throws IOException {
boolean sliceSet = false;
String domainDatatype = accessor.getDomainDatatype(dimensionName);
if (domainDatatype != null) {
PARSER.setValues(slicePoint, selectedValues, domainDatatype);
} else {
// Try with recursive settings
// Try setting the value as a time
if (!sliceSet) {
sliceSet = PARSER.setAsDate(slicePoint, selectedValues);
}
// Try setting the value as an integer
if (!sliceSet) {
sliceSet = PARSER.setAsInteger(slicePoint, selectedValues);
}
// Try setting the value as a double
if (!sliceSet) {
sliceSet = PARSER.setAsDouble(slicePoint, selectedValues);
}
if (!sliceSet) {
// Setting it as a String
selectedValues.add(slicePoint);
}
}
}
/**
* Return a {@link GridCoverageRequest} instance containing specified subsetting dimensions.
*
*
* @throws IOException
*/
public GridCoverageRequest createGridCoverageRequestSubset() throws IOException {
final WCSEnvelope spatialSubset = extractSubsettingEnvelope();
assert spatialSubset != null && !spatialSubset.isEmpty();
Map<String, List<Object>> dimensionsSubset = null;
DateRange temporalSubset = null;
NumberRange elevationSubset = null;
// Parse specified subset values (if any)
if (enabledDimensions != null && !enabledDimensions.isEmpty()) {
// extract temporal subsetting
temporalSubset = extractTemporalSubset();
// extract elevation subsetting
elevationSubset = extractElevationSubset();
// extract dimensions subsetting
dimensionsSubset = extractDimensionsSubset();
}
// Prepare subsetting request
GridCoverageRequest subsettingRequest = new GridCoverageRequest();
subsettingRequest.setSpatialSubset(spatialSubset);
subsettingRequest.setElevationSubset(elevationSubset);
subsettingRequest.setTemporalSubset(temporalSubset);
subsettingRequest.setDimensionsSubset(dimensionsSubset);
subsettingRequest.setFilter(request.getFilter());
// Handle default values and update subsetting values if needed
String coverageName = getCoverageName();
//TODO consider dealing with the Format instance instead of a String parsing or check against WCSUtils.isSupportedMDOutputFormat(String).
if (!GetCoverage.formatSupportMDOutput(request.getFormat())) {
// Right now, only a few formats support multidimensional output.
// Let's use default values for the others.
WCSDefaultValuesHelper defaultValuesHelper = new WCSDefaultValuesHelper(reader, accessor, request, coverageName);
defaultValuesHelper.setDefaults(subsettingRequest);
}
return subsettingRequest;
}
/**
* Return the coverageName for the underlying {@link CoverageInfo} by accessing the nativeCoverageName if available.
* Since the nativeCoverageName may be null for single coverage formats we get the first grid coverage name from
* the reader as backup.
*
*
* @throws IOException
*/
private String getCoverageName() throws IOException {
final String nativeName = coverageInfo.getNativeCoverageName();
return (nativeName != null ? nativeName : reader.getGridCoverageNames()[0]);
}
/**
* Split the current GridCoverageRequest by creating a list of new GridCoverageRequests: A query will be performed
* with the current specified subsets, returning N granules (if any).
* Then new N GridCoverageRequests will be created (one for each granule) having subsets setup on top
* of the specific values of the dimensions for that N-th granule.
*
* This method only works for StructuredGridCoverage2DReaders
* @param cinfo the CoverageInfo associated to the coverage
* @param gcr the main {@link GridCoverageRequest} to be split
* @param reader the {@link StructuredGridCoverage2DReader} instance to be used for query, and dimensions management
* @return a List of new {@link GridCoverageRequest}s
* @throws UnsupportedOperationException
* @throws IOException
* @throws MismatchedDimensionException
* @throws TransformException
* @throws FactoryException
*/
public List<GridCoverageRequest> splitRequest()
throws UnsupportedOperationException, IOException, MismatchedDimensionException, TransformException, FactoryException {
StructuredGridCoverage2DReader structuredReader = null;
if (reader instanceof StructuredGridCoverage2DReader) {
structuredReader = (StructuredGridCoverage2DReader) reader;
} else {
throw new IllegalArgumentException("The method is only supported for StructuredGridCoverage2DReaders");
}
// Getting the granule source
final String coverageName = getCoverageName();
final GranuleSource source = structuredReader.getGranules(coverageName, true);
if (source == null) {
throw new IllegalArgumentException("No granule source available for that coverageName");
}
// Preparing a query containing all the specified dimensions.
// This will allow to get back only the granules respecting the specified request
final Query query = prepareDimensionsQuery(structuredReader, coverageName, gridCoverageRequest, source);
// Getting the granules for that query; Loop over the granules to create subRequest with single elements dimensions sets
final SimpleFeatureCollection collection = source.getGranules(query);
final SimpleFeatureIterator iterator = collection.features();
final List<GridCoverageRequest> requests = new ArrayList<GridCoverageRequest>();
try {
while (iterator.hasNext()) {
final SimpleFeature feature = iterator.next();
// Prepare subRequest setting up dimensions matching the values of the current granule
final GridCoverageRequest subRequest = new GridCoverageRequest();
// Setting up constant elements (outputCRS, spatial subset, interpolation
subRequest.setOutputCRS(gridCoverageRequest.getOutputCRS());
subRequest.setSpatialInterpolation(gridCoverageRequest.getSpatialInterpolation());
subRequest.setSpatialSubset(gridCoverageRequest.getSpatialSubset());
subRequest.setTemporalInterpolation(gridCoverageRequest.getTemporalInterpolation());
//Setting up specific dimensions subset
updateDimensions(subRequest, feature, structuredReader, coverageName);
requests.add(subRequest);
}
} finally {
iterator.close();
}
return requests;
}
public Set<GridCoverageRequest> splitRequestToSet() throws MismatchedDimensionException, UnsupportedOperationException, IOException, TransformException, FactoryException {
List<GridCoverageRequest> list = splitRequest();
Set<GridCoverageRequest> set = new HashSet<GridCoverageRequest>();
for (GridCoverageRequest request : list) {
set.add(request);
}
return set;
}
/**
* Update the subset (temporal, vertical, custom) of the request by inspecting the reader DimensionsDescriptor and
* collecting proper values from the current feature.
* @param subRequest the subRequest to be updated with subsets
* @param feature the current feature containing dimensions value to be used for the subsetting
* @param reader the reader to be used for the inspection.
* @param coverageName the name of the coverage.
*/
private void updateDimensions(
final GridCoverageRequest subRequest,
final SimpleFeature feature,
final StructuredGridCoverage2DReader reader,
final String coverageName) {
// ----------------------------------
// Updating temporal dimension subset
// ----------------------------------
String startTimeAttribute = null;
String endTimeAttribute = null;
DimensionDescriptor timeDescriptor = WCSDimensionsHelper.getDimensionDescriptor(reader, coverageName, "TIME");
if (timeDescriptor != null) {
startTimeAttribute = timeDescriptor.getStartAttribute();
endTimeAttribute = timeDescriptor.getEndAttribute();
Date startDate = (Date) feature.getAttribute(startTimeAttribute);
Date endDate = (endTimeAttribute != null) ? (Date) feature.getAttribute(endTimeAttribute) : startDate;
DateRange range = new DateRange(startDate, endDate);
subRequest.setTemporalSubset(range);
}
// ----------------------------------
// Updating vertical dimension subset
// ----------------------------------
String startElevationAttribute = null;
String endElevationAttribute = null;
DimensionDescriptor elevationDescriptor = WCSDimensionsHelper.getDimensionDescriptor(reader, coverageName, "ELEVATION");
if (elevationDescriptor != null) {
startElevationAttribute = elevationDescriptor.getStartAttribute();
endElevationAttribute = elevationDescriptor.getEndAttribute();
Number startValue = (Number) feature.getAttribute(startElevationAttribute);
Number endValue = (endElevationAttribute != null) ? (Number) feature.getAttribute(endElevationAttribute) : startValue;
NumberRange range = new NumberRange(startValue.getClass(), startValue, endValue);
subRequest.setElevationSubset(range);
}
// ---------------------------------
// Updating custom dimensions subset
// ---------------------------------
List<String> customDomains = (List<String>) (accessor != null ? accessor.getCustomDomains() : Collections.emptyList());
Map<String, List<Object>> dimensionsSubset = new HashMap<String, List<Object>>();
for (String customDomain: customDomains) {
String startAttribute = null;
String endAttribute = null;
DimensionDescriptor descriptor = WCSDimensionsHelper.getDimensionDescriptor(reader, coverageName, customDomain);
if (descriptor != null) {
startAttribute = descriptor.getStartAttribute();
endAttribute = descriptor.getEndAttribute();
Object value = feature.getAttribute(startAttribute);
if (endAttribute != null) {
Object endValue = feature.getAttribute(endAttribute);
Class objectClass = endValue.getClass();
String classDataType = objectClass.toString();
if (classDataType.endsWith("Timestamp")) {
value = new DateRange(new Date(((Timestamp) value).getTime()), new Date(
((Timestamp) endValue).getTime()));
} else if (classDataType.endsWith("Date")) {
value = new DateRange((Date) value, (Date) endValue);
} else {
value = new NumberRange(objectClass, (Number) value, (Number) endValue);
}
}
List<Object> dimensionValues = new ArrayList<Object>();
dimensionValues.add(value);
dimensionsSubset.put(descriptor.getName().toUpperCase(), dimensionValues);
}
}
subRequest.setDimensionsSubset(dimensionsSubset);
}
/**
* Prepare a query by inspecting the specified dimensions and setting the proper attribute values
*/
private Query prepareDimensionsQuery(
final StructuredGridCoverage2DReader reader,
final String coverageName,
final GridCoverageRequest gcr,
final GranuleSource source) throws UnsupportedOperationException, IOException, MismatchedDimensionException, TransformException, FactoryException {
// spatial subset
Filter filter = filterSpatial(gcr, reader, source);;
// temporal subset
filter = filterTime(filter, gcr, coverageName, reader);
// elevation subset
filter = filterElevation(filter, gcr, coverageName, reader);
// dimensionsSubset
filter = filterDimensions(filter, gcr, coverageName, reader);
Query query = new Query(null, filter);
return query;
}
private Filter filterSpatial(GridCoverageRequest gcr,
StructuredGridCoverage2DReader reader, GranuleSource source) throws IOException, MismatchedDimensionException, TransformException, FactoryException {
WCSEnvelope envelope = gcr.getSpatialSubset();
Polygon llPolygon = JTS.toGeometry(new ReferencedEnvelope(envelope));
GeometryDescriptor geom = source.getSchema().getGeometryDescriptor();
PropertyName geometryProperty = ff.property(geom.getLocalName());
Geometry nativeCRSPolygon = JTS.transform(llPolygon, CRS.findMathTransform(envelope.getCoordinateReferenceSystem(), reader.getCoordinateReferenceSystem()));
Literal polygonLiteral = ff.literal(nativeCRSPolygon);
// if(overlaps) {
return ff.intersects(geometryProperty, polygonLiteral);
// } else {
// filter = ff.within(geometryProperty, polygonLiteral);
// }
}
/**
* Update the filter with a vertical Filter in case the current {@link GridCoverageRequest} has an elevation subset.
* @param ff
* @param filter
* @param gcr
* @param coverageName
* @param reader
*
*/
private Filter filterElevation(Filter filter, GridCoverageRequest gcr,
String coverageName, StructuredGridCoverage2DReader reader) {
NumberRange elevationRange = gcr.getElevationSubset();
String startElevation = null;
String endElevation = null;
DimensionDescriptor elevationDescriptor = null;
Filter elevationFilter = filter;
if (elevationRange != null && filter != Filter.EXCLUDE) {
elevationDescriptor = WCSDimensionsHelper.getDimensionDescriptor(reader, coverageName, "ELEVATION");
startElevation = elevationDescriptor.getStartAttribute();
endElevation = elevationDescriptor.getEndAttribute();
elevationFilter = filter(startElevation, endElevation, elevationRange.getMinValue(), elevationRange.getMaxValue(), filter);
}
return elevationFilter;
}
/**
* Update the filter with a temporal Filter in case the current {@link GridCoverageRequest} has a temporal subset.
* @param ff
* @param filter
* @param gcr
* @param coverageName
* @param reader
*
*/
private Filter filterTime(Filter filter, GridCoverageRequest gcr, String coverageName, StructuredGridCoverage2DReader reader) {
DateRange timeRange = gcr.getTemporalSubset();
DimensionDescriptor timeDescriptor = null;
String startTime = null;
String endTime = null;
Filter timeFilter = filter;
if (timeRange != null && filter != Filter.EXCLUDE) {
timeDescriptor = WCSDimensionsHelper.getDimensionDescriptor(reader, coverageName, "TIME");
startTime = timeDescriptor.getStartAttribute();
endTime = timeDescriptor.getEndAttribute();
timeFilter = filter(startTime, endTime, timeRange.getMinValue(), timeRange.getMaxValue(), filter);
}
return timeFilter;
}
private Filter filterDimensions(Filter filter, GridCoverageRequest gcr, String coverageName,
StructuredGridCoverage2DReader reader) {
Map<String, List<Object>> subset = gcr.getDimensionsSubset();
Filter dimensionsFilter = filter;
if (subset != null && !subset.isEmpty()) {
Set<String> dimensions = subset.keySet();
Iterator<String> dimensionsIt = dimensions.iterator();
// Filtering over the dimensions
while (dimensionsIt.hasNext()) {
final String dimensionName = dimensionsIt.next();
List<Object> dimensionValues = subset.get(dimensionName);
if (dimensionValues == null || dimensionValues.isEmpty()) {
continue;
}
DimensionDescriptor dimensionDescriptor = WCSDimensionsHelper.getDimensionDescriptor(reader, coverageName, dimensionName);
if (dimensionDescriptor != null) {
final String startAttrib = dimensionDescriptor.getStartAttribute();
final String endAttrib = dimensionDescriptor.getEndAttribute();
dimensionsFilter = filterDimension(startAttrib, endAttrib, dimensionValues, filter);
} else {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.warning("The specified dimension " + dimensionName + "has no descriptors in the reader. Skipping it");
continue;
}
}
}
}
return dimensionsFilter;
}
/**
* @param startAttribute
* @param endAttribute
* @param dimensionValues
* @param filter
*
*/
private Filter filterDimension(String startAttribute, String endAttribute,
List<Object> dimensionValues, Filter filter) {
Filter localFilter = null;
if (dimensionValues != null && !dimensionValues.isEmpty()) {
// Note that currently, dimensionValues only contain one element (slicing specifies a single value for a dimension)
Object element = dimensionValues.get(0);
Object min = null;
Object max = null;
if (element instanceof DateRange) {
DateRange dateRange = (DateRange) element;
min = dateRange.getMinValue();
max = dateRange.getMaxValue();
} else if (element instanceof NumberRange) {
NumberRange numberRange = (NumberRange) element;
min = numberRange.getMinValue();
max = numberRange.getMaxValue();
} else if (element instanceof Date || element instanceof Number || element instanceof String) {
min = element;
max = element;
} else {
throw new IllegalArgumentException("Unsupported object type");
}
if (endAttribute == null) {
// single value time
localFilter = ff.between(ff.property(startAttribute), ff.literal(min), ff.literal(max));
} else {
// range value, we need to account for containment then
Filter f1 = ff.lessOrEqual(ff.property(startAttribute), ff.literal(max));
Filter f2 = ff.greaterOrEqual(ff.property(endAttribute), ff.literal(min));
localFilter = ff.and(Arrays.asList(f1, f2));
}
if (filter == null) {
filter = localFilter;
} else {
filter = ff.and(filter, localFilter);
}
}
return filter;
}
/**
* Setup an intersection filter
* @param startAttribute
* @param endAttribute
* @param minValue
* @param maxValue
* @param filter
*
*/
private Filter filter(String startAttribute, String endAttribute, Comparable minValue,
Comparable maxValue, Filter filter) {
Filter localFilter = null;
if(endAttribute == null) {
// single value time
localFilter = ff.between(ff.property(startAttribute), ff.literal(minValue), ff.literal(maxValue));
} else {
// range value, we need to account for containment then
Filter f1 = ff.lessOrEqual(ff.property(startAttribute), ff.literal(maxValue));
Filter f2 = ff.greaterOrEqual(ff.property(endAttribute), ff.literal(minValue));
localFilter = ff.and(Arrays.asList(f1, f2));
}
if (filter == null) {
filter = localFilter;
} else {
filter = ff.and(filter, localFilter);
}
return filter;
}
/**
* Prepare the DimensionBean list for this reader
*
* @throws IOException
*/
public List<DimensionBean> setupDimensions() throws IOException {
StructuredGridCoverage2DReader structuredReader = null;
if (reader instanceof StructuredGridCoverage2DReader) {
structuredReader = (StructuredGridCoverage2DReader) reader;
} else {
// TODO: only structuredGridCoverage2DReaders are currently supported.
throw new UnsupportedOperationException("Only structuredGridCoverage2DReaders are currently supported");
}
List<DimensionBean> dimensions = new ArrayList<DimensionBean>();
if (accessor == null) {
return dimensions;
}
List<String> customDimensions = (List<String>) (accessor != null ? accessor.getCustomDomains() : Collections.emptyList());
// Put custom dimensions as first
for (String customDimension: customDimensions) {
dimensions.add(setupDimensionBean(structuredReader, customDimension));
}
// Put known dimensions afterwards similarly to what COARDS convention suggest: 1) Time -> 2) Elevation
DimensionBean timeD = setupDimensionBean(structuredReader, "TIME");
if (timeD != null) {
dimensions.add(timeD);
}
DimensionBean elevationD = setupDimensionBean(structuredReader, "ELEVATION");
if (elevationD != null) {
dimensions.add(elevationD);
}
return dimensions;
}
/**
* Setup a {@link DimensionBean} instance for the specified dimensionID, extracting it from the provided {@link StructuredGridCoverage2DReader}
*
* @param structuredReader the reader used to retrieve dimensionDescriptor and metadata
* @param dimensionID the ID of the dimension to be setup
* @throws IOException
*/
private DimensionBean setupDimensionBean(StructuredGridCoverage2DReader structuredReader, String dimensionID) throws IOException {
Utilities.ensureNonNull("structuredReader", structuredReader);
// Retrieve the proper dimension descriptor
final String coverageName = getCoverageName();
final DimensionDescriptor descriptor = WCSDimensionsHelper.getDimensionDescriptor(structuredReader, coverageName, dimensionID);
if (descriptor == null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Unable to find a valid descriptor for the specified dimension ID: " + dimensionID +
" for the specified coverage " + coverageName + "\n Returning no DimensionBean");
}
return null;
}
final String dimensionName = descriptor.getName();
final DimensionType dimensionType = dimensionID.equalsIgnoreCase("TIME") ? DimensionType.TIME : dimensionID.equalsIgnoreCase("ELEVATION") ? DimensionType.ELEVATION : DimensionType.CUSTOM;
final DimensionInfo info = enabledDimensions.get(dimensionID);
String units = null;
String symbol = null;
if (info != null) {
units = info.getUnits();
symbol = info.getUnitSymbol();
}
// Fallback... set units and symbol from descriptor in case dimensions are not available.
if (units == null) {
units = descriptor.getUnits();
}
if (symbol == null) {
symbol = descriptor.getUnitSymbol();
}
return new DimensionBean(dimensionName, units, symbol, accessor.getDomainDatatype(dimensionName), dimensionType, descriptor.getEndAttribute() != null);
}
/**
* Add an entry in the coverage properties map, containing the value of the specified coverageDimension
* @param properties
* @param coverageRequest a {@link GridCoverageRequest} containing single subsettings for the current coverage
* @param coverageDimension
*/
public void setCoverageDimensionProperty(Map properties, GridCoverageRequest coverageRequest, DimensionBean coverageDimension) {
Utilities.ensureNonNull("properties", properties);
Utilities.ensureNonNull("coverageDimension", coverageDimension);
final DimensionType dimensionType = coverageDimension.getDimensionType();
Object value = null;
switch (dimensionType) {
case TIME:
value = coverageRequest.getTemporalSubset();
break;
case ELEVATION:
value = coverageRequest.getElevationSubset();
break;
case CUSTOM:
Map<String, List<Object>> dimensionsSubset = coverageRequest.getDimensionsSubset();
List<Object> elements = dimensionsSubset == null ? null : dimensionsSubset.get(coverageDimension.getName().toUpperCase());
if (elements == null) {
throw new IllegalArgumentException("No dimension subset has been found");
}
if (elements.size() > 1) {
throw new UnsupportedOperationException("Multiple elements in additional dimensions are not supported on splitted requests");
}
value = elements.get(0);
break;
}
properties.put(coverageDimension.getName(), value);
}
}