/* (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; import java.awt.Color; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.awt.image.IndexColorModel; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import org.geoserver.platform.ServiceException; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.map.Layer; import org.geotools.map.MapContent; import org.geotools.referencing.operation.matrix.XAffineTransform; import org.geotools.renderer.lite.RendererUtilities; import org.geotools.renderer.lite.StreamingRenderer; /** * Extends DefaultMapContext to provide the whole set of request parameters a WMS GetMap request can * have. * * <p> * In particular, adds holding for the following parameter values: * * <ul> * <li>WIDTH</li> * <li>HEIGHT</li> * <li>BGCOLOR</li> * <li>TRANSPARENT</li> * </ul> * </p> * * @author Gabriel Roldan * @author Simone Giannecchini - GeoSolutions SAS * @version $Id$ */ public class WMSMapContent extends MapContent { /** requested map image width in output units (pixels) */ private int mapWidth; /** requested map image height in output units (pixels) */ private int mapHeight; /** Requested BGCOLOR, defaults to white according to WMS spec */ private Color bgColor = Color.white; /** true if background transparency is requested */ private boolean transparent; /** suggested output tile size */ private int tileSize = -1; /** map rotation in degrees */ private double angle; private List<GetMapCallback> callbacks; private Map<String, Object> metadata = new HashMap<>(); private Integer maxRenderingTime; public int getTileSize() { return tileSize; } public void setTileSize(int tileSize) { this.tileSize = tileSize; } /** * the rendering buffer used to avoid issues with tiled rendering and big strokes that may cross * tile boundaries */ private int buffer; /** * The {@link IndexColorModel} the user required for the resulting map */ private IndexColorModel icm; private GetMapRequest request; // hold onto it so we can grab info from it // (request URL etc...) public WMSMapContent() { super(); } public WMSMapContent(GetMapRequest req) { super(); request = req; } public WMSMapContent(WMSMapContent other, boolean copyLayers) { this.mapWidth = other.mapWidth; this.mapHeight = other.mapHeight; this.bgColor = other.bgColor; this.transparent = other.transparent; this.tileSize = other.tileSize; this.angle = other.angle; this.callbacks = new ArrayList<>(other.callbacks); this.buffer = other.buffer; this.icm = other.icm; this.request = other.request; if(copyLayers) { this.layers().addAll(other.layers()); } this.getViewport().setBounds(other.getViewport().getBounds()); } public Color getBgColor() { return this.bgColor; } public void setBgColor(Color bgColor) { this.bgColor = bgColor; } public int getMapHeight() { return this.mapHeight; } public void setMapHeight(int mapHeight) { this.mapHeight = mapHeight; } public int getMapWidth() { return this.mapWidth; } public void setMapWidth(int mapWidth) { this.mapWidth = mapWidth; } public boolean isTransparent() { return this.transparent; } public void setTransparent(boolean transparent) { this.transparent = transparent; } public GetMapRequest getRequest() { return request; } public void setRequest(GetMapRequest request) { this.request = request; } public int getBuffer() { return buffer; } public void setBuffer(int buffer) { this.buffer = buffer; } public IndexColorModel getPalette() { return icm; } public void setPalette(IndexColorModel paletteInverter) { this.icm = paletteInverter; } /** * The clockwise rotation angle of the map, in degrees * * */ public double getAngle() { return angle; } public void setAngle(double rotation) { this.angle = rotation; } @Override public boolean addLayer(Layer layer) { layer = fireLayerCallbacks(layer); if(layer != null) { return super.addLayer(layer); } else { return false; } } private Layer fireLayerCallbacks(Layer layer) { // if no callbacks, return the layer as is if(callbacks == null) { return layer; } // process through the callbacks for (GetMapCallback callback : callbacks) { layer = callback.beforeLayer(this, layer); if(layer == null) { return null; } } return layer; } @Override public int addLayers(Collection<? extends Layer> layers) { List<Layer> filtered = new ArrayList<Layer>(layers.size()); for (Layer layer : layers) { layer = fireLayerCallbacks(layer); if(layer != null) { filtered.add(layer); } } if(filtered.size() > 0) { return super.addLayers(filtered); } else { return 0; } } /** * Returns the transformation going from the map area space to the screen space taking into * account map rotation * * */ public AffineTransform getRenderingTransform() { Rectangle paintArea = new Rectangle(0, 0, getMapWidth(), getMapHeight()); ReferencedEnvelope dataArea = getViewport().getBounds(); AffineTransform tx; if (getAngle() != 0.0) { tx = new AffineTransform(); tx.translate(paintArea.width / 2, paintArea.height / 2); tx.rotate(Math.toRadians(getAngle())); tx.translate(-paintArea.width / 2, -paintArea.height / 2); tx.concatenate(RendererUtilities.worldToScreenTransform(dataArea, paintArea)); } else { tx = RendererUtilities.worldToScreenTransform(dataArea, paintArea); } return tx; } /** * Returns the actual area that should be drawn taking into account the map rotation account map * rotation * * */ public ReferencedEnvelope getRenderingArea() { ReferencedEnvelope dataArea = getViewport().getBounds(); if (getAngle() == 0) return dataArea; AffineTransform tx = new AffineTransform(); double offsetX = dataArea.getMinX() + dataArea.getWidth() / 2; double offsetY = dataArea.getMinY() + dataArea.getHeight() / 2; tx.translate(offsetX, offsetY); tx.rotate(Math.toRadians(getAngle())); tx.translate(-offsetX, -offsetY); Rectangle2D dataAreaShape = new Rectangle2D.Double(dataArea.getMinX(), dataArea.getMinY(), dataArea.getWidth(), dataArea.getHeight()); Rectangle2D transformedBounds = tx.createTransformedShape(dataAreaShape).getBounds2D(); return new ReferencedEnvelope(transformedBounds, dataArea.getCoordinateReferenceSystem()); } /** * Get the contact information associated with this context, returns an empty string if * contactInformation has not been set. * * @return the ContactInformation or an empty string if not present */ public String getContactInformation(){ String contact = (String) getUserData().get("contact"); return contact == null ? "" : contact; } /** * Set contact information associated with this class. * * @param contactInformation * the ContactInformation. */ public void setContactInformation(final String contactInformation){ getUserData().put("contact", contactInformation); } /** * Get an array of keywords associated with this context, returns an empty array if no keywords * have been set. The array returned is a copy, changes to the returned array won't influence * the MapContextState * * @return array of keywords */ public String[] getKeywords(){ Object obj = getUserData().get("keywords"); if (obj == null) { return new String[0]; } else if (obj instanceof String) { String keywords = (String) obj; return keywords.split(","); } else if (obj instanceof String[]) { String keywords[] = (String[]) obj; String[] copy = new String[keywords.length]; System.arraycopy(keywords, 0, copy, 0, keywords.length); return copy; } else if (obj instanceof Collection) { Collection<String> keywords = (Collection) obj; return keywords.toArray(new String[keywords.size()]); } else { return new String[0]; } } /** * Set an array of keywords to associate with this context. * * @param keywords * the Keywords. */ public void setKeywords(final String[] keywords){ getUserData().put("keywords", keywords); } /** * Get the abstract which describes this interface, returns an empty string if this has not been * set yet. * * @return The Abstract or an empty string if not present */ public String getAbstract(){ String description = (String) getUserData().get("abstract"); return description == null ? "" : description; } /** * Set an abstract which describes this context. * * @param conAbstract * the Abstract. */ public void setAbstract(final String contextAbstract){ getUserData().put("abstract", contextAbstract); } public void setGetMapCallbacks(final List<GetMapCallback> callbacks) { this.callbacks = callbacks; } public double getScaleDenominator() { return getScaleDenominator(false); } public double getScaleDenominator(boolean considerDPI) { java.util.Map hints = new HashMap(); if(considerDPI) { // compute the DPI if (request.getFormatOptions().get("dpi") != null) { hints.put(StreamingRenderer.DPI_KEY, (request.getFormatOptions().get("dpi"))); } } if (request.getScaleMethod() == ScaleComputationMethod.Accurate) { if (request.getAngle() != 0) { throw new ServiceException( "Accurate scale computation is not supported when using the angle parameter. " + "This functionality could be added, please provide a pull request for it ;-)"); } try { return RendererUtilities.calculateScale(getViewport().getBounds(), getMapWidth(), getMapHeight(), hints); } catch (Exception e) { throw new ServiceException("Failed to compute accurate scale denominator", e); } } else { AffineTransform at = getRenderingTransform(); if (Math.abs(XAffineTransform.getRotation(at)) != 0.0) { return RendererUtilities.calculateOGCScaleAffine(getCoordinateReferenceSystem(), at, hints); } else { return RendererUtilities.calculateOGCScale(getViewport().getBounds(), getMapWidth(), hints); } } } /** * Computes the StreamingRenderer scale computation method hint based on the current request * * @param request * */ public String getRendererScaleMethod() { if (request.getScaleMethod() == ScaleComputationMethod.Accurate) { return StreamingRenderer.SCALE_ACCURATE; } else { return StreamingRenderer.SCALE_OGC; } } /** * Generic map attached to the map content, can be used to persist information around the life cycle * when the {@link WebMap} is not appropriate, or to persist state across the various response callbacks * @return */ public Map<String, Object> getMetadata() { return metadata; } @Override public void dispose() { this.request = null; this.callbacks = null; this.metadata = null; super.dispose(); } }