001: /* Copyright (c) 2001 - 2007 TOPP - www.openplans.org. All rights reserved.
002: * This code is licensed under the GPL 2.0 license, availible at the root
003: * application directory.
004: */
005: package org.vfny.geoserver.wms.responses.featureInfo;
006:
007: import java.awt.geom.AffineTransform;
008: import java.awt.geom.NoninvertibleTransformException;
009: import java.awt.geom.Point2D;
010: import java.io.IOException;
011: import java.io.OutputStream;
012: import java.util.ArrayList;
013: import java.util.List;
014: import java.util.logging.Logger;
015:
016: import org.geotools.coverage.GridSampleDimension;
017: import org.geotools.coverage.grid.GridCoverage2D;
018: import org.geotools.data.DataUtilities;
019: import org.geotools.data.DefaultQuery;
020: import org.geotools.data.Query;
021: import org.geotools.factory.CommonFactoryFinder;
022: import org.geotools.factory.GeoTools;
023: import org.geotools.feature.AttributeType;
024: import org.geotools.feature.AttributeTypeFactory;
025: import org.geotools.feature.FeatureCollection;
026: import org.geotools.feature.FeatureType;
027: import org.geotools.feature.FeatureTypeBuilder;
028: import org.geotools.feature.IllegalAttributeException;
029: import org.geotools.feature.SchemaException;
030: import org.geotools.filter.IllegalFilterException;
031: import org.geotools.geometry.DirectPosition2D;
032: import org.geotools.geometry.jts.JTS;
033: import org.geotools.referencing.CRS;
034: import org.opengis.coverage.PointOutsideCoverageException;
035: import org.opengis.filter.Filter;
036: import org.opengis.filter.FilterFactory2;
037: import org.opengis.geometry.DirectPosition;
038: import org.opengis.geometry.MismatchedDimensionException;
039: import org.opengis.referencing.FactoryException;
040: import org.opengis.referencing.crs.CoordinateReferenceSystem;
041: import org.opengis.referencing.operation.MathTransform;
042: import org.opengis.referencing.operation.TransformException;
043: import org.vfny.geoserver.ServiceException;
044: import org.vfny.geoserver.global.CoverageInfo;
045: import org.vfny.geoserver.global.FeatureTypeInfo;
046: import org.vfny.geoserver.global.GeoServer;
047: import org.vfny.geoserver.global.MapLayerInfo;
048: import org.vfny.geoserver.wms.WmsException;
049: import org.vfny.geoserver.wms.requests.GetFeatureInfoRequest;
050: import org.vfny.geoserver.wms.requests.GetMapRequest;
051:
052: import com.vividsolutions.jts.geom.Coordinate;
053: import com.vividsolutions.jts.geom.Envelope;
054: import com.vividsolutions.jts.geom.GeometryFactory;
055: import com.vividsolutions.jts.geom.LinearRing;
056: import com.vividsolutions.jts.geom.Polygon;
057:
058: /**
059: * Abstract class to do the common work of the FeatureInfoResponse subclasses.
060: * Subclasses should just need to implement writeTo(), to write the actual
061: * response, the executions are handled here, figuring out where on the map
062: * the pixel is located.
063: *
064: * <p>
065: * Would be nice to have some greater control over the pixels that are
066: * selected. Ideally we would be able to detect things like the size of the
067: * mark, so that users need not click on the exact center, or the exact pixel.
068: * This is not a big deal for polygons, but is for lines and points. One
069: * half solution to make things a bit nicer would be a global parameter to set
070: * a wider pixel range.
071: * </p>
072: *
073: * @author James Macgill, PSU
074: * @author Gabriel Roldan, Axios
075: * @author Chris Holmes, TOPP
076: * @author Brent Owens, TOPP
077: */
078: public abstract class AbstractFeatureInfoResponse extends
079: GetFeatureInfoDelegate {
080: /** A logger for this class. */
081: protected static final Logger LOGGER = org.geotools.util.logging.Logging
082: .getLogger("org.vfny.geoserver.responses.wms.featureinfo");
083:
084: /** The formats supported by this map delegate. */
085: protected List supportedFormats = null;
086: protected List results;
087: protected List metas;
088:
089: /**
090: * setted in execute() from the requested output format, it's holded just
091: * to be sure that method has been called before getContentType() thus
092: * supporting the workflow contract of the request processing
093: */
094: protected String format = null;
095:
096: /**
097: * Creates a new GetMapDelegate object.
098: */
099:
100: /**
101: * Autogenerated proxy constructor.
102: */
103: public AbstractFeatureInfoResponse() {
104: super ();
105: }
106:
107: /**
108: * Returns the content encoding for the output data.
109: *
110: * <p>
111: * Note that this reffers to an encoding applied to the response stream
112: * (such as GZIP or DEFLATE), and not to the MIME response type, wich is
113: * returned by <code>getContentType()</code>
114: * </p>
115: *
116: * @return <code>null</code> since no special encoding is performed while
117: * wrtting to the output stream.
118: */
119: public String getContentEncoding() {
120: return null;
121: }
122:
123: /**
124: * Writes the image to the client.
125: *
126: * @param out The output stream to write to.
127: *
128: * @throws ServiceException DOCUMENT ME!
129: * @throws IOException DOCUMENT ME!
130: */
131: public abstract void writeTo(OutputStream out)
132: throws ServiceException, IOException;
133:
134: /**
135: * The formats this delegate supports.
136: *
137: * @return The list of the supported formats
138: */
139: public List getSupportedFormats() {
140: return supportedFormats;
141: }
142:
143: /**
144: * DOCUMENT ME!
145: *
146: * @param gs app context
147: *
148: * @task TODO: implement
149: */
150: public void abort(GeoServer gs) {
151: }
152:
153: /**
154: * Gets the content type. This is set by the request, should only be
155: * called after execute. GetMapResponse should handle this though.
156: *
157: * @param gs server configuration
158: *
159: * @return The mime type that this response will generate.
160: *
161: * @throws IllegalStateException if<code>execute()</code> has not been
162: * previously called
163: */
164: public String getContentType(GeoServer gs) {
165: if (format == null) {
166: throw new IllegalStateException(
167: "Content type unknown since execute() has not been called yet");
168: }
169:
170: // chain geoserver charset so that multibyte feature info responses
171: // gets properly encoded, same as getCapabilities responses
172: return format + ";charset=" + gs.getCharSet().name();
173: }
174:
175: /**
176: * Performs the execute request using geotools rendering.
177: *
178: * @param requestedLayers The information on the types requested.
179: * @param queries The results of the queries to generate maps with.
180: * @param x DOCUMENT ME!
181: * @param y DOCUMENT ME!
182: *
183: * @throws WmsException For any problems.
184: */
185: protected void execute(MapLayerInfo[] requestedLayers,
186: Filter[] filters, int x, int y) throws WmsException {
187: GetFeatureInfoRequest request = getRequest();
188: this .format = request.getInfoFormat();
189:
190: GetMapRequest getMapReq = request.getGetMapRequest();
191: CoordinateReferenceSystem requestedCRS = getMapReq.getCrs(); // optional, may be null
192:
193: int width = getMapReq.getWidth();
194: int height = getMapReq.getHeight();
195: Envelope bbox = getMapReq.getBbox();
196:
197: Coordinate upperLeft = pixelToWorld(x - 2, y - 2, bbox, width,
198: height);
199: Coordinate middle = pixelToWorld(x, y, bbox, width, height);
200: Coordinate lowerRight = pixelToWorld(x + 2, y + 2, bbox, width,
201: height);
202:
203: Coordinate[] coords = new Coordinate[5];
204: coords[0] = upperLeft;
205: coords[1] = new Coordinate(lowerRight.x, upperLeft.y);
206: coords[2] = lowerRight;
207: coords[3] = new Coordinate(upperLeft.x, lowerRight.y);
208: coords[4] = coords[0];
209:
210: GeometryFactory geomFac = new GeometryFactory();
211: LinearRing boundary = geomFac.createLinearRing(coords); // this needs to be done with each FT so it can be reprojected
212: FilterFactory2 filterFac = CommonFactoryFinder
213: .getFilterFactory2(GeoTools.getDefaultHints());
214:
215: final int layerCount = requestedLayers.length;
216: results = new ArrayList(layerCount);
217: metas = new ArrayList(layerCount);
218:
219: try {
220: for (int i = 0; i < layerCount; i++) {
221: if (requestedLayers[i].getType() == org.vfny.geoserver.global.Data.TYPE_VECTOR
222: .intValue()) {
223: FeatureTypeInfo finfo = requestedLayers[i]
224: .getFeature();
225:
226: CoordinateReferenceSystem dataCRS = finfo
227: .getFeatureType().getDefaultGeometry()
228: .getCoordinateSystem();
229:
230: // reproject the bounding box
231: Polygon pixelRect = geomFac.createPolygon(boundary,
232: null);
233: if ((requestedCRS != null)
234: && !CRS.equalsIgnoreMetadata(dataCRS,
235: requestedCRS)) {
236: try {
237: MathTransform transform = CRS
238: .findMathTransform(requestedCRS,
239: dataCRS, true);
240: pixelRect = (Polygon) JTS.transform(
241: pixelRect, transform); // reprojected
242: } catch (MismatchedDimensionException e) {
243: LOGGER.severe(e.getLocalizedMessage());
244: } catch (TransformException e) {
245: LOGGER.severe(e.getLocalizedMessage());
246: } catch (FactoryException e) {
247: LOGGER.severe(e.getLocalizedMessage());
248: }
249: }
250:
251: Filter getFInfoFilter = null;
252: try {
253: getFInfoFilter = filterFac.intersects(filterFac
254: .property(finfo.getFeatureType()
255: .getDefaultGeometry()
256: .getLocalName()), filterFac
257: .literal(pixelRect));
258: } catch (IllegalFilterException e) {
259: e.printStackTrace();
260: throw new WmsException(null,
261: "Internal error : " + e.getMessage());
262: }
263:
264: // include the eventual layer definition filter
265: if (filters[i] != null) {
266: getFInfoFilter = filterFac.and(getFInfoFilter,
267: filters[i]);
268: }
269:
270: Query q = new DefaultQuery(finfo.getTypeName(),
271: null, getFInfoFilter, request
272: .getFeatureCount(),
273: Query.ALL_NAMES, null);
274: FeatureCollection match = finfo.getFeatureSource()
275: .getFeatures(q);
276:
277: //this was crashing Gml2FeatureResponseDelegate due to not setting
278: //the featureresults, thus not being able of querying the SRS
279: //if (match.getCount() > 0) {
280: results.add(match);
281: metas.add(requestedLayers[i]);
282:
283: //}
284: } else {
285: CoverageInfo cinfo = requestedLayers[i]
286: .getCoverage();
287: GridCoverage2D coverage = ((GridCoverage2D) cinfo
288: .getCoverage()).geophysics(true);
289: // MathTransform mathTrans = coverage.getGridGeometry().getGridToCRS().inverse();
290: // DirectPosition position = mathTrans.transform(new DirectPosition2D(middle.x, middle.y), null);
291: DirectPosition position = new DirectPosition2D(
292: requestedCRS, middle.x, middle.y);
293: try {
294: double[] pixelValues = coverage.evaluate(
295: position, (double[]) null);
296: FeatureCollection pixel = wrapPixelInFeatureCollection(
297: coverage, pixelValues, cinfo.getName());
298: metas.add(requestedLayers[i]);
299: results.add(pixel);
300: } catch (PointOutsideCoverageException e) {
301: // it's fine, users might legitimately query point outside, we just don't return anything
302: }
303: }
304: }
305: } catch (Exception e) {
306: throw new WmsException(null, "Internal error occurred", e);
307: }
308: }
309:
310: private FeatureCollection wrapPixelInFeatureCollection(
311: GridCoverage2D coverage, double[] pixelValues,
312: String coverageName) throws SchemaException,
313: IllegalAttributeException {
314: GridSampleDimension[] sampleDimensions = coverage
315: .getSampleDimensions();
316: AttributeType[] types = new AttributeType[sampleDimensions.length];
317: FeatureType gridType;
318: try {
319: for (int i = 0; i < types.length; i++) {
320: types[i] = AttributeTypeFactory
321: .newAttributeType(sampleDimensions[i]
322: .getDescription().toString(),
323: Double.class);
324: }
325: gridType = FeatureTypeBuilder.newFeatureType(types,
326: coverageName);
327: } catch (Exception e) {
328: // sometimes a grid coverage format does not assign unique descriptions to coverages
329: for (int i = 0; i < types.length; i++) {
330: types[i] = AttributeTypeFactory.newAttributeType(
331: "Band " + (i + 1), Double.class);
332: }
333: gridType = FeatureTypeBuilder.newFeatureType(types,
334: coverageName);
335: }
336:
337: Double[] values = new Double[pixelValues.length];
338: for (int i = 0; i < values.length; i++) {
339: values[i] = new Double(pixelValues[i]);
340: }
341: return DataUtilities.collection(gridType.create(values, ""));
342: }
343:
344: /**
345: * Converts a coordinate expressed on the device space back to real world
346: * coordinates. Stolen from LiteRenderer but without the need of a
347: * Graphics object
348: *
349: * @param x horizontal coordinate on device space
350: * @param y vertical coordinate on device space
351: * @param map The map extent
352: * @param width image width
353: * @param height image height
354: *
355: * @return The correspondent real world coordinate
356: *
357: * @throws RuntimeException DOCUMENT ME!
358: */
359: private Coordinate pixelToWorld(int x, int y, Envelope map,
360: int width, int height) {
361: //set up the affine transform and calculate scale values
362: AffineTransform at = worldToScreenTransform(map, width, height);
363:
364: Point2D result = null;
365:
366: try {
367: result = at.inverseTransform(
368: new java.awt.geom.Point2D.Double(x, y),
369: new java.awt.geom.Point2D.Double());
370: } catch (NoninvertibleTransformException e) {
371: throw new RuntimeException(e);
372: }
373:
374: Coordinate c = new Coordinate(result.getX(), result.getY());
375:
376: return c;
377: }
378:
379: /**
380: * Sets up the affine transform. Stolen from liteRenderer code.
381: *
382: * @param mapExtent the map extent
383: * @param width the screen size
384: * @param height DOCUMENT ME!
385: *
386: * @return a transform that maps from real world coordinates to the screen
387: */
388: private AffineTransform worldToScreenTransform(Envelope mapExtent,
389: int width, int height) {
390: double scaleX = (double) width / mapExtent.getWidth();
391: double scaleY = (double) height / mapExtent.getHeight();
392:
393: double tx = -mapExtent.getMinX() * scaleX;
394: double ty = (mapExtent.getMinY() * scaleY) + height;
395:
396: AffineTransform at = new AffineTransform(scaleX, 0.0d, 0.0d,
397: -scaleY, tx, ty);
398:
399: return at;
400: }
401: }
|