/* (c) 2014 - 2016 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.map; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.geoserver.ows.LocalPublished; import org.geoserver.ows.LocalWorkspace; import org.geoserver.ows.URLMangler.URLType; import org.geoserver.ows.util.ResponseUtils; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.platform.ServiceException; import org.geoserver.wms.GetMapOutputFormat; import org.geoserver.wms.GetMapRequest; import org.geoserver.wms.MapLayerInfo; import org.geoserver.wms.MapProducerCapabilities; import org.geoserver.wms.WMS; import org.geoserver.wms.WMSMapContent; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.map.Layer; import org.geotools.map.WMSLayer; import org.geotools.referencing.CRS; import org.geotools.referencing.CRS.AxisOrder; import org.geotools.renderer.crs.ProjectionHandler; import org.geotools.renderer.crs.ProjectionHandlerFinder; import org.geotools.renderer.crs.WrappingProjectionHandler; import org.geotools.util.Converters; import org.geotools.util.logging.Logging; import org.opengis.feature.type.FeatureType; import org.opengis.geometry.MismatchedDimensionException; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.crs.ProjectedCRS; import freemarker.ext.beans.BeansWrapper; import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; /** * * @see RawMapResponse */ public class OpenLayersMapOutputFormat implements GetMapOutputFormat { /** A logger for this class. */ private static final Logger LOGGER = Logging.getLogger(OpenLayersMapOutputFormat.class); /** * The mime type for the response header */ public static final String MIME_TYPE = "text/html; subtype=openlayers"; /** * System property name to toggle OL3 support. */ public static final String ENABLE_OL3 = "ENABLE_OL3"; /** * The formats accepted in a GetMap request for this producer and stated in getcaps */ private static final Set<String> OUTPUT_FORMATS = new HashSet<String>(Arrays.asList( "application/openlayers", "openlayers", MIME_TYPE)); /** * Default capabilities for OpenLayers format. * * <p> * <ol> * <li>tiled = supported</li> * <li>multipleValues = unsupported</li> * <li>paletteSupported = supported</li> * <li>transparency = supported</li> * </ol> */ private static MapProducerCapabilities CAPABILITIES= new MapProducerCapabilities(true, false, true, true, null); /** * Set of parameters that we can ignore, since they are not part of the OpenLayers WMS request */ private static final Set<String> ignoredParameters; static { ignoredParameters = new HashSet<String>(); ignoredParameters.add("REQUEST"); ignoredParameters.add("TILED"); ignoredParameters.add("BBOX"); ignoredParameters.add("SERVICE"); ignoredParameters.add("VERSION"); ignoredParameters.add("FORMAT"); ignoredParameters.add("WIDTH"); ignoredParameters.add("HEIGHT"); ignoredParameters.add("SRS"); } /** * static freemaker configuration */ private static Configuration cfg; static { cfg = new Configuration(); cfg.setClassForTemplateLoading(OpenLayersMapOutputFormat.class, ""); BeansWrapper bw = new BeansWrapper(); bw.setExposureLevel(BeansWrapper.EXPOSE_PROPERTIES_ONLY); cfg.setObjectWrapper(bw); } /** * wms configuration */ private WMS wms; public OpenLayersMapOutputFormat(WMS wms) { this.wms = wms; } /** * @see org.geoserver.wms.GetMapOutputFormat#getOutputFormatNames() */ public Set<String> getOutputFormatNames() { return OUTPUT_FORMATS; } /** * @see org.geoserver.wms.GetMapOutputFormat#getMimeType() */ public String getMimeType() { return MIME_TYPE; } /** * @see org.geoserver.wms.GetMapOutputFormat#produceMap(org.geoserver.wms.WMSMapContent) */ public RawMap produceMap(WMSMapContent mapContent) throws ServiceException, IOException { try { // create the template String templateName; boolean useOpenLayers3 = isOL3Enabled(mapContent) && browserSupportsOL3(mapContent); if(useOpenLayers3) { templateName = "OpenLayers3MapTemplate.ftl"; } else { templateName = "OpenLayers2MapTemplate.ftl"; } Template template = cfg.getTemplate(templateName); HashMap<String, Object> map = new HashMap<String, Object>(); map.put("context", mapContent); map.put("pureCoverage", hasOnlyCoverages(mapContent)); map.put("styles", styleNames(mapContent)); GetMapRequest request = mapContent.getRequest(); map.put("request", request); map.put("yx", String.valueOf(isWms13FlippedCRS(request.getCrs()))); map.put("maxResolution", new Double(getMaxResolution(mapContent.getRenderingArea()))); ProjectionHandler handler = null; try { handler = ProjectionHandlerFinder.getHandler( new ReferencedEnvelope(request.getCrs()), request.getCrs(), wms.isContinuousMapWrappingEnabled()); } catch (MismatchedDimensionException e) { LOGGER.log(Level.FINER, e.getMessage(), e); } catch (FactoryException e) { LOGGER.log(Level.FINER, e.getMessage(), e); } map.put("global", String.valueOf( handler != null && handler instanceof WrappingProjectionHandler)); String baseUrl = ResponseUtils.buildURL(request.getBaseUrl(), "/", null, URLType.RESOURCE); String queryString = null; // remove query string from baseUrl if (baseUrl.indexOf("?") > 0) { int idx = baseUrl.indexOf("?"); queryString = baseUrl.substring(idx); // include question mark baseUrl = baseUrl.substring(0, idx); // leave out question mark } map.put("baseUrl", canonicUrl(baseUrl)); // TODO: replace service path with call to buildURL since it does this // same dance String servicePath = "wms"; if (LocalPublished.get() != null) { servicePath = LocalPublished.get().getName() + "/" + servicePath; } if (LocalWorkspace.get() != null) { servicePath = LocalWorkspace.get().getName() + "/" + servicePath; } // append query string to servicePath if (queryString != null) { servicePath += queryString; } map.put("servicePath", servicePath); map.put("parameters", getLayerParameter(request.getRawKvp())); map.put("units", useOpenLayers3 ? getOL3Units(request) : getOL2Units(request)); if (mapContent.layers().size() == 1) { map.put("layerName", mapContent.layers().get(0).getTitle()); } else { map.put("layerName", "Geoserver layers"); } template.setOutputEncoding("UTF-8"); ByteArrayOutputStream buff = new ByteArrayOutputStream(); template.process(map, new OutputStreamWriter(buff, Charset.forName("UTF-8"))); RawMap result = new RawMap(mapContent, buff, MIME_TYPE); return result; } catch (TemplateException e) { throw new ServiceException(e); } } private boolean isOL3Enabled(WMSMapContent mapContent) { GetMapRequest req = mapContent.getRequest(); // check format options Object enableOL3 = Converters.convert(req.getFormatOptions().get(ENABLE_OL3), Boolean.class); if (enableOL3 == null) { // check system property enableOL3 = GeoServerExtensions.getProperty(ENABLE_OL3); } // enable by default return enableOL3 == null || Converters.convert(enableOL3, Boolean.class); } private boolean browserSupportsOL3(WMSMapContent mc) { String agent = mc.getRequest().getHttpRequestHeader("USER-AGENT"); if(agent == null) { // play it safe return false; } Pattern MSIE_PATTERN = Pattern.compile("MSIE (\\d+)\\."); Matcher matcher = MSIE_PATTERN.matcher(agent); if(!matcher.matches()) { return true; } else { return Integer.valueOf(matcher.group(1)) > 8; } } private boolean isWms13FlippedCRS(CoordinateReferenceSystem crs) { try { String code = "EPSG:" + CRS.lookupIdentifier(crs, false); code = WMS.toInternalSRS(code, WMS.version("1.3.0")); CoordinateReferenceSystem crs13 = CRS.decode(code); return CRS.getAxisOrder(crs13) == AxisOrder.NORTH_EAST; } catch(Exception e) { LOGGER.log(Level.WARNING, "Failed to determine CRS axis order, assuming is EN", e); return false; } } /** * Guesses if the map context is made only of coverage layers by looking at the wrapping feature * type. Ugly, if you come up with better means of doing so, fix it. * * @param mapContent * */ private boolean hasOnlyCoverages(WMSMapContent mapContent) { for (Layer layer : mapContent.layers()) { FeatureType schema = layer.getFeatureSource().getSchema(); boolean grid = schema.getName().getLocalPart().equals("GridCoverage") && schema.getDescriptor("geom") != null && schema.getDescriptor("grid") != null && !(layer instanceof WMSLayer); if (!grid) return false; } return true; } private List<String> styleNames(WMSMapContent mapContent) { if (mapContent.layers().size() != 1 || mapContent.getRequest() == null) return Collections.emptyList(); MapLayerInfo info = mapContent.getRequest().getLayers().get(0); return info.getOtherStyleNames(); } /** * OL does support only a limited number of unit types, we have to try and return one of those, * otherwise the scale won't be shown. From the OL guide: possible values are "degrees" (or * "dd"), "m", "ft", "km", "mi", "inches". * * @param request * */ private String getOL2Units(GetMapRequest request) { CoordinateReferenceSystem crs = request.getCrs(); // first rough approximation, meters for projected CRS, degrees for the // others String result = crs instanceof ProjectedCRS ? "m" : "degrees"; try { String unit = crs.getCoordinateSystem().getAxis(0).getUnit().toString(); // use the unicode escape sequence for the degree sign so its not // screwed up by different local encodings final String degreeSign = "\u00B0"; if (degreeSign.equals(unit) || "degrees".equals(unit) || "dd".equals(unit)) result = "degrees"; else if ("m".equals(unit) || "meters".equals(unit)) result = "m"; else if ("km".equals(unit) || "kilometers".equals(unit)) result = "mi"; else if ("in".equals(unit) || "inches".equals(unit)) result = "inches"; else if ("ft".equals(unit) || "feets".equals(unit)) result = "ft"; else if ("mi".equals(unit) || "miles".equals(unit)) result = "mi"; } catch (Exception e) { LOGGER.log(Level.WARNING, "Error trying to determine unit of measure", e); } return result; } /** * OL3 does support a very limited set of unit types, we have to try and return one of those, * otherwise the scale won't be shown. * * @param request * */ private String getOL3Units(GetMapRequest request) { CoordinateReferenceSystem crs = request.getCrs(); // first rough approximation, meters for projected CRS, degrees for the // others String result = crs instanceof ProjectedCRS ? "m" : "degrees"; try { String unit = crs.getCoordinateSystem().getAxis(0).getUnit().toString(); // use the unicode escape sequence for the degree sign so its not // screwed up by different local encodings if ("ft".equals(unit) || "feets".equals(unit)) result = "feet"; } catch (Exception e) { LOGGER.log(Level.WARNING, "Error trying to determine unit of measure", e); } return result; } /** * Returns a list of maps with the name and value of each parameter that we have to forward to * OpenLayers. Forwarded parameters are all the provided ones, besides a short set contained in * {@link #ignoredParameters}. * * * * @param rawKvp * */ private List<Map<String, String>> getLayerParameter(Map<String, String> rawKvp) { List<Map<String, String>> result = new ArrayList<Map<String, String>>(rawKvp.size()); for (Map.Entry<String, String> en : rawKvp.entrySet()) { String paramName = en.getKey(); if (ignoredParameters.contains(paramName.toUpperCase())) { continue; } // this won't work for multi-valued parameters, but we have none so // far (they are common just in HTML forms...) Map<String, String> map = new HashMap<String, String>(); map.put("name", paramName); map.put("value", en.getValue()); result.add(map); } return result; } /** * Makes sure the url does not end with "/", otherwise we would have URL lik * "http://localhost:8080/geoserver//wms?LAYERS=..." and Jetty 6.1 won't digest them... * * @param baseUrl * */ private String canonicUrl(String baseUrl) { if (baseUrl.endsWith("/")) { return baseUrl.substring(0, baseUrl.length() - 1); } else { return baseUrl; } } private double getMaxResolution(ReferencedEnvelope areaOfInterest) { double w = areaOfInterest.getWidth(); double h = areaOfInterest.getHeight(); return ((w > h) ? w : h) / 256; } public MapProducerCapabilities getCapabilities(String format) { return CAPABILITIES; } }