/* (c) 2014 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.eo.response;
import java.io.IOException;
import java.io.Serializable;
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.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.opengis.wcs20.DescribeEOCoverageSetType;
import net.opengis.wcs20.DimensionTrimType;
import net.opengis.wcs20.Section;
import org.geoserver.catalog.CoverageInfo;
import org.geoserver.catalog.DimensionInfo;
import org.geoserver.catalog.ResourceInfo;
import org.geoserver.ows.util.ResponseUtils;
import org.geoserver.platform.OWS20Exception.OWSExceptionCode;
import org.geoserver.wcs.WCSInfo;
import org.geoserver.wcs2_0.GetCoverage;
import org.geoserver.wcs2_0.eo.EOCoverageResourceCodec;
import org.geoserver.wcs2_0.eo.WCSEOMetadata;
import org.geoserver.wcs2_0.exception.WCS20Exception;
import org.geoserver.wcs2_0.response.WCS20DescribeCoverageTransformer;
import org.geoserver.wcs2_0.response.WCS20DescribeCoverageTransformer.WCS20DescribeCoverageTranslator;
import org.geoserver.wcs2_0.response.WCSDimensionsHelper;
import org.geoserver.wcs2_0.util.EnvelopeAxesLabelsMapper;
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.DataUtilities;
import org.geotools.data.Query;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.data.store.MaxFeaturesFeatureCollection;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.factory.GeoTools;
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.referencing.CRS.AxisOrder;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.util.DateRange;
import org.geotools.util.NumberRange;
import org.geotools.util.logging.Logging;
import org.geotools.xml.impl.DatatypeConverterImpl;
import org.geotools.xml.transform.TransformerBase;
import org.geotools.xml.transform.Translator;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
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.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.helpers.AttributesImpl;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.Polygon;
/**
* Encodes a DescribeEOCoverageSet response
*
* @author Andrea Aime - GeoSolutions
*/
public class DescribeEOCoverageSetTransformer extends TransformerBase {
static Logger LOGGER = Logging.getLogger(WCS20DescribeEOCoverageSetTranslator.class);
EOCoverageResourceCodec codec;
WCS20DescribeCoverageTransformer dcTransformer;
EnvelopeAxesLabelsMapper envelopeAxisMapper;
WCSInfo wcs;
public DescribeEOCoverageSetTransformer(WCSInfo wcs, EOCoverageResourceCodec codec,
EnvelopeAxesLabelsMapper envelopeAxesMapper,
WCS20DescribeCoverageTransformer dcTransformer) {
this.wcs = wcs;
this.codec = codec;
this.envelopeAxisMapper = envelopeAxesMapper;
this.dcTransformer = dcTransformer;
setIndentation(2);
}
@Override
public Translator createTranslator(ContentHandler handler) {
WCS20DescribeCoverageTranslator dcTranslator = dcTransformer.createTranslator(handler);
return new WCS20DescribeEOCoverageSetTranslator(handler, dcTranslator);
}
public class WCS20DescribeEOCoverageSetTranslator extends TranslatorSupport {
private WCS20DescribeCoverageTranslator dcTranslator;
public WCS20DescribeEOCoverageSetTranslator(ContentHandler handler,
WCS20DescribeCoverageTranslator dcTranslator) {
super(handler, null, null);
this.dcTranslator = dcTranslator;
}
/**
* Encode the object.
*/
@Override
public void encode(Object o) throws IllegalArgumentException {
DescribeEOCoverageSetType dcs = (DescribeEOCoverageSetType) o;
List<CoverageInfo> coverages = getCoverages(dcs);
List<CoverageGranules> coverageGranules = getCoverageGranules(dcs, coverages);
int granuleCount = getGranuleCount(coverageGranules);
Integer maxCoverages = getMaxCoverages(dcs);
int returned;
if(maxCoverages != null) {
returned = granuleCount < maxCoverages ? granuleCount : maxCoverages;
} else {
returned = granuleCount;
}
String eoSchemaLocation = ResponseUtils.buildSchemaURL(dcs.getBaseUrl(),
"wcseo/1.0/wcsEOAll.xsd");
Attributes atts = atts(
"xmlns:eop",
"http://www.opengis.net/eop/2.0", //
"xmlns:gml",
"http://www.opengis.net/gml/3.2", //
"xmlns:wcsgs",
"http://www.geoserver.org/wcsgs/2.0", //
"xmlns:gmlcov", "http://www.opengis.net/gmlcov/1.0", "xmlns:om",
"http://www.opengis.net/om/2.0", "xmlns:swe", "http://www.opengis.net/swe/2.0",
"xmlns:wcs", "http://www.opengis.net/wcs/2.0", "xmlns:wcseo",
"http://www.opengis.net/wcseo/1.0", "xmlns:xlink",
"http://www.w3.org/1999/xlink", "xmlns:xsi",
"http://www.w3.org/2001/XMLSchema-instance", "numberMatched",
String.valueOf(granuleCount), "numberReturned", String.valueOf(returned),
"xsi:schemaLocation", "http://www.opengis.net/wcseo/1.0 " + eoSchemaLocation);
start("wcseo:EOCoverageSetDescription", atts);
if(!coverageGranules.isEmpty()) {
List<CoverageGranules> reducedGranules = coverageGranules;
if(maxCoverages != null) {
reducedGranules = applyMaxCoverages(coverageGranules, maxCoverages);
}
boolean allSections = dcs.getSections() == null
|| dcs.getSections().getSection() == null
|| dcs.getSections().getSection().contains(Section.ALL);
if (allSections
|| dcs.getSections().getSection().contains(Section.COVERAGEDESCRIPTIONS)) {
handleCoverageDescriptions(reducedGranules);
}
if (allSections
|| dcs.getSections().getSection().contains(Section.DATASETSERIESDESCRIPTIONS)) {
handleDatasetSeriesDescriptions(coverageGranules);
}
}
end("wcseo:EOCoverageSetDescription");
}
/**
* Returns the max number of coverages to return, if any (null otherwise)
*
* @param dcs
* @return
*/
private Integer getMaxCoverages(DescribeEOCoverageSetType dcs) {
if (dcs.getCount() > 0) {
return dcs.getCount();
}
// fall back on the the default value, it's ok if it's null
return wcs.getMetadata().get(WCSEOMetadata.COUNT_DEFAULT.key, Integer.class);
}
private List<CoverageGranules> applyMaxCoverages(List<CoverageGranules> coverageGranules,
Integer maxCoverages) {
List<CoverageGranules> result = new ArrayList<DescribeEOCoverageSetTransformer.CoverageGranules>();
for (CoverageGranules cg : coverageGranules) {
int size = cg.granules.size();
if (size > maxCoverages) {
cg.granules = DataUtilities
.simple(new MaxFeaturesFeatureCollection<SimpleFeatureType, SimpleFeature>(
cg.granules, maxCoverages));
}
result.add(cg);
maxCoverages -= size;
if (maxCoverages <= 0) {
break;
}
}
return result;
}
private void handleDatasetSeriesDescriptions(List<CoverageGranules> coverages) {
start("wcseo:DatasetSeriesDescriptions");
for (CoverageGranules gc : coverages) {
CoverageInfo ci = gc.coverage;
String datasetId = codec.getDatasetName(ci);
start("wcseo:DatasetSeriesDescription", atts("gml:id", datasetId));
try {
GridCoverage2DReader reader = (GridCoverage2DReader) ci.getGridCoverageReader(
null, null);
// encode the bbox
encodeDatasetBounds(ci, reader);
// the dataset series id
element("wcseo:DatasetSeriesId", datasetId);
// encode the time
DimensionInfo time = ci.getMetadata().get(ResourceInfo.TIME, DimensionInfo.class);
WCSDimensionsHelper timeHelper = new WCSDimensionsHelper(time, reader, datasetId);
dcTranslator.encodeTimePeriod(timeHelper.getBeginTime(), timeHelper.getEndTime(), datasetId + "_timeperiod", null, null);
end("wcseo:DatasetSeriesDescription");
} catch (IOException e) {
throw new WCS20Exception("Failed to build the description for dataset series "
+ codec.getDatasetName(ci), e);
}
}
end("wcseo:DatasetSeriesDescriptions");
}
private void encodeDatasetBounds(CoverageInfo ci, GridCoverage2DReader reader)
throws IOException {
// get the crs and look for an EPSG code
final CoordinateReferenceSystem crs = ci.getCRS();
GeneralEnvelope envelope = reader.getOriginalEnvelope();
List<String> axesNames = envelopeAxisMapper.getAxesNames(envelope, 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 StringBuilder builder = new StringBuilder();
for (String axisName : axesNames) {
builder.append(axisName).append(" ");
}
String axesLabel = builder.substring(0, builder.length() - 1);
dcTranslator.handleBoundedBy(envelope, axisSwap, srsName, axesLabel, null);
}
private void handleCoverageDescriptions(List<CoverageGranules> coverageGranules) {
start("wcs:CoverageDescriptions");
for (CoverageGranules cg : coverageGranules) {
SimpleFeatureIterator features = cg.granules.features();
try {
while (features.hasNext()) {
SimpleFeature f = features.next();
String granuleId = codec.getGranuleId(cg.coverage, f.getID());
dcTranslator.handleCoverageDescription(granuleId, new GranuleCoverageInfo(
cg.coverage, f, cg.dimensionDescriptors));
}
} finally {
if (features != null) {
features.close();
}
}
}
end("wcs:CoverageDescriptions");
}
private int getGranuleCount(List<CoverageGranules> granules) {
int sum = 0;
for (CoverageGranules cg : granules) {
sum += cg.granules.size();
}
return sum;
}
private List<CoverageInfo> getCoverages(DescribeEOCoverageSetType dcs) {
List<CoverageInfo> results = new ArrayList<CoverageInfo>();
for (String id : dcs.getEoId()) {
CoverageInfo ci = codec.getDatasetCoverage(id);
if (ci == null) {
throw new IllegalArgumentException(
"The dataset id is invalid, should have been checked earlier?");
}
results.add(ci);
}
return results;
}
private List<CoverageGranules> getCoverageGranules(DescribeEOCoverageSetType dcs,
List<CoverageInfo> coverages) {
List<CoverageGranules> results = new ArrayList<CoverageGranules>();
for (CoverageInfo ci : coverages) {
GranuleSource source = null;
try {
StructuredGridCoverage2DReader reader = (StructuredGridCoverage2DReader) ci
.getGridCoverageReader(null, GeoTools.getDefaultHints());
String name = codec.getCoverageName(ci);
source = reader.getGranules(name, true);
Query q = buildQueryFromDimensionTrims(dcs, reader, name);
SimpleFeatureCollection collection = source.getGranules(q);
// only report in output coverages that have at least one matched granule
if(!collection.isEmpty()) {
List<DimensionDescriptor> descriptors = getActiveDimensionDescriptor(ci, reader, name);
CoverageGranules granules = new CoverageGranules(ci, name, reader, collection, descriptors);
results.add(granules);
}
} catch (IOException e) {
throw new WCS20Exception("Failed to load the coverage granules for covearge "
+ ci.prefixedName(), e);
} finally {
try {
if (source != null) {
source.dispose();
}
} catch (IOException e) {
LOGGER.log(Level.FINE, "Failed to dispose granule source", e);
}
}
}
return results;
}
private List<DimensionDescriptor> getActiveDimensionDescriptor(CoverageInfo ci,
StructuredGridCoverage2DReader reader, String name) throws IOException {
// map the source descriptors for easy retrieval
Map<String, DimensionDescriptor> sourceDescriptors = new HashMap<String, DimensionDescriptor>();
for (DimensionDescriptor dimensionDescriptor : reader.getDimensionDescriptors(name)) {
sourceDescriptors.put(dimensionDescriptor.getName().toUpperCase(), dimensionDescriptor);
}
// select only those that have been activated vai the GeoServer GUI
List<DimensionDescriptor> enabledDescriptors = new ArrayList<DimensionDescriptor>();
for(Entry<String, Serializable> entry : ci.getMetadata().entrySet()) {
if(entry.getValue() instanceof DimensionInfo) {
DimensionInfo di = (DimensionInfo) entry.getValue();
if(di.isEnabled()) {
String dimensionName = entry.getKey();
if(dimensionName.startsWith(ResourceInfo.CUSTOM_DIMENSION_PREFIX)) {
dimensionName = dimensionName.substring(ResourceInfo.CUSTOM_DIMENSION_PREFIX.length());
}
DimensionDescriptor selected = sourceDescriptors.get(dimensionName.toUpperCase());
if(selected != null) {
enabledDescriptors.add(selected);
}
}
}
}
return enabledDescriptors;
}
private Query buildQueryFromDimensionTrims(DescribeEOCoverageSetType dcs,
StructuredGridCoverage2DReader reader, String coverageName) throws IOException {
// no selection, get all
if (dcs.getDimensionTrim() == null || dcs.getDimensionTrim().size() == 0) {
return Query.ALL;
}
// find out what the selection is about
DateRange timeRange = null;
NumberRange<Double> lonRange = null;
NumberRange<Double> latRange = null;
for (DimensionTrimType trim : dcs.getDimensionTrim()) {
String name = trim.getDimension();
if ("Long".equals(name)) {
if (lonRange != null) {
throw new WCS20Exception("Long trim specified more than once",
OWSExceptionCode.InvalidParameterValue, "subset");
}
lonRange = parseNumberRange(trim);
} else if ("Lat".equals(name)) {
if (latRange != null) {
throw new WCS20Exception("Lat trim specified more than once",
OWSExceptionCode.InvalidParameterValue, "subset");
}
latRange = parseNumberRange(trim);
} else if ("phenomenonTime".equals(name)) {
if (timeRange != null) {
throw new WCS20Exception("phenomenonTime trim specified more than once",
OWSExceptionCode.InvalidParameterValue, "subset");
}
timeRange = parseDateRange(trim);
} else {
throw new WCS20Exception("Invalid dimension name " + name
+ ", the only valid values "
+ "by WCS EO spec are Long, Lat and phenomenonTime",
OWSExceptionCode.InvalidParameterValue, "subset");
}
}
// check the desired containment
boolean overlaps;
String containment = dcs.getContainmentType();
if (containment == null || "overlaps".equals(containment)) {
overlaps = true;
} else if ("contains".equals(containment)) {
overlaps = false;
} else {
throw new WCS20Exception("Invalid containment value " + containment
+ ", the only valid values by WCS EO spec are contains and overlaps",
OWSExceptionCode.InvalidParameterValue, "containment");
}
// spatial subset
FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2();
Filter filter = null;
if (lonRange != null || latRange != null) {
try {
// we are going to intersect the trims with the original envelope
// since the trims can only be expressed in wgs84 we need to reproject back and forth
ReferencedEnvelope original = new ReferencedEnvelope(reader.getOriginalEnvelope())
.transform(DefaultGeographicCRS.WGS84, true);
if (lonRange != null) {
ReferencedEnvelope lonTrim = new ReferencedEnvelope(lonRange.getMinimum(),
lonRange.getMaximum(), -90, 90, DefaultGeographicCRS.WGS84);
original = new ReferencedEnvelope(original.intersection(lonTrim), DefaultGeographicCRS.WGS84);
}
if (latRange != null) {
ReferencedEnvelope latTrim = new ReferencedEnvelope(-180, 180,
latRange.getMinimum(), latRange.getMaximum(),
DefaultGeographicCRS.WGS84);
original = new ReferencedEnvelope(original.intersection(latTrim), DefaultGeographicCRS.WGS84);
}
if (original.isEmpty()) {
filter = Filter.EXCLUDE;
} else {
Polygon llPolygon = JTS.toGeometry(original);
GeometryDescriptor geom = reader.getGranules(coverageName, true).getSchema().getGeometryDescriptor();
PropertyName geometryProperty = ff.property(geom.getLocalName());
Geometry nativeCRSPolygon = JTS.transform(llPolygon, CRS.findMathTransform(DefaultGeographicCRS.WGS84, reader.getCoordinateReferenceSystem()));
Literal polygonLiteral = ff.literal(nativeCRSPolygon);
if(overlaps) {
filter = ff.intersects(geometryProperty, polygonLiteral);
} else {
filter = ff.within(geometryProperty, polygonLiteral);
}
}
} catch(Exception e) {
throw new WCS20Exception("Failed to translate the spatial trim into a native filter", e);
}
}
// temporal subset
if (timeRange != null && filter != Filter.EXCLUDE) {
DimensionDescriptor timeDescriptor = WCSDimensionsHelper.getDimensionDescriptor(reader, coverageName, "TIME");
String start = timeDescriptor.getStartAttribute();
String end = timeDescriptor.getEndAttribute();
Filter timeFilter;
if(end == null) {
// single value time
timeFilter = ff.between(ff.property(start), ff.literal(timeRange.getMinValue()), ff.literal(timeRange.getMaxValue()));
} else {
// range value, we need to account for containment then
if(overlaps) {
Filter f1 = ff.lessOrEqual(ff.property(start), ff.literal(timeRange.getMaxValue()));
Filter f2 = ff.greaterOrEqual(ff.property(end), ff.literal(timeRange.getMinValue()));
timeFilter = ff.and(Arrays.asList(f1, f2));
} else {
Filter f1 = ff.greaterOrEqual(ff.property(start), ff.literal(timeRange.getMinValue()));
Filter f2 = ff.lessOrEqual(ff.property(end), ff.literal(timeRange.getMaxValue()));
timeFilter = ff.and(Arrays.asList(f1, f2));
}
}
if(filter == null) {
filter = timeFilter;
} else {
filter = ff.and(filter, timeFilter);
}
}
return new Query(null, filter);
}
private DateRange parseDateRange(DimensionTrimType trim) {
DatatypeConverterImpl xmlTimeConverter = DatatypeConverterImpl.getInstance();
try {
final Date low = xmlTimeConverter.parseDateTime(trim.getTrimLow()).getTime();
final Date high = xmlTimeConverter.parseDateTime(trim.getTrimHigh()).getTime();
// low > high???
if (low.compareTo(high) > 0) {
throw new WCS20Exception("Low greater than High in trim for dimension: "
+ trim.getDimension(),
WCS20Exception.WCS20ExceptionCode.InvalidSubsetting, "dimensionTrim");
}
return new DateRange(low, high);
} catch (IllegalArgumentException e) {
throw new WCS20Exception("Invalid date value",
OWSExceptionCode.InvalidParameterValue, "dimensionTrim", e);
}
}
private NumberRange<Double> parseNumberRange(DimensionTrimType trim) {
try {
double low = Double.parseDouble(trim.getTrimLow());
double high = Double.parseDouble(trim.getTrimHigh());
// low > high???
if (low > high) {
throw new WCS20Exception("Low greater than High in trim for dimension: "
+ trim.getDimension(),
WCS20Exception.WCS20ExceptionCode.InvalidSubsetting, "dimensionTrim");
}
return new NumberRange<Double>(Double.class, low, high);
} catch (NumberFormatException e) {
throw new WCS20Exception("Invalid numeric value",
OWSExceptionCode.InvalidParameterValue, "dimensionTrim", e);
}
}
Attributes atts(String... atts) {
AttributesImpl attributes = new AttributesImpl();
for (int i = 0; i < atts.length; i += 2) {
attributes.addAttribute(null, atts[i], atts[i], null, atts[i + 1]);
}
return attributes;
}
}
static class CoverageGranules {
CoverageInfo coverage;
StructuredGridCoverage2DReader reader;
SimpleFeatureCollection granules;
private String name;
private List<DimensionDescriptor> dimensionDescriptors;
public CoverageGranules(CoverageInfo coverage, String name, StructuredGridCoverage2DReader reader,
SimpleFeatureCollection granules, List<DimensionDescriptor> dimensionDescriptors) {
this.coverage = coverage;
this.name = name;
this.reader = reader;
this.granules = granules;
this.dimensionDescriptors = dimensionDescriptors;
}
}
}