/* (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.wms.ncwms;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.TreeSet;
import org.geoserver.catalog.CoverageInfo;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.ServiceException;
import org.geoserver.wms.FeatureInfoRequestParameters;
import org.geoserver.wms.GetFeatureInfoRequest;
import org.geoserver.wms.MapLayerInfo;
import org.geoserver.wms.WMS;
import org.geoserver.wms.featureinfo.LayerIdentifier;
import org.geotools.data.DataUtilities;
import org.geotools.data.Query;
import org.geotools.data.collection.ListFeatureCollection;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.util.DateRange;
import org.geotools.util.SimpleInternationalString;
import org.opengis.feature.Feature;
import org.opengis.feature.Property;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import net.opengis.wfs.FeatureCollectionType;
import net.opengis.wfs.WfsFactory;
/**
* Implements the methods of the NcWMS service which are not included on the WMS standard. For the moment, only GetTimeSeries method is supported.
*/
public class NcWmsService {
public static final String TIME_SERIES_INFO_FORMAT_PARAM_NAME = "TIME_SERIES_INFO_FORMAT";
public static final String GET_TIME_SERIES_REQUEST = "GetTimeSeries";
private WMS wms;
/**
* Simple helper to enforce rendering timeout
*/
private static class CountdownClock {
long end;
long maxRenderingTime;
CountdownClock(long maxRenderingTime) {
this.maxRenderingTime = maxRenderingTime;
if (maxRenderingTime > 0) {
this.end = System.currentTimeMillis() + maxRenderingTime;
}
}
public void checkTimeout() {
if (maxRenderingTime > 0 && System.currentTimeMillis() > end) {
throw new ServiceException(
"This request used more time than allowed and has been forcefully stopped. "
+ "Max rendering time is " + (maxRenderingTime / 1000.0) + "s");
}
}
}
public NcWmsService(final WMS wms) {
this.wms = wms;
}
private LayerIdentifier getLayerIdentifier(MapLayerInfo layer) {
List<LayerIdentifier> identifiers = GeoServerExtensions.extensions(LayerIdentifier.class);
for (LayerIdentifier identifier : identifiers) {
if (identifier.canHandle(layer)) {
return identifier;
}
}
throw new ServiceException("Could not find any identifier that can handle layer "
+ layer.getLayerInfo().prefixedName() + " among these identifiers: " + identifiers);
}
/**
* Implements the GetTimeSeries method, which can retrieve a time series of values on a certain point, using a syntax similar to the
* GetFeatureInfo operation.
*
* @param request
* @return
*/
@SuppressWarnings("rawtypes")
public FeatureCollectionType getTimeSeries(GetFeatureInfoRequest request) {
FeatureCollectionType result = WfsFactory.eINSTANCE.createFeatureCollectionType();
WfsFactory.eINSTANCE.createFeatureCollectionType();
result.setTimeStamp(Calendar.getInstance());
// Process the request only if we have a time range
if (request.getGetMapRequest().getTime() == null
|| request.getGetMapRequest().getTime().size() != 1) {
throw new ServiceException(
"The TIME parameter was not a valid WMS time range or was missing");
}
Object queryRangePlain = (DateRange) request.getGetMapRequest().getTime().get(0);
if (queryRangePlain == null || !(queryRangePlain instanceof DateRange)) {
throw new ServiceException("The TIME parameter was not a valid WMS time range");
}
DateRange queryRange = (DateRange) queryRangePlain;
final List<MapLayerInfo> requestedLayers = request.getQueryLayers();
// Process the request only if we have single layer
if (requestedLayers.size() != 1) {
throw new ServiceException(
"The QUERY_LAYERS parameter must specify a single coverage layer for the GetTimeSeries operation");
}
final MapLayerInfo layer = requestedLayers.get(0);
CoverageInfo coverage;
try {
coverage = layer.getCoverage();
} catch (Exception cex) {
throw new ServiceException(
"The GetTimeSeries operation is only defined for coverage layers");
}
// control how much time we spend doing queries to gather times and values
int maxRenderingTime = wms.getMaxRenderingTime(request.getGetMapRequest());
CountdownClock countdownClock = new CountdownClock(maxRenderingTime);
LayerIdentifier identifier = getLayerIdentifier(layer);
SimpleFeatureBuilder featureBuilder = getResultFeatureBuilder(layer.getName(),
buildTypeDescription(layer));
try {
// Get available dates, then perform an identify operation per each date in the range
TreeSet availableDates = wms.queryCoverageTimes(coverage, queryRange,
Query.DEFAULT_MAX);
ListFeatureCollection features = new ListFeatureCollection(featureBuilder.getFeatureType());
for (Object d : availableDates) {
// check timeout
countdownClock.checkTimeout();
// run query
Date date = (Date) d;
DateRange currentDate = new DateRange(date, date);
request.getGetMapRequest().getTime().remove(0);
request.getGetMapRequest().getTime().add(currentDate);
FeatureInfoRequestParameters requestParams = new FeatureInfoRequestParameters(request);
List<FeatureCollection> identifiedCollections = identifier.identify(requestParams, 1);
// collect the data
if(identifiedCollections != null) {
for (FeatureCollection c : identifiedCollections) {
try (FeatureIterator featIter = c.features()) {
if (featIter.hasNext()) { // no need to loop, we just want one value
Feature inFeat = featIter.next();
Iterator<Property> propIter = inFeat.getProperties().iterator();
if (propIter.hasNext()) {
Property prop = propIter.next();
featureBuilder.add(date);
featureBuilder.add(prop.getValue());
SimpleFeature newFeat = featureBuilder.buildFeature(null);
features.add(newFeat);
}
}
}
}
}
}
result.getFeature().add(features);
} catch (Exception e) {
throw new ServiceException("Error processing the operation", e);
} finally {
// restore the original range
request.getGetMapRequest().getTime().remove(0);
request.getGetMapRequest().getTime().add(queryRange);
}
return result;
}
public String buildTypeDescription(MapLayerInfo layer) {
String name = layer.getName();
if (layer.getCoverage() != null && layer.getCoverage().getDimensions().size() == 1
&& layer.getCoverage().getDimensions().get(0).getName() != null
&& layer.getCoverage().getDimensions().get(0).getUnit() != null) {
name = layer.getCoverage().getDimensions().get(0).getName() + " ("
+ layer.getCoverage().getDimensions().get(0).getUnit() + ")";
}
return name;
}
private SimpleFeatureBuilder getResultFeatureBuilder(String name, String description) {
// create the builder
SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder();
// set global state
builder.setName(name);
builder.setDescription(new SimpleInternationalString(description));
builder.setNamespaceURI("http://www.geoserver.org/");
builder.setSRS("EPSG:4326");
// add attributes
builder.add("date", Date.class);
builder.add("value", Double.class);
SimpleFeatureType featureType = builder.buildFeatureType();
SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(featureType);
return featureBuilder;
}
}