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;
006:
007: import java.io.IOException;
008: import java.io.OutputStream;
009: import java.util.Collection;
010: import java.util.Collections;
011: import java.util.HashMap;
012: import java.util.HashSet;
013: import java.util.Iterator;
014: import java.util.Map;
015: import java.util.Set;
016: import java.util.logging.Level;
017: import java.util.logging.Logger;
018:
019: import org.geoserver.platform.GeoServerExtensions;
020: import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader;
021: import org.geotools.data.DefaultQuery;
022: import org.geotools.data.FeatureSource;
023: import org.geotools.data.Query;
024: import org.geotools.factory.FactoryConfigurationError;
025: import org.geotools.feature.IllegalAttributeException;
026: import org.geotools.feature.SchemaException;
027: import org.geotools.map.DefaultMapLayer;
028: import org.geotools.referencing.crs.DefaultGeographicCRS;
029: import org.geotools.resources.coverage.FeatureUtilities;
030: import org.geotools.styling.Style;
031: import org.opengis.filter.Filter;
032: import org.opengis.parameter.ParameterDescriptor;
033: import org.opengis.parameter.ParameterNotFoundException;
034: import org.opengis.parameter.ParameterValue;
035: import org.opengis.parameter.ParameterValueGroup;
036: import org.opengis.referencing.crs.CoordinateReferenceSystem;
037: import org.opengis.referencing.operation.TransformException;
038: import org.springframework.context.ApplicationContext;
039: import org.vfny.geoserver.Request;
040: import org.vfny.geoserver.Response;
041: import org.vfny.geoserver.ServiceException;
042: import org.vfny.geoserver.global.GeoServer;
043: import org.vfny.geoserver.global.MapLayerInfo;
044: import org.vfny.geoserver.global.Service;
045: import org.vfny.geoserver.global.WMS;
046: import org.vfny.geoserver.util.CoverageUtils;
047: import org.vfny.geoserver.wms.GetLegendGraphicProducerSpi;
048: import org.vfny.geoserver.wms.GetMapProducer;
049: import org.vfny.geoserver.wms.GetMapProducerFactorySpi;
050: import org.vfny.geoserver.wms.RasterMapProducer;
051: import org.vfny.geoserver.wms.WMSMapContext;
052: import org.vfny.geoserver.wms.WmsException;
053: import org.vfny.geoserver.wms.requests.GetMapRequest;
054: import org.vfny.geoserver.wms.responses.map.metatile.MetatileMapProducer;
055:
056: import com.vividsolutions.jts.geom.Envelope;
057:
058: /**
059: * A GetMapResponse object is responsible of generating a map based on a GetMap
060: * request. The way the map is generated is independent of this class, wich will
061: * use a delegate object based on the output format requested
062: *
063: * @author Gabriel Roldan, Axios Engineering
064: * @author Simone Giannecchini - GeoSolutions SAS
065: * @version $Id: GetMapResponse.java 8438 2008-02-25 08:52:45Z aaime $
066: */
067: public class GetMapResponse implements Response {
068: /** DOCUMENT ME! */
069: private static final Logger LOGGER = org.geotools.util.logging.Logging
070: .getLogger(GetMapResponse.class.getPackage().getName());
071:
072: /**
073: * The map producer that will be used for the production of a map in the
074: * requested format.
075: */
076: private GetMapProducer delegate;
077:
078: /**
079: * The map context
080: */
081: private WMSMapContext map;
082:
083: /**
084: * WMS module
085: */
086: private WMS wms;
087:
088: /**
089: * custom response headers
090: */
091: private HashMap responseHeaders;
092:
093: String headerContentDisposition;
094:
095: private ApplicationContext applicationContext;
096:
097: /**
098: * Creates a new GetMapResponse object.
099: *
100: * @param applicationContext
101: */
102: public GetMapResponse(WMS wms, ApplicationContext applicationContext) {
103: this .wms = wms;
104: this .applicationContext = applicationContext;
105: responseHeaders = new HashMap(10);
106: }
107:
108: /**
109: * Returns any extra headers that this service might want to set in the HTTP
110: * response object.
111: */
112: public HashMap getResponseHeaders() {
113: return responseHeaders;
114: }
115:
116: /**
117: * DOCUMENT ME!
118: *
119: * @param req
120: * DOCUMENT ME!
121: *
122: * @throws ServiceException
123: * DOCUMENT ME!
124: * @throws WmsException
125: * DOCUMENT ME!
126: */
127: public void execute(Request req) throws ServiceException {
128: GetMapRequest request = (GetMapRequest) req;
129:
130: final String outputFormat = request.getFormat();
131:
132: this .delegate = getDelegate(outputFormat, wms);
133: // JD:make instance variable in order to release resources later
134: // final WMSMapContext map = new WMSMapContext();
135: map = new WMSMapContext(request);
136: this .delegate.setMapContext(map);
137:
138: // enable on the fly meta tiling if request looks like a tiled one
139: if (MetatileMapProducer.isRequestTiled(request, delegate)) {
140: if (LOGGER.isLoggable(Level.FINER)) {
141: LOGGER
142: .finer("Tiled request detected, activating on the fly meta tiler");
143: }
144:
145: this .delegate = new MetatileMapProducer(request,
146: (RasterMapProducer) delegate);
147: this .delegate.setMapContext(map);
148: }
149:
150: final MapLayerInfo[] layers = request.getLayers();
151: final Style[] styles = (Style[]) request.getStyles().toArray(
152: new Style[] {});
153: final Filter[] filters = ((request.getFilter() != null) ? (Filter[]) request
154: .getFilter().toArray(new Filter[] {})
155: : null);
156:
157: // DJB: the WMS spec says that the request must not be 0 area
158: // if it is, throw a service exception!
159: final Envelope env = request.getBbox();
160: if (env == null) {
161: throw new WmsException(
162: "GetMap requests must include a BBOX parameter.");
163: }
164: if (env.isNull() || (env.getWidth() <= 0)
165: || (env.getHeight() <= 0)) {
166: throw new WmsException(new StringBuffer(
167: "The request bounding box has zero area: ").append(
168: env).toString());
169: }
170:
171: // DJB DONE: replace by setAreaOfInterest(Envelope,
172: // CoordinateReferenceSystem)
173: // with the user supplied SRS parameter
174:
175: // if there's a crs in the request, use that. If not, assume its 4326
176: final CoordinateReferenceSystem mapcrs = request.getCrs();
177:
178: // DJB: added this to be nicer about the "NONE" srs.
179: if (mapcrs != null) {
180: map.setAreaOfInterest(env, mapcrs);
181: } else {
182: map.setAreaOfInterest(env, DefaultGeographicCRS.WGS84);
183: }
184:
185: map.setMapWidth(request.getWidth());
186: map.setMapHeight(request.getHeight());
187: map.setBgColor(request.getBgColor());
188: map.setTransparent(request.isTransparent());
189: map.setBuffer(request.getBuffer());
190: map.setPaletteInverter(request.getPalette());
191:
192: // //
193: //
194: // Check to see if we really have something to display. Sometimes width
195: // or height or both are non positivie or the requested area is null.
196: //
197: // ///
198: if ((request.getWidth() <= 0) || (request.getHeight() <= 0)
199: || (map.getAreaOfInterest().getLength(0) <= 0)
200: || (map.getAreaOfInterest().getLength(1) <= 0)) {
201: if (LOGGER.isLoggable(Level.FINE)) {
202: LOGGER
203: .fine("We are not going to render anything because either the are is null ar the dimensions are not positive.");
204: }
205:
206: return;
207: }
208:
209: if (LOGGER.isLoggable(Level.FINE)) {
210: LOGGER.fine("setting up map");
211: }
212:
213: try { // mapcontext can leak memory -- we make sure we done (see
214: // finally block)
215:
216: // track the external caching strategy for any map layers
217: boolean cachingPossible = request.getHttpServletRequest()
218: .getMethod().equals("GET");
219: String featureVersion = request.getFeatureVersion();
220: int maxAge = Integer.MAX_VALUE;
221:
222: final int length = layers.length;
223:
224: for (int i = 0; i < length; i++) {
225: final Style style = styles[i];
226: Filter optionalFilter;
227: try {
228: optionalFilter = filters[i];
229: } catch (Exception e) {
230: optionalFilter = null;
231: }
232:
233: final DefaultMapLayer layer;
234: if (layers[i].getType() == MapLayerInfo.TYPE_REMOTE_VECTOR) {
235: cachingPossible = false;
236:
237: final FeatureSource source = layers[i]
238: .getRemoteFeatureSource();
239: layer = new DefaultMapLayer(source, style);
240: layer.setTitle(layers[i].getName());
241:
242: final DefaultQuery definitionQuery;
243: if (optionalFilter != null) {
244: definitionQuery = new DefaultQuery(source
245: .getSchema().getTypeName(),
246: optionalFilter);
247: definitionQuery.setVersion(featureVersion);
248:
249: layer.setQuery(definitionQuery);
250: } else if (featureVersion != null) {
251: definitionQuery = new DefaultQuery(source
252: .getSchema().getTypeName());
253: definitionQuery.setVersion(featureVersion);
254:
255: layer.setQuery(definitionQuery);
256: }
257:
258: map.addLayer(layer);
259: } else if (layers[i].getType() == MapLayerInfo.TYPE_VECTOR) {
260: if (cachingPossible) {
261: if (layers[i].getFeature().isCachingEnabled()) {
262: int nma = Integer.parseInt(layers[i]
263: .getFeature().getCacheMaxAge());
264:
265: // suppose the map contains multiple cachable
266: // layers...we can only cache the combined map for
267: // the
268: // time specified by the shortest-cached layer.
269: if (nma < maxAge) {
270: maxAge = nma;
271: }
272: } else {
273: // if one layer isn't cachable, then we can't cache
274: // any of them. Disable caching.
275: cachingPossible = false;
276: }
277: }
278:
279: final FeatureSource source;
280: // /////////////////////////////////////////////////////////
281: //
282: // Adding a feature layer
283: //
284: // /////////////////////////////////////////////////////////
285: try {
286: source = layers[i].getFeature()
287: .getFeatureSource(true);
288:
289: // NOTE for the feature. Here there was some code that
290: // sounded like:
291: // * get the bounding box from feature source
292: // * eventually reproject it to the actual CRS used for
293: // map
294: // * if no intersection, don't bother adding the feature
295: // source to the map
296: // This is not an optimization, on the contrary,
297: // computing the bbox may be
298: // very expensive depending on the data size. Using
299: // sigma.openplans.org data
300: // and a tiled client like OpenLayers, it dragged the
301: // server to his knees
302: // and the client simply timed out
303: } catch (IOException exp) {
304: if (LOGGER.isLoggable(Level.SEVERE)) {
305: LOGGER.log(Level.SEVERE, new StringBuffer(
306: "Getting feature source: ").append(
307: exp.getMessage()).toString(), exp);
308: }
309:
310: throw new WmsException(null, new StringBuffer(
311: "Internal error : ").append(
312: exp.getMessage()).toString());
313: }
314:
315: layer = new DefaultMapLayer(source, style);
316: layer.setTitle(layers[i].getName());
317:
318: final Filter definitionFilter = layers[i]
319: .getFeature().getDefinitionQuery();
320: final DefaultQuery definitionQuery;
321: if (definitionFilter != null) {
322: definitionQuery = new DefaultQuery(source
323: .getSchema().getTypeName(),
324: definitionFilter);
325: definitionQuery.setVersion(featureVersion);
326:
327: layer.setQuery(definitionQuery);
328: } else if (optionalFilter != null) {
329: definitionQuery = new DefaultQuery(source
330: .getSchema().getTypeName(),
331: optionalFilter);
332: definitionQuery.setVersion(featureVersion);
333:
334: layer.setQuery(definitionQuery);
335: } else if (featureVersion != null) {
336: definitionQuery = new DefaultQuery(source
337: .getSchema().getTypeName());
338: definitionQuery.setVersion(featureVersion);
339:
340: layer.setQuery(definitionQuery);
341: }
342:
343: map.addLayer(layer);
344: } else if (layers[i].getType() == MapLayerInfo.TYPE_RASTER) {
345: // /////////////////////////////////////////////////////////
346: //
347: // Adding a coverage layer
348: //
349: // /////////////////////////////////////////////////////////
350: AbstractGridCoverage2DReader reader = (AbstractGridCoverage2DReader) layers[i]
351: .getCoverage().getReader();
352: if (reader != null) {
353: // /////////////////////////////////////////////////////////
354: //
355: // Setting coverage reading params.
356: //
357: // /////////////////////////////////////////////////////////
358:
359: /*
360: * Test if the parameter "TIME" is present in the WMS
361: * request, and by the way in the reading parameters. If
362: * it is the case, one can adds it to the request. If an
363: * exception is thrown, we have nothing to do.
364: */
365: try {
366: ParameterValue time = reader.getFormat()
367: .getReadParameters().parameter(
368: "TIME");
369: if (time != null
370: && request.getTime() != null) {
371: time.setValue(request.getTime());
372: }
373: } catch (ParameterNotFoundException p) {
374: }
375:
376: // uncomment when the DIM_RANGE vendor parameter will be enabled
377: // try {
378: // ParameterValue dimRange = reader.getFormat().getReadParameters()
379: // .parameter("DIM_RANGE");
380: // if (dimRange != null && request.getDimRange() != null) {
381: // dimRange.setValue(request.getDimRange());
382: // }
383: // } catch (ParameterNotFoundException p) {
384: // }
385:
386: try {
387: ParameterValue elevation = reader
388: .getFormat().getReadParameters()
389: .parameter("ELEVATION");
390: if (elevation != null
391: && request.getElevation() != null) {
392: elevation.setValue(request
393: .getElevation().intValue());
394: }
395: } catch (ParameterNotFoundException p) {
396: }
397:
398: try {
399: final ParameterValueGroup params = reader
400: .getFormat().getReadParameters();
401:
402: layer = new DefaultMapLayer(
403: FeatureUtilities
404: .wrapGridCoverageReader(
405: reader,
406: CoverageUtils
407: .getParameters(
408: params,
409: layers[i]
410: .getCoverage()
411: .getParameters())),
412: style);
413:
414: layer.setTitle(layers[i].getName());
415: layer.setQuery(Query.ALL);
416: map.addLayer(layer);
417: } catch (IllegalArgumentException e) {
418: if (LOGGER.isLoggable(Level.SEVERE)) {
419: LOGGER
420: .log(
421: Level.SEVERE,
422: new StringBuffer(
423: "Wrapping GC in feature source: ")
424: .append(
425: e
426: .getLocalizedMessage())
427: .toString(), e);
428: }
429:
430: throw new WmsException(
431: null,
432: new StringBuffer(
433: "Internal error : unable to get reader for this coverage layer ")
434: .append(
435: layers[i]
436: .toString())
437: .toString());
438: }
439: } else {
440: throw new WmsException(
441: null,
442: new StringBuffer(
443: "Internal error : unable to get reader for this coverage layer ")
444: .append(layers[i].toString())
445: .toString());
446: }
447: }
448: }
449:
450: // /////////////////////////////////////////////////////////
451: //
452: // Producing the map in the requested format.
453: //
454: // /////////////////////////////////////////////////////////
455: this .delegate.produceMap();
456:
457: if (cachingPossible) {
458: responseHeaders.put("Cache-Control", "max-age="
459: + maxAge + ", must-revalidate");
460: }
461:
462: final String contentDisposition = this .delegate
463: .getContentDisposition();
464: if (contentDisposition != null) {
465: this .headerContentDisposition = contentDisposition;
466: }
467: } catch (Exception e) {
468: clearMapContext();
469: throw new WmsException(e, "Internal error ", "");
470: }
471: }
472:
473: /**
474: * asks the internal GetMapDelegate for the MIME type of the map that it
475: * will generate or is ready to, and returns it
476: *
477: * @param gs
478: * DOCUMENT ME!
479: *
480: * @return the MIME type of the map generated or ready to generate
481: *
482: * @throws IllegalStateException
483: * if a GetMapDelegate is not setted yet
484: */
485: public String getContentType(GeoServer gs)
486: throws IllegalStateException {
487: if (this .delegate == null) {
488: throw new IllegalStateException(
489: "No request has been processed");
490: }
491:
492: return this .delegate.getContentType();
493: }
494:
495: /**
496: * DOCUMENT ME!
497: *
498: * @return DOCUMENT ME!
499: */
500: public String getContentEncoding() {
501: if (LOGGER.isLoggable(Level.FINER)) {
502: LOGGER.finer("returning content encoding null");
503: }
504:
505: return null;
506: }
507:
508: /**
509: * if a GetMapDelegate is set, calls it's abort method. Elsewere do nothing.
510: *
511: * @param gs
512: * DOCUMENT ME!
513: */
514: public void abort(Service gs) {
515: if (this .delegate != null) {
516: if (LOGGER.isLoggable(Level.FINE)) {
517: LOGGER.fine("asking delegate for aborting the process");
518: }
519:
520: this .delegate.abort();
521: }
522: }
523:
524: /**
525: * delegates the writing and encoding of the results of the request to the
526: * <code>GetMapDelegate</code> wich is actually processing it, and has
527: * been obtained when <code>execute(Request)</code> was called
528: *
529: * @param out
530: * the output to where the map must be written
531: *
532: * @throws ServiceException
533: * if the delegate throws a ServiceException inside its
534: * <code>writeTo(OuptutStream)</code>, mostly due to
535: * @throws IOException
536: * if the delegate throws an IOException inside its
537: * <code>writeTo(OuptutStream)</code>, mostly due to
538: * @throws IllegalStateException
539: * if this method is called before <code>execute(Request)</code>
540: * has succeed
541: */
542: public void writeTo(OutputStream out) throws ServiceException,
543: IOException {
544: try { // mapcontext can leak memory -- we make sure we done (see
545: // finally block)
546:
547: if (this .delegate == null) {
548: throw new IllegalStateException(
549: "No GetMapDelegate is setted, make sure you have called execute and it has succeed");
550: }
551:
552: if (LOGGER.isLoggable(Level.FINER)) {
553: LOGGER.finer(new StringBuffer(
554: "asking delegate for write to ").append(out)
555: .toString());
556: }
557:
558: this .delegate.writeTo(out);
559: } finally {
560: clearMapContext();
561: }
562: }
563:
564: /**
565: * Clearing the map context is paramount, otherwise we end up with a memory leak
566: */
567: void clearMapContext() {
568: try {
569: if (map != null && map.getLayerCount() > 0)
570: map.clearLayerList();
571: } catch (Exception e) // we dont want to propogate a new error
572: {
573: if (LOGGER.isLoggable(Level.SEVERE)) {
574: LOGGER.log(Level.SEVERE, new StringBuffer(
575: "Getting feature source: ").append(
576: e.getMessage()).toString(), e);
577: }
578: }
579: }
580:
581: /**
582: * Creates a GetMapDelegate specialized in generating the requested map
583: * format
584: *
585: * @param outputFormat
586: * a request parameter object wich holds the processed request
587: * objects, such as layers, bbox, outpu format, etc.
588: *
589: * @return A specialization of <code>GetMapDelegate</code> wich can
590: * produce the requested output map format
591: *
592: * @throws WmsException
593: * if no specialization is configured for the output format
594: * specified in <code>request</code> or if it can't be
595: * instantiated
596: */
597: private GetMapProducer getDelegate(String outputFormat, WMS wms)
598: throws WmsException {
599: final Collection producers = GeoServerExtensions
600: .extensions(GetMapProducerFactorySpi.class);
601:
602: for (Iterator iter = producers.iterator(); iter.hasNext();) {
603: final GetMapProducerFactorySpi factory = (GetMapProducerFactorySpi) iter
604: .next();
605:
606: if (factory.canProduce(outputFormat)) {
607: return factory.createMapProducer(outputFormat, wms);
608: }
609: }
610:
611: WmsException e = new WmsException(
612: "There is no support for creating maps in "
613: + outputFormat + " format");
614: e.setCode("InvalidFormat");
615: throw e;
616: }
617:
618: /**
619: * Convenient mehtod to inspect the available
620: * <code>GetMapProducerFactorySpi</code> and return the set of all the map
621: * formats' MIME types that the producers can handle
622: *
623: * @return a Set<String> with the supported mime types.
624: */
625: public Set getMapFormats() {
626: Set wmsGetMapFormats = loadImageFormats(applicationContext);
627:
628: return wmsGetMapFormats;
629: }
630:
631: /**
632: * Convenience method for processing the GetMapProducerFactorySpi extension
633: * point and returning the set of available image formats.
634: *
635: * @param applicationContext
636: * The application context.
637: *
638: */
639: public static Set loadImageFormats(
640: ApplicationContext applicationContext) {
641: final Collection producers = GeoServerExtensions
642: .extensions(GetMapProducerFactorySpi.class);
643: final Set formats = new HashSet();
644:
645: for (Iterator iter = producers.iterator(); iter.hasNext();) {
646: final GetMapProducerFactorySpi producer = (GetMapProducerFactorySpi) iter
647: .next();
648: formats.addAll(producer.getSupportedFormats());
649: }
650:
651: return Collections.unmodifiableSet(formats);
652: }
653:
654: public String getContentDisposition() {
655: return headerContentDisposition;
656: }
657:
658: protected void finalize() throws Throwable {
659: clearMapContext();
660: }
661: }
|