/* (c) 2014 - 2015 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wms;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import net.opengis.wfs.FeatureCollectionType;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.ServiceException;
import org.geoserver.sld.GetStyles;
import org.geoserver.sld.GetStylesRequest;
import org.geoserver.wms.animate.Animator;
import org.geoserver.wms.capabilities.Capabilities_1_3_0_Transformer;
import org.geoserver.wms.capabilities.GetCapabilitiesTransformer;
import org.geoserver.wms.describelayer.DescribeLayerModel;
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.geotools.styling.Style;
import org.geotools.styling.StyledLayerDescriptor;
import org.geotools.xml.transform.TransformerBase;
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.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import com.vividsolutions.jts.geom.Envelope;
/**
* A default implementation of a {@link WebMapService}
* <p>
* This implementations relies on the code setting up the instance to provide the operation beans
* through the following properties:
* <ul>
* <li>{@link #setDescribeLayer DescribeLayer} for the {@link #describeLayer(DescribeLayerRequest)}
* operation
* <li>{@link #setGetCapabilities GetCapabilities} for the
* {@link #getCapabilities(GetCapabilitiesRequest)} operation
* <li>{@link #setGetFeatureInfo GetFeatureInfo} for the
* {@link #getFeatureInfo(GetFeatureInfoRequest)} operation
* <li>{@link #setGetLegendGraphic GetLegendGraphic} for the
* {@link #getLegendGraphic(GetLegendGraphicRequest)} operation
* <li>{@link #setGetMap GetMap} for the {@link #getMap(GetMapRequest)} operation
* <li>{@link #setGetStyles GetStyles} for the {@link #getStyles(GetStylesRequest)} operation
* </ul>
* If an operation is called for which its corresponding operation bean is not set, the call will
* result in an {@link UnsupportedOperationException}
* </p>
*
* @author Andrea Aime
* @author Justin Deoliveira
* @author Gabriel Roldan
*/
public class DefaultWebMapService implements WebMapService, ApplicationContextAware,
DisposableBean {
/**
* default for 'format' parameter.
*/
public static String FORMAT = "image/png";
/**
* default for 'styles' parameter.
*/
public static List<Style> STYLES = Collections.emptyList();
/**
* longest side for the preview
*/
public static int MAX_SIDE = 768;
/**
* minimum height to have a reasonable looking OL preview
*/
public static int MIN_OL_HEIGHT = 330;
/**
* minimum width to have a reasonable looking OL preview
*/
public static int MIN_OL_WIDTH = 330;
/**
* max height to have a reasonable looking OL preview
*/
public static int MAX_OL_HEIGHT = 768;
/**
* max width to have a reasonable looking OL preview
*/
public static int MAX_OL_WIDTH = 1024;
/**
* default for 'srs' parameter.
*/
public static String SRS = "EPSG:4326";
/**
* default for 'transparent' parameter.
*/
public static Boolean TRANSPARENT = Boolean.TRUE;
/**
* default for 'transparent' parameter.
*/
public static ExecutorService RENDERING_POOL;
/**
* default for 'bbox' paramter
*/
public static ReferencedEnvelope BBOX = new ReferencedEnvelope(
new Envelope(-180, 180, -90, 90), DefaultGeographicCRS.WGS84);
/**
* wms configuration
*/
private final WMS wms;
/**
* Temporary field that handles the usage of the line width optimization code
*/
private static Boolean OPTIMIZE_LINE_WIDTH = null;
/**
* This variable is used to bypass direct raster rendering.
*/
private static boolean BYPASS_DIRECT = Boolean.getBoolean("org.geoserver.render.raster.direct.disable");
/**
* Max number of rule filters to be used against the data source
*/
private static Integer MAX_FILTER_RULES = null;
/**
* Use a global rendering pool, or use a new pool each time
*/
private static Boolean USE_GLOBAL_RENDERING_POOL = null;
private GetCapabilities getCapabilities;
private DescribeLayer describeLayer;
private GetMap getMap;
private GetFeatureInfo getFeatureInfo;
private GetStyles getStyles;
private GetLegendGraphic getLegendGraphic;
public DefaultWebMapService(WMS wms) {
this.wms = wms;
}
/**
* @see WebMapService#getServiceInfo()
*/
public WMSInfo getServiceInfo() {
return wms.getServiceInfo();
}
/**
* Establishes the operation bean responsible for executing the GetCapabilities requests
*/
public void setGetCapabilities(GetCapabilities getCapabilities) {
this.getCapabilities = getCapabilities;
}
/**
* Establishes the operation bean responsible for executing the DescribeLayer requests
*/
public void setDescribeLayer(DescribeLayer describeLayer) {
this.describeLayer = describeLayer;
}
/**
* Establishes the operation bean responsible for executing the GetMap requests
*/
public void setGetMap(GetMap getMap) {
this.getMap = getMap;
}
/**
* Establishes the operation bean responsible for executing the GetFeatureInfo requests
*/
public void setGetFeatureInfo(GetFeatureInfo getFeatureInfo) {
this.getFeatureInfo = getFeatureInfo;
}
/**
* Establishes the operation bean responsible for executing the GetStyles requests
*/
public void setGetStyles(GetStyles getStyles) {
this.getStyles = getStyles;
}
/**
* Establishes the operation bean responsible for executing the GetLegendGraphics requests
*/
public void setGetLegendGraphic(GetLegendGraphic getLegendGraphic) {
this.getLegendGraphic = getLegendGraphic;
}
/**
* @see ApplicationContextAware#setApplicationContext(ApplicationContext)
*/
public void setApplicationContext(ApplicationContext context) throws BeansException {
// 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 (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);
}
// control usage of the global rendering thread pool
if (USE_GLOBAL_RENDERING_POOL == null) {
String usePool = GeoServerExtensions.getProperty("USE_GLOBAL_RENDERING_POOL", context);
// default to true, but allow switching off
if (usePool == null)
USE_GLOBAL_RENDERING_POOL = true;
else
USE_GLOBAL_RENDERING_POOL = Boolean.valueOf(usePool);
}
}
/**
* Checks wheter the line width optimization is enabled, or not (defaults to true unless the
* user sets the OPTIMIZE_LINE_WIDTH property to false)
*
*
*/
public static boolean isLineWidthOptimizationEnabled() {
return OPTIMIZE_LINE_WIDTH;
}
/**
* 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)
*
*
*/
public static int getMaxFilterRules() {
return MAX_FILTER_RULES;
}
/**
* If true (default) the direct raster rendering path is enabled
*
*/
public static boolean isDirectRasterPathEnabled() {
return !BYPASS_DIRECT;
}
/**
* @see WebMapService#getCapabilities(GetCapabilitiesRequest)
* @see GetCapabilitiesTransformer
* @see Capabilities_1_3_0_Transformer
*/
public TransformerBase getCapabilities(GetCapabilitiesRequest request) {
if (null == getCapabilities) {
throw new UnsupportedOperationException(
"Operation not properly configured, make sure the operation bean has been set");
}
return getCapabilities.run(request);
}
/**
* @see WebMapService#capabilities(GetCapabilitiesRequest)
*/
public TransformerBase capabilities(GetCapabilitiesRequest request) {
return getCapabilities(request);
}
/**
* @see WebMapService#describeLayer(DescribeLayerRequest)
*/
@Override
public DescribeLayerModel describeLayer(DescribeLayerRequest request) {
if (null == describeLayer) {
throw new UnsupportedOperationException(
"Operation not properly configured, make sure the operation bean has been set");
}
return describeLayer.run(request);
}
/**
* @see WebMapService#getMap(GetMapRequest)
*/
public WebMap getMap(GetMapRequest request) {
if (null == getMap) {
throw new UnsupportedOperationException(
"Operation not properly configured, make sure the operation bean has been set");
}
return getMap.run(request);
}
/**
* @see WebMapService#map(GetMapRequest)
*/
public WebMap map(GetMapRequest request) {
return getMap(request);
}
/**
* @see WebMapService#getFeatureInfo(GetFeatureInfoRequest)
*/
public FeatureCollectionType getFeatureInfo(final GetFeatureInfoRequest request) {
if (null == getFeatureInfo) {
throw new UnsupportedOperationException(
"Operation not properly configured, make sure the operation bean has been set");
}
return getFeatureInfo.run(request);
}
/**
* @see WebMapService#getLegendGraphic(GetLegendGraphicRequest)
*/
public Object getLegendGraphic(GetLegendGraphicRequest request) {
if (null == getLegendGraphic) {
throw new UnsupportedOperationException(
"Operation not properly configured, make sure the operation bean has been set");
}
return getLegendGraphic.run(request);
}
public WebMap kml(GetMapRequest getMap) {
throw new ServiceException("kml service is not available, please include a KML module in WEB-INF/lib");
}
/**
* Method for generation of WMS animations.
*
* @param getMap GetMapRequest
* @return the <WebMap> output
*/
public WebMap animate(GetMapRequest getMap) {
try {
return Animator.produce(getMap, this, wms);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @see WebMapService#reflect(GetMapRequest)
*/
public WebMap reflect(GetMapRequest request) {
return getMapReflect(request);
}
/**
* @see org.geoserver.wms.WebMapService#getStyles(org.geoserver.sld.GetStylesRequest)
*/
public StyledLayerDescriptor getStyles(GetStylesRequest request) {
return getStyles.run(request);
}
/**
* s
*
* @see WebMapService#getMapReflect(GetMapRequest)
*/
public WebMap getMapReflect(GetMapRequest request) {
GetMapRequest getMap = autoSetMissingProperties(request);
return getMap(getMap);
}
public static GetMapRequest autoSetMissingProperties(GetMapRequest getMap) {
// 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().size() > 0)) {
ArrayList<Style> styles = new ArrayList<Style>(getMap.getLayers().size());
for (int i = 0; i < getMap.getLayers().size(); i++) {
styles.add(getMap.getLayers().get(i).getDefaultStyle());
}
getMap.setStyles(styles);
} else {
getMap.setStyles(STYLES);
}
}
// auto-magic missing info configuration
autoSetBoundsAndSize(getMap);
return 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
List<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.size(); i++) {
if (layers.get(i) != null) {
String layerSRS = layers.get(i).getSRS();
useNativeBounds = reqSRS.equalsIgnoreCase(layerSRS)
&& layers.get(i).getResource().getNativeBoundingBox() != null;
} else {
useNativeBounds = false;
}
}
CoordinateReferenceSystem reqCRS;
try {
reqCRS = CRS.decode(reqSRS);
} catch (Exception e) {
throw new ServiceException(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.size(); i++) {
MapLayerInfo layerInfo = layers.get(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 ServiceException(
"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;
}
// OL specific adjustments
if ("application/openlayers".equalsIgnoreCase(getMap.getFormat())
|| "openlayers".equalsIgnoreCase(getMap.getFormat())) {
if (mheight < MIN_OL_HEIGHT) {
mheight = MIN_OL_HEIGHT;
} else if (mheight > MAX_OL_HEIGHT) {
mheight = MAX_OL_HEIGHT;
}
if (mwidth < MIN_OL_WIDTH) {
mwidth = MIN_OL_WIDTH;
} else if (mwidth > MAX_OL_WIDTH) {
mwidth = MAX_OL_WIDTH;
}
}
}
}
// Actually set the bounding box and size of image
getMap.setBbox(aggregateBbox);
getMap.setWidth((int) mwidth);
getMap.setHeight((int) mheight);
}
}
private static String guessCommonSRS(List<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;
}
/**
* Returns a app wide cached rendering pool that can be used for parallelized rendering
*
*
*/
public static ExecutorService getRenderingPool() {
if(USE_GLOBAL_RENDERING_POOL && RENDERING_POOL == null) {
synchronized (DefaultWebMapService.class) {
if(RENDERING_POOL == null) {
RENDERING_POOL = Executors.newCachedThreadPool();
}
}
}
return RENDERING_POOL;
}
public void destroy() throws Exception {
if (RENDERING_POOL != null) {
RENDERING_POOL.shutdown();
RENDERING_POOL.awaitTermination(10, TimeUnit.SECONDS);
RENDERING_POOL = null;
}
}
}