gt: 				return resolutions.get(resolutions.size() - 1);
context: /*
   * This is part of Geomajas, a GIS framework, http://www.geomajas.org/.
   *
   * Copyright 2008-2016 Geosparc nv, http://www.geosparc.com/, Belgium.
   *
   * The program is available in open source according to the GNU Affero
   * General Public License. All contributions in this program are covered
   * by the Geomajas Contributors License Agreement. For full licensing
   * details, see LICENSE.txt in the project root.
   */
  package org.geomajas.layer.wms;
  
  import com.vividsolutions.jts.geom.Coordinate;
  import com.vividsolutions.jts.geom.Envelope;
  
  import org.apache.commons.io.IOUtils;
  import org.geomajas.annotation.Api;
  import org.geomajas.configuration.Parameter;
  import org.geomajas.configuration.RasterLayerInfo;
  import org.geomajas.configuration.client.ScaleInfo;
  import org.geomajas.geometry.Bbox;
  import org.geomajas.geometry.Crs;
  import org.geomajas.geometry.CrsTransform;
  import org.geomajas.global.ExceptionCode;
  import org.geomajas.global.GeomajasException;
  import org.geomajas.layer.LayerException;
  import org.geomajas.layer.RasterLayer;
  import org.geomajas.layer.common.proxy.LayerAuthentication;
  import org.geomajas.layer.common.proxy.LayerAuthenticationMethod;
  import org.geomajas.layer.common.proxy.LayerHttpService;
  import org.geomajas.layer.common.proxy.ProxyAuthentication;
  import org.geomajas.layer.common.proxy.ProxyLayerSupport;
  import org.geomajas.layer.feature.Attribute;
  import org.geomajas.layer.feature.Feature;
  import org.geomajas.layer.feature.attribute.StringAttribute;
  import org.geomajas.layer.feature.attribute.UrlAttribute;
  import org.geomajas.layer.tile.RasterTile;
  import org.geomajas.layer.tile.TileCode;
  import org.geomajas.plugin.caching.service.CacheManagerService;
  import org.geomajas.security.SecurityContext;
  import org.geomajas.service.DispatcherUrlService;
  import org.geomajas.service.DtoConverterService;
  import org.geomajas.service.GeoService;
  import org.geotools.GML;
  import org.geotools.GML.Version;
  import org.geotools.feature.FeatureCollection;
  import org.geotools.feature.FeatureIterator;
  import org.opengis.feature.simple.SimpleFeature;
  import org.opengis.feature.type.AttributeDescriptor;
  import org.opengis.geometry.MismatchedDimensionException;
  import org.opengis.referencing.crs.CoordinateReferenceSystem;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.xml.sax.SAXException;
  
  import javax.annotation.PostConstruct;
  import javax.swing.text.html.HTML;
  import javax.xml.parsers.ParserConfigurationException;
  
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.UnsupportedEncodingException;
  import java.net.URLEncoder;
  import java.text.DecimalFormat;
  import java.text.DecimalFormatSymbols;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.List;
  
  /**
   * <p>
   * Layer model for accessing raster data from WMS servers. It has support for most WMS versions: up to 1.3.0. When using
   * this layer, note that the following fields are required:
   * <ul>
   * <li><b>baseWmsUrl</b>: The base URL to the WMS server.</li>
   * <li><b>format</b>: The format for the returned images.</li>
   * <li><b>version</b>: The version of WMS to use.</li>
   * <li><b>styles</b>: The styles to use when rendering the WMS images.</li>
   * <li><b>useProxy</b>: Set to true to use a proxy for rendering the WMS, hiding the URL from the client. This
   * automatically happens when setting the authentication object.</li>
   * </ul>
   * There always is the option of adding additional parameters to the WMS GetMap requests, by filling the
   * <code>parameters</code> list. Such parameters could include optional WMS GetMap parameters, such as "transparency",
   * but also "user" and "password".
   * </p>
   * <p>
   * This layer also supports BASIC and DIGEST authentication. To use this functionality, set the
   * <code>authentication</code> field.
   * </p>
   * 
   * @author Jan De Moerloose
   * @author Pieter De Graef
   * @author Oliver May
   * @author Joachim Van der Auwera
   * @since 1.7.1
   */
  @Api
  public class WmsLayer implements RasterLayer, LayerFeatureInfoSupport, ProxyLayerSupport {
  
  	/**
  	 * Feature info format for WMS layers.
  	 * @since 1.15.0
  	 */
  	@Api
  	public enum WmsFeatureInfoFormat {
  		GML2("application/vnd.ogc.gml"), GML3("application/vnd.ogc.gml/3.1.1"), HTML("text/html"), TEXT("text/plain"),
  		JSON("application/json");
  
  		private String format;
  
  		private WmsFeatureInfoFormat(String format) {
  			this.format = format;
  		}
  
  		public String toString() {
  			return format;
  		}
  
  		public static WmsFeatureInfoFormat parseFormat(String value) {
  			for (WmsFeatureInfoFormat format : WmsFeatureInfoFormat.values()) {
  				if (format.toString().equals(value))  {
  					return format;
  				}
  			}
  			return null;
  		}
  	}
  
  	private final Logger log = LoggerFactory.getLogger(WmsLayer.class);
  
  	private final List<Resolution> resolutions = new ArrayList<Resolution>();
  
  	// @NotNull this seems to cause problems, it is tested in @PostConstruct anyway
  	private String baseWmsUrl;
  
  	private String format = "image/png";
  
  	private String version = "1.1.1";
  
  	private String styles = "";
  
  	private List<Parameter> parameters;
  	
  	private boolean enableFeatureInfoSupport;
  
  	private RasterLayerInfo layerInfo;
  
  	private Crs crs;
  
  	private String id;
  
  	private WmsFeatureInfoFormat featureInfoFormat = WmsFeatureInfoFormat.GML2;
  
  	/**
  	 * @deprecated use layerAuthentication
  	 */
  	@Deprecated
  	private WmsAuthentication authentication;
  	 
  	private LayerAuthentication layerAuthentication;
  
  	private boolean useProxy;
  	
  	private boolean useCache;
  
  	@Autowired
  	private GeoService geoService;
  
  	@Autowired
  	private LayerHttpService httpService;
  
  	@Autowired
  	private DtoConverterService converterService;
  
  	@Autowired(required = false)
  	private DispatcherUrlService dispatcherUrlService;
  
  	@Autowired(required = false)
  	private CacheManagerService cacheManagerService;
  	
  	@Autowired
  	private SecurityContext securityContext;
  
  	/**
  	 * Return the layers identifier.
  	 * 
  	 * @return layer id
  	 */
  	public String getId() {
  		return id;
  	}
  
  	/**
  	 * Set the layer identifier.
  	 * 
  	 * @param id
  	 *            layer id
  	 */
  	public void setId(String id) {
  		this.id = id;
  	}
  
  	/**
  	 * Return the layer info object.
  	 * 
  	 * @since 1.7.1
  	 */
  	public RasterLayerInfo getLayerInfo() {
  		return layerInfo;
  	}
  
  	/**
  	 * Get coordinate reference system for this layer.
  	 * 
  	 * @return Coordinate reference system for this layer.
  	 * @deprecated use {@link org.geomajas.layer.LayerService#getCrs(org.geomajas.layer.Layer)}
  	 */
  	@Deprecated
  	public CoordinateReferenceSystem getCrs() {
  		return crs;
  	}
  
  	@PostConstruct
  	protected void postConstruct() throws GeomajasException {
  		if (null == baseWmsUrl) {
  			throw new GeomajasException(ExceptionCode.PARAMETER_MISSING, "baseWmsUrl");
  		}
  
  		crs = geoService.getCrs2(getLayerInfo().getCrs());
  
  		// calculate resolutions
  		List<ScaleInfo> scales = layerInfo.getZoomLevels();
  		if (scales != null) {
  			List<Double> r = new ArrayList<Double>();
  			for (ScaleInfo scale : scales) {
  				r.add(1. / scale.getPixelPerUnit());
  			}
  
  			// sort in decreasing order !!!
  			Collections.sort(r);
  			Collections.reverse(r);
  
  			int level = 0;
  			for (double resolution : r) {
  				resolutions
  						.add(new Resolution(resolution, level++, layerInfo.getTileWidth(), layerInfo.getTileHeight()));
  			}
  		}
  	}
  
  	/**
  	 * Get the feature info format.
  	 *
  	 * @return the feature info format.
  	 * @since 1.15.0
  	 */
  	@Api
  	public WmsFeatureInfoFormat getFeatureInfoFormat() {
  		return featureInfoFormat;
  	}
  
  	/**
  	 * Get the feature info format.
  	 *
  	 * @param featureInfoFormat the feature info format
  	 * @since 1.15.0
  	 */
  	@Api
  	public void setFeatureInfoFormat(WmsFeatureInfoFormat featureInfoFormat) {
  		this.featureInfoFormat = featureInfoFormat;
  	}
  
  	/**
  	 * Set the layer configuration.
  	 * 
  	 * @param layerInfo
  	 *            layer information
  	 * @throws LayerException
  	 *             oops
  	 * @since 1.7.1
  	 */
  	@Api
  	public void setLayerInfo(RasterLayerInfo layerInfo) throws LayerException {
  		this.layerInfo = layerInfo;
  	}
  
  	/** {@inheritDoc}. */
  	public List<Feature> getFeaturesByLocation(Coordinate layerCoordinate, double layerScale, int pixelTolerance)
  			throws LayerException {
  		if (!isEnableFeatureInfoSupport()) {
  			return Collections.emptyList();
  		}
  
  		List<Feature> features = new ArrayList<Feature>();
  
  		Resolution bestResolution = getResolutionForScale(layerScale);
  		RasterGrid grid = getRasterGrid(new Envelope(layerCoordinate), bestResolution.getTileWidth(),
  				bestResolution.getTileHeight());
  		int x = (int) (((layerCoordinate.x - grid.getLowerLeft().x) * bestResolution.getTileWidthPx()) / grid
  				.getTileWidth());
  		int y = (int) (bestResolution.getTileHeightPx() - (((layerCoordinate.y - grid.getLowerLeft().y) * bestResolution
  				.getTileHeightPx()) / grid.getTileHeight()));
  
  		Bbox layerBox = new Bbox(grid.getLowerLeft().x, grid.getLowerLeft().y, grid.getTileWidth(),
  				grid.getTileHeight());
  
  		InputStream stream = null;
  		try {
  			String url = formatGetFeatureInfoUrl(bestResolution.getTileWidthPx(), bestResolution.getTileHeightPx(),
  					layerBox, x, y, featureInfoFormat == WmsFeatureInfoFormat.HTML);
  			log.debug("getFeaturesByLocation: {} {} {} {}", new Object[] { layerCoordinate, layerScale, pixelTolerance,
  					url });
  			stream = httpService.getStream(url, this);
  
  			switch (featureInfoFormat) {
  				case GML2:
  					features = getGmlFeatures(stream, Version.GML2);
  					break;
  				case GML3:
  					features = getGmlFeatures(stream, Version.GML3);
  					break;
  				case HTML:
  					features = getHtmlFeatures(url);
  					break;
  				case TEXT:
  					features = getTextFeatures(stream);
  					break;
  				case JSON:
  					features = getTextFeatures(stream);
  					break;
  			}
  
  		} catch (LayerException le) {
  			throw le; // NOSONAR
  		} catch (Exception e) { // NOSONAR
  			throw new LayerException(e, ExceptionCode.UNEXPECTED_PROBLEM);
  		} finally {
  			if (null != stream) {
  				try {
  					stream.close();
  				} catch (IOException ioe) {
  					// ignore, closing anyway
  				}
  			}
  		}
  
  		return features;
  	}
  
  	private List<Feature> getTextFeatures(InputStream stream) throws IOException {
  		List<Feature> features;
  		String html = IOUtils.toString(stream);
  		Feature feature = new Feature();
  		feature.setId("text");
  		feature.setAttributes(new HashMap<String, Attribute>());
  		feature.getAttributes().put("wmsStringAttribute", new StringAttribute(html));
  		features = Arrays.asList(feature);
  		return features;
  	}
  
  	private List<Feature> getHtmlFeatures(String url) throws IOException {
  		List<Feature> features;
  		Feature feature = new Feature();
  		feature.setId("html");
  		feature.setAttributes(new HashMap<String, Attribute>());
  		feature.getAttributes().put("wmsHtmlAttribute", new UrlAttribute(url));
  		features = Arrays.asList(feature);
  		return features;
  	}
  
  	private List<Feature> getGmlFeatures(InputStream stream, Version version) throws IOException, SAXException,
  			ParserConfigurationException {
  		List<Feature> features = new ArrayList<Feature>();
  		GML gml = new GML(version);
  		FeatureCollection<?, SimpleFeature> collection = gml.decodeFeatureCollection(stream);
  		FeatureIterator<SimpleFeature> it = collection.features();
  
  		while (it.hasNext()) {
  			features.add(toDto(it.next()));
  		}
  		return features;
  	}
  
  	private Feature toDto(SimpleFeature feature) {
  		if (feature == null) {
  			return null;
  		}
  		Feature dto = new Feature(feature.getID());
  
  		HashMap<String, Attribute> attributes = new HashMap<String, Attribute>();
  
  		for (AttributeDescriptor desc : feature.getType().getAttributeDescriptors()) {
  			Object obj = feature.getAttribute(desc.getName());
  			if (null != obj) {
  				attributes.put(desc.getLocalName(), new StringAttribute(obj.toString()));
  			}
  		}
  		dto.setAttributes(attributes);
  		dto.setId(feature.getID());
  
  		dto.setUpdatable(false);
  		dto.setDeletable(false);
  		return dto;
  
  	}
  
  	/**
  	 * Paints the specified bounds optimized for the specified scale in pixel/unit.
  	 * 
  	 * @param targetCrs
  	 *            Coordinate reference system used for bounds
  	 * @param bounds
  	 *            bounds to request images for
  	 * @param scale
  	 *            scale or zoom level (unit?)
  	 * @return a list of raster images that covers the bounds
  	 * @throws GeomajasException
  	 *             oops
  	 */
  	public List<RasterTile> paint(CoordinateReferenceSystem targetCrs, Envelope bounds, double scale)
  			throws GeomajasException {
  		Envelope layerBounds = bounds;
  		double layerScale = scale;
  		CrsTransform layerToMap = null;
  		boolean needTransform = !crs.equals(targetCrs);
  
  		try {
  			// We don't necessarily need to split into same CRS and different CRS cases, the latter implementation uses
  			// identity transform if crs's are equal for map and layer but might introduce bugs in rounding and/or
  			// conversions.
  			if (needTransform) {
  				layerToMap = geoService.getCrsTransform(crs, targetCrs);
  				CrsTransform mapToLayer = geoService.getCrsTransform(targetCrs, crs);
  
  				// Translate the map coordinates to layer coordinates, assumes equal x-y orientation
  				layerBounds = geoService.transform(bounds, mapToLayer);
  				layerScale = bounds.getWidth() * scale / layerBounds.getWidth();
  			}
  		} catch (MismatchedDimensionException e) {
  			throw new GeomajasException(e, ExceptionCode.RENDER_DIMENSION_MISMATCH);
  		}
  		layerBounds = clipBounds(layerBounds);
  		if (layerBounds.isNull()) {
  			return new ArrayList<RasterTile>(0);
  		}
  
  		// Grid is in layer coordinate space!
  		Resolution bestResolution = getResolutionForScale(layerScale);
  		RasterGrid grid = getRasterGrid(layerBounds, bestResolution.getTileWidth(), bestResolution.getTileHeight());
  
  		// We calculate the first tile's screen box with this assumption
  		List<RasterTile> result = new ArrayList<RasterTile>();
  		for (int i = grid.getXmin(); i < grid.getXmax(); i++) {
  			for (int j = grid.getYmin(); j < grid.getYmax(); j++) {
  				double x = grid.getLowerLeft().x + (i - grid.getXmin()) * grid.getTileWidth();
  				double y = grid.getLowerLeft().y + (j - grid.getYmin()) * grid.getTileHeight();
  				// layer coordinates
  				Bbox worldBox;
  				Bbox layerBox;
  				if (needTransform) {
  					layerBox = new Bbox(x, y, grid.getTileWidth(), grid.getTileHeight());
  					// Transforming back to map coordinates will only result in a proper grid if the transformation
  					// is nearly affine
  					worldBox = geoService.transform(layerBox, layerToMap);
  				} else {
  					worldBox = new Bbox(x, y, grid.getTileWidth(), grid.getTileHeight());
  					layerBox = worldBox;
  				}
  				// Rounding to avoid white space between raster tiles lower-left becomes upper-left in inverted y-space
  				Bbox screenBox = new Bbox(Math.round(scale * worldBox.getX()), -Math.round(scale * worldBox.getMaxY()),
  						Math.round(scale * worldBox.getMaxX()) - Math.round(scale * worldBox.getX()), Math.round(scale
  								* worldBox.getMaxY())
  								- Math.round(scale * worldBox.getY()));
  
  				RasterTile image = new RasterTile(screenBox, getId() + "." + bestResolution.getLevel() + "." + i + ","
  						+ j);
  
  				image.setCode(new TileCode(bestResolution.getLevel(), i, j));
  				String url = formatUrl(bestResolution.getTileWidthPx(), bestResolution.getTileHeightPx(), layerBox);
  				image.setUrl(url);
  				result.add(image);
  			}
  		}
  
  		return result;
  	}
  
  	private String getWmsTargetUrl() {
  		if (useProxy || null != authentication || useCache) {
  			if (null != dispatcherUrlService) {
  				String url = dispatcherUrlService.getDispatcherUrl();
  				if (!url.endsWith("/")) {
  					url += "/";
  				}
  				return url + "wms/" + getId() + "/";
  			} else {
  				return "./d/wms/" + getId() + "/";
  			}
  		} else {
  			return baseWmsUrl;
  		}
  	}
  
  	private String formatGetFeatureInfoUrl(int width, int height, Bbox box, int x, int y,
  			boolean internal) throws GeomajasException {
  		// Always use direct url
  		try {
  			StringBuilder url;
  			if (internal) {
  				url = formatBaseUrl(getWmsTargetUrl(), width, height, box);
  			} else {
  				url = formatBaseUrl(baseWmsUrl, width, height, box);
  			}
  
  			String layers = getId();
  			if (layerInfo.getDataSourceName() != null) {
  				layers = layerInfo.getDataSourceName();
  			}
  			url.append("&QUERY_LAYERS=");
  			url.append(URLEncoder.encode(layers, "UTF8"));
  			url.append("&request=GetFeatureInfo");
  			url.append("&X=");
  			url.append(Integer.toString(x));
  			url.append("&Y=");
  			url.append(Integer.toString(y));
  			url.append("&INFO_FORMAT=");
  			url.append(featureInfoFormat);
  			return url.toString();
  		} catch (UnsupportedEncodingException uee) {
  			throw new IllegalStateException("Cannot find UTF8 encoding?", uee);
  		}
  	}
  
  	private String formatUrl(int width, int height, Bbox box) throws GeomajasException {
  		StringBuilder url = formatBaseUrl(getWmsTargetUrl(), width, height, box);
  		url.append("&request=GetMap");
  		return url.toString();
  	}
  
  	/**
  	 * Build the base part of the url (doesn't change for getMap or getFeatureInfo requests).
  	 * 
  	 * @param targetUrl
  	 *            base url
  	 * @param width
  	 *            image width
  	 * @param height
  	 *            image height
  	 * @param box
  	 *            bounding box
  	 * @return base WMS url
  	 * @throws GeomajasException
  	 *             missing parameter
  	 */
  	private StringBuilder formatBaseUrl(String targetUrl, int width, int height, Bbox box) throws GeomajasException {
  		try {
  			StringBuilder url = new StringBuilder(targetUrl);
  			int pos = url.lastIndexOf("?");
  			if (pos > 0) {
  				url.append("&SERVICE=WMS");
  			} else {
  				url.append("?SERVICE=WMS");
  			}
  			String layers = getId();
  			if (layerInfo.getDataSourceName() != null) {
  				layers = layerInfo.getDataSourceName();
  			}
  			url.append("&layers=");
  			url.append(URLEncoder.encode(layers, "UTF8"));
  			url.append("&WIDTH=");
  			url.append(Integer.toString(width));
  			url.append("&HEIGHT=");
  			url.append(Integer.toString(height));
  			DecimalFormat decimalFormat = new DecimalFormat(); // create new as this is not thread safe
  			decimalFormat.setDecimalSeparatorAlwaysShown(false);
  			decimalFormat.setGroupingUsed(false);
  			decimalFormat.setMinimumFractionDigits(0);
  			decimalFormat.setMaximumFractionDigits(100);
  			DecimalFormatSymbols symbols = new DecimalFormatSymbols();
  			symbols.setDecimalSeparator('.');
  			decimalFormat.setDecimalFormatSymbols(symbols);
  
  			url.append("&bbox=");
  			url.append(decimalFormat.format(box.getX()));
  			url.append(",");
  			url.append(decimalFormat.format(box.getY()));
  			url.append(",");
  			url.append(decimalFormat.format(box.getMaxX()));
  			url.append(",");
  			url.append(decimalFormat.format(box.getMaxY()));
  			url.append("&format=");
  			url.append(format);
  			url.append("&version=");
  			url.append(version);
  			if ("1.3.0".equals(version)) {
  				url.append("&crs=");
  			} else {
  				url.append("&srs=");
  			}
  			url.append(URLEncoder.encode(layerInfo.getCrs(), "UTF8"));
  			url.append("&styles=");
  			url.append(styles);
  			if (null != parameters) {
  				for (Parameter p : parameters) {
  					url.append("&");
  					url.append(URLEncoder.encode(p.getName(), "UTF8"));
  					url.append("=");
  					url.append(URLEncoder.encode(p.getValue(), "UTF8"));
  				}
  			}
  			if (useProxy && null != securityContext.getToken()) {
  				url.append("&userToken=");
  				url.append(securityContext.getToken());
  			}
  			return url;
  		} catch (UnsupportedEncodingException uee) {
  			throw new IllegalStateException("Cannot find UTF8 encoding?", uee);
  		}
  	}
  
  	private Resolution getResolutionForScale(double scale) {
  		if (null == resolutions || resolutions.size() == 0) {
  			return calculateBestQuadTreeResolution(scale);
  		} else {
  			double screenResolution = 1.0 / scale;
  			if (screenResolution >= resolutions.get(0).getResolution()) {
  				return resolutions.get(0);
  			} else if (screenResolution <= resolutions.get(resolutions.size() - 1).getResolution()) {
  