/* (c) 2014 - 2016 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.wms.capabilities;
import java.io.IOException;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.CoverageInfo;
import org.geoserver.catalog.CoverageStoreInfo;
import org.geoserver.catalog.DimensionDefaultValueSetting;
import org.geoserver.catalog.DimensionInfo;
import org.geoserver.catalog.DimensionPresentation;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.ResourceInfo;
import org.geoserver.catalog.util.ReaderDimensionsAccessor;
import org.geoserver.platform.ServiceException;
import org.geoserver.util.ISO8601Formatter;
import org.geoserver.wms.WMS;
import org.geoserver.wms.dimension.DimensionDefaultValueSelectionStrategy;
import org.geotools.coverage.grid.io.GridCoverage2DReader;
import org.geotools.temporal.object.DefaultPeriodDuration;
import org.geotools.util.Converters;
import org.geotools.util.DateRange;
import org.geotools.util.NumberRange;
import org.geotools.util.logging.Logging;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.AttributesImpl;
/**
* Helper class avoiding to duplicate the time/elevation management code between WMS 1.1 and 1.3
*
* @author Andrea Aime - GeoSolutions
*/
abstract class DimensionHelper {
static final Logger LOGGER = Logging.getLogger(DimensionHelper.class);
enum Mode {
WMS11, WMS13
}
Mode mode;
WMS wms;
public DimensionHelper(Mode mode, WMS wms) {
this.mode = mode;
this.wms = wms;
}
/**
* Implement to write out an element
*/
protected abstract void element(String element, String content);
/**
* Implement to write out an element
*/
protected abstract void element(String element, String content, Attributes atts);
void handleVectorLayerDimensions(LayerInfo layer) {
//TODO: custom dimension handling
// do we have time and elevation?
FeatureTypeInfo typeInfo = (FeatureTypeInfo) layer.getResource();
DimensionInfo timeInfo = typeInfo.getMetadata().get(ResourceInfo.TIME,
DimensionInfo.class);
DimensionInfo elevInfo = typeInfo.getMetadata().get(ResourceInfo.ELEVATION,
DimensionInfo.class);
boolean hasTime = timeInfo != null && timeInfo.isEnabled();
boolean hasElevation = elevInfo != null && elevInfo.isEnabled();
// skip if no need
if (!hasTime && !hasElevation) {
return;
}
if (mode == Mode.WMS11) {
String elevUnits = hasElevation ? elevInfo.getUnits() : "";
String elevUnitSymbol = hasElevation ? elevInfo.getUnitSymbol() : "";
declareWMS11Dimensions(hasTime, hasElevation, elevUnits, elevUnitSymbol, null);
}
// Time dimension
if (hasTime) {
try {
handleTimeDimensionVector(typeInfo);
} catch (IOException e) {
throw new RuntimeException("Failed to handle time attribute for layer", e);
}
}
// elevation dimension
if (hasElevation) {
try {
handleElevationDimensionVector(typeInfo);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
/**
* Writes down the raster layer dimensions, if any
*
* @param layer
* @throws RuntimeException
* @throws IOException
*/
void handleRasterLayerDimensions(final LayerInfo layer) throws RuntimeException, IOException {
// do we have time and elevation?
CoverageInfo cvInfo = (CoverageInfo) layer.getResource();
if (cvInfo == null)
throw new ServiceException("Unable to acquire coverage resource for layer: "
+ layer.getName());
DimensionInfo timeInfo = null;
DimensionInfo elevInfo = null;
Map<String, DimensionInfo> customDimensions = new HashMap<String, DimensionInfo>();
GridCoverage2DReader reader = null;
for (Map.Entry<String, Serializable> e : cvInfo.getMetadata().entrySet()) {
String key = e.getKey();
Object value = e.getValue();
if (key.equals(ResourceInfo.TIME)) {
timeInfo = Converters.convert(value, DimensionInfo.class);
} else if (key.equals(ResourceInfo.ELEVATION)) {
elevInfo = Converters.convert(value, DimensionInfo.class);
} else if (value instanceof DimensionInfo) {
DimensionInfo dimInfo = (DimensionInfo) value;
if (dimInfo.isEnabled()) {
if (key.startsWith(ResourceInfo.CUSTOM_DIMENSION_PREFIX)) {
String dimensionName = key.substring(ResourceInfo.CUSTOM_DIMENSION_PREFIX
.length());
customDimensions.put(dimensionName, dimInfo);
} else {
LOGGER.log(Level.SEVERE, "Skipping custom dimension with key " + key
+ " since it does not start with "
+ ResourceInfo.CUSTOM_DIMENSION_PREFIX);
}
}
}
}
boolean hasTime = timeInfo != null && timeInfo.isEnabled();
boolean hasElevation = elevInfo != null && elevInfo.isEnabled();
boolean hasCustomDimensions = !customDimensions.isEmpty();
// skip if nothing is configured
if (!hasTime && !hasElevation && !hasCustomDimensions) {
return;
}
Catalog catalog = cvInfo.getCatalog();
if (catalog == null)
throw new ServiceException("Unable to acquire catalog resource for layer: "
+ layer.getName());
CoverageStoreInfo csinfo = cvInfo.getStore();
if (csinfo == null)
throw new ServiceException("Unable to acquire coverage store resource for layer: "
+ layer.getName());
try {
reader = (GridCoverage2DReader) cvInfo.getGridCoverageReader(null, null);
} catch (Throwable t) {
LOGGER.log(Level.SEVERE, "Unable to acquire a reader for this coverage with format: "
+ csinfo.getFormat().getName(), t);
}
if (reader == null) {
throw new ServiceException("Unable to acquire a reader for this coverage with format: "
+ csinfo.getFormat().getName());
}
ReaderDimensionsAccessor dimensions = new ReaderDimensionsAccessor(reader);
// Process only custom dimensions supported by the reader
if (hasCustomDimensions) {
for (String key : customDimensions.keySet()) {
if (!dimensions.hasDomain(key)) customDimensions.remove(key);
}
}
if (mode == Mode.WMS11) {
String elevUnits = hasElevation ? elevInfo.getUnits() : "";
String elevUnitSymbol = hasElevation ? elevInfo.getUnitSymbol() : "";
declareWMS11Dimensions(hasTime, hasElevation, elevUnits, elevUnitSymbol, customDimensions);
}
// timeDimension
if (hasTime && dimensions.hasTime()) {
handleTimeDimensionRaster(cvInfo, timeInfo, dimensions);
}
// elevationDomain
if (hasElevation && dimensions.hasElevation()) {
handleElevationDimensionRaster(cvInfo, elevInfo, dimensions);
}
// custom dimensions
if (hasCustomDimensions) {
for (String key : customDimensions.keySet()) {
DimensionInfo dimensionInfo = customDimensions.get(key);
handleCustomDimensionRaster(cvInfo, key, dimensionInfo, dimensions);
}
}
}
private void handleElevationDimensionRaster(CoverageInfo cvInfo, DimensionInfo elevInfo, ReaderDimensionsAccessor dimensions) throws IOException {
TreeSet<Object> elevations = dimensions.getElevationDomain();
String elevationMetadata = getZDomainRepresentation(elevInfo, elevations);
String defaultValue = getDefaultValueRepresentation(cvInfo, ResourceInfo.ELEVATION, "0");
writeElevationDimension(elevations, elevationMetadata,
elevInfo.getUnits(), elevInfo.getUnitSymbol(), defaultValue);
}
private String getDefaultValueRepresentation(ResourceInfo resource, String dimensionName, String fallback) {
DimensionInfo dimensionInfo = wms.getDimensionInfo(resource, dimensionName);
DimensionDefaultValueSelectionStrategy strategy = wms.getDefaultValueStrategy(resource, dimensionName, dimensionInfo);
String defaultValue = strategy.getCapabilitiesRepresentation(resource, dimensionName, dimensionInfo);
if(defaultValue == null) {
defaultValue = fallback;
}
return defaultValue;
}
private void handleTimeDimensionRaster(CoverageInfo cvInfo, DimensionInfo timeInfo, ReaderDimensionsAccessor dimension) throws IOException {
TreeSet<Object> temporalDomain = dimension.getTimeDomain();
String timeMetadata = getTemporalDomainRepresentation(timeInfo, temporalDomain);
String defaultValue = getDefaultValueRepresentation(cvInfo, ResourceInfo.TIME, DimensionDefaultValueSetting.TIME_CURRENT);
writeTimeDimension(timeMetadata, defaultValue);
}
private void handleCustomDimensionRaster(CoverageInfo cvInfo, String dimName, DimensionInfo dimension,
ReaderDimensionsAccessor dimAccessor) throws IOException {
final List<String> values = dimAccessor.getDomain(dimName);
String metadata = getCustomDomainRepresentation(dimension, values);
String defaultValue = wms.getDefaultCustomDimensionValue(dimName, cvInfo, String.class);
writeCustomDimension(dimName, metadata, defaultValue, dimension.getUnits(), dimension.getUnitSymbol());
}
/**
* Writes WMS 1.1.1 conforming dimensions (WMS 1.3 squashed dimensions and extent in the same tag instead)
* @param hasTime - <tt>true</tt> if the layer has the time dimension, <tt>false</tt> otherwise
* @param hasElevation - <tt>true</tt> if the layer has the elevation dimension, <tt>false</tt> otherwise
* @param elevUnits - <tt>units</tt> attribute of the elevation dimension
* @param elevUnitSymbol - <tt>unitSymbol</tt> attribute of the elevation dimension
*/
private void declareWMS11Dimensions(boolean hasTime, boolean hasElevation, String elevUnits, String elevUnitSymbol, Map<String, DimensionInfo> customDimensions) {
// we have to declare time and elevation before the extents
if (hasTime) {
AttributesImpl timeDim = new AttributesImpl();
timeDim.addAttribute("", "name", "name", "", "time");
timeDim.addAttribute("", "units", "units", "", "ISO8601");
element("Dimension", null, timeDim);
}
if (hasElevation) {
// same as WMS 1.3 except no values
writeElevationDimensionElement(null, null, elevUnits, elevUnitSymbol);
}
if (customDimensions != null) {
for (String dim : customDimensions.keySet()) {
DimensionInfo di = customDimensions.get(dim);
AttributesImpl custDim = new AttributesImpl();
custDim.addAttribute("", "name", "name", "", dim);
String units = di.getUnits();
String unitSymbol = di.getUnitSymbol();
custDim.addAttribute("", "units", "units", "", units != null ? units : "");
if(unitSymbol != null) {
custDim.addAttribute("", "unitSymbol", "unitSymbol", "", unitSymbol);
}
element("Dimension", null, custDim);
}
}
}
protected String getZDomainRepresentation(DimensionInfo dimension, TreeSet<? extends Object> values) {
String elevationMetadata = null;
final StringBuilder buff = new StringBuilder();
if (DimensionPresentation.LIST == dimension.getPresentation()) {
for (Object val : values) {
if(val instanceof Double) {
buff.append(val);
} else {
NumberRange<Double> range = (NumberRange<Double>) val;
buff.append(range.getMinimum()).append("/").append(range.getMaximum()).append("/0");
}
buff.append(",");
}
elevationMetadata = buff.substring(0, buff.length() - 1).replaceAll("\\[",
"").replaceAll("\\]", "").replaceAll(" ", "");
} else if (DimensionPresentation.CONTINUOUS_INTERVAL == dimension.getPresentation()) {
NumberRange<Double> range = getMinMaxZInterval(values);
buff.append(range.getMinimum());
buff.append("/");
buff.append(range.getMaximum());
buff.append("/0");
elevationMetadata = buff.toString();
} else if (DimensionPresentation.DISCRETE_INTERVAL == dimension.getPresentation()) {
NumberRange<Double> range = getMinMaxZInterval(values);
buff.append(range.getMinimum());
buff.append("/");
buff.append(range.getMaximum());
buff.append("/");
BigDecimal resolution = dimension.getResolution();
if (resolution != null) {
buff.append(resolution.doubleValue());
} else {
if (values.size() >= 2 && allDoubles(values)) {
int count = 2, i = 2;
Double[] zPositions = new Double[count];
for (Object val : values) {
zPositions[count - i--] = (Double) val;
if (i == 0)
break;
}
double span = zPositions[count - 1] - zPositions[count - 2];
buff.append(span);
} else {
buff.append(0);
}
}
elevationMetadata = buff.toString();
}
return elevationMetadata;
}
/**
* Builds the proper presentation given the current
*
* @param dimension
* @param values
*
*/
String getTemporalDomainRepresentation(DimensionInfo dimension, TreeSet<? extends Object> values) {
String timeMetadata = null;
final StringBuilder buff = new StringBuilder();
final ISO8601Formatter df = new ISO8601Formatter();
if (DimensionPresentation.LIST == dimension.getPresentation()) {
for (Object date : values) {
buff.append(df.format(date));
buff.append(",");
}
timeMetadata = buff.substring(0, buff.length() - 1).replaceAll("\\[", "")
.replaceAll("\\]", "").replaceAll(" ", "");
} else if (DimensionPresentation.CONTINUOUS_INTERVAL == dimension.getPresentation()) {
DateRange interval = getMinMaxTimeInterval(values);
buff.append(df.format(interval.getMinValue()));
buff.append("/");
buff.append(df.format(interval.getMaxValue()));
buff.append("/PT1S");
timeMetadata = buff.toString();
} else if (DimensionPresentation.DISCRETE_INTERVAL == dimension.getPresentation()) {
DateRange interval = getMinMaxTimeInterval(values);
buff.append(df.format(interval.getMinValue()));
buff.append("/");
buff.append(df.format(interval.getMaxValue()));
buff.append("/");
final BigDecimal resolution = dimension.getResolution();
if (resolution != null) {
// resolution has been provided
buff.append(new DefaultPeriodDuration(resolution.longValue()).toString());
} else {
if (values.size() >= 2 && allDates(values)) {
int count = 2, i = 2;
Date[] timePositions = new Date[count];
for (Object date : values) {
timePositions[count - i--] = (Date) date;
if (i == 0)
break;
}
long durationInMilliSeconds = timePositions[count - 1].getTime()
- timePositions[count - 2].getTime();
buff.append(new DefaultPeriodDuration(durationInMilliSeconds).toString());
} else {
// assume 1 second and be done with it...
buff.append("PT1S");
}
}
timeMetadata = buff.toString();
}
return timeMetadata;
}
/**
* Builds a single time range from the domain, be it made of Date or TimeRange objects
* @param values
*
*/
private DateRange getMinMaxTimeInterval(TreeSet<? extends Object> values) {
Object minValue = values.first();
Object maxValue = values.last();
Date min, max;
if(minValue instanceof DateRange) {
min = ((DateRange) minValue).getMinValue();
} else {
min = (Date) minValue;
}
if(maxValue instanceof DateRange) {
max = ((DateRange) maxValue).getMaxValue();
} else {
max = (Date) maxValue;
}
return new DateRange(min, max);
}
/**
* Builds a single Z range from the domain, be it made of Double or NumberRange objects
* @param values
*
*/
private NumberRange<Double> getMinMaxZInterval(TreeSet<? extends Object> values) {
Object minValue = values.first();
Object maxValue = values.last();
Double min, max;
if(minValue instanceof NumberRange) {
min = ((NumberRange<Double>) minValue).getMinValue();
} else {
min = (Double) minValue;
}
if(maxValue instanceof NumberRange) {
max = ((NumberRange<Double>) maxValue).getMaxValue();
} else {
max = (Double) maxValue;
}
return new NumberRange<Double>(Double.class, min, max);
}
/**
* Returns true if all the values in the set are Date instances
*
* @param values
*
*/
private boolean allDates(TreeSet<? extends Object> values) {
for(Object value : values) {
if(!(value instanceof Date)) {
return false;
}
}
return true;
}
/**
* Returns true if all the values in the set are Double instances
*
* @param values
*
*/
private boolean allDoubles(TreeSet<? extends Object> values) {
for(Object value : values) {
if(!(value instanceof Double)) {
return false;
}
}
return true;
}
/**
* Builds the proper presentation given the specified value domain
*
* @param dimension
* @param values
*
*/
String getCustomDomainRepresentation(DimensionInfo dimension, List<String> values) {
String metadata = null;
final StringBuilder buff = new StringBuilder();
if (DimensionPresentation.LIST == dimension.getPresentation()) {
for (String value : values) {
buff.append(value.trim());
buff.append(",");
}
metadata = buff.substring(0, buff.length() - 1);
} else if (DimensionPresentation.DISCRETE_INTERVAL == dimension.getPresentation()) {
buff.append(values.get(0));
buff.append("/");
buff.append(values.get(0));
buff.append("/");
final BigDecimal resolution = dimension.getResolution();
if (resolution != null) {
buff.append(resolution);
}
metadata = buff.toString();
}
return metadata;
}
/**
* Writes out metadata for the time dimension
*
* @param typeInfo
* @throws IOException
*/
private void handleTimeDimensionVector(FeatureTypeInfo typeInfo) throws IOException {
// build the time dim representation
TreeSet<Date> values = wms.getFeatureTypeTimes(typeInfo);
String timeMetadata;
if (values != null && !values.isEmpty()) {
DimensionInfo timeInfo = typeInfo.getMetadata().get(ResourceInfo.TIME,
DimensionInfo.class);
timeMetadata = getTemporalDomainRepresentation(timeInfo, values);
} else {
timeMetadata = "";
}
String defaultValue = getDefaultValueRepresentation(typeInfo, ResourceInfo.TIME, DimensionDefaultValueSetting.TIME_CURRENT);
writeTimeDimension(timeMetadata, defaultValue);
}
private void handleElevationDimensionVector(FeatureTypeInfo typeInfo) throws IOException {
TreeSet<Double> elevations = wms.getFeatureTypeElevations(typeInfo);
String elevationMetadata;
DimensionInfo di = typeInfo.getMetadata().get(ResourceInfo.ELEVATION,
DimensionInfo.class);
String units = di.getUnits();
String unitSymbol = di.getUnitSymbol();
if (elevations != null && !elevations.isEmpty()) {
elevationMetadata = getZDomainRepresentation(di, elevations);
} else {
elevationMetadata = "";
}
String defaultValue = getDefaultValueRepresentation(typeInfo, ResourceInfo.ELEVATION, "0");
writeElevationDimension(elevations, elevationMetadata, units, unitSymbol, defaultValue);
}
private void writeTimeDimension(String timeMetadata, String defaultTimeStr) {
AttributesImpl timeDim = new AttributesImpl();
if(defaultTimeStr == null) {
defaultTimeStr = DimensionDefaultValueSetting.TIME_CURRENT;
}
if (mode == Mode.WMS11) {
timeDim.addAttribute("", "name", "name", "", "time");
timeDim.addAttribute("", "default", "default", "", defaultTimeStr);
element("Extent", timeMetadata, timeDim);
} else {
timeDim.addAttribute("", "name", "name", "", "time");
timeDim.addAttribute("", "default", "default", "", defaultTimeStr);
timeDim.addAttribute("", "units", "units", "", DimensionInfo.TIME_UNITS);
element("Dimension", timeMetadata, timeDim);
}
}
private void writeElevationDimension(TreeSet<? extends Object> elevations, final String elevationMetadata,
final String units, final String unitSymbol, String defaultValue) {
if (mode == Mode.WMS11) {
AttributesImpl elevDim = new AttributesImpl();
elevDim.addAttribute("", "name", "name", "", "elevation");
elevDim.addAttribute("", "default", "default", "", defaultValue);
element("Extent", elevationMetadata, elevDim);
} else {
writeElevationDimensionElement(elevationMetadata, defaultValue,
units, unitSymbol);
}
}
private void writeElevationDimensionElement(final String elevationMetadata, final String defaultValue,
final String units, final String unitSymbol) {
AttributesImpl elevDim = new AttributesImpl();
String unitsNotNull = units;
String unitSymNotNull = (unitSymbol == null) ? "" : unitSymbol;
if (units == null) {
unitsNotNull = DimensionInfo.ELEVATION_UNITS;
unitSymNotNull = DimensionInfo.ELEVATION_UNIT_SYMBOL;
}
elevDim.addAttribute("", "name", "name", "", "elevation");
if (defaultValue != null) {
elevDim.addAttribute("", "default", "default", "", defaultValue);
}
elevDim.addAttribute("", "units", "units", "", unitsNotNull);
if (!"".equals(unitsNotNull) && !"".equals(unitSymNotNull)) {
elevDim.addAttribute("", "unitSymbol", "unitSymbol", "", unitSymNotNull);
}
element("Dimension", elevationMetadata, elevDim);
}
private void writeCustomDimension(String name, String metadata, String defaultValue, String unit, String unitSymbol) {
AttributesImpl dim = new AttributesImpl();
dim.addAttribute("", "name", "name", "", name);
if (mode == Mode.WMS11) {
if (defaultValue != null) {
dim.addAttribute("", "default", "default", "", defaultValue);
}
element("Extent", metadata, dim);
} else {
if (defaultValue != null) {
dim.addAttribute("", "default", "default", "", defaultValue);
}
dim.addAttribute("", "units", "units", "", unit != null ? unit : "");
if (unitSymbol != null && !"".equals(unitSymbol)) {
dim.addAttribute("", "unitSymbol", "unitSymbol", "", unitSymbol);
}
element("Dimension", metadata, dim);
}
}
}