/* (c) 2014 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.awt.Color; import java.awt.image.DataBuffer; import java.awt.image.IndexColorModel; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import org.geotools.filter.FilterAttributeExtractor; import org.geotools.styling.AnchorPoint; import org.geotools.styling.ChannelSelection; import org.geotools.styling.ColorMap; import org.geotools.styling.ColorMapEntry; import org.geotools.styling.ContrastEnhancement; import org.geotools.styling.Displacement; import org.geotools.styling.ExternalGraphic; import org.geotools.styling.FeatureTypeConstraint; import org.geotools.styling.FeatureTypeStyle; import org.geotools.styling.Fill; import org.geotools.styling.Graphic; import org.geotools.styling.Halo; import org.geotools.styling.ImageOutline; import org.geotools.styling.LinePlacement; import org.geotools.styling.LineSymbolizer; import org.geotools.styling.Mark; import org.geotools.styling.NamedLayer; import org.geotools.styling.OverlapBehavior; import org.geotools.styling.PointPlacement; import org.geotools.styling.PointSymbolizer; import org.geotools.styling.PolygonSymbolizer; import org.geotools.styling.RasterSymbolizer; import org.geotools.styling.Rule; import org.geotools.styling.SelectedChannelType; import org.geotools.styling.ShadedRelief; import org.geotools.styling.Stroke; import org.geotools.styling.Style; import org.geotools.styling.StyleVisitor; import org.geotools.styling.StyledLayer; import org.geotools.styling.StyledLayerDescriptor; import org.geotools.styling.Symbol; import org.geotools.styling.Symbolizer; import org.geotools.styling.TextSymbolizer; import org.geotools.styling.TextSymbolizer2; import org.geotools.styling.UserLayer; import org.opengis.filter.Filter; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.Literal; /** * A style visitor whose purpose is to extract a minimal palette for the * provided style. This is to be used when no antialiasing is used, since that * would introduce various colors not included in the style. <br> * At the moment the palette is extracted only if external graphics aren't * referenced (a future version may learn to extract a palette merging the ones * of the external graphics). */ public class PaletteExtractor extends FilterAttributeExtractor implements StyleVisitor { public static final Color TRANSPARENT = new Color(255,255,255,0); private static final int TRANSPARENT_CODE = 255 << 16 | 255 << 8 | 255; Set/*<Color>*/ colors; boolean translucentSymbolizers; boolean externalGraphicsSymbolizers; boolean unknownColors; boolean rasterUsed; /** * Initializes a new palette extractor * @param background background color, or null if transparent */ public PaletteExtractor(Color background) { super(null); colors = new HashSet(); if(background == null) background = TRANSPARENT; colors.add(background); } public boolean canComputePalette() { // hard fail conditions if(translucentSymbolizers || externalGraphicsSymbolizers || unknownColors || rasterUsed) return false; // impossible to devise a palette (0 shuold never happen, but you never know...) if(colors.size() == 0 || colors.size() > 256) return false; return true; } /** * Returns the palette, or null if it wasn't possible to devise one * */ public IndexColorModel getPalette() { if(!canComputePalette()) return null; int[] cmap = new int[colors.size()]; int i = 0; for (Iterator it = colors.iterator(); it.hasNext();) { Color color = (Color) it.next(); cmap[i++] = (color.getAlpha() << 24) | (color.getRed() << 16) | (color.getGreen() << 8) | color.getBlue(); } // have a nice looking palette Arrays.sort(cmap); int transparentIndex = cmap[cmap.length - 1] == TRANSPARENT_CODE ? cmap.length - 1 : -1; // find out the minimum number of bits required to represent the palette, and return it int bits = 8; if(cmap.length <= 2) { bits = 1; } else if(cmap.length <= 4) { bits = 2; } else if(cmap.length <= 16) { bits = 4; } // workaround for GEOS-1341, GEOS-1337 will try to find a solution // int length = (int) Math.pow(2, bits); int length = bits == 1 ? 2 : 256; if(cmap.length < length) { int[] temp = new int[length]; System.arraycopy(cmap, 0, temp, 0, cmap.length); cmap = temp; } return new IndexColorModel(bits, cmap.length, cmap, 0, true, transparentIndex, DataBuffer.TYPE_BYTE); } /** * Checks whether translucency is used in the provided expression. Raises the flag * of used translucency unless it's possible to determine it's not. * @param opacity */ void handleOpacity(Expression opacity) { if(opacity == null) return; if(opacity instanceof Literal) { Literal lo = (Literal) opacity; double value = ((Double) lo.evaluate(null, Double.class)).doubleValue(); translucentSymbolizers = translucentSymbolizers || value != 1; } else { // we cannot know, so we assume some will be non opaque translucentSymbolizers = true; } } /** * Adds a color to the color set, and raises the unknown color flag if the color * is an expression other than a literal * @param color */ void handleColor(Expression color) { if(color == null) return; if(color instanceof Literal) { Literal lc = (Literal) color; String rgbColor = (String) lc.evaluate(null, String.class); colors.add(Color.decode(rgbColor)); } else { unknownColors = true; } } /** * @see org.geotools.styling.StyleVisitor#visit(org.geotools.styling.Style) */ public void visit(Style style) { FeatureTypeStyle[] ftStyles = style.getFeatureTypeStyles(); for (int i = 0; i < ftStyles.length; i++) { ftStyles[i].accept(this); } } /** * @see org.geotools.styling.StyleVisitor#visit(org.geotools.styling.Rule) */ public void visit(Rule rule) { Filter filter = rule.getFilter(); if (filter != null) { filter.accept(this, null); } Symbolizer[] symbolizers = rule.getSymbolizers(); if (symbolizers != null) { for (int i = 0; i < symbolizers.length; i++) { Symbolizer symbolizer = symbolizers[i]; symbolizer.accept(this); } } } /** * @see org.geotools.styling.StyleVisitor#visit(org.geotools.styling.FeatureTypeStyle) */ public void visit(FeatureTypeStyle fts) { Rule[] rules = fts.getRules(); for (int i = 0; i < rules.length; i++) { Rule rule = rules[i]; rule.accept(this); } } public void visit(StyledLayerDescriptor sld) { StyledLayer[] layers = sld.getStyledLayers(); for (int i = 0; i < layers.length; i++) { if (layers[i] instanceof NamedLayer) { ((NamedLayer) layers[i]).accept(this); } else if (layers[i] instanceof UserLayer) { ((UserLayer) layers[i]).accept(this); } } } public void visit(NamedLayer layer) { Style[] styles = layer.getStyles(); for (int i = 0; i < styles.length; i++) { styles[i].accept(this); } } public void visit(UserLayer layer) { Style[] styles = layer.getUserStyles(); for (int i = 0; i < styles.length; i++) { styles[i].accept(this); } } public void visit(FeatureTypeConstraint ftc) { // nothing to do } /** * @see org.geotools.styling.StyleVisitor#visit(org.geotools.styling.Symbolizer) */ public void visit(Symbolizer sym) { if (sym instanceof PointSymbolizer) { visit((PointSymbolizer) sym); } if (sym instanceof LineSymbolizer) { visit((LineSymbolizer) sym); } if (sym instanceof PolygonSymbolizer) { visit((PolygonSymbolizer) sym); } if (sym instanceof TextSymbolizer) { visit((TextSymbolizer) sym); } if (sym instanceof RasterSymbolizer) { visit((RasterSymbolizer) sym); } } /** * @see org.geotools.styling.StyleVisitor#visit(org.geotools.styling.Fill) */ public void visit(Fill fill) { handleColor(fill.getBackgroundColor()); handleColor(fill.getColor()); if(fill.getGraphicFill() != null) fill.getGraphicFill().accept(this); handleOpacity(fill.getOpacity()); } /** * @see org.geotools.styling.StyleVisitor#visit(org.geotools.styling.Stroke) */ public void visit(Stroke stroke) { handleColor(stroke.getColor()); if (stroke.getGraphicFill() != null) { stroke.getGraphicFill().accept(this); } if (stroke.getGraphicStroke() != null) { stroke.getGraphicStroke().accept(this); } handleOpacity(stroke.getOpacity()); } public void visit(RasterSymbolizer rs) { rasterUsed = true; } /** * @see org.geotools.styling.StyleVisitor#visit(org.geotools.styling.PointSymbolizer) */ public void visit(PointSymbolizer ps) { if (ps.getGraphic() != null) { ps.getGraphic().accept(this); } } /** * @see org.geotools.styling.StyleVisitor#visit(org.geotools.styling.LineSymbolizer) */ public void visit(LineSymbolizer line) { if (line.getStroke() != null) { line.getStroke().accept(this); } } /** * @see org.geotools.styling.StyleVisitor#visit(org.geotools.styling.PolygonSymbolizer) */ public void visit(PolygonSymbolizer poly) { if (poly.getStroke() != null) { poly.getStroke().accept(this); } if (poly.getFill() != null) { poly.getFill().accept(this); } } /** * @see org.geotools.styling.StyleVisitor#visit(org.geotools.styling.TextSymbolizer) */ public void visit(TextSymbolizer text) { if (text instanceof TextSymbolizer2) { if (((TextSymbolizer2) text).getGraphic() != null) ((TextSymbolizer2) text).getGraphic().accept(this); } if (text.getFill() != null) { text.getFill().accept(this); } if (text.getHalo() != null) { text.getHalo().accept(this); } } /** * @see org.geotools.styling.StyleVisitor#visit(org.geotools.styling.Graphic) */ public void visit(Graphic gr) { if (gr.getSymbols() != null) { Symbol[] symbols = gr.getSymbols(); for (int i = 0; i < symbols.length; i++) { Symbol symbol = symbols[i]; symbol.accept(this); } } handleOpacity(gr.getOpacity()); } /** * @see org.geotools.styling.StyleVisitor#visit(org.geotools.styling.Mark) */ public void visit(Mark mark) { if (mark.getFill() != null) { mark.getFill().accept(this); } if (mark.getStroke() != null) { mark.getStroke().accept(this); } } /** * @see org.geotools.styling.StyleVisitor#visit(org.geotools.styling.ExternalGraphic) */ public void visit(ExternalGraphic exgr) { externalGraphicsSymbolizers = true; } /** * @see org.geotools.styling.StyleVisitor#visit(org.geotools.styling.PointPlacement) */ public void visit(PointPlacement pp) { // nothing to do } /** * @see org.geotools.styling.StyleVisitor#visit(org.geotools.styling.AnchorPoint) */ public void visit(AnchorPoint ap) { // nothing to do } /** * @see org.geotools.styling.StyleVisitor#visit(org.geotools.styling.Displacement) */ public void visit(Displacement dis) { // nothing to do } /** * @see org.geotools.styling.StyleVisitor#visit(org.geotools.styling.LinePlacement) */ public void visit(LinePlacement lp) { // nothing to do } /** * @see org.geotools.styling.StyleVisitor#visit(org.geotools.styling.Halo) */ public void visit(Halo halo) { if (halo.getFill() != null) { halo.getFill().accept(this); } } public void visit(ColorMap map) { // for the moment we don't do anything unknownColors = true; } public void visit(ColorMapEntry entry) { unknownColors = true; } public void visit(ContrastEnhancement contrastEnhancement) { unknownColors = true; } public void visit(ImageOutline outline) { unknownColors = true; } public void visit(ChannelSelection cs) { unknownColors = true; } public void visit(OverlapBehavior ob) { unknownColors = true; } public void visit(SelectedChannelType sct) { unknownColors = true; } public void visit(ShadedRelief sr) { unknownColors = true; } }