/* 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.geoserver.wms;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.geoserver.platform.GeoServerExtensions;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.CRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.referencing.operation.projection.ProjectionException;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.TransformException;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.vfny.geoserver.wms.WmsException;
import org.vfny.geoserver.wms.requests.DescribeLayerRequest;
import org.vfny.geoserver.wms.requests.GetFeatureInfoRequest;
import org.vfny.geoserver.wms.requests.GetLegendGraphicRequest;
import org.vfny.geoserver.wms.requests.GetMapRequest;
import org.vfny.geoserver.wms.requests.WMSCapabilitiesRequest;
import org.vfny.geoserver.wms.responses.DescribeLayerResponse;
import org.vfny.geoserver.wms.responses.GetFeatureInfoResponse;
import org.vfny.geoserver.wms.responses.GetLegendGraphicResponse;
import org.vfny.geoserver.wms.responses.GetMapResponse;
import org.vfny.geoserver.wms.responses.WMSCapabilitiesResponse;
import org.vfny.geoserver.wms.responses.map.kml.KMLReflector;
import org.vfny.geoserver.wms.servlets.Capabilities;
import org.vfny.geoserver.wms.servlets.DescribeLayer;
import org.vfny.geoserver.wms.servlets.GetFeatureInfo;
import org.vfny.geoserver.wms.servlets.GetLegendGraphic;
import org.vfny.geoserver.wms.servlets.GetMap;
import com.vividsolutions.jts.geom.Envelope;
public class DefaultWebMapService implements WebMapService,
ApplicationContextAware {
/**
* default for 'format' parameter.
*/
public static String FORMAT = "image/png";
/**
* default for 'styles' parameter.
*/
public static List STYLES = Collections.EMPTY_LIST;
/**
* longest side for the preview
*/
public static int MAX_SIDE = 512;
/**
* minimum height to have a decent looking OL preview
*/
public static int MIN_OL_HEIGHT = 330;
/**
* default for 'srs' parameter.
*/
public static String SRS = "EPSG:4326";
/**
* default for 'transparent' parameter.
*/
public static Boolean TRANSPARENT = Boolean.TRUE;
/**
* default for 'bbox' paramter
*/
public static ReferencedEnvelope BBOX = new ReferencedEnvelope(
new Envelope(-180, 180, -90, 90), DefaultGeographicCRS.WGS84);
/**
* wms configuration
*/
WMS wms;
/**
* Application context
*/
ApplicationContext context;
/**
* Temporary field that handles the usage of the line width optimization code
*/
private static Boolean OPTIMIZE_LINE_WIDTH = null;
/**
* Temporary field that handles the choice of renderer to be used
*/
private static Boolean USE_STREAMING_RENDERER = null;
/**
* Max number of rule filters to be used against the data source
*/
public static Integer MAX_FILTER_RULES = null;
/**
* Activate advanced projection handling
*/
private static Boolean ADVANCED_PROJECTION_HANDLING = null;
public DefaultWebMapService( WMS wms ) {
this.wms = wms;
}
/**
* @see WebMapService#getServiceInfo()
*/
public WMSInfo getServiceInfo() {
return wms.getServiceInfo();
}
/**
* @see ApplicationContextAware#setApplicationContext(ApplicationContext)
*/
public void setApplicationContext(ApplicationContext context)
throws BeansException {
this.context = context;
// first time initialization of line width optimization flag
if (OPTIMIZE_LINE_WIDTH == null) {
String enabled = GeoServerExtensions.getProperty("OPTIMIZE_LINE_WIDTH", context);
// default to true, but allow switching off
if(enabled == null)
OPTIMIZE_LINE_WIDTH = false;
else
OPTIMIZE_LINE_WIDTH = Boolean.valueOf(enabled);
}
// initialization of the renderer choice flag
if (USE_STREAMING_RENDERER == null) {
String enabled = GeoServerExtensions.getProperty("USE_STREAMING_RENDERER", context);
// default to true, but allow switching off
if(enabled == null)
USE_STREAMING_RENDERER = false;
else
USE_STREAMING_RENDERER = Boolean.valueOf(enabled);
}
// initialization of the renderer choice flag
if (MAX_FILTER_RULES == null) {
String rules = GeoServerExtensions.getProperty("MAX_FILTER_RULES", context);
// default to true, but allow switching off
if(rules == null)
MAX_FILTER_RULES = 20;
else
MAX_FILTER_RULES = Integer.valueOf(rules);
}
// first time initialization of advanced projection handling
if (ADVANCED_PROJECTION_HANDLING == null) {
String enabled = GeoServerExtensions.getProperty("ADVANCED_PROJECTION_HANDLING", context);
// default to true, but allow switching off
if(enabled == null)
ADVANCED_PROJECTION_HANDLING = false;
else
ADVANCED_PROJECTION_HANDLING = Boolean.valueOf(enabled);
}
}
/**
* Checks wheter the line width optimization is enabled, or not (defaults to true
* unless the user sets the OPTIMIZE_LINE_WIDTH property to false)
* @return
*/
public static boolean isLineWidthOptimizationEnabled() {
return OPTIMIZE_LINE_WIDTH;
}
/**
* Checks wheter the line streaming renderer is enabled, or not (defaults to false
* unless the user sets the USE_STREAMING_RENDERER property to true)
* @return
*/
public static boolean useStreamingRenderer() {
return USE_STREAMING_RENDERER;
}
/**
* If true (default) use the sld rule filters to compose the query to the DB,
* otherwise don't and get down only with the bbox and eventual definition filter)
* @return
*/
public static int getMaxFilterRules() {
return MAX_FILTER_RULES;
}
/**
* Returns true if projection geometry cutting and wrapping is enabled
* @return
*/
public static boolean isAdvancedProjectionHandlingEnabled() {
return ADVANCED_PROJECTION_HANDLING;
}
/**
* @see WebMapService#getCapabilities(WMSCapabilitiesRequest)
*/
public WMSCapabilitiesResponse getCapabilities(
WMSCapabilitiesRequest request) {
Capabilities capabilities = (Capabilities) context
.getBean("wmsGetCapabilities");
return (WMSCapabilitiesResponse) capabilities.getResponse();
}
/**
* @see WebMapService#capabilities(WMSCapabilitiesRequest)
*/
public WMSCapabilitiesResponse capabilities(WMSCapabilitiesRequest request) {
return getCapabilities(request);
}
/**
* @see WebMapService#describeLayer(DescribeLayerRequest)
*/
public DescribeLayerResponse describeLayer(DescribeLayerRequest request) {
DescribeLayer describeLayer = (DescribeLayer) context
.getBean("wmsDescribeLayer");
return (DescribeLayerResponse) describeLayer.getResponse();
}
/**
* @see WebMapService#getMap(GetMapRequest)
*/
public GetMapResponse getMap(GetMapRequest request) {
return new GetMapResponse(WMSExtensions.findMapProducers(context));
}
/**
* @see WebMapService#map(GetMapRequest)
*/
public GetMapResponse map(GetMapRequest request) {
return getMap(request);
}
/**
* @see WebMapService#getFeatureInfo(GetFeatureInfoRequest)
*/
public GetFeatureInfoResponse getFeatureInfo(GetFeatureInfoRequest request) {
GetFeatureInfo getFeatureInfo =
(GetFeatureInfo) context.getBean("wmsGetFeatureInfo");
return (GetFeatureInfoResponse) getFeatureInfo.getResponse();
}
/**
* @see WebMapService#getLegendGraphic(GetLegendGraphicRequest)
*/
public GetLegendGraphicResponse getLegendGraphic(
GetLegendGraphicRequest request) {
GetLegendGraphic getLegendGraphic =
(GetLegendGraphic) context.getBean("wmsGetLegendGraphic");
return (GetLegendGraphicResponse) getLegendGraphic.getResponse();
}
public void kml(GetMapRequest getMap, HttpServletResponse response){
try{
KMLReflector.doWms(getMap, response, this);
// return response;
} catch (Exception e){
throw new RuntimeException(e);
}
}
/**
* @see WebMapService#reflect(GetMapRequest)
*/
public GetMapResponse reflect(GetMapRequest request) {
return getMapReflect(request);
}
/**
* @see WebMapService#getMapReflect(GetMapRequest)
*/
public GetMapResponse getMapReflect(GetMapRequest request) {
GetMapRequest getMap = (GetMapRequest) request;
// set the defaults
if (getMap.getFormat() == null) {
getMap.setFormat(FORMAT);
}
if ((getMap.getStyles() == null) || getMap.getStyles().isEmpty()) {
// set styles to be the defaults for the specified layers
// TODO: should this be part of core WMS logic? is so lets throw
// this
// into the GetMapKvpRequestReader
if ((getMap.getLayers() != null) && (getMap.getLayers().length > 0)) {
ArrayList styles = new ArrayList(getMap.getLayers().length);
for (int i = 0; i < getMap.getLayers().length; i++) {
styles.add(getMap.getLayers()[i].getDefaultStyle());
}
getMap.setStyles(styles);
} else {
getMap.setStyles(STYLES);
}
}
// auto-magic missing info configuration
autoSetBoundsAndSize(getMap);
return getMap(getMap);
}
/**
* This method tries to automatically determine SRS, bounding box and output
* size based on the layers provided by the user and any other parameters.
*
* If bounds are not specified by the user, they are automatically se to the
* union of the bounds of all layers.
*
* The size of the output image defaults to 512 pixels, the height is
* automatically determined based on the width to height ratio of the
* requested layers. This is also true if either height or width are
* specified by the user. If both height and width are specified by the
* user, the automatically determined bounding box will be adjusted to fit
* inside these bounds.
*
* General idea
* 1) Figure out whether SRS has been specified, fall back to EPSG:4326
* 2) Determine whether all requested layers use the same SRS,
* - if so, try to do bounding box calculations in native coordinates
* 3) Aggregate the bounding boxes (in EPSG:4326 or native)
* 4a) If bounding box has been specified, adjust height of image to match
* 4b) If bounding box has not been specified, but height has, adjust bounding box
*/
public static void autoSetBoundsAndSize(GetMapRequest getMap) {
// Get the layers
MapLayerInfo[] layers = getMap.getLayers();
/** 1) Check what SRS has been requested */
String reqSRS = getMap.getSRS();
// if none, try to determine which SRS to use
// and keep track of whether we can use native all the way
boolean useNativeBounds = true;
if(reqSRS == null) {
reqSRS = guessCommonSRS(layers);
forceSRS(getMap, reqSRS);
}
/** 2) Compare requested SRS */
for (int i = 0; useNativeBounds && i < layers.length; i++) {
if (layers[i] != null) {
String layerSRS = layers[i].getSRS();
useNativeBounds = reqSRS.equalsIgnoreCase(layerSRS);
} else {
useNativeBounds = false;
}
}
CoordinateReferenceSystem reqCRS;
try {
reqCRS = CRS.decode(reqSRS);
} catch(Exception e) {
throw new WmsException(e);
}
// Ready to determine the bounds based on the layers, if not specified
Envelope aggregateBbox = getMap.getBbox();
boolean specifiedBbox = true;
// If bbox is not specified by request
if (aggregateBbox == null) {
specifiedBbox = false;
// Get the bounding box from the layers
for (int i = 0; i < layers.length; i++) {
MapLayerInfo layerInfo = layers[i];
ReferencedEnvelope curbbox;
try{
curbbox = layerInfo.getLatLongBoundingBox();
if(useNativeBounds){
ReferencedEnvelope nativeBbox = layerInfo.getBoundingBox();
if(nativeBbox == null){
try {
CoordinateReferenceSystem nativeCrs = layerInfo.getCoordinateReferenceSystem();
nativeBbox = curbbox.transform(nativeCrs, true);
} catch(Exception e) {
throw new WmsException("Best effort native bbox computation failed", "", e);
}
}
curbbox = nativeBbox;
}
}catch(Exception e){
throw new RuntimeException(e);
}
if (aggregateBbox != null) {
aggregateBbox.expandToInclude(curbbox);
} else {
aggregateBbox = curbbox;
}
}
ReferencedEnvelope ref = null;
// Reproject back to requested SRS if we have to
if (!useNativeBounds && ! reqSRS.equalsIgnoreCase(SRS)) {
try {
ref = new ReferencedEnvelope(aggregateBbox,
CRS.decode("EPSG:4326"));
aggregateBbox = ref.transform(reqCRS, true);
} catch (ProjectionException pe) {
ref.expandBy( -1 * ref.getWidth() / 50, -1 * ref.getHeight() / 50);
try {
aggregateBbox = ref.transform(reqCRS, true);
} catch (FactoryException e) {
e.printStackTrace();
} catch (TransformException e) {
e.printStackTrace();
}
// And again...
} catch (NoSuchAuthorityCodeException e) {
e.printStackTrace();
} catch (TransformException e) {
e.printStackTrace();
} catch (FactoryException e) {
e.printStackTrace();
}
}
}
// Just in case
if (aggregateBbox == null) {
forceSRS(getMap, DefaultWebMapService.SRS);
aggregateBbox = DefaultWebMapService.BBOX;
}
// Start the processing of adjust either the bounding box
// or the pixel height / width
double bbheight = aggregateBbox.getHeight();
double bbwidth = aggregateBbox.getWidth();
double bbratio = bbwidth / bbheight;
double mheight = getMap.getHeight();
double mwidth = getMap.getWidth();
if (mheight > 0.5 && mwidth > 0.5 && specifiedBbox) {
// This person really doesnt want our help,
// we'll warp it any way they like it...
} else {
if (mheight > 0.5 && mwidth > 0.5) {
// Fully specified, need to adjust bbox
double mratio = mwidth / mheight;
// Adjust bounds to be less than ideal to meet spec
if (bbratio > mratio) {
// Too wide, need to increase height of bb
double diff = ((bbwidth / mratio) - bbheight) / 2;
aggregateBbox.expandBy(0, diff);
} else {
// Too tall, need to increase width of bb
double diff = ((bbheight * mratio) - bbwidth) / 2;
aggregateBbox.expandBy(diff, 0);
}
adjustBounds(reqSRS, aggregateBbox);
} else if (mheight > 0.5) {
mwidth = bbratio * mheight;
} else {
if (mwidth > 0.5) {
mheight = (mwidth / bbratio >= 1) ? mwidth / bbratio : 1;
} else {
if(bbratio > 1) {
mwidth = MAX_SIDE;
mheight = (mwidth / bbratio >= 1) ? mwidth / bbratio : 1;
} else {
mheight = MAX_SIDE;
mwidth = (mheight * bbratio >= 1) ? mheight * bbratio : 1;
}
// make sure OL output height is sufficient to show the OL scale bar fully
if(mheight < MIN_OL_HEIGHT && (
"application/openlayers".equalsIgnoreCase(getMap.getFormat())
|| "openlayers".equalsIgnoreCase(getMap.getFormat()))) {
mheight = MIN_OL_HEIGHT;
mwidth = (mheight * bbratio >= 1) ? mheight * bbratio : 1;
}
}
}
// Actually set the bounding box and size of image
getMap.setBbox(aggregateBbox);
getMap.setWidth((int) mwidth);
getMap.setHeight((int) mheight);
}
}
private static String guessCommonSRS(MapLayerInfo[] layers) {
String SRS = null;
for (MapLayerInfo layer : layers) {
String layerSRS = layer.getSRS();
if(SRS == null) {
SRS = layerSRS.toUpperCase();
} else if(!SRS.equals(layerSRS)) {
// layers with mixed native SRS, let's just use the default
return DefaultWebMapService.SRS;
}
}
if(SRS == null) {
return DefaultWebMapService.SRS;
}
return SRS;
}
private static void forceSRS(GetMapRequest getMap, String srs) {
getMap.setSRS(srs);
try {
getMap.setCrs( CRS.decode(srs) );
} catch (NoSuchAuthorityCodeException e) {
e.printStackTrace();
} catch (FactoryException e) {
e.printStackTrace();
}
}
/**
* This adjusts the bounds by zooming out 2%, but also ensuring that
* the maximum bounds do not exceed the world bounding box
*
* This only applies if the SRS is EPSG:4326 or EPSG:900913
*
* @param reqSRS the SRS
* @param bbox the current bounding box
* @return the adjusted bounding box
*/
private static Envelope adjustBounds(String reqSRS, Envelope bbox) {
if(reqSRS.equalsIgnoreCase("EPSG:4326")) {
bbox.expandBy(bbox.getWidth() / 100, bbox.getHeight() / 100);
Envelope maxEnv = new Envelope(
-180.0,-90.0,
180.0,90.0 );
return bbox.intersection(maxEnv);
} else if(reqSRS.equalsIgnoreCase("EPSG:900913")) {
bbox.expandBy(bbox.getWidth() / 100, bbox.getHeight() / 100);
Envelope maxEnv = new Envelope(
-20037508.33, -20037508.33,
20037508.33, 20037508.33 );
return bbox.intersection(maxEnv);
}
return bbox;
}
}