/* 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.legendgraphic; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.RenderingHints.Key; import java.awt.image.BufferedImage; import java.awt.image.IndexColorModel; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.logging.Logger; import org.geoserver.wms.legendgraphic.LegendUtils.HAlign; import org.geoserver.wms.legendgraphic.LegendUtils.VAlign; import org.geoserver.wms.map.ImageUtils; import org.geotools.styling.ColorMap; import org.geotools.styling.ColorMapEntry; import org.geotools.styling.SelectedChannelType; /** * This class is responsible for building a legend out of a {@link ColorMap} SLD 1.0 element. * * <p> * Notice that the {@link ColorMapLegendCreator} is immutable. * * @author Simone Giannecchini, GeoSolutions. */ @SuppressWarnings("deprecation") class ColorMapLegendCreator { private static final Logger LOGGER=org.geotools.util.logging.Logging.getLogger(ColorMapLegendCreator.class); /** * Builder class for building a {@link ColorMapLegendCreator}. * * <p> * The builder is not threa-safe. * * <p> The correct way to use it is as follows: * * <code> * // colormap element final ColorMap cmap = rasterSymbolizer.getColorMap(); final Builder cmapLegendBuilder= new ColorMapLegendCreator.Builder(); if (cmap != null && cmap.getColorMapEntries() != null && cmap.getColorMapEntries().length > 0) { // passing additional options cmapLegendBuilder.setAdditionalOptions(request.getLegendOptions()); // setting type of colormap cmapLegendBuilder.setColorMapType(cmap.getType()); // is this colormap using extended colors cmapLegendBuilder.setExtended(cmap.getExtendedColors()); // setting the requested colormap entries cmapLegendBuilder.setRequestedDimension(new Dimension(width,height)); // setting transparency and background bkgColor cmapLegendBuilder.setTransparent(transparent); cmapLegendBuilder.setBackgroundColor(bgColor); //setting band // Setting label font and font bkgColor cmapLegendBuilder.setLabelFont(LegendUtils.getLabelFont(request)); cmapLegendBuilder.setLabelFontColor(LegendUtils.getLabelFontColor(request)); //set band final ChannelSelection channelSelection = rasterSymbolizer.getChannelSelection(); cmapLegendBuilder.setBand(channelSelection!=null?channelSelection.getGrayChannel():null); // adding the colormap entries final ColorMapEntry[] colorMapEntries = cmap.getColorMapEntries(); for (ColorMapEntry ce : colorMapEntries) if (ce != null) cmapLegendBuilder.addColorMapEntry(ce); cMapLegendCreator=cmapLegendBuilder.create(); } * </code> * @author Simone Giannecchini, GeoSolutions SAS * */ static class Builder{ private final Queue<ColorMapEntryLegendBuilder> bodyRows = new LinkedList<ColorMapEntryLegendBuilder>(); private ColorMapType colorMapType; private ColorMapEntry previousCMapEntry; private final Map<String, Object> additionalOptions = new HashMap<String, Object>(); private Color backgroundColor; private String grayChannelName = LegendUtils.DEFAULT_CHANNEL_NAME; private boolean extended = false; private Color borderColor = LegendUtils.DEFAULT_BORDER_COLOR; private boolean fontAntiAliasing = true; private HAlign hAlign = HAlign.LEFT; private Font labelFont; private Color labelFontColor; private Dimension requestedDimension; private boolean transparent; private VAlign vAlign = VAlign.BOTTOM; private boolean forceRule = false; private double rowMarginPercentage = LegendUtils.rowPaddingFactor; private double vMarginPercentage = LegendUtils.marginFactor; private double columnMarginPercentage = LegendUtils.columnPaddingFactor; private double hMarginPercentage = LegendUtils.marginFactor; private boolean border = false; private boolean borderLabel = false; private boolean borderRule = false; private boolean bandInformation=false; /** * Adds a {@link ColorMapEntry} element to this builder so that it can take it into account * for building the legend. * * @param cEntry a {@link ColorMapEntry} element for this builder so that it can take it into account * for building the legend. It must be not <code>null</code>. */ public void addColorMapEntry(final ColorMapEntry cEntry) { PackagedUtils.ensureNotNull(cEntry,"cEntry"); //build a ColorMapEntryLegendBuilder for the specified colorMapEntry final ColorMapEntryLegendBuilder element; switch(colorMapType){ case UNIQUE_VALUES: element=new SingleColorMapEntryLegendBuilder(Arrays.asList(cEntry), hAlign, vAlign, backgroundColor, 1.0, grayChannelName, requestedDimension, labelFont, labelFontColor, extended, borderColor); break; case RAMP: element=new RampColorMapEntryLegendBuilder(Arrays.asList(previousCMapEntry,cEntry), hAlign, vAlign, backgroundColor, 1.0, grayChannelName, requestedDimension, labelFont, labelFontColor, extended, borderColor); break; case CLASSES: element=new ClassesEntryLegendBuilder(Arrays.asList(previousCMapEntry,cEntry), hAlign, vAlign, backgroundColor, 1.0, grayChannelName, requestedDimension, labelFont, labelFontColor, extended, borderColor); break; default: throw new IllegalArgumentException("Unrecognized colormap type"); } //add to the table, we can use matrix algebra knowing that W==3 for this matrix bodyRows.add(element); //set last used element previousCMapEntry=cEntry; } /** * @param legendOptions * @uml.property name="additionalOptions" */ public void setAdditionalOptions(final Map<String,Object> legendOptions) { this.additionalOptions.putAll(legendOptions); } /** * @param backGroundColor * @uml.property name="backgroundColor" */ public void setBackgroundColor(final Color backGroundColor) { PackagedUtils.ensureNotNull(backGroundColor,"backGroundColor"); this.backgroundColor = backGroundColor; } public void setBand(final SelectedChannelType grayChannel) { if(grayChannel!=null) this.grayChannelName=grayChannel.getChannelName(); if(grayChannelName==null) this.grayChannelName=LegendUtils.DEFAULT_CHANNEL_NAME; } /** * Sets the {@link ColorMapType} for this legend builder in order to instruct it on how to build the legend. * * @param colorMapType the {@link ColorMapType} for this legend builder in order to instruct it on how to build the legend. */ public void setColorMapType(final ColorMapType colorMapType) { this.colorMapType = colorMapType; } /** * Sets the {@link ColorMapType} for this legend builder in order to instruct it on how to build the legend. * * @param colorMapType a int representing a {@link ColorMapType} for this legend builder in order to instruct it on how to build the legend. */ public void setColorMapType(final int type) { this.colorMapType=ColorMapType.create(type); } /** * @param extended * @uml.property name="extended" */ public void setExtended(final boolean extended) { this.extended = extended; } /** * @param labelFont * @uml.property name="labelFont" */ public void setLabelFont(final Font labelFont) { PackagedUtils.ensureNotNull(labelFont,"labelFont"); this.labelFont=labelFont; } /** * @param labelFontColor * @uml.property name="labelFontColor" */ public void setLabelFontColor(final Color labelFontColor) { PackagedUtils.ensureNotNull(labelFontColor,"labelFontColor"); this.labelFontColor=labelFontColor; } /** * @param dimension * @uml.property name="requestedDimension" */ public void setRequestedDimension(final Dimension dimension) { this.requestedDimension=(Dimension) dimension.clone(); } /** * @param transparent * @uml.property name="transparent" */ public void setTransparent(final boolean transparent) { this.transparent=transparent; } public void setBorderLabel(boolean borderLabel) { this.borderLabel = borderLabel; } public void setBorderRule(boolean borderRule) { this.borderRule = borderRule; } /** * Creates a {@link ColorMapLegendCreator} using the provided elements. * * @return a {@link ColorMapLegendCreator}. */ public ColorMapLegendCreator create(){ return new ColorMapLegendCreator(this); } void checkAdditionalOptions() { fontAntiAliasing = false; if (additionalOptions.get("fontAntiAliasing") instanceof String) { String aaVal = (String)additionalOptions.get("fontAntiAliasing"); if (aaVal.equalsIgnoreCase("on") || aaVal.equalsIgnoreCase("true") || aaVal.equalsIgnoreCase("yes") || aaVal.equalsIgnoreCase("1")) { fontAntiAliasing = true; } } if (additionalOptions.get("dx") instanceof String) { columnMarginPercentage=Double.parseDouble((String) additionalOptions.get("dx")); } if (additionalOptions.get("bandInfo") instanceof String) { bandInformation=Boolean.parseBoolean((String) additionalOptions.get("bandInfo")); } if (additionalOptions.get("dy") instanceof String) { rowMarginPercentage=Double.parseDouble((String) additionalOptions.get("dy")); } if (additionalOptions.get("mx") instanceof String) { hMarginPercentage=Double.parseDouble((String) additionalOptions.get("mx")); } if (additionalOptions.get("my") instanceof String) { vMarginPercentage=Double.parseDouble((String) additionalOptions.get("my")); } if (additionalOptions.get("borderColor") instanceof String) { borderColor=LegendUtils.color((String) additionalOptions.get("borderColor")); } if (additionalOptions.get("border") instanceof String) { border=Boolean.valueOf((String) additionalOptions.get("border")); } // if (additionalOptions.get("borderRule") instanceof String) { // borderRule=Boolean.parseBoolean((String) additionalOptions.get("borderRule")); // // } // // if (additionalOptions.get("borderLabel") instanceof String) { // borderLabel=Boolean.parseBoolean((String) additionalOptions.get("borderLabel")); // // } if (additionalOptions.get("forceRule") instanceof String) { forceRule=Boolean.parseBoolean((String) additionalOptions.get("forceRule")); } // if all the labels are null, we MUST draw the rules if(!forceRule) for(ColorMapEntryLegendBuilder row:bodyRows){ // //row number i // // label final Cell labelM= row.getLabelManager(); if(labelM==null) forceRule=true; else { forceRule=false; break; } } } public void setBandInformation(boolean bandInformation) { this.bandInformation = bandInformation; } } /** * * @author Simone Giannecchini, GeoSolutions SAS * */ enum ColorMapType { UNIQUE_VALUES, RAMP, CLASSES; public static ColorMapType create(final String value) { if (value.equalsIgnoreCase("intervals")) return CLASSES; else if (value.equalsIgnoreCase("ramp")) { return RAMP; } else if (value.equalsIgnoreCase("values")) { return UNIQUE_VALUES; } else return ColorMapType.valueOf(value); } public static ColorMapType create(final int value) { switch (value) { case ColorMap.TYPE_INTERVALS: return ColorMapType.CLASSES; case ColorMap.TYPE_RAMP: return ColorMapType.RAMP; case ColorMap.TYPE_VALUES: return ColorMapType.UNIQUE_VALUES; default: throw new IllegalArgumentException("Unable to create ColorMapType for value "+value); } } } private ColorMapType colorMapType; private boolean extended=false; private boolean transparent; private Dimension requestedDimension; private Color backgroundColor; private Font labelFont; private Color labelFontColor; private final Queue<ColorMapEntryLegendBuilder> bodyRows= new LinkedList<ColorMapEntryLegendBuilder>(); private final List<Cell> footerRows = new ArrayList<Cell>(); private HAlign hAlign=HAlign.LEFT; private VAlign vAlign=VAlign.BOTTOM; private double vMarginPercentage=LegendUtils.marginFactor; private double hMarginPercentage=LegendUtils.marginFactor; private double rowMarginPercentage=LegendUtils.rowPaddingFactor; private double columnMarginPercentage=LegendUtils.columnPaddingFactor; private Color borderColor=LegendUtils.DEFAULT_BORDER_COLOR; private boolean borderLabel=false; private boolean borderRule=false; private double margin; private double rowH; private double colorW; private double ruleW; private double labelW; private double footerW; private String grayChannelName="1"; private boolean fontAntiAliasing=true; private boolean forceRule=false; private BufferedImage legend; private boolean border=false; private double dx; private double dy; private boolean bandInformation; public ColorMapLegendCreator(final Builder builder) { this.backgroundColor=builder.backgroundColor; this.bodyRows.addAll(builder.bodyRows); this.border=builder.border; this.borderColor=builder.borderColor; this.borderLabel=builder.borderLabel; this.borderRule=builder.borderRule; this.colorMapType=builder.colorMapType; this.columnMarginPercentage=builder.columnMarginPercentage; this.extended=builder.extended; this.fontAntiAliasing=builder.fontAntiAliasing; this.forceRule=builder.forceRule; this.grayChannelName=builder.grayChannelName; this.hAlign=builder.hAlign; this.vAlign=builder.vAlign; this.labelFont=builder.labelFont; this.labelFontColor=builder.labelFontColor; this.rowMarginPercentage=builder.rowMarginPercentage; this.columnMarginPercentage=builder.columnMarginPercentage; this.hMarginPercentage=builder.hMarginPercentage; this.vMarginPercentage=builder.vMarginPercentage; this.requestedDimension=(Dimension) builder.requestedDimension.clone(); this.transparent=builder.transparent; this.bandInformation=builder.bandInformation; } public synchronized BufferedImage getLegend() { //do we laraedy have a legend if(legend==null) { //init all the values init(); //now build the individuals legends // // header // //XXX no header for the moment // // body // final Queue<BufferedImage> body=createBody(); // // footer // if(bandInformation){ final Queue<BufferedImage> footer= createFooter(); body.addAll(footer); } //now merge them legend= mergeRows(body); } return legend; } private void init() { // // create a sample image for computing dimensions of text strings // BufferedImage image = ImageUtils.createImage(1, 1, (IndexColorModel)null, transparent); final Map<Key, Object> hintsMap = new HashMap<Key, Object>(); Graphics2D graphics = ImageUtils.prepareTransparency(transparent, backgroundColor, image, hintsMap); //elements used to compute maximum dimensions for rows and cells rowH = 0; colorW = 0; ruleW = 0; labelW = 0; // //BODY // //cycle over all the body elements cycleBodyRows(graphics); // //FOOTER // //set footer strings if(bandInformation){ final String bandNameString = "Band selection is " + this.grayChannelName; footerRows.add(new TextManager(bandNameString, vAlign, hAlign, backgroundColor, requestedDimension, labelFont, labelFontColor, fontAntiAliasing, borderColor)); // set footer strings final String colorMapTypeString = "ColorMap type is " + this.colorMapType.toString(); footerRows.add(new TextManager(colorMapTypeString, vAlign, hAlign, backgroundColor, requestedDimension, labelFont, labelFontColor, fontAntiAliasing, borderColor)); // extended colors or not final String extendedCMapString = "ColorMap is " + (this.extended ? "" : "not") + " extended"; footerRows.add(new TextManager(extendedCMapString, vAlign, hAlign, backgroundColor, requestedDimension, labelFont, labelFontColor, fontAntiAliasing, borderColor)); cycleFooterRows(graphics); } // // compute dimensions //this. //final dimension are different between ramp and others since ramp does not have margin for rows final double maxW = Math.max(colorW+ruleW+labelW, footerW); dx=maxW*columnMarginPercentage; dy=colorMapType==ColorMapType.RAMP?0:rowH*rowMarginPercentage; final double mx=maxW*hMarginPercentage; final double my=rowH*vMarginPercentage; margin=Math.max(mx, my); } private void cycleFooterRows(Graphics2D graphics) { int numRows=this.footerRows.size(),i=0; footerW=Double.NEGATIVE_INFINITY; for(i=0;i<numRows;i++){ // //row number i // // color element final Cell cell= this.footerRows.get(i); final Dimension cellDim=cell.getPreferredDimension(graphics); rowH=Math.max(rowH, cellDim.getHeight()); footerW=Math.max(footerW, cellDim.getWidth()); } } /** * @param graphics */ private void cycleBodyRows(Graphics2D graphics) { for(ColorMapEntryLegendBuilder row:bodyRows){ // //row number i // // color element final Cell cm= row.getColorManager(); final Dimension colorDim=cm.getPreferredDimension(graphics); rowH=Math.max(rowH, colorDim.getHeight()); colorW=Math.max(colorW, colorDim.getWidth()); // rule if(forceRule){ final Cell ruleM= row.getRuleManager(); final Dimension ruleDim=ruleM.getPreferredDimension(graphics); rowH=Math.max(rowH, ruleDim.getHeight()); ruleW=Math.max(ruleW, ruleDim.getWidth()); } // label final Cell labelM= row.getLabelManager(); if(labelM==null) continue; final Dimension labelDim=labelM.getPreferredDimension(graphics); rowH=Math.max(rowH, labelDim.getHeight()); labelW=Math.max(labelW, labelDim.getWidth()); } } private Queue<BufferedImage> createFooter() { // creating a backbuffer image on which we should draw the bkgColor for this colormap element final BufferedImage image = ImageUtils.createImage(1, 1, (IndexColorModel)null, transparent); final Map<Key, Object> hintsMap = new HashMap<Key, Object>(); final Graphics2D graphics = ImageUtils.prepareTransparency(transparent, backgroundColor, image, hintsMap); //list where we store the rows for the footer final Queue<BufferedImage> queue= new LinkedList<BufferedImage>(); // //the height is already fixed // final int rowHeight=(int)Math.round(rowH); final int rowWidth=(int)Math.round(footerW); // final Rectangle clipboxA=new Rectangle(0,0,rowWidth,rowHeight); // // footer // // //draw the various bodyCells for(Cell cell:footerRows){ //get dim final Dimension dim= cell.getPreferredDimension(graphics); // final int rowWidth=(int)Math.round(dim.getWidth()); final int rowHeight=(int)Math.round(dim.getHeight()); final Rectangle clipboxA=new Rectangle(0,0,rowWidth,rowHeight); //draw it final BufferedImage colorCellLegend = new BufferedImage(rowWidth, rowHeight, BufferedImage.TYPE_INT_ARGB); Graphics2D rlg = colorCellLegend.createGraphics(); rlg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); cell.draw(rlg, clipboxA,border); rlg.dispose(); queue.add(colorCellLegend); } graphics.dispose(); return queue;//mergeRows(queue); } private Queue<BufferedImage> createBody() { final Queue<BufferedImage> queue= new LinkedList<BufferedImage>(); // // draw the various elements // //create the boxes for drawing later final int rowHeight=(int)Math.round(rowH); final int colorWidth=(int)Math.round(colorW); final int ruleWidth=(int)Math.round(ruleW); final int labelWidth=(int)Math.round(labelW); final Rectangle clipboxA=new Rectangle(0,0,colorWidth,rowHeight); final Rectangle clipboxB=new Rectangle(0,0,ruleWidth,rowHeight); final Rectangle clipboxC=new Rectangle(0,0,labelWidth,rowHeight); // // Body // // //draw the various bodyCells for(ColorMapEntryLegendBuilder row:bodyRows){ // //row number i // //get element for color default behavior final Cell colorCell= row.getColorManager(); //draw it final BufferedImage colorCellLegend = new BufferedImage(colorWidth, rowHeight, BufferedImage.TYPE_INT_ARGB); Graphics2D rlg = colorCellLegend.createGraphics(); rlg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); rlg.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); colorCell.draw(rlg, clipboxA,border); rlg.dispose(); BufferedImage ruleCellLegend=null; if(forceRule){ //get element for rule final Cell ruleCell= row.getRuleManager(); //draw it ruleCellLegend = new BufferedImage(ruleWidth, rowHeight, BufferedImage.TYPE_INT_ARGB); rlg = ruleCellLegend.createGraphics(); rlg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); rlg.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); ruleCell.draw(rlg, clipboxB,borderRule); rlg.dispose(); } //draw it if it is present if(labelWidth>0){ //get element for label final Cell labelCell= row.getLabelManager(); if(labelCell!=null) { final BufferedImage labelCellLegend = new BufferedImage(labelWidth, rowHeight, BufferedImage.TYPE_INT_ARGB); rlg = labelCellLegend.createGraphics(); rlg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); rlg.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); labelCell.draw(rlg, clipboxC,borderLabel); rlg.dispose(); // // merge the bodyCells for this row // // final Map<Key, Object> hintsMap = new HashMap<Key, Object>(); queue.add(LegendUtils.hMergeBufferedImages(colorCellLegend,ruleCellLegend,labelCellLegend, hintsMap,transparent,backgroundColor,dx)); } else { final Map<Key, Object> hintsMap = new HashMap<Key, Object>(); queue.add(LegendUtils.hMergeBufferedImages(colorCellLegend,ruleCellLegend,null, hintsMap,transparent,backgroundColor,dx)); } } else{ // // merge the bodyCells for this row // // final Map<Key, Object> hintsMap = new HashMap<Key, Object>(); queue.add(LegendUtils.hMergeBufferedImages(colorCellLegend,ruleCellLegend,null, hintsMap,transparent,backgroundColor,dx)); } } //return the list of legends return queue;//mergeRows(queue); } private BufferedImage mergeRows(Queue<BufferedImage> legendsQueue) { //create the final image // I am doing a straight cast since I know that I built this // dimension object by using the widths and heights of the various // bufferedimages for the various bkgColor map entries. final Dimension finalDimension = new Dimension(); final int numRows = legendsQueue.size(); finalDimension.setSize(Math.max(footerW,colorW + ruleW + labelW) + 2 * dx + 2* margin, rowH * numRows + 2 * margin + (numRows - 1) * dy); final int totalWidth=(int) finalDimension.getWidth(); final int totalHeight=(int) finalDimension.getHeight(); final BufferedImage finalLegend = ImageUtils.createImage(totalWidth, totalHeight, (IndexColorModel)null, transparent); final Map<Key, Object> hintsMap = new HashMap<Key, Object>(); Graphics2D finalGraphics = ImageUtils.prepareTransparency(transparent, backgroundColor, finalLegend, hintsMap); hintsMap.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); finalGraphics.setRenderingHints(hintsMap); int topOfRow = (int) (margin+0.5); for (int i = 0; i < numRows; i++) { final BufferedImage img = legendsQueue.remove(); // draw the image finalGraphics.drawImage(img, (int) (margin+0.5), topOfRow, null); topOfRow += img.getHeight()+dy; } finalGraphics.dispose(); return finalLegend; } }