/* (c) 2013 - 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.wms.dynamic.legendgraphic;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.CoverageDimensionInfo;
import org.geoserver.catalog.CoverageInfo;
import org.geoserver.catalog.CoverageStoreInfo;
import org.geoserver.catalog.DimensionInfo;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.MetadataMap;
import org.geoserver.catalog.ResourceInfo;
import org.geoserver.catalog.util.ReaderDimensionsAccessor;
import org.geoserver.data.util.CoverageUtils;
import org.geoserver.ows.AbstractDispatcherCallback;
import org.geoserver.ows.Dispatcher;
import org.geoserver.ows.DispatcherCallback;
import org.geoserver.ows.Request;
import org.geoserver.ows.kvp.TimeKvpParser;
import org.geoserver.platform.Operation;
import org.geoserver.platform.ServiceException;
import org.geoserver.wms.GetLegendGraphicRequest;
import org.geoserver.wms.GetLegendGraphicRequest.LegendRequest;
import org.geoserver.wms.RasterCleaner;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridEnvelope2D;
import org.geotools.coverage.grid.GridGeometry2D;
import org.geotools.coverage.grid.io.AbstractGridFormat;
import org.geotools.coverage.grid.io.GridCoverage2DReader;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.process.function.ProcessFunction;
import org.geotools.process.raster.DynamicColorMapProcess;
import org.geotools.process.raster.FilterFunction_svgColorMap;
import org.geotools.referencing.operation.matrix.XAffineTransform;
import org.geotools.styling.ColorMap;
import org.geotools.styling.FeatureTypeStyle;
import org.geotools.styling.RasterSymbolizer;
import org.geotools.styling.Style;
import org.geotools.styling.StyleBuilder;
import org.opengis.coverage.grid.GridGeometry;
import org.opengis.filter.expression.Expression;
import org.opengis.parameter.GeneralParameterDescriptor;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.operation.MathTransform;
/**
* A {@link DispatcherCallback} which intercepts a getLegendGraphicRequest and check whether that request involve a dynamicColorRamp rendering
* transformation. In that case, it setup a legend based on the dynamic values coming from the request. The callback works under the assumption that
* there is only one style and one layer involved, it won't work for a multilayer/multistyle request (that could be arranged, but we'd need to open an
* extension point in the legend graphics builder to treat rendering transformations instead).
*
* @author Daniele Romagnoli, GeoSolutions SAS
*
*/
public class DynamicGetLegendGraphicDispatcherCallback extends AbstractDispatcherCallback {
private static final Logger LOGGER = org.geotools.util.logging.Logging
.getLogger(DynamicGetLegendGraphicDispatcherCallback.class.getPackage().getName());
private Catalog catalog;
TimeKvpParser parser = new TimeKvpParser("time");
public DynamicGetLegendGraphicDispatcherCallback(Catalog catalog) {
this.catalog = catalog;
}
@Override
public Operation operationDispatched(Request request, Operation operation) {
final String id = operation.getId();
// only intercepting getLegendGraphic invokation
if (id.equalsIgnoreCase("getLegendGraphic")) {
final Object[] params = operation.getParameters();
if (params != null && params.length > 0
&& params[0] instanceof GetLegendGraphicRequest) {
final GetLegendGraphicRequest getLegendRequest = (GetLegendGraphicRequest) params[0];
try {
for (LegendRequest legend : getLegendRequest.getLegends()) {
ProcessFunction transformation = getDynamicColorMapTransformation(legend);
if (transformation != null) {
LayerInfo layer = legend.getLayerInfo();
if(layer != null && layer.getResource() instanceof CoverageInfo) {
CoverageInfo coverageInfo = (CoverageInfo) layer.getResource();
List<CoverageDimensionInfo> dimensions = coverageInfo.getDimensions();
String unit = "";
if (dimensions != null && !dimensions.isEmpty()) {
CoverageDimensionInfo dimensionInfo = dimensions.get(0);
unit = dimensionInfo.getUnit();
if (unit == null) {
unit = "";
}
}
Style style = getDynamicStyle(coverageInfo, transformation);
if (style != null) {
legend.setStyle(style);
}
}
}
}
} catch (Exception e) {
throw new ServiceException("Failed to extract legend", e);
}
}
}
return operation;
}
/**
* Look for a ColorRamp string definition used by a {@link FilterFunction_svgColorMap} if any.
*
* @param styles
*
*/
private ProcessFunction getDynamicColorMapTransformation(LegendRequest legendRequest) {
if (legendRequest.getStyle() != null) {
final Style style = legendRequest.getStyle();
final FeatureTypeStyle[] featureTypeStyles = style.featureTypeStyles()
.toArray(new FeatureTypeStyle[0]);
for (FeatureTypeStyle featureTypeStyle : featureTypeStyles) {
// Getting the main transformation
Expression transformation = featureTypeStyle.getTransformation();
if (transformation instanceof ProcessFunction) {
final ProcessFunction processFunction = (ProcessFunction) transformation;
final String processName = processFunction.getName();
// Checking whether the processFunction is a DynamicColorMapProcess
if (processName.equals(DynamicColorMapProcess.NAME)
|| processName.equals("ras:" + DynamicColorMapProcess.NAME)) {
return processFunction;
}
}
}
}
return null;
}
/**
* Look for a ColorRamp definition used by a {@link DynamicColorMapProcess} rendering transformation.
*
* @param processFunction
* @throws IOException
* @throws ParseException
*
*/
private Style getDynamicStyle(CoverageInfo coverageInfo, ProcessFunction transformation)
throws IOException, ParseException {
GridCoverage2D coverage = null;
try {
// Getting coverage to parse statistics
final CoverageStoreInfo storeInfo = coverageInfo.getStore();
final GridCoverage2DReader reader = (GridCoverage2DReader) catalog.getResourcePool()
.getGridCoverageReader(storeInfo, null);
GeneralParameterValue[] parameters = parseReadParameters(coverageInfo, reader);
coverage = (GridCoverage2D) reader.read(parameters);
ColorMap cm = null;
double opacity = 1.0;
for (Expression param : transformation.getParameters()) {
// these functions evaluate to a singleton map
Map map = param.evaluate(coverage, Map.class);
Object paramValue = map.values().iterator().next();
Object key = map.keySet().iterator().next();
if (paramValue instanceof ColorMap) {
cm = (ColorMap) paramValue;
} else {
if ("opacity".equals(key) && paramValue != null) {
opacity = ((Number) paramValue).doubleValue();
}
}
}
if (cm != null) {
StyleBuilder sb = new StyleBuilder();
RasterSymbolizer rs = sb.createRasterSymbolizer(cm, opacity);
return sb.createStyle(rs);
}
} finally {
if (coverage != null) {
RasterCleaner.addCoverage(coverage);
}
}
return null;
}
/**
* Parse the read parameter from the getLegendGraphicRequest in order to access the proper coverage slice to retrieve the proper statistics.
*
* @param coverageInfo the coverage to be accessed
* @param map the request parameters
* @param reader the reader to be used to access the coverage
* @return parameters setup on top of requested values.
* @throws IOException
* @throws ParseException
*/
private GeneralParameterValue[] parseReadParameters(final CoverageInfo coverageInfo,
final GridCoverage2DReader reader) throws IOException, ParseException {
// Parameters
final ParameterValueGroup readParametersDescriptor = reader.getFormat().getReadParameters();
GeneralParameterValue[] readParameters = CoverageUtils
.getParameters(readParametersDescriptor, coverageInfo.getParameters(), false);
final List<GeneralParameterDescriptor> parameterDescriptors = new ArrayList<GeneralParameterDescriptor>(
readParametersDescriptor.getDescriptor().descriptors());
// add the descriptors for custom dimensions
Set<ParameterDescriptor<List>> dynamicParameters = reader.getDynamicParameters();
parameterDescriptors.addAll(dynamicParameters);
final ReaderDimensionsAccessor dimensions = new ReaderDimensionsAccessor(reader);
final MetadataMap metadata = coverageInfo.getMetadata();
// Setup small envelope to get a piece of coverage to retrieve stats
final ReferencedEnvelope testEnvelope = createTestEnvelope(coverageInfo);
final GridGeometry2D gridGeometry = new GridGeometry2D(
new GridEnvelope2D(new Rectangle(0, 0, 2, 2)), testEnvelope);
readParameters = CoverageUtils.mergeParameter(parameterDescriptors, readParameters,
gridGeometry, AbstractGridFormat.READ_GRIDGEOMETRY2D.getName().toString());
// Parse Time
Map<String, Object> map = Dispatcher.REQUEST.get().getKvp();
readParameters = parseTimeParameter(metadata, readParameters, parameterDescriptors, map);
// Parse Elevation
readParameters = parseElevationParameter(metadata, readParameters, parameterDescriptors,
map);
// Parse custom domains
readParameters = parseCustomDomains(dimensions, metadata, readParameters,
parameterDescriptors, map);
return readParameters;
}
/**
* Parse custom dimension values if present
*
* @param dimensions
* @param metadata
* @param readParameters
* @param parameterDescriptors
* @param map
*
* @throws IOException
*/
private GeneralParameterValue[] parseCustomDomains(final ReaderDimensionsAccessor dimensions,
final MetadataMap metadata, GeneralParameterValue[] readParameters,
final List<GeneralParameterDescriptor> parameterDescriptors,
final Map<String, Object> map) throws IOException {
List<String> customDomains = new ArrayList(dimensions.getCustomDomains());
if (customDomains != null && customDomains.size() > 0) {
Set<String> params = map.keySet();
for (String paramName : params) {
if (paramName.regionMatches(true, 0, "dim_", 0, 4)) {
String name = paramName.substring(4);
// Getting the dimension
name = caseInsensitiveLookup(customDomains, name);
if (name != null) {
final DimensionInfo customInfo = metadata.get(
ResourceInfo.CUSTOM_DIMENSION_PREFIX + name, DimensionInfo.class);
if (dimensions.hasDomain(name) && customInfo != null
&& customInfo.isEnabled()) {
final ArrayList<String> val = new ArrayList<String>(1);
String value = (String) map.get(paramName);
if (value.indexOf(",") > 0) {
String[] elements = value.split(",");
val.addAll(Arrays.asList(elements));
} else {
val.add(value);
}
readParameters = CoverageUtils.mergeParameter(parameterDescriptors,
readParameters, val, name);
}
}
}
}
}
return readParameters;
}
/**
* Parse the elevation parameter if present.
*
* @param metadata the elevationInfo metadata object
* @param readParameters the readParameters to be set
* @param parameterDescriptors the reader's parameter descriptors
* @param map the request's parameters
* @return the updated parameter set
* @throws ParseException
*/
private GeneralParameterValue[] parseElevationParameter(final MetadataMap metadata,
GeneralParameterValue[] readParameters,
final List<GeneralParameterDescriptor> parameterDescriptors,
final Map<String, Object> map) {
final DimensionInfo elevationInfo = metadata.get(ResourceInfo.ELEVATION,
DimensionInfo.class);
if (elevationInfo != null && elevationInfo.isEnabled()) {
// handle "current"
Set<String> params = map.keySet();
for (String param : params) {
if (param.equalsIgnoreCase("elevation")) {
readParameters = CoverageUtils.mergeParameter(parameterDescriptors,
readParameters, Double.valueOf((String) map.get(param)), "ELEVATION",
"Elevation");
break;
}
}
}
return readParameters;
}
/**
* Parse the time parameter if present.
*
* @param metadata the timeInfo metadata object
* @param readParameters the readParameters to be set
* @param parameterDescriptors the reader's parameter descriptors
* @param map the request's parameters
* @return the updated parameter set
* @throws ParseException
*/
private GeneralParameterValue[] parseTimeParameter(final MetadataMap metadata,
GeneralParameterValue[] readParameters,
final List<GeneralParameterDescriptor> parameterDescriptors,
final Map<String, Object> map) throws ParseException {
final DimensionInfo timeInfo = metadata.get(ResourceInfo.TIME, DimensionInfo.class);
if (timeInfo != null && timeInfo.isEnabled()) {
final Set<String> params = map.keySet();
for (String param : params) {
if (param.equalsIgnoreCase("time")) {
// pass down the parameters
readParameters = CoverageUtils.mergeParameter(parameterDescriptors,
readParameters, parser.parse((String) map.get(param)), "TIME", "Time");
break;
}
}
}
return readParameters;
}
private String caseInsensitiveLookup(List<String> names, String name) {
for (String s : names) {
if (name.equalsIgnoreCase(s)) {
return s;
}
}
return null;
}
/**
* Create a small 2x2 envelope to be used to read a small coverage in order to retrieve statistics from it
*
* @param coverageInfo
*
*/
private ReferencedEnvelope createTestEnvelope(final CoverageInfo coverageInfo) {
final ReferencedEnvelope envelope = coverageInfo.getNativeBoundingBox();
final GridGeometry geometry = coverageInfo.getGrid();
final MathTransform transform = geometry.getGridToCRS();
// Creating a 2x2 envelope to get a sample coverage to retrieve statistics from that small piece
final double scaleX = XAffineTransform.getScaleX0((AffineTransform) transform);
final double scaleY = XAffineTransform.getScaleY0((AffineTransform) transform);
final double minX = envelope.getMinimum(0);
final double minY = envelope.getMinimum(1);
final ReferencedEnvelope newEnvelope = new ReferencedEnvelope(minX, minX + scaleX * 2, minY,
minY + scaleY * 2, envelope.getCoordinateReferenceSystem());
return newEnvelope;
}
}