/* 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.decoration;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.ResourceInfo;
import org.geoserver.platform.ServiceException;
import org.geoserver.wms.WMS;
import org.geoserver.wms.WMSMapContent;
import org.geoserver.wms.legendgraphic.LegendUtils;
import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.geometry.jts.LiteShape2;
import org.geotools.map.Layer;
import org.geotools.renderer.lite.RendererUtilities;
import org.geotools.renderer.lite.StyledShapePainter;
import org.geotools.renderer.style.SLDStyleFactory;
import org.geotools.renderer.style.Style2D;
import org.geotools.styling.FeatureTypeStyle;
import org.geotools.styling.LineSymbolizer;
import org.geotools.styling.PointSymbolizer;
import org.geotools.styling.PolygonSymbolizer;
import org.geotools.styling.RasterSymbolizer;
import org.geotools.styling.Rule;
import org.geotools.styling.Style;
import org.geotools.styling.Symbolizer;
import org.geotools.styling.TextSymbolizer;
import org.geotools.util.NumberRange;
import org.opengis.feature.IllegalAttributeException;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.PropertyDescriptor;
import org.opengis.feature.type.PropertyType;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.Polygon;
public class LegendDecoration implements MapDecoration {
/** A logger for this class. */
private static final Logger LOGGER =
org.geotools.util.logging.Logging.getLogger("org.geoserver.wms.responses");
private static final int TITLE_INDENT = 4;
private static SLDStyleFactory styleFactory = new SLDStyleFactory();
private Color bgcolor = Color.WHITE;
private Color fgcolor = Color.BLACK;
private static final GeometryFactory geomFac = new GeometryFactory();
/**
* Just a holder to avoid creating many polygon shapes from inside
* <code>getSampleShape()</code>
*/
private LiteShape2 sampleRect;
/**
* Just a holder to avoid creating many line shapes from inside
* <code>getSampleShape()</code>
*/
private LiteShape2 sampleLine;
/**
* Just a holder to avoid creating many point shapes from inside
* <code>getSampleShape()</code>
*/
private LiteShape2 samplePoint;
private final WMS wms;
private static final StyledShapePainter shapePainter = new StyledShapePainter();
public LegendDecoration(WMS wms){
this.wms = wms;
}
public void loadOptions(Map<String, String> options){
Color tmp = parseColor(options.get("bgcolor"));
if (tmp != null) this.bgcolor = tmp;
tmp = parseColor(options.get("fgcolor"));
if (tmp != null) this.fgcolor = tmp;
}
Catalog findCatalog(WMSMapContent mapContent){
return wms.getGeoServer().getCatalog();
}
public Dimension findOptimalSize(Graphics2D g2d, WMSMapContent mapContent){
int x = 0, y = 0;
Catalog catalog = findCatalog(mapContent);
FontMetrics metrics = g2d.getFontMetrics(g2d.getFont().deriveFont(Font.BOLD));
double scaleDenominator = RendererUtilities.calculateOGCScale(
mapContent.getRenderingArea(),
mapContent.getRequest().getWidth(),
null
);
for (Layer layer : mapContent.layers()){
SimpleFeatureType type = (SimpleFeatureType)layer.getFeatureSource().getSchema();
if (!isGridLayer(type)) {
try {
Dimension legend = getLegendSize(
type,
layer.getStyle(),
scaleDenominator,
g2d
);
x = Math.max(x, (int)legend.width);
x = Math.max(x, TITLE_INDENT + metrics.stringWidth(findTitle(layer, catalog)));
y += legend.height + metrics.getHeight();
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Error sizing legend for " + layer);
continue;
}
} else {
LOGGER.log(Level.FINE, "Skipping raster layer: " + layer);
}
}
x += metrics.getDescent();
return new Dimension(x, y);
}
public void paint(Graphics2D g2d, Rectangle paintArea, WMSMapContent mapContent)
throws Exception {
Catalog catalog = wms.getGeoServer().getCatalog();
Dimension d = findOptimalSize(g2d, mapContent);
Rectangle bgRect = new Rectangle(0, 0, d.width, d.height);
double scaleDenominator = RendererUtilities.calculateOGCScale(
mapContent.getRenderingArea(),
mapContent.getRequest().getWidth(),
new HashMap()
);
Color oldColor = g2d.getColor();
AffineTransform oldTransform = (AffineTransform)g2d.getTransform().clone();
Font oldFont = g2d.getFont();
Stroke oldStroke = g2d.getStroke();
g2d.translate(paintArea.getX(), paintArea.getY());
AffineTransform tx = new AffineTransform();
FontMetrics metrics = g2d.getFontMetrics(g2d.getFont().deriveFont(Font.BOLD));
double scaleFactor = (paintArea.getWidth() / d.getWidth());
scaleFactor = Math.min(scaleFactor, paintArea.getHeight() / d.getHeight());
if (scaleFactor < 1.0) {
g2d.scale(scaleFactor, scaleFactor);
}
AffineTransform bgTransform = g2d.getTransform();
g2d.setColor(bgcolor);
g2d.fill(bgRect);
g2d.setColor(fgcolor);
for (Layer layer : mapContent.layers()){
SimpleFeatureType type = (SimpleFeatureType)layer.getFeatureSource().getSchema();
if (!isGridLayer(type)) {
try {
g2d.translate(0, metrics.getHeight());
g2d.setFont(g2d.getFont().deriveFont(Font.BOLD));
g2d.drawString(findTitle(layer, catalog), TITLE_INDENT, 0 - metrics.getDescent());
g2d.setFont(g2d.getFont().deriveFont(Font.PLAIN));
Dimension dim = drawLegend(
type,
layer.getStyle(),
scaleDenominator,
g2d
);
g2d.translate(0, dim.getHeight());
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Couldn't make a legend for " + type.getName(), e);
}
} else {
LOGGER.log(Level.FINE, "Skipping raster layer " + type.getName() + " in legend decoration");
}
}
g2d.setTransform(bgTransform);
g2d.setStroke(new BasicStroke(1));
g2d.draw(new Rectangle(bgRect.x, bgRect.y, bgRect.width -1, bgRect.height - 1));
g2d.setStroke(oldStroke);
g2d.setTransform(oldTransform);
g2d.setFont(oldFont);
g2d.setColor(oldColor);
}
private String findTitle(Layer layer, Catalog catalog) {
String[] nameparts = layer.getTitle().split(":");
ResourceInfo resource = nameparts.length > 1
? catalog.getResourceByName(nameparts[0], nameparts[1], ResourceInfo.class)
: catalog.getResourceByName(nameparts[0], ResourceInfo.class);
return resource != null
? resource.getTitle()
: layer.getTitle();
}
public Dimension getLegendSize(
final SimpleFeatureType layer,
final Style style,
final double scaleDenominator,
Graphics2D g2d
) throws ServiceException {
final SimpleFeature sampleFeature = createSampleFeature(layer);
final FeatureTypeStyle[] ftStyles = style.getFeatureTypeStyles();
final Rule[] applicableRules = LegendUtils.getApplicableRules(ftStyles, scaleDenominator);
final NumberRange<Double> scaleRange =
NumberRange.create(scaleDenominator, scaleDenominator);
final int ruleCount = applicableRules.length;
final int w = 20;
final int h = 20;
FontMetrics metrics = g2d.getFontMetrics();
float totalHeight = 0, totalWidth = 0;
for (int i = 0; i < ruleCount; i++) {
final Symbolizer[] symbolizers = applicableRules[i].getSymbolizers();
for (int sIdx = 0; sIdx < symbolizers.length; sIdx++) {
final Symbolizer symbolizer = symbolizers[sIdx];
if (symbolizer instanceof RasterSymbolizer) {
throw new IllegalStateException(
"It is not legal to have a RasterSymbolizer here"
);
}
}
String label = applicableRules[i].getTitle();
if (label == null) label = applicableRules[i].getName();
if (label == null) label = "";
float heightIncrement = Math.max(h, metrics.getHeight());
totalHeight = totalHeight + heightIncrement;
totalWidth =
Math.max(totalWidth, w + metrics.getDescent() + metrics.stringWidth(label));
}
return new Dimension((int)totalWidth, (int)totalHeight);
}
public Dimension drawLegend(
final SimpleFeatureType layer,
final Style style,
final double scaleDenominator,
Graphics2D g2d) throws ServiceException {
final SimpleFeature sampleFeature = createSampleFeature(layer);
final FeatureTypeStyle[] ftStyles = style.getFeatureTypeStyles();
final Rule[] applicableRules = LegendUtils.getApplicableRules(ftStyles, scaleDenominator);
final NumberRange<Double> scaleRange =
NumberRange.create(scaleDenominator, scaleDenominator);
final int ruleCount = applicableRules.length;
final int w = 20;
final int h = 20;
FontMetrics metrics = g2d.getFontMetrics();
AffineTransform oldTransform = g2d.getTransform();
Composite oldComposite = g2d.getComposite();
float totalHeight = 0, totalWidth = 0;
for (int i = 0; i < ruleCount; i++) {
final Symbolizer[] symbolizers = applicableRules[i].getSymbolizers();
for (int sIdx = 0; sIdx < symbolizers.length; sIdx++) {
final Symbolizer symbolizer = symbolizers[sIdx];
if (symbolizer instanceof RasterSymbolizer) {
throw new IllegalStateException(
"It is not legal to have a RasterSymbolizer here"
);
} else {
Style2D style2d =
styleFactory.createStyle(sampleFeature, symbolizer, scaleRange);
LiteShape2 shape = getSampleShape(symbolizer, w, h);
if (style2d != null) {
shapePainter.paint(g2d, shape, style2d, scaleDenominator);
}
}
}
String label = applicableRules[i].getTitle();
if (label == null) label = applicableRules[i].getName();
if (label == null) label = "";
g2d.setColor(Color.BLACK);
g2d.setComposite(AlphaComposite.SrcOver);
g2d.drawString(
label,
h + metrics.getDescent(),
metrics.getHeight()
);
float heightIncrement = Math.max(h, metrics.getHeight());
g2d.translate(0, heightIncrement);
totalHeight = totalHeight + heightIncrement;
totalWidth =
Math.max(totalWidth, w + metrics.getDescent() + metrics.stringWidth(label));
}
g2d.setTransform(oldTransform);
g2d.setComposite(oldComposite);
return new Dimension((int)totalWidth, (int)totalHeight);
}
/**
* Creates a sample Feature instance in the hope that it can be used in the
* rendering of the legend graphic.
*
* @param schema the schema for which to create a sample Feature instance
*
* @throws ServiceException
*/
private static SimpleFeature createSampleFeature(SimpleFeatureType schema)
throws ServiceException {
SimpleFeature sampleFeature;
try {
sampleFeature = SimpleFeatureBuilder.template(schema, null);
} catch (IllegalAttributeException e) {
throw new ServiceException(e);
}
return sampleFeature;
}
/**
* Returns a <code>java.awt.Shape</code> appropiate to render a legend
* graphic given the symbolizer type and the legend dimensions.
*
* @param symbolizer the Symbolizer for whose type a sample shape will be
* created
* @param legendWidth the requested width, in output units, of the legend
* graphic
* @param legendHeight the requested height, in output units, of the legend
* graphic
*
* @return an appropiate Line2D, Rectangle2D or LiteShape(Point) for the
* symbolizer, wether it is a LineSymbolizer, a PolygonSymbolizer,
* or a Point ot Text Symbolizer
*
* @throws IllegalArgumentException if an unknown symbolizer impl was
* passed in.
*/
private LiteShape2 getSampleShape(Symbolizer symbolizer, int legendWidth, int legendHeight) {
LiteShape2 sampleShape;
final float hpad = (legendWidth * LegendUtils.hpaddingFactor);
final float vpad = (legendHeight * LegendUtils.vpaddingFactor);
if (symbolizer instanceof LineSymbolizer) {
if (this.sampleLine == null) {
Coordinate[] coords = {
new Coordinate(hpad, legendHeight - vpad),
new Coordinate(legendWidth - hpad, vpad)
};
LineString geom = geomFac.createLineString(coords);
try {
this.sampleLine = new LiteShape2(geom, null, null, false);
} catch (Exception e) {
this.sampleLine = null;
}
}
sampleShape = this.sampleLine;
} else if ((symbolizer instanceof PolygonSymbolizer)
|| (symbolizer instanceof RasterSymbolizer)) {
if (this.sampleRect == null) {
final float w = legendWidth - (2 * hpad);
final float h = legendHeight - (2 * vpad);
Coordinate[] coords = {
new Coordinate(hpad, vpad), new Coordinate(hpad, vpad + h),
new Coordinate(hpad + w, vpad + h), new Coordinate(hpad + w, vpad),
new Coordinate(hpad, vpad)
};
LinearRing shell = geomFac.createLinearRing(coords);
Polygon geom = geomFac.createPolygon(shell, null);
try {
this.sampleRect = new LiteShape2(geom, null, null, false);
} catch (Exception e) {
this.sampleRect = null;
}
}
sampleShape = this.sampleRect;
} else if (symbolizer instanceof PointSymbolizer || symbolizer instanceof TextSymbolizer) {
if (this.samplePoint == null) {
Coordinate coord = new Coordinate(legendWidth / 2, legendHeight / 2);
try {
this.samplePoint = new LiteShape2(geomFac.createPoint(coord), null, null, false);
} catch (Exception e) {
this.samplePoint = null;
}
}
sampleShape = this.samplePoint;
} else {
throw new IllegalArgumentException("Unknown symbolizer: " + symbolizer);
}
return sampleShape;
}
public static boolean isGridLayer(final SimpleFeatureType layer) {
for(PropertyDescriptor descriptor : layer.getDescriptors()){
final PropertyType type = descriptor.getType();
if (type.getBinding().isAssignableFrom(AbstractGridCoverage2DReader.class)) {
return true;
}
}
return false;
}
public static Color parseColor(String origInput) {
if (origInput == null) return null;
String input = origInput.trim();
input = input.replaceFirst("\\A#", "");
int r, g, b, a;
switch (input.length()){
case 1:
case 2:
return new Color(Integer.valueOf(input, 16));
case 3:
r = Integer.valueOf(input.substring(0, 1), 16);
g = Integer.valueOf(input.substring(1, 2), 16);
b = Integer.valueOf(input.substring(2, 3), 16);
return new Color(r, g, b);
case 4:
r = Integer.valueOf(input.substring(0, 1), 16);
g = Integer.valueOf(input.substring(1, 2), 16);
b = Integer.valueOf(input.substring(2, 3), 16);
a = Integer.valueOf(input.substring(3, 4), 16);
return new Color(r, g, b, a);
case 6:
r = Integer.valueOf(input.substring(0, 2), 16);
g = Integer.valueOf(input.substring(2, 4), 16);
b = Integer.valueOf(input.substring(4, 6), 16);
return new Color(r, g, b);
case 8:
r = Integer.valueOf(input.substring(0, 2), 16);
g = Integer.valueOf(input.substring(2, 4), 16);
b = Integer.valueOf(input.substring(4, 6), 16);
a = Integer.valueOf(input.substring(6, 8), 16);
return new Color(r, g, b, a);
default:
throw new RuntimeException("Couldn't decode color value: " + origInput
+ " (" + input +")"
);
}
}
}