/* (c) 2016 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.gwc.wmts.dimensions;
import org.geoserver.catalog.*;
import org.geoserver.gwc.wmts.Tuple;
import org.geoserver.wms.WMS;
import org.geoserver.wms.dimension.DimensionDefaultValueSelectionStrategy;
import org.geoserver.wms.dimension.DimensionFilterBuilder;
import org.geotools.data.FeatureSource;
import org.geotools.data.Query;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.factory.GeoTools;
import org.geotools.feature.FeatureCollection;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.gml2.bindings.GML2EncodingUtils;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import java.io.IOException;
import java.util.*;
/**
* <p>
* This class represents a dimension providing an abstraction over all types of
* dimensions and resources types (like raster and vectors).
* </p>
* <p>
* Restrictions can be applied to a dimension and converted into a filter. This
* makes possible to merge several dimensions restrictions when working with domains.
* </p>
*/
public abstract class Dimension {
private final WMS wms;
private final String dimensionName;
private final LayerInfo layerInfo;
private final DimensionInfo dimensionInfo;
private final ResourceInfo resourceInfo;
private final FilterFactory filterFactory = CommonFactoryFinder.getFilterFactory();
private ReferencedEnvelope boundingBox;
private final List<Object> domainRestrictions = new ArrayList<>();
public Dimension(WMS wms, String dimensionName, LayerInfo layerInfo, DimensionInfo dimensionInfo) {
this.wms = wms;
this.dimensionName = dimensionName;
this.layerInfo = layerInfo;
this.dimensionInfo = dimensionInfo;
resourceInfo = layerInfo.getResource();
}
/**
* Returns this dimension domain values filtered with the provided filter.
* The provided filter can be NULL. Duplicate values may be included if
* noDuplicates parameter is set to FALSE.
*/
public abstract Tuple<ReferencedEnvelope, List<Object>> getDomainValues(Filter filter, boolean noDuplicates);
/**
* Returns a filter that will contain all the restrictions applied to this dimension.
*/
public abstract Filter getFilter();
/**
* <p>
* Computes an histogram of this dimension domain values. The provided resolution value can be NULL
* or AUTO to let the server decide the proper resolution. If a resolution is provided it needs
* to be a number for numerical domains or a period syntax for time domains. For enumerated domains
* (i.e. string values) the resolution will be ignored.
* </p>
* <p>
* A filter can be provided to filter the domain values. The provided filter can be NULL.
* </p>
* <p>
* The first element of the returned tuple will contain the description of the histogram domain as
* start, end and resolution. The second element of the returned tuple will contain a list of the
* histogram values represented as strings. If no description of the domain can be provided (for
* example enumerated values) NULL will be returned and the same allies the histogram values.
* </p>
*/
public Tuple<String, List<Integer>> getHistogram(Filter filter, String resolution) {
return HistogramUtils.buildHistogram(getDomainValues(filter, false).second, resolution);
}
protected abstract String getDefaultValueFallbackAsString();
protected WMS getWms() {
return wms;
}
ResourceInfo getResourceInfo() {
return resourceInfo;
}
public String getDimensionName() {
return dimensionName;
}
protected DimensionInfo getDimensionInfo() {
return dimensionInfo;
}
public void setBoundingBox(ReferencedEnvelope boundingBox) {
this.boundingBox = boundingBox;
}
public void addDomainRestriction(Object domainRestriction) {
if (domainRestriction instanceof Collection) {
domainRestrictions.addAll((Collection) domainRestriction);
} else {
domainRestrictions.add(domainRestriction);
}
}
/**
* Returns this dimension values represented as strings taking in account this
* dimension representation strategy. The returned values will be sorted. The
* provided filter will be used to filter the domain values. The provided filter
* can be NULL.
*/
public Tuple<ReferencedEnvelope, Tuple<Integer, List<String>>> getDomainValuesAsStrings(Filter filter) {
Tuple<ReferencedEnvelope, List<Object>> domainValues = getDomainValues(filter, true);
return Tuple.tuple(domainValues.first,
Tuple.tuple(domainValues.second.size(), DimensionsUtils.getDomainValuesAsStrings(dimensionInfo, domainValues.second)));
}
/**
* Return this dimension default value as a string taking in account this dimension default strategy.
*/
public String getDefaultValueAsString() {
DimensionDefaultValueSelectionStrategy strategy = wms.getDefaultValueStrategy(resourceInfo, dimensionName, dimensionInfo);
String defaultValue = strategy.getCapabilitiesRepresentation(resourceInfo, dimensionName, dimensionInfo);
return defaultValue != null ? defaultValue : getDefaultValueFallbackAsString();
}
/**
* Helper method that can be used by vectors types to create a filter with the current restrictions.
*/
Filter buildVectorFilter() {
FeatureTypeInfo typeInfo = (FeatureTypeInfo) getResourceInfo();
Filter filter = Filter.INCLUDE;
if (boundingBox != null) {
// we have a bounding box so lets build a filter for it
String geometryAttributeName;
try {
// let's find out the geometry attribute
geometryAttributeName = typeInfo.getFeatureSource(null, null).getSchema().getGeometryDescriptor().getLocalName();
} catch (IOException exception) {
throw new RuntimeException(String.format("Exception accessing feature source of vector type '%s'.",
typeInfo.getName()), exception);
}
// creating the bounding box filter and append it to our filter
filter = appendBoundingBoxFilter(filter, geometryAttributeName);
}
if (domainRestrictions != null) {
// we have a domain filter
filter = appendDomainRestrictionsFilter(filter, dimensionInfo.getAttribute(), dimensionInfo.getEndAttribute());
}
return filter;
}
/**
* Helper method that can be used by raster types to create a filter with the current restrictions.
*/
Filter buildRasterFilter() {
CoverageInfo typeInfo = (CoverageInfo) getResourceInfo();
Filter filter = Filter.INCLUDE;
if (boundingBox != null) {
// we have a bounding box so lets build a filter for it
try {
filter = appendBoundingBoxFilter(filter, typeInfo);
} catch (IOException exception) {
throw new RuntimeException(String.format("Exception accessing feature source of raster type '%s'.",
typeInfo.getName()), exception);
}
}
if (domainRestrictions != null) {
CoverageDimensionsReader reader = CoverageDimensionsReader.instantiateFrom(typeInfo);
Tuple<String, String> attributes = reader.getDimensionAttributesNames(getDimensionName());
if (attributes.first == null) {
throw new RuntimeException(String.format(
"Could not found start attribute name for dimension '%s' in raster '%s'.", getDimensionName(), typeInfo.getName()));
}
// ok time to build the domain values filter
filter = appendDomainRestrictionsFilter(filter, attributes.first, attributes.second);
}
return filter;
}
/**
* Helper method that extract the geomtry attribute name from the current type info and invoke
* the method that will actually build the spatial filter.
*/
private Filter appendBoundingBoxFilter(Filter filter, CoverageInfo typeInfo) throws IOException {
// getting the geometry attribute name
CoverageDimensionsReader reader = CoverageDimensionsReader.instantiateFrom(typeInfo);
String geometryAttributeName = reader.getGeometryAttributeName();
// checking if we have a valid geometry attribute
if (geometryAttributeName == null) {
// this raster doesn't supports spatial filtering
return filter;
}
// creating the filter
return appendBoundingBoxFilter(filter, geometryAttributeName);
}
/**
* Helper method that will build a bounding box filter using the provided geometry attribute name and
* the current bounding box restriction. The bounding box filter will be merged with the provided filter.
*/
private Filter appendBoundingBoxFilter(Filter filter, String geometryAttributeName) {
CoordinateReferenceSystem coordinateReferenceSystem = boundingBox.getCoordinateReferenceSystem();
String epsgCode = coordinateReferenceSystem == null ? null : GML2EncodingUtils.toURI(coordinateReferenceSystem);
Filter spatialFilter = filterFactory.bbox(geometryAttributeName, boundingBox.getMinX(), boundingBox.getMinY(),
boundingBox.getMaxX(), boundingBox.getMaxY(), epsgCode);
return filterFactory.and(filter, spatialFilter);
}
/**
* Helper method that will build a dimension domain values filter based on this dimension start and end
* attributes. The created filter will be merged with the provided filter.
*/
private Filter appendDomainRestrictionsFilter(Filter filter, String startAttributeName, String endAttributeName) {
DimensionFilterBuilder dimensionFilterBuilder = new DimensionFilterBuilder(filterFactory);
dimensionFilterBuilder.appendFilters(startAttributeName, endAttributeName, domainRestrictions);
return filterFactory.and(filter, dimensionFilterBuilder.getFilter());
}
/**
* Helper method used to get domain values from a raster type.
*/
Tuple<ReferencedEnvelope, List<Object>> getRasterDomainValues(Filter filter, boolean noDuplicates,
CoverageDimensionsReader.DataType dataType, Comparator<Object> comparator) {
CoverageDimensionsReader reader = CoverageDimensionsReader.instantiateFrom((CoverageInfo) resourceInfo);
if (noDuplicates) {
// no duplicate values should be included
Tuple<ReferencedEnvelope, Set<Object>> values = reader.readWithoutDuplicates(getDimensionName(), filter, dataType, comparator);
List<Object> list = new ArrayList<>(values.second.size());
list.addAll(values.second);
return Tuple.tuple(values.first, list);
}
// we need the duplicate values (this is useful for some operations like get histogram operation)
return reader.readWithDuplicates(getDimensionName(), filter, dataType, comparator);
}
/**
* Helper method used to get domain values from a vector type.
*/
Tuple<ReferencedEnvelope, List<Object>> getVectorDomainValues(Filter filter, boolean noDuplicates, Comparator<Object> comparator) {
FeatureCollection featureCollection = getVectorDomainValues(filter);
if (noDuplicates) {
// no duplicate values should be included
Set<Object> values = DimensionsUtils.
getValuesWithoutDuplicates(dimensionInfo.getAttribute(), featureCollection, comparator);
List<Object> list = new ArrayList<>(values.size());
list.addAll(values);
return Tuple.tuple(featureCollection.getBounds(), list);
}
// we need the duplicate values (this is useful for some operations like get histogram operation)
return Tuple.tuple(featureCollection.getBounds(),
DimensionsUtils.getValuesWithDuplicates(dimensionInfo.getAttribute(), featureCollection, comparator));
}
/**
* Helper method used to get domain values from a vector type in the form of a feature collection.
*/
private FeatureCollection getVectorDomainValues(Filter filter) {
FeatureTypeInfo typeInfo = (FeatureTypeInfo) getResourceInfo();
FeatureSource source;
try {
source = typeInfo.getFeatureSource(null, GeoTools.getDefaultHints());
} catch (Exception exception) {
throw new RuntimeException(String.format(
"Error getting feature source of vector '%s'.", resourceInfo.getName()), exception);
}
Query query = new Query(source.getSchema().getName().getLocalPart(), filter == null ? Filter.INCLUDE : filter);
try {
return source.getFeatures(query);
} catch (Exception exception) {
throw new RuntimeException(String.format(
"Error reading feature from layer '%s' for dimension '%s'.",
resourceInfo.getName(), getDimensionName()), exception);
}
}
/**
* Return dimension start and end attributes, values may be NULL.
*/
public Tuple<String, String> getAttributes() {
ResourceInfo resourceInfo = layerInfo.getResource();
if (resourceInfo instanceof FeatureTypeInfo) {
// for vectors this information easily available
return Tuple.tuple(dimensionInfo.getAttribute(), dimensionInfo.getEndAttribute());
}
if (resourceInfo instanceof CoverageInfo) {
return CoverageDimensionsReader.instantiateFrom((CoverageInfo) resourceInfo).getDimensionAttributesNames(getDimensionName());
}
return Tuple.tuple(null, null);
}
@Override
public String toString() {
return "Dimension{" + ", name='" + dimensionName + '\'' + ", layer=" + layerInfo.getName() + '}';
}
}