/* Copyright (c) 2001 - 2007 TOPP - www.openplans.org. All rights reserved.
* This code is licensed under the GPL 2.0 license, availible at the root
* application directory.
*/
package org.vfny.geoserver.wms.responses.featureInfo;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geoserver.catalog.CoverageInfo;
import org.geoserver.config.GeoServer;
import org.geoserver.config.ServiceInfo;
import org.geoserver.data.util.CoverageUtils;
import org.geoserver.platform.ServiceException;
import org.geoserver.wms.MapLayerInfo;
import org.geotools.coverage.GridSampleDimension;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridEnvelope2D;
import org.geotools.coverage.grid.GridGeometry2D;
import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader;
import org.geotools.coverage.grid.io.AbstractGridFormat;
import org.geotools.data.DataUtilities;
import org.geotools.data.DefaultQuery;
import org.geotools.data.FeatureSource;
import org.geotools.data.Query;
import org.geotools.data.store.FilteringFeatureCollection;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.factory.GeoTools;
import org.geotools.factory.Hints;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.SchemaException;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.filter.IllegalFilterException;
import org.geotools.filter.visitor.SimplifyingFilterVisitor;
import org.geotools.geometry.DirectPosition2D;
import org.geotools.geometry.TransformedDirectPosition;
import org.geotools.geometry.jts.JTS;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.parameter.Parameter;
import org.geotools.referencing.CRS;
import org.geotools.renderer.lite.MetaBufferEstimator;
import org.geotools.renderer.lite.RendererUtilities;
import org.geotools.resources.geometry.XRectangle2D;
import org.geotools.styling.FeatureTypeStyle;
import org.geotools.styling.Rule;
import org.geotools.styling.Style;
import org.geotools.util.NullProgressListener;
import org.opengis.coverage.CannotEvaluateException;
import org.opengis.coverage.PointOutsideCoverageException;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.feature.Feature;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.FeatureType;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.feature.type.Name;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.Or;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.vfny.geoserver.Response;
import org.vfny.geoserver.wms.WmsException;
import org.vfny.geoserver.wms.requests.GetFeatureInfoRequest;
import org.vfny.geoserver.wms.requests.GetMapRequest;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.Polygon;
/**
* Abstract class to do the common work of the FeatureInfoResponse subclasses.
* Subclasses should just need to implement writeTo(), to write the actual
* response, the executions are handled here, figuring out where on the map
* the pixel is located.
*
* <p>
* Would be nice to have some greater control over the pixels that are
* selected. Ideally we would be able to detect things like the size of the
* mark, so that users need not click on the exact center, or the exact pixel.
* This is not a big deal for polygons, but is for lines and points. One
* half solution to make things a bit nicer would be a global parameter to set
* a wider pixel range.
* </p>
*
* @author James Macgill, PSU
* @author Gabriel Roldan, Axios
* @author Chris Holmes, TOPP
* @author Brent Owens, TOPP
*/
public abstract class AbstractFeatureInfoResponse extends GetFeatureInfoDelegate {
/** A logger for this class. */
protected static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger(
"org.vfny.geoserver.responses.wms.featureinfo");
/** The formats supported by this map delegate. */
protected List supportedFormats = null;
protected List<FeatureCollection<? extends FeatureType, ? extends Feature>> results;
protected List<MapLayerInfo> metas;
/**
* setted in execute() from the requested output format, it's holded just
* to be sure that method has been called before getContentType() thus
* supporting the workflow contract of the request processing
*/
protected String format = null;
/**
* Creates a new GetMapDelegate object.
*/
/**
* Autogenerated proxy constructor.
*/
public AbstractFeatureInfoResponse() {
super();
}
/**
* Returns the content encoding for the output data.
*
* <p>
* Note that this reffers to an encoding applied to the response stream
* (such as GZIP or DEFLATE), and not to the MIME response type, wich is
* returned by <code>getContentType()</code>
* </p>
*
* @return <code>null</code> since no special encoding is performed while
* wrtting to the output stream.
*/
public String getContentEncoding() {
return null;
}
/**
* Writes the image to the client.
*
* @param out The output stream to write to.
*
* @throws ServiceException DOCUMENT ME!
* @throws IOException DOCUMENT ME!
*/
public abstract void writeTo(OutputStream out) throws ServiceException, IOException;
/**
* The formats this delegate supports.
*
* @return The list of the supported formats
*/
public List getSupportedFormats() {
return supportedFormats;
}
/**
* Gets the content type. This is set by the request, should only be
* called after execute. GetMapResponse should handle this though.
*
* @param gs server configuration
*
* @return The mime type that this response will generate.
*
* @throws IllegalStateException if<code>execute()</code> has not been
* previously called
*/
public String getContentType(GeoServer gs) {
if (format == null) {
throw new IllegalStateException(
"Content type unknown since execute() has not been called yet");
}
// chain geoserver charset so that multibyte feature info responses
// gets properly encoded, same as getCapabilities responses
return format + ";charset=" + gs.getGlobal().getCharset();
}
/**
* Does nothing, override as needed
* @see Response#getContentDisposition()
*/
public String getContentDisposition() {
return null;
}
/**
* Does nothing, override as needed
* @see Response#abort(ServiceInfo)
*/
public void abort(ServiceInfo gs) {
//nothing to do here
}
/**
* Performs the execute request using geotools rendering.
*
* @param requestedLayers The information on the types requested.
* @param queries The results of the queries to generate maps with.
* @param x DOCUMENT ME!
* @param y DOCUMENT ME!
*
* @throws WmsException For any problems.
*/
@SuppressWarnings("deprecation")
@Override
//@SuppressWarnings("unchecked")
protected void execute(MapLayerInfo[] requestedLayers, Style[] styles, Filter[] filters, int x, int y, int buffer)
throws WmsException {
GetFeatureInfoRequest request = getRequest();
this.format = request.getInfoFormat();
GetMapRequest getMapReq = request.getGetMapRequest();
CoordinateReferenceSystem requestedCRS = getMapReq.getCrs(); // optional, may be null
// basic information about the request
int width = getMapReq.getWidth();
int height = getMapReq.getHeight();
ReferencedEnvelope bbox = new ReferencedEnvelope(getMapReq.getBbox(), getMapReq.getCrs());
double scaleDenominator = RendererUtilities.calculateOGCScale(bbox, width, new HashMap());
FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2(GeoTools.getDefaultHints());
final int layerCount = requestedLayers.length;
results = new ArrayList<FeatureCollection<? extends FeatureType,? extends Feature>>(layerCount);
metas = new ArrayList<MapLayerInfo>(layerCount);
try {
for (int i = 0; i < layerCount; i++) {
List<Rule> rules = getActiveRules(styles[i], scaleDenominator);
if(rules.size() == 0)
continue;
MapLayerInfo layerInfo = requestedLayers[i];
if (layerInfo.getType() == MapLayerInfo.TYPE_VECTOR) {
CoordinateReferenceSystem dataCRS = layerInfo.getCoordinateReferenceSystem();
// compute the request radius
double radius;
if(buffer <= 0) {
// estimate the radius given the currently active rules
MetaBufferEstimator estimator = new MetaBufferEstimator();
for (Rule rule : rules) {
rule.accept(estimator);
}
if(estimator.getBuffer() < 6.0 || !estimator.isEstimateAccurate()) {
radius = 3.0;
} else {
radius = estimator.getBuffer() / 2.0;
}
} else {
radius = buffer;
}
// make sure we don't go overboard, the admin might have set a maximum
int maxRadius = request.getWMS().getMaxBuffer();
if(maxRadius > 0 && radius > maxRadius)
radius = maxRadius;
Polygon pixelRect = getEnvelopeFilter(x, y, width, height, bbox, radius);
if ((requestedCRS != null) && !CRS.equalsIgnoreMetadata(dataCRS, requestedCRS)) {
try {
MathTransform transform = CRS.findMathTransform(requestedCRS, dataCRS, true);
pixelRect = (Polygon) JTS.transform(pixelRect, transform); // reprojected
} catch (MismatchedDimensionException e) {
LOGGER.severe(e.getLocalizedMessage());
} catch (TransformException e) {
LOGGER.severe(e.getLocalizedMessage());
} catch (FactoryException e) {
LOGGER.severe(e.getLocalizedMessage());
}
}
final FeatureSource<? extends FeatureType, ? extends Feature> featureSource;
featureSource = layerInfo.getFeatureSource(false);
FeatureType schema = featureSource.getSchema();
Filter getFInfoFilter = null;
try {
GeometryDescriptor geometryDescriptor = schema.getGeometryDescriptor();
String localName = geometryDescriptor.getLocalName();
getFInfoFilter = ff.intersects(ff.property(localName), ff.literal(pixelRect));
} catch (IllegalFilterException e) {
e.printStackTrace();
throw new WmsException(null, "Internal error : " + e.getMessage());
}
// include the eventual layer definition filter
if (filters[i] != null) {
getFInfoFilter = ff.and(getFInfoFilter, filters[i]);
}
// see if we can include the rule filters as well, if too many we'll do them in memory
Filter postFilter = Filter.INCLUDE;
Filter rulesFilters = buildRulesFilter(ff, rules);
if(!(rulesFilters instanceof Or) ||
(rulesFilters instanceof Or && ((Or) rulesFilters).getChildren().size() <= 20)) {
getFInfoFilter = ff.and(getFInfoFilter, rulesFilters);
} else {
postFilter = rulesFilters;
}
String typeName = schema.getName().getLocalPart();
Query q = new DefaultQuery(typeName, null, getFInfoFilter, request.getFeatureCount(), Query.ALL_NAMES, null);
FeatureCollection<? extends FeatureType, ? extends Feature> match;
match = featureSource.getFeatures(q);
// if we could not include the rules filter into the query, post process in memory
if(!Filter.INCLUDE.equals(postFilter))
match = new FilteringFeatureCollection(match, postFilter);
//this was crashing Gml2FeatureResponseDelegate due to not setting
//the featureresults, thus not being able of querying the SRS
//if (match.getCount() > 0) {
results.add(match);
metas.add(layerInfo);
//}
} else {
final CoverageInfo cinfo = requestedLayers[i].getCoverage();
final AbstractGridCoverage2DReader reader=(AbstractGridCoverage2DReader) cinfo.getGridCoverageReader(new NullProgressListener(),GeoTools.getDefaultHints());
final ParameterValueGroup params = reader.getFormat().getReadParameters();
final GeneralParameterValue[] parameters = CoverageUtils.getParameters(params, requestedLayers[i].getCoverage().getParameters(),true);
//get the original grid geometry
final GridGeometry2D coverageGeometry=(GridGeometry2D) cinfo.getGrid();
// set the requested position in model space for this request
final Coordinate middle = pixelToWorld(x, y, bbox, width, height);
DirectPosition position = new DirectPosition2D(requestedCRS, middle.x, middle.y);
//change from request crs to coverage crs in order to compute a minimal request area,
// TODO this code need to be made much more robust
if (requestedCRS != null) {
final CoordinateReferenceSystem targetCRS = coverageGeometry.getCoordinateReferenceSystem();
final TransformedDirectPosition arbitraryToInternal = new
TransformedDirectPosition(requestedCRS, targetCRS, new Hints(Hints.LENIENT_DATUM_SHIFT,Boolean.TRUE));
try {
arbitraryToInternal.transform(position);
} catch (TransformException exception) {
throw new CannotEvaluateException("Unable to answer the geatfeatureinfo",exception);
}
position=arbitraryToInternal;
}
//check that the provided point is inside the bbox for this coverage
if(!reader.getOriginalEnvelope().contains(position)) {
continue;
}
//now get the position in raster space using the world to grid related to corner
final MathTransform worldToGrid=reader.getOriginalGridToWorld(PixelInCell.CELL_CORNER).inverse();
final DirectPosition rasterMid = worldToGrid.transform(position,null);
// create a 20X20 rectangle aruond the mid point and then intersect with the original range
final Rectangle2D.Double rasterArea= new Rectangle2D.Double();
rasterArea.setFrameFromCenter(rasterMid.getOrdinate(0), rasterMid.getOrdinate(1), rasterMid.getOrdinate(0)+10, rasterMid.getOrdinate(1)+10);
final Rectangle integerRasterArea=rasterArea.getBounds();
final GridEnvelope gridEnvelope=reader.getOriginalGridRange();
final Rectangle originalArea=
(gridEnvelope instanceof GridEnvelope2D)?
(GridEnvelope2D)gridEnvelope:
new Rectangle();
XRectangle2D.intersect(integerRasterArea, originalArea, integerRasterArea);
//paranoiac check, did we fall outside the coverage raster area? This should never really happne if the request is well formed.
if(integerRasterArea.isEmpty())
return;
// now set the grid geometry for this request
for(int k=0;k<parameters.length;k++){
if(!(parameters[k] instanceof Parameter<?>))
continue;
final Parameter<?> parameter = (Parameter<?>) parameters[k];
if(parameter.getDescriptor().getName().equals(AbstractGridFormat.READ_GRIDGEOMETRY2D.getName()))
{
//
//create a suitable geometry for this request reusing the getmap (we could probably optimize)
//
parameter.setValue(new GridGeometry2D(
new GridEnvelope2D(integerRasterArea),
reader.getOriginalGridToWorld(PixelInCell.CELL_CENTER),
reader.getCrs()
));
}
}
final GridCoverage2D coverage=(GridCoverage2D) reader.read(parameters);
if(coverage==null)
{
if(LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Unable to load raster data for this request.");
return;
}
try {
final double[] pixelValues = coverage.evaluate(position,(double[]) null);
final FeatureCollection<SimpleFeatureType, SimpleFeature> pixel;
pixel = wrapPixelInFeatureCollection(coverage, pixelValues, cinfo.getQualifiedName());
metas.add(requestedLayers[i]);
results.add(pixel);
} catch(PointOutsideCoverageException e) {
// it's fine, users might legitimately query point outside, we just don't return anything
}
}
}
} catch (Exception e) {
throw new WmsException(null, "Internal error occurred", e);
}
}
private Filter buildRulesFilter(org.opengis.filter.FilterFactory ff, List<Rule> rules) {
// build up a or of all the rule filters
List<Filter> filters = new ArrayList<Filter>();
for (Rule rule : rules) {
if(rule.getFilter() == null)
return Filter.INCLUDE;
filters.add(rule.getFilter());
}
// not or and and simplify (if there is any include/exclude we'll get
// a very simple result ;-)
Filter or = ff.or(filters);
SimplifyingFilterVisitor simplifier = new SimplifyingFilterVisitor();
return (Filter) or.accept(simplifier, null);
}
/**
* Selects the rules active at this zoom level
* @param style
* @param scaleDenominator
* @return
*/
private List<Rule> getActiveRules(Style style, double scaleDenominator) {
List<Rule> result = new ArrayList<Rule>();
for(FeatureTypeStyle fts : style.getFeatureTypeStyles()) {
for (Rule r : fts.rules()) {
if((r.getMinScaleDenominator() <= scaleDenominator)
&& (r.getMaxScaleDenominator() > scaleDenominator)) {
result.add(r);
}
}
}
return result;
}
private Polygon getEnvelopeFilter(int x, int y, int width, int height, Envelope bbox, double radius) {
Coordinate upperLeft = pixelToWorld(x - radius, y - radius, bbox, width, height);
Coordinate lowerRight = pixelToWorld(x + radius, y + radius, bbox, width, height);
Coordinate[] coords = new Coordinate[5];
coords[0] = upperLeft;
coords[1] = new Coordinate(lowerRight.x, upperLeft.y);
coords[2] = lowerRight;
coords[3] = new Coordinate(upperLeft.x, lowerRight.y);
coords[4] = coords[0];
GeometryFactory geomFac = new GeometryFactory();
LinearRing boundary = geomFac.createLinearRing(coords); // this needs to be done with each FT so it can be reprojected
Polygon pixelRect = geomFac.createPolygon(boundary, null);
return pixelRect;
}
private FeatureCollection<SimpleFeatureType, SimpleFeature> wrapPixelInFeatureCollection(
GridCoverage2D coverage, double[] pixelValues, Name coverageName)
throws SchemaException {
GridSampleDimension[] sampleDimensions = coverage.getSampleDimensions();
SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder();
builder.setName(coverageName);
final Set<String> bandNames=new HashSet<String>();
for (int i = 0; i < sampleDimensions.length; i++) {
String name=sampleDimensions[i].getDescription().toString();
//GEOS-2518
if(bandNames.contains(name))
// it might happen again that the name already exists but it pretty difficult I'd say
name= new StringBuilder(name).append("_Band").append(i).toString();
bandNames.add(name);
builder.add(name, Double.class);
}
SimpleFeatureType gridType = builder.buildFeatureType();
Double[] values = new Double[pixelValues.length];
for (int i = 0; i < values.length; i++) {
values[i] = new Double(pixelValues[i]);
}
return DataUtilities.collection(SimpleFeatureBuilder.build(gridType, values, ""));
}
/**
* Converts a coordinate expressed on the device space back to real world
* coordinates. Stolen from LiteRenderer but without the need of a
* Graphics object
*
* @param x horizontal coordinate on device space
* @param y vertical coordinate on device space
* @param map The map extent
* @param width image width
* @param height image height
*
* @return The correspondent real world coordinate
*
* @throws RuntimeException DOCUMENT ME!
*/
private Coordinate pixelToWorld(double x, double y, Envelope map, double width, double height) {
//set up the affine transform and calculate scale values
AffineTransform at = worldToScreenTransform(map, width, height);
Point2D result = null;
try {
result = at.inverseTransform(new java.awt.geom.Point2D.Double(x, y),
new java.awt.geom.Point2D.Double());
} catch (NoninvertibleTransformException e) {
throw new RuntimeException(e);
}
Coordinate c = new Coordinate(result.getX(), result.getY());
return c;
}
/**
* Sets up the affine transform. Stolen from liteRenderer code.
*
* @param mapExtent the map extent
* @param width the screen size
* @param height DOCUMENT ME!
*
* @return a transform that maps from real world coordinates to the screen
*/
private AffineTransform worldToScreenTransform(Envelope mapExtent, double width, double height) {
double scaleX = (double) width / mapExtent.getWidth();
double scaleY = (double) height / mapExtent.getHeight();
double tx = -mapExtent.getMinX() * scaleX;
double ty = (mapExtent.getMinY() * scaleY) + height;
AffineTransform at = new AffineTransform(scaleX, 0.0d, 0.0d, -scaleY, tx, ty);
return at;
}
}