/* (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.CoverageInfo;
import org.geoserver.gwc.wmts.Tuple;
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.memory.MemoryFeatureCollection;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.util.DateRange;
import org.geotools.util.NumberRange;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.filter.Filter;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.function.Function;
/**
* This class allow us to abstract from the type of different raster readers (structured and non structured ones).
*/
abstract class CoverageDimensionsReader {
public enum DataType {
TEMPORAL, NUMERIC, CUSTOM
}
abstract Tuple<String, String> getDimensionAttributesNames(String dimensionName);
abstract String getGeometryAttributeName();
abstract Tuple<String, FeatureCollection> getValues(String dimensionName, Filter filter, DataType dataType);
Tuple<ReferencedEnvelope, List<Object>> readWithDuplicates(String dimensionName, Filter filter, DataType dataType, Comparator<Object> comparator) {
// getting the feature collection with the values and the attribute name
Tuple<String, FeatureCollection> values = getValues(dimensionName, filter, dataType);
if (values == null) {
return Tuple.tuple(new ReferencedEnvelope(), Collections.emptyList());
}
// extracting the values removing the duplicates
return Tuple.tuple(values.second.getBounds(),
DimensionsUtils.getValuesWithDuplicates(values.first, values.second, comparator));
}
Tuple<ReferencedEnvelope, Set<Object>> readWithoutDuplicates(String dimensionName, Filter filter, DataType dataType, Comparator<Object> comparator) {
// getting the feature collection with the values and the attribute name
Tuple<String, FeatureCollection> values = getValues(dimensionName, filter, dataType);
if (values == null) {
return Tuple.tuple(new ReferencedEnvelope(), new TreeSet<>());
}
// extracting the values keeping the duplicates
return Tuple.tuple(values.second.getBounds(),
DimensionsUtils.getValuesWithoutDuplicates(values.first, values.second, comparator));
}
/**
* Instantiate a coverage reader from the provided read. If the reader is a structured one good we can use some
* optimizations otherwise we will have to really on the layer metadata.
*/
static CoverageDimensionsReader instantiateFrom(CoverageInfo typeInfo) {
// let's get this coverage reader
GridCoverage2DReader reader;
try {
reader = (GridCoverage2DReader) typeInfo.getGridCoverageReader(null, null);
} catch (Exception exception) {
throw new RuntimeException("Error getting coverage reader.", exception);
}
if (reader instanceof StructuredGridCoverage2DReader) {
// good we have a structured coverage reader
return new WrapStructuredGridCoverageDimensions2DReader(typeInfo, (StructuredGridCoverage2DReader) reader);
}
// non structured reader let's do our best
return new WrapNonStructuredReader(typeInfo, reader);
}
private static final class WrapStructuredGridCoverageDimensions2DReader extends CoverageDimensionsReader {
private final CoverageInfo typeInfo;
private final StructuredGridCoverage2DReader reader;
private WrapStructuredGridCoverageDimensions2DReader(CoverageInfo typeInfo, StructuredGridCoverage2DReader reader) {
this.typeInfo = typeInfo;
this.reader = reader;
}
@Override
public Tuple<String, String> getDimensionAttributesNames(String dimensionName) {
try {
// raster dimensions don't provide start and end attributes so we need the ask the dimension descriptors
List<DimensionDescriptor> descriptors = reader.getDimensionDescriptors(reader.getGridCoverageNames()[0]);
// we have this raster dimension descriptors let's find the descriptor for our dimension
String startAttributeName = null;
String endAttributeName = null;
// let's find the descriptor for our dimension
for (DimensionDescriptor descriptor : descriptors) {
if (dimensionName.equalsIgnoreCase(descriptor.getName())) {
// descriptor found
startAttributeName = descriptor.getStartAttribute();
endAttributeName = descriptor.getEndAttribute();
}
}
return Tuple.tuple(startAttributeName, endAttributeName);
} catch (IOException exception) {
throw new RuntimeException("Error extracting dimensions descriptors from raster.", exception);
}
}
@Override
public String getGeometryAttributeName() {
try {
// getting the source of our coverage
GranuleSource source = reader.getGranules(reader.getGridCoverageNames()[0], true);
// well returning the geometry attribute
return source.getSchema().getGeometryDescriptor().getLocalName();
} catch (Exception exception) {
throw new RuntimeException("Error getting coverage geometry attribute.");
}
}
/**
* Helper method that can be used to read the domain values of a dimension from a raster.
* The provided filter will be used to filter the domain values that should be returned,
* if the provided filter is NULL nothing will be filtered.
*/
@Override
public Tuple<String, FeatureCollection> getValues(String dimensionName, Filter filter, DataType dataType) {
try {
// opening the source and descriptors for our raster
GranuleSource source = reader.getGranules(reader.getGridCoverageNames()[0], true);
List<DimensionDescriptor> descriptors = reader.getDimensionDescriptors(reader.getGridCoverageNames()[0]);
// let's find our dimension and query the data
for (DimensionDescriptor descriptor : descriptors) {
if (dimensionName.equalsIgnoreCase(descriptor.getName())) {
// we found our dimension descriptor, creating a query
Query query = new Query(source.getSchema().getName().getLocalPart());
if (filter != null) {
query.setFilter(filter);
}
// reading the features using the build query
FeatureCollection featureCollection = source.getGranules(query);
// get the features attribute that contain our dimension values
String attributeName = descriptor.getStartAttribute();
return Tuple.tuple(attributeName, featureCollection);
}
}
// well our dimension was not found
return null;
} catch (Exception exception) {
throw new RuntimeException("Error reading domain values.", exception);
}
}
}
private static final class WrapNonStructuredReader extends CoverageDimensionsReader {
private final CoverageInfo typeInfo;
private final GridCoverage2DReader reader;
private WrapNonStructuredReader(CoverageInfo typeInfo, GridCoverage2DReader reader) {
this.typeInfo = typeInfo;
this.reader = reader;
}
private static final ThreadLocal<DateFormat> DATE_FORMATTER = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
dateFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
return dateFormatter;
}
};
private static Date formatDate(String rawValue) {
try {
return DATE_FORMATTER.get().parse(rawValue);
} catch (Exception exception) {
throw new RuntimeException(String.format("Error parsing date '%s'.", rawValue), exception);
}
}
private static final Function<String, Object> TEMPORAL_CONVERTER = (rawValue) -> {
if (rawValue.contains("/")) {
String[] parts = rawValue.split("/");
return new DateRange(formatDate(parts[0]), formatDate(parts[1]));
} else {
return formatDate(rawValue);
}
};
private static final Function<String, Object> NUMERICAL_CONVERTER = (rawValue) -> {
if (rawValue.contains("/")) {
String[] parts = rawValue.split("/");
return new NumberRange<>(Double.class, Double.parseDouble(parts[0]), Double.parseDouble(parts[1]));
} else {
return Double.parseDouble(rawValue);
}
};
private static final Function<String, Object> STRING_CONVERTER = (rawValue) -> rawValue;
@Override
public Tuple<String, String> getDimensionAttributesNames(String dimensionName) {
// by convention the metadata entry that contains a dimension information follows the pattern
// [DIMENSION_NAME]_DOMAIN, i.e. TIME_DOMAIN, ELEVATION_DOMAIN or HAS_CUSTOM_DOMAIN
String attributeName = dimensionName.toUpperCase() + "_DOMAIN";
// we only have one value no start and end values
return Tuple.tuple(attributeName, null);
}
@Override
public String getGeometryAttributeName() {
// spatial filtering is not supported for non structured readers
return null;
}
@Override
public Tuple<String, FeatureCollection> getValues(String dimensionName, Filter filter, DataType dataType) {
String metaDataValue;
try {
metaDataValue = reader.getMetadataValue(dimensionName.toUpperCase() + "_DOMAIN");
} catch (Exception exception) {
throw new RuntimeException(String.format(
"Error extract dimension '%s' values from raster '%s'.",
dimensionName, typeInfo.getName()), exception);
}
if (metaDataValue == null || metaDataValue.isEmpty()) {
return Tuple.tuple(getDimensionAttributesNames(dimensionName).first, null);
}
String[] rawValues = metaDataValue.split(",");
dataType = normalizeDataType(rawValues[0], dataType);
Tuple<SimpleFeatureType, Function<String, Object>> featureTypeAndConverter =
getFeatureTypeAndConverter(dimensionName, rawValues[0], dataType);
MemoryFeatureCollection featureCollection = new MemoryFeatureCollection(featureTypeAndConverter.first);
for (int i = 0; i < rawValues.length; i++) {
SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(featureTypeAndConverter.first);
featureBuilder.add(featureTypeAndConverter.second.apply(rawValues[i]));
SimpleFeature feature = featureBuilder.buildFeature(String.valueOf(i));
if (filter == null || filter.evaluate(feature)) {
featureCollection.add(feature);
}
}
return Tuple.tuple(getDimensionAttributesNames(dimensionName).first, featureCollection);
}
private DataType normalizeDataType(String rawValue, DataType dataType) {
if (dataType.equals(DataType.CUSTOM)) {
try {
TEMPORAL_CONVERTER.apply(rawValue);
return DataType.TEMPORAL;
} catch (Exception exception) {
// not a temporal value
}
try {
NUMERICAL_CONVERTER.apply(rawValue);
return DataType.NUMERIC;
} catch (Exception exception) {
// not a numerical value
}
}
return dataType;
}
private Tuple<SimpleFeatureType, Function<String, Object>> getFeatureTypeAndConverter(String dimensionName, String rawValue, DataType dataType) {
SimpleFeatureTypeBuilder featureTypeBuilder = new SimpleFeatureTypeBuilder();
featureTypeBuilder.setName(typeInfo.getName());
switch (dataType) {
case TEMPORAL:
featureTypeBuilder.add(getDimensionAttributesNames(dimensionName).first, TEMPORAL_CONVERTER.apply(rawValue).getClass());
return Tuple.tuple(featureTypeBuilder.buildFeatureType(), TEMPORAL_CONVERTER);
case NUMERIC:
featureTypeBuilder.add(getDimensionAttributesNames(dimensionName).first, NUMERICAL_CONVERTER.apply(rawValue).getClass());
return Tuple.tuple(featureTypeBuilder.buildFeatureType(), NUMERICAL_CONVERTER);
}
featureTypeBuilder.add(getDimensionAttributesNames(dimensionName).first, String.class);
return Tuple.tuple(featureTypeBuilder.buildFeatureType(), STRING_CONVERTER);
}
}
}