package org.geoserver.wcs2_0.response;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.opengis.wcs20.GetCoverageType;
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.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.filter.SortByImpl;
import org.geotools.geometry.jts.JTS;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.CRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.resources.coverage.FeatureUtilities;
import org.geotools.util.DateRange;
import org.geotools.util.NumberRange;
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.filter.sort.SortBy;
import org.opengis.filter.sort.SortOrder;
import org.opengis.geometry.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.Polygon;
/**
* A class which takes care of handling default values for unspecified dimensions (if needed).
*
* @author Daniele Romagnoli, GeoSolutions SAS
*
*/
public class WCSDefaultValuesHelper {
FilterFactory2 FF = CommonFactoryFinder.getFilterFactory2();
private String coverageName;
private GridCoverage2DReader reader;
private GetCoverageType request;
private ReaderDimensionsAccessor accessor;
private final static WCSDimensionsValueParser PARSER = new WCSDimensionsValueParser();
public WCSDefaultValuesHelper(GridCoverage2DReader reader, ReaderDimensionsAccessor accessor, GetCoverageType request, String coverageName) throws IOException {
super();
this.accessor = accessor == null ? new ReaderDimensionsAccessor(reader) : accessor; // Force the creation of an accessor
this.reader = reader;
this.request = request;
this.coverageName = coverageName;
}
/**
* Check the current request and update default values if needed.
* Return the updated {@link GridCoverageRequest}
*
*
* @param subsettingRequest
*
* @throws IOException
*/
public void setDefaults(GridCoverageRequest subsettingRequest) throws IOException {
// Deal with default values
final String format = request.getFormat();
if (format != null && !GetCoverage.formatSupportMDOutput(format)) {
// TODO: Revisit this code and change that Format String.
// Formats supporting multidimensional output format don't neede to setup default values
// Therefore, no need to set default values
// For 2D output format, we can setup default values to reduce the number of results
if (! (reader instanceof StructuredGridCoverage2DReader)) {
// use standard code which gets default value from each domain which hasn't be subset
setStandardReaderDefaults(subsettingRequest);
} else {
// Use optimized code for structured grid coverage reader which uses granuleSource queries
// to determine valid default values
setDefaultsFromStructuredReader(subsettingRequest);
}
}
}
/**
* Set default values by querying a {@link GranuleSource} from {@link StructuredGridCoverage2DReader} in
* order to update unspecified dimensions values from attributes values obtained from the query.
*
* @param subsettingRequest
*
* @throws IOException
*/
private GridCoverageRequest setDefaultsFromStructuredReader(GridCoverageRequest subsettingRequest) throws IOException {
// Get subsetting request
DateRange temporalSubset = subsettingRequest.getTemporalSubset();
NumberRange<?> elevationSubset = subsettingRequest.getElevationSubset();
Map<String, List<Object>> dimensionsSubset = subsettingRequest.getDimensionsSubset();
Envelope envelopeSubset = subsettingRequest.getSpatialSubset();
Filter originalFilter = subsettingRequest.getFilter();
final int specifiedDimensionsSubset = dimensionsSubset != null ? dimensionsSubset.size() : 0;
// Casting to StructuredGridCoverage2DReader
final StructuredGridCoverage2DReader structuredReader = (StructuredGridCoverage2DReader) reader;
// Getting dimension descriptors
final List<DimensionDescriptor> dimensionDescriptors = structuredReader.getDimensionDescriptors(coverageName);
DimensionDescriptor timeDimension = null;
DimensionDescriptor elevationDimension = null;
final List<DimensionDescriptor> customDimensions = new ArrayList<DimensionDescriptor>();
int dimensions = 0;
// Collect dimension Descriptor info
for (DimensionDescriptor dimensionDescriptor: dimensionDescriptors) {
if (dimensionDescriptor.getName().equalsIgnoreCase(ResourceInfo.TIME)) {
timeDimension = dimensionDescriptor;
} else if (dimensionDescriptor.getName().equalsIgnoreCase(ResourceInfo.ELEVATION)) {
elevationDimension = dimensionDescriptor;
} else {
customDimensions.add(dimensionDescriptor);
dimensions++;
}
}
final boolean defaultTimeNeeded = temporalSubset == null && timeDimension != null;
final boolean defaultElevationNeeded = elevationSubset == null && elevationDimension != null;
final boolean defaultCustomDimensionsNeeded = dimensions != specifiedDimensionsSubset;
// Note that only Slicing is currently supported;
if (defaultTimeNeeded || defaultElevationNeeded || defaultCustomDimensionsNeeded) {
// Get granules source
GranuleSource source = structuredReader.getGranules(coverageName, true);
// Set filtering query matching the specified subsets.
Filter finalFilter = setFilters(originalFilter, temporalSubset, elevationSubset, envelopeSubset, dimensionsSubset, structuredReader, timeDimension, elevationDimension, customDimensions);
Query query = new Query();
// Set sorting order (default Policy is using Max... therefore Descending order)
sortBy(query, timeDimension, elevationDimension);
query.setFilter(finalFilter);
// Returning a single feature matching the filtering
query.setMaxFeatures(1);
// Get granules from query
SimpleFeatureCollection granulesCollection = source.getGranules(query);
SimpleFeatureIterator features = granulesCollection.features();
try {
if (features.hasNext()) {
final SimpleFeature feature = features.next();
// Default time
if (defaultTimeNeeded && timeDimension != null) {
temporalSubset = setDefaultTemporalSubset(timeDimension, feature);
subsettingRequest.setTemporalSubset(temporalSubset);
}
// Default elevation
if (defaultElevationNeeded && elevationDimension != null) {
elevationSubset = setDefaultElevationSubset(elevationDimension, feature);
subsettingRequest.setElevationSubset(elevationSubset);
}
// Default custom dimensions
if (defaultCustomDimensionsNeeded && !customDimensions.isEmpty()) {
dimensionsSubset = setDefaultDimensionsSubset(customDimensions, feature);
subsettingRequest.setDimensionsSubset(dimensionsSubset);
}
}
} finally {
if (features != null) {
features.close();
}
}
}
return subsettingRequest;
}
/**
* Set default for custom dimensions, taking values from the feature resulting from the query.
* @param customDimensions
* @param feature
*
*/
private Map<String, List<Object>> setDefaultDimensionsSubset(
List<DimensionDescriptor> customDimensions, SimpleFeature feature) {
Map<String, List<Object>> dimensionsSubset = new HashMap<String, List<Object>>();
for (DimensionDescriptor dimensionDescriptor: customDimensions) {
// TODO: Add support for ranged additional dimensions
final String start = dimensionDescriptor.getStartAttribute();
Object value = feature.getAttribute(start);
//Replace specified values since they have been anyway set in the filters
List<Object> dimensionValues = new ArrayList<Object>();
dimensionValues.add(value);
dimensionsSubset.put(dimensionDescriptor.getName().toUpperCase(), dimensionValues);
}
return dimensionsSubset;
}
/**
* Set default elevation value from the provided feature
* @param elevationDimension
* @param f
*
*/
private NumberRange<?> setDefaultElevationSubset(DimensionDescriptor elevationDimension, SimpleFeature f) {
final String start = elevationDimension.getStartAttribute();
final String end = elevationDimension.getEndAttribute();
Number startTime = (Number) f.getAttribute(start);
Number endTime = startTime;
if (end != null) {
endTime = (Number) f.getAttribute(end);
}
return new NumberRange(startTime.getClass(), startTime, endTime);
}
/**
* Set default time value from the provided feature
* @param timeDimension
* @param f
*
*/
private DateRange setDefaultTemporalSubset(DimensionDescriptor timeDimension, SimpleFeature f) {
final String start = timeDimension.getStartAttribute();
final String end = timeDimension.getEndAttribute();
Date startTime = (Date) f.getAttribute(start);
Date endTime = startTime;
if (end != null) {
endTime = (Date) f.getAttribute(end);
}
return new DateRange(startTime, endTime);
}
/**
* Current policy is to use the max value as default for time and min value as default for elevation.
*
* @param query the originating query
* @param timeDimension
* @param elevationDimension
* TODO: Consider also sorting on custom dimensions
*/
private void sortBy(Query query, DimensionDescriptor timeDimension, DimensionDescriptor elevationDimension) {
final List<SortBy> clauses = new ArrayList<SortBy>();
// TODO: Check sortBy clause is supported
if (timeDimension != null) {
clauses.add(new SortByImpl(FeatureUtilities.DEFAULT_FILTER_FACTORY.property(timeDimension.getStartAttribute()),
SortOrder.DESCENDING));
}
if (elevationDimension != null) {
clauses.add(new SortByImpl(FeatureUtilities.DEFAULT_FILTER_FACTORY.property(elevationDimension.getStartAttribute()),
SortOrder.ASCENDING));
}
final SortBy[] sb = clauses.toArray(new SortBy[] {});
query.setSortBy(sb);
}
/**
* Setup filter query on top of specified subsets values to return only granules satisfying the specified conditions.
* @param originalFilter
* @param temporalSubset
* @param elevationSubset
* @param envelopeSubset
* @param dimensionSubset
* @param reader
* @param timeDimension
* @param elevationDimension
* @param additionalDimensions
*
* @throws IOException
*/
private Filter setFilters(Filter originalFilter, DateRange temporalSubset,
NumberRange<?> elevationSubset, Envelope envelopeSubset,
Map<String, List<Object>> dimensionSubset, StructuredGridCoverage2DReader reader, DimensionDescriptor timeDimension,
DimensionDescriptor elevationDimension, List<DimensionDescriptor> additionalDimensions)
throws IOException {
List<Filter> filters = new ArrayList<Filter>();
// Setting temporal filter
Filter timeFilter = temporalSubset == null && timeDimension == null ? null
: setTimeFilter(temporalSubset, timeDimension.getStartAttribute(),
timeDimension.getEndAttribute());
// Setting elevation filter
Filter elevationFilter = elevationSubset == null && elevationDimension == null ? null
: setElevationFilter(elevationSubset,
elevationDimension.getStartAttribute(),
elevationDimension.getEndAttribute());
// setting envelope filter
Filter envelopeFilter = setEnevelopeFilter(envelopeSubset, reader);
// Setting dimensional filters
Filter additionalDimensionsFilter = setAdditionalDimensionsFilter(dimensionSubset, additionalDimensions);
// Updating filters
if(originalFilter != null) {
filters.add(originalFilter);
}
if (elevationFilter != null) {
filters.add(elevationFilter);
}
if (timeFilter != null) {
filters.add(timeFilter);
}
if (envelopeFilter != null) {
filters.add(envelopeFilter);
}
if (additionalDimensionsFilter != null) {
filters.add(additionalDimensionsFilter);
}
// Merging all filters
Filter finalFilter = FF.and(filters);
return finalFilter;
}
/**
* Set envelope filter to restrict the results to the specified envelope
* @param envelopeSubset
* @param reader
*
* @throws IOException
*/
private Filter setEnevelopeFilter(Envelope envelopeSubset,
StructuredGridCoverage2DReader reader) throws IOException {
Filter envelopeFilter = null;
if (envelopeSubset != null) {
Polygon polygon = JTS.toGeometry(new ReferencedEnvelope(envelopeSubset));
GeometryDescriptor geom = reader.getGranules(coverageName, true).getSchema().getGeometryDescriptor();
PropertyName geometryProperty = FF.property(geom.getLocalName());
Geometry nativeCRSPolygon;
try {
nativeCRSPolygon = JTS.transform(polygon, CRS.findMathTransform(DefaultGeographicCRS.WGS84,
reader.getCoordinateReferenceSystem()));
Literal polygonLiteral = FF.literal(nativeCRSPolygon);
// TODO: Check that geom operation. Should I do intersection or containment check?
envelopeFilter = FF.intersects(geometryProperty, polygonLiteral);
// envelopeFilter = FF.within(geometryProperty, polygonLiteral);
} catch (Exception e) {
throw new IOException(e);
}
}
return envelopeFilter;
}
/**
* Set filter to match specified additional dimensions values
* @param dimensionSubset
* @param additionalDimensions
*
*/
private Filter setAdditionalDimensionsFilter(Map<String, List<Object>> dimensionSubset, List<DimensionDescriptor> additionalDimensions) {
Filter additionalDimensionsFilter = null;
// Check whether the number of specified additional dimensions values doesn't match the number of available additional dimensions
if (additionalDimensions != null && dimensionSubset != null && additionalDimensions.size() != dimensionSubset.size() && dimensionSubset.size() > 0) {
List<Filter> additionalDimensionFilterList = new ArrayList<Filter>();
Set<String> dimensionKeys = dimensionSubset.keySet();
for (String dimension : dimensionKeys) {
// Look for the specified dimension
Filter dimensionFilter = createCustomDimensionFilter(dimension, dimensionSubset, additionalDimensions);
if (dimensionFilter != null) {
additionalDimensionFilterList.add(dimensionFilter);
}
}
if (!additionalDimensionFilterList.isEmpty()) {
additionalDimensionsFilter = FF.and(additionalDimensionFilterList);
}
}
return additionalDimensionsFilter;
}
/**
* Create a filter matching the specified additional dimension value
* @param dimension
* @param dimensionSubset
* @param customDimensions
*
*/
private Filter createCustomDimensionFilter(String dimension,
Map<String, List<Object>> dimensionSubset, List<DimensionDescriptor> customDimensions) {
List<Object> dimensionSelection = dimensionSubset.get(dimension);
// Only supporting slicing right now. Dealing with a single dimension value
Object dimensionValue = dimensionSelection.get(0);
for (DimensionDescriptor dimensionDescriptor : customDimensions) {
if (dimensionDescriptor.getName().equalsIgnoreCase(dimension)) {
String attribute = dimensionDescriptor.getStartAttribute();
return FF.equals(FF.property(attribute), FF.literal(dimensionValue));
}
}
return null;
}
/**
* Set a {@link Filter} based on the specified time subset, or null if missing.
* @param timeRange
* @param start
* @param end
*
*/
private Filter setTimeFilter(DateRange timeRange, String start, String end) {
if (timeRange != null) {
if(end == null) {
// single value time
return betweenFilter(start, timeRange.getMinValue(), timeRange.getMaxValue());
} else {
return rangeFilter(start, end, timeRange.getMinValue(), timeRange.getMaxValue());
}
}
return null;
}
/**
* Set a {@link Filter} based on the specified elevation subset, or null if missing.
* @param elevationSubset
* @param start
* @param end
*
*/
private Filter setElevationFilter(NumberRange elevationSubset, String start, String end) {
if (elevationSubset != null) {
if(end == null) {
// single value elevation
return betweenFilter(start, elevationSubset.getMinValue(), elevationSubset.getMaxValue());
} else {
return rangeFilter(start, end, elevationSubset.getMinValue(), elevationSubset.getMaxValue());
}
}
return null;
}
/**
* A simple filter making sure a property is contained between minValue and maxValue
* @param start
* @param minValue
* @param maxValue
*
*/
private Filter betweenFilter(String start, Object minValue, Object maxValue) {
return FF.between(FF.property(start), FF.literal(minValue), FF.literal(maxValue));
}
/**
* A simple filter for range containment
* @param start
* @param end
* @param minValue
* @param maxValue
*
*/
private Filter rangeFilter(String start, String end, Object minValue, Object maxValue) {
Filter f1 = FF.lessOrEqual(FF.property(start), FF.literal(maxValue));
Filter f2 = FF.greaterOrEqual(FF.property(end), FF.literal(minValue));
return FF.and(Arrays.asList(f1, f2));
// Filter f1 = FF.greaterOrEqual(FF.property(start), FF.literal(minValue));
// Filter f2 = FF.lessOrEqual(FF.property(end), FF.literal(maxValue));
// return FF.and(Arrays.asList(f1, f2));
}
/**
* Set default values for the standard reader case (no DimensionsDescriptor available)
*
* @param subsettingRequest
*
* @throws IOException
*/
private GridCoverageRequest setStandardReaderDefaults(GridCoverageRequest subsettingRequest) throws IOException {
DateRange temporalSubset = subsettingRequest.getTemporalSubset();
NumberRange<?> elevationSubset = subsettingRequest.getElevationSubset();
Map<String, List<Object>> dimensionSubset = subsettingRequest.getDimensionsSubset();
// Reader is not a StructuredGridCoverage2DReader instance. Set default ones with policy "time = max, elevation = min".
// Setting default time
if (temporalSubset == null) {
// use "max" as the default
Date maxTime = accessor.getMaxTime();
if (maxTime != null) {
temporalSubset = new DateRange(maxTime, maxTime);
}
}
// Setting default elevation
if (elevationSubset == null) {
// use "min" as the default
Number minElevation = accessor.getMinElevation();
if (minElevation != null) {
elevationSubset = new NumberRange(minElevation.getClass(), minElevation, minElevation);
}
}
// Setting default custom dimensions
final List<String> customDomains = accessor.getCustomDomains();
int availableCustomDimensions = 0;
int specifiedCustomDimensions = 0;
if (customDomains != null && !customDomains.isEmpty()) {
availableCustomDimensions = customDomains.size();
specifiedCustomDimensions = dimensionSubset != null ? dimensionSubset.size() : 0;
if (dimensionSubset == null) {
dimensionSubset = new HashMap<String, List<Object>>();
}
}
if (availableCustomDimensions != specifiedCustomDimensions) {
setDefaultCustomDimensions(customDomains, dimensionSubset);
}
subsettingRequest.setDimensionsSubset(dimensionSubset);
subsettingRequest.setTemporalSubset(temporalSubset);
subsettingRequest.setElevationSubset(elevationSubset);
return subsettingRequest;
}
/**
* Set default custom dimensions
* @param customDomains
* @param dimensionSubset
* @throws IOException
*/
private void setDefaultCustomDimensions(List<String> customDomains,
Map<String, List<Object>> dimensionSubset) throws IOException {
// Scan available custom dimensions
for (String customDomain : customDomains) {
if (!dimensionSubset.containsKey(customDomain)) {
List<Object> dimensionValue = new ArrayList<Object>();
// set default of the proper datatype (in case of known Domain datatype)
String defaultValue = accessor.getCustomDomainDefaultValue(customDomain);
String dataType = reader.getMetadataValue(customDomain + "_DOMAIN_DATATYPE");
if (dataType != null) {
PARSER.setValues(defaultValue, dimensionValue, dataType);
} else {
dimensionValue.add(defaultValue);
}
dimensionSubset.put(customDomain, dimensionValue);
}
}
}
}