/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2003-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.renderer.style; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Canvas; import java.awt.Color; import java.awt.Composite; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.Stroke; import java.awt.TexturePaint; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.Icon; import javax.swing.ImageIcon; import org.geotools.factory.CommonFactoryFinder; import org.geotools.styling.ExternalGraphic; import org.geotools.styling.Fill; import org.geotools.styling.Font; import org.geotools.styling.Graphic; import org.geotools.styling.Halo; import org.geotools.styling.LabelPlacement; import org.geotools.styling.LinePlacement; import org.geotools.styling.LineSymbolizer; import org.geotools.styling.Mark; import org.geotools.styling.PointPlacement; import org.geotools.styling.PointSymbolizer; import org.geotools.styling.PolygonSymbolizer; import org.geotools.styling.StyleAttributeExtractorTruncated; import org.geotools.styling.StyleFactoryFinder; import org.geotools.styling.Symbolizer; import org.geotools.styling.TextSymbolizer; import org.geotools.styling.TextSymbolizer2; import org.geotools.util.Range; import org.geotools.util.SoftValueHashMap; import org.opengis.feature.Feature; import org.opengis.feature.simple.SimpleFeature; import org.opengis.filter.FilterFactory; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.Literal; import org.opengis.filter.expression.PropertyName; import org.opengis.style.GraphicalSymbol; import com.vividsolutions.jts.geom.Geometry; /** * Factory object that converts SLD style into rendered styles. * * DJB: I've made a few changes to this. The old behavior was for this class to * convert <LinePlacement> tags to <PointPlacement> tags. (ie. there never was a * LinePlacement option) This is *certainly* not the correct place to do this, * and it was doing a very poor job of it too, and the renderer was not * expecting it to be doing it! * * I added support in TextStyle3D for this and had this class correctly set * Line/Point placement selection. NOTE: PointPlacement is the default if not * present. * * @author aaime * @author dblasby * * @source $URL$ */ /* * orginal message on the subject: * * I was attempting to write documentation for label placement (plus fix all the * inconsistencies with the spec), and I noticed some problems with the * SLDStyleFactory and TextStyle2D. * * It turns out the SLDStyleFactory is actually trying to do [poor] label * placement (see around line 570)! This also results in a loss of information * if you're using a <LinePlacement> element in your SLD. * * * 1. remove the placement code from SLDStyleFactory! 2. get rid of the * "AbsoluteLineDisplacement" stuff and replace it with something that * represents <PointPlacement>/<LinePlacement> elements in the TextSymbolizer. * * The current implementation seems to try to convert a <LinePlacement> and an * actual line into a <PointPlacement> (and setting the AbsoluteLineDisplacement * flag)!! This should be done by the real labeling code. * * This change could affect the j2d renderer as it appears to use the * "AbsoluteLineDisplacement" flag. * * @source $URL$ */ public class SLDStyleFactory { /** The logger for the rendering module. */ private static final Logger LOGGER = org.geotools.util.logging.Logging .getLogger("org.geotools.rendering"); /** * The threshold at which we switch from pre-rasterized icons to dynamically * painted ones (to avoid OOM) */ private static final int MAX_RASTERIZATION_SIZE = 512; /** Holds a lookup bewteen SLD names and java constants. */ private static final java.util.Map joinLookup = new java.util.HashMap(); /** Holds a lookup bewteen SLD names and java constants. */ private static final java.util.Map capLookup = new java.util.HashMap(); /** Holds a lookup bewteen SLD names and java constants. */ private static final java.util.Map fontStyleLookup = new java.util.HashMap(); private static final FilterFactory ff = CommonFactoryFinder .getFilterFactory(null); /** This one is used as the observer object in image tracks */ private static final Canvas obs = new Canvas(); static { // static block to populate the lookups joinLookup.put("miter", new Integer(BasicStroke.JOIN_MITER)); joinLookup.put("bevel", new Integer(BasicStroke.JOIN_BEVEL)); joinLookup.put("round", new Integer(BasicStroke.JOIN_ROUND)); capLookup.put("butt", new Integer(BasicStroke.CAP_BUTT)); capLookup.put("round", new Integer(BasicStroke.CAP_ROUND)); capLookup.put("square", new Integer(BasicStroke.CAP_SQUARE)); fontStyleLookup.put("normal", new Integer(java.awt.Font.PLAIN)); fontStyleLookup.put("italic", new Integer(java.awt.Font.ITALIC)); fontStyleLookup.put("oblique", new Integer(java.awt.Font.ITALIC)); fontStyleLookup.put("bold", new Integer(java.awt.Font.BOLD)); } /** Symbolizers that depend on attributes */ Map dynamicSymbolizers = new SoftValueHashMap(); /** Symbolizers that do not depend on attributes */ Map staticSymbolizers = new SoftValueHashMap(); /** * Build a default rendering hint to avoid NPE */ RenderingHints renderingHints = new RenderingHints( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_DEFAULT); /** * Whether to turn all line widths less than 1.5 pixels to 0 to speed up * line rendering. */ private boolean lineOptimizationEnabled = false; /** * Whether to turn on vector rendering or not. Normal behavior is to have it * turned off, which is faster but may not be the best thing when printing * due to quality loss. */ private boolean vectorRenderingEnabled = false; private long hits; private long requests; /** * Holds value of property mapScaleDenominator. */ private double mapScaleDenominator = Double.NaN; /** * The factory builds a fair number of buffered images to deal with external * graphics that need resizing and the like. This hints will be used in * those drawing operations. */ public RenderingHints getRenderingHints() { return renderingHints; } public void setRenderingHints(RenderingHints renderingHints) { if (renderingHints == null) return; this.renderingHints = renderingHints; } /** * Enabled by default, this optimization speeds up line rendering when the * line width is less than 1.5 pixels when antialiasing is disblaed. * Unfortunately it also prevents fine line width control when antialiasing * is enabled. Given that the optimization has been hard coded for more than * six years, we give the user control on this one since turning this off * will change the rendering of all existing styles using thin line widths. */ public boolean isLineOptimizationEnabled() { return lineOptimizationEnabled; } public void setLineOptimizationEnabled(boolean lineOptimizationEnabled) { this.lineOptimizationEnabled = lineOptimizationEnabled; } /** * Indicates whether vector rendering should be preferred when painting * graphic fills (e.g., using a Mark as stipple) or vector Graphic objects * such as SVG ExternalGraphics. The default behavior is to be disabled, * meaning that graphic fills are painted as raster images using Java * TexturePaint, and SVGs are rendered to a BufferedImage prior to painting * on the target Graphics. This common behavior is faster and more suitable * for on-screen rendering. Enabling this flag is recommended for rendering * to off-screen Graphics such as when printing, cases in which the full * quality of the original data should normally be preserved. */ public boolean isVectorRenderingEnabled() { return vectorRenderingEnabled; } /** * Sets whether vector rendering should be preferred when painting graphic * fills (see {@link #isVectorRenderingEnabled()} for more details). * * @param vectorRenderingEnabled * a boolean value indicating whether vector rendering should be * enabled or not. */ public void setVectorRenderingEnabled(boolean vectorRenderingEnabled) { this.vectorRenderingEnabled = vectorRenderingEnabled; } public double getHitRatio() { return (double) hits / (double) requests; } public long getHits() { return hits; } public long getRequests() { return requests; } /** * <p> * Creates a rendered style * </p> * * <p> * Makes use of a symbolizer cache based on identity to avoid recomputing * over and over the same style object and to reduce memory usage. The same * Style2D object will be returned by subsequent calls using the same * feature independent symbolizer with the same scaleRange. * </p> * * @param drawMe * The feature * @param symbolizer * The SLD symbolizer * @param scaleRange * The scale range in which the feature should be painted * according to the symbolizer * * @return A rendered style equivalent to the symbolizer */ public Style2D createStyle(Object drawMe, Symbolizer symbolizer, Range scaleRange) { Style2D style = null; SymbolizerKey key = new SymbolizerKey(symbolizer, scaleRange); style = (Style2D) staticSymbolizers.get(key); requests++; if (style != null) { hits++; } else { style = createStyleInternal(drawMe, symbolizer, scaleRange); // for some legitimate cases some styles cannot be turned into a // valid Style2D // e.g., point symbolizer that contains no graphic that can be used // due to network issues if (style == null) { return null; } // if known dynamic symbolizer return the style if (dynamicSymbolizers.containsKey(key)) { return style; } else { // lets see if it's static or dynamic StyleAttributeExtractorTruncated sae = new StyleAttributeExtractorTruncated(); sae.visit(symbolizer); Set nameSet = sae.getAttributeNameSet(); boolean noAttributes = (nameSet == null) || (nameSet.size() == 0); if (noAttributes && !sae.isUsingVolatileFunctions()) { staticSymbolizers.put(key, style); } else { dynamicSymbolizers.put(key, Boolean.TRUE); } } } return style; } /** * Really creates the symbolizer * * @param drawMe * DOCUMENT ME! * @param symbolizer * DOCUMENT ME! * @param scaleRange * DOCUMENT ME! * * @return DOCUMENT ME! */ private Style2D createStyleInternal(Object drawMe, Symbolizer symbolizer, Range scaleRange) { Style2D style = null; if (symbolizer instanceof PolygonSymbolizer) { style = createPolygonStyle(drawMe, (PolygonSymbolizer) symbolizer, scaleRange); } else if (symbolizer instanceof LineSymbolizer) { style = createLineStyle(drawMe, (LineSymbolizer) symbolizer, scaleRange); } else if (symbolizer instanceof PointSymbolizer) { style = createPointStyle(drawMe, (PointSymbolizer) symbolizer, scaleRange); } else if (symbolizer instanceof TextSymbolizer) { style = createTextStyle(drawMe, (TextSymbolizer) symbolizer, scaleRange); } return style; } /** * Creates a rendered style * * @param f * The feature * @param symbolizer * The SLD symbolizer * @param scaleRange * The scale range in which the feature should be painted * according to the symbolizer * * @return A rendered style equivalent to the symbolizer * * @throws UnsupportedOperationException * if an unknown symbolizer is passed to this method */ public Style2D createDynamicStyle(SimpleFeature f, Symbolizer symbolizer, Range scaleRange) { Style2D style = null; if (symbolizer instanceof PolygonSymbolizer) { style = createDynamicPolygonStyle(f, (PolygonSymbolizer) symbolizer, scaleRange); } else if (symbolizer instanceof LineSymbolizer) { style = createDynamicLineStyle(f, (LineSymbolizer) symbolizer, scaleRange); } else { throw new UnsupportedOperationException( "This kind of symbolizer is not yet supported"); } return style; } PolygonStyle2D createPolygonStyle(Object feature, PolygonSymbolizer symbolizer, Range scaleRange) { PolygonStyle2D style = new PolygonStyle2D(); setScaleRange(style, scaleRange); style.setStroke(getStroke(symbolizer.getStroke(), feature)); style.setGraphicStroke(getGraphicStroke(symbolizer.getStroke(), feature, scaleRange)); style.setContour(getStrokePaint(symbolizer.getStroke(), feature)); style.setContourComposite(getStrokeComposite(symbolizer.getStroke(), feature)); setPolygonStyleFill(feature, style, symbolizer, scaleRange); return style; } /** * Sets a polygon style fill, which includes regular color fill, fill * composite, and possibly a Style2D fill. * * @param feature * @param style * @param symbolizer * @param scaleRange */ void setPolygonStyleFill(Object feature, PolygonStyle2D style, PolygonSymbolizer symbolizer, Range scaleRange) { Fill fill = symbolizer.getFill(); if (fill == null) return; // sets Style2D fill making sure we don't use too much memory for the // rasterization if (fill.getGraphicFill() != null) { double size = evalToDouble(fill.getGraphicFill().getSize(), feature, 0); if (isVectorRenderingEnabled() || size > MAX_RASTERIZATION_SIZE) { // sets graphic fill if available and vector rendering is // enabled Style2D style2DFill = createPointStyle(feature, fill .getGraphicFill(), scaleRange, false); style.setGraphicFill(style2DFill); return; } } // otherwise, sets regular fill using Java raster-based Paint objects style.setFill(getPaint(symbolizer.getFill(), feature)); style.setFillComposite(getComposite(symbolizer.getFill(), feature)); } Style2D createDynamicPolygonStyle(SimpleFeature feature, PolygonSymbolizer symbolizer, Range scaleRange) { PolygonStyle2D style = new DynamicPolygonStyle2D(feature, symbolizer); setScaleRange(style, scaleRange); // setStroke(style, symbolizer.getStroke(), feature); // setFill(style, symbolizer.getFill(), feature); return style; } Style2D createLineStyle(Object feature, LineSymbolizer symbolizer, Range scaleRange) { LineStyle2D style = new LineStyle2D(); setScaleRange(style, scaleRange); style.setStroke(getStroke(symbolizer.getStroke(), feature)); style.setGraphicStroke(getGraphicStroke(symbolizer.getStroke(), feature, scaleRange)); style.setContour(getStrokePaint(symbolizer.getStroke(), feature)); style.setContourComposite(getStrokeComposite(symbolizer.getStroke(), feature)); return style; } Style2D createDynamicLineStyle(SimpleFeature feature, LineSymbolizer symbolizer, Range scaleRange) { LineStyle2D style = new DynamicLineStyle2D(feature, symbolizer); setScaleRange(style, scaleRange); // setStroke(style, symbolizer.getStroke(), feature); return style; } /** * Style used to render the provided feature as a point. * <p> * Depending on the symbolizers used: * <ul> * <li>MarkStyle2D * <li>GraphicStyle2D - used to render a glymph * </ul> * * @param feature * @param symbolizer * @param scaleRange * @return */ Style2D createPointStyle(Object feature, PointSymbolizer symbolizer, Range scaleRange) { return createPointStyle(feature, symbolizer.getGraphic(), scaleRange, false); } /** * Style used to render the provided feature as a point. * <p> * Depending on the symbolizers used: * <ul> * <li>MarkStyle2D * <li>GraphicStyle2D - used to render a glymph * </ul> * * @param feature * @param symbolizer * @param scaleRange * @return */ Style2D createPointStyle(Object feature, Graphic sldGraphic, Range scaleRange, boolean forceVector) { Style2D retval = null; // extract base properties float opacity = evalOpacity(sldGraphic.getOpacity(), feature); Composite composite = AlphaComposite.getInstance( AlphaComposite.SRC_OVER, opacity); float displacementX = 0; float displacementY = 0; if (sldGraphic.getDisplacement() != null) { displacementX = evalToFloat(sldGraphic.getDisplacement() .getDisplacementX(), feature, 0f); displacementY = evalToFloat(sldGraphic.getDisplacement() .getDisplacementY(), feature, 0f); } double size = 0; // by spec size is optional, and the default value is context dependend, // the natural size of the image for an external graphic is the size of // the raster, // while: // - for a external graphic the default size shall be 16x16 // - for a mark such as star or square the default size shall be 6x6 try { if (sldGraphic.getSize() != null && !Expression.NIL.equals(sldGraphic.getSize())) size = evalToDouble(sldGraphic.getSize(), feature, 0); } catch (NumberFormatException nfe) { // nothing to do } float rotation = (float) ((evalToFloat(sldGraphic.getRotation(), feature, 0) * Math.PI) / 180); // Extract the sequence of external graphics and symbols and process // them in order // to recognize which one will be used for rendering List<GraphicalSymbol> symbols = sldGraphic.graphicalSymbols(); if (symbols == null || symbols.isEmpty()) { return null; } final int length = symbols.size(); ExternalGraphic eg; BufferedImage img = null; double dsize; AffineTransform scaleTx; AffineTransformOp ato; BufferedImage scaledImage; Mark mark; Shape shape; MarkStyle2D ms2d; for (GraphicalSymbol symbol : symbols) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("trying to render symbol " + symbol); } // try loading external graphic and creating a GraphicsStyle2D if (symbol instanceof ExternalGraphic) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("rendering External graphic"); } eg = (ExternalGraphic) symbol; // if the icon size becomes too big we switch to vector // rendering too, since // pre-rasterizing and caching the result will use too much // memory if (vectorRenderingEnabled || forceVector || size > MAX_RASTERIZATION_SIZE) { Icon icon = getIcon(eg, feature, -1); if (icon == null) { // no icon -> no image either, there is no raster // fallback continue; } else if(icon instanceof ImageIcon) { // when the icon is an image better use the graphic style, we have // better rendering code for it GraphicStyle2D g2d = getGraphicStyle(eg, (Feature) feature, size, 1); if (g2d == null) { continue; } else { g2d.setRotation(rotation); g2d.setOpacity(opacity); retval = g2d; break; } } else { if (icon.getIconHeight() != size && size != 0) { double scale = ((double) size) / icon.getIconHeight(); icon = new RescaledIcon(icon, scale); } retval = new IconStyle2D(icon, feature, displacementX, displacementY, rotation, composite); break; } } else { GraphicStyle2D g2d = getGraphicStyle(eg, (Feature) feature, size, 1); if (g2d == null) { continue; } else { g2d.setRotation(rotation); g2d.setOpacity(opacity); retval = g2d; break; } } } if (symbol instanceof Mark) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("rendering mark @ PointRenderer " + symbol.toString()); } mark = (Mark) symbol; shape = getShape(mark, feature); if (shape == null) throw new IllegalArgumentException("The specified mark " + mark.getWellKnownName() + " was not found!"); ms2d = new MarkStyle2D(); ms2d.setShape(shape); ms2d.setFill(getPaint(mark.getFill(), feature)); ms2d.setFillComposite(getComposite(mark.getFill(), feature)); ms2d.setStroke(getStroke(mark.getStroke(), feature)); ms2d.setContour(getStrokePaint(mark.getStroke(), feature)); ms2d.setContourComposite(getStrokeComposite(mark.getStroke(), feature)); // in case of Mark we don't have a natural size, so we default // to 16 if (size <= 0) size = 16; ms2d.setSize((int) size); ms2d.setRotation(rotation); retval = ms2d; break; } } if (retval != null) { setScaleRange(retval, scaleRange); } return retval; } /** * Turns a floating point style into a integer size useful to specify the * size of a BufferedImage. Will return 1 between 0 and 1 (0 excluded), will * otherwise round the size to integer. * * @param size * @return */ int toImageSize(double size) { if (size == -1) { return -1; } if (size > 0 && size < 0.5d) { return 1; } else { return (int) Math.round(size); } } Style2D createTextStyle(Object feature, TextSymbolizer symbolizer, Range scaleRange) { TextStyle2D ts2d = new TextStyle2D(); setScaleRange(ts2d, scaleRange); if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("creating text style"); } String geomName = symbolizer.getGeometryPropertyName(); if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("geomName = " + geomName); } // extract label (from ows5 extensions, we could have the label element // empty) String label = evalToString(symbolizer.getLabel(), feature, ""); if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("label is " + label); } ts2d.setLabel(label); // get the sequence of fonts to be used and set the first one available Font[] fonts = symbolizer.getFonts(); java.awt.Font javaFont = getFont(feature, fonts); ts2d.setFont(javaFont); // compute label position, anchor, rotation and displacement LabelPlacement placement = symbolizer.getLabelPlacement(); double anchorX = 0; double anchorY = 0; double rotation = 0; double dispX = 0; double dispY = 0; if (placement instanceof PointPlacement) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("setting pointPlacement"); } // compute anchor point and displacement PointPlacement p = (PointPlacement) placement; if (p.getAnchorPoint() != null) { anchorX = evalToDouble(p.getAnchorPoint().getAnchorPointX(), feature, 0); anchorY = evalToDouble(p.getAnchorPoint().getAnchorPointY(), feature, 0.5); } if (p.getDisplacement() != null) { dispX = evalToDouble(p.getDisplacement().getDisplacementX(), feature, 0); dispY = evalToDouble(p.getDisplacement().getDisplacementY(), feature, 0); ; } // rotation if ((symbolizer instanceof TextSymbolizer2) && (((TextSymbolizer2) symbolizer).getGraphic() != null)) { // don't rotate labels that are being placed on shields. rotation = 0.0; } else { rotation = evalToDouble(p.getRotation(), feature, 0); rotation *= (Math.PI / 180.0); } ts2d.setPointPlacement(true); } else if (placement instanceof LinePlacement) { // this code used to really really really really suck, so I removed // it! if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("setting pointPlacement"); } ts2d.setPointPlacement(false); LinePlacement p = (LinePlacement) placement; int displace = evalToInt(p.getPerpendicularOffset(), feature, 0); ts2d.setPerpendicularOffset(displace); } ts2d.setAnchorX(anchorX); ts2d.setAnchorY(anchorY); ts2d.setRotation((float) rotation); ts2d.setDisplacementX(dispX); ts2d.setDisplacementY(dispY); // setup fill and composite ts2d.setFill(getPaint(symbolizer.getFill(), feature)); ts2d.setComposite(getComposite(symbolizer.getFill(), feature)); // compute halo parameters Halo halo = symbolizer.getHalo(); if (halo != null) { ts2d.setHaloFill(getPaint(halo.getFill(), feature)); ts2d.setHaloComposite(getComposite(halo.getFill(), feature)); ts2d.setHaloRadius(evalToFloat(halo.getRadius(), feature, 1)); } Graphic graphicShield = null; if (symbolizer instanceof TextSymbolizer2) { graphicShield = ((TextSymbolizer2) symbolizer).getGraphic(); if (graphicShield != null) { Style2D shieldStyle = createPointStyle(feature, graphicShield, scaleRange, true); ts2d.setGraphic(shieldStyle); } } return ts2d; } /** * Extracts the named geometry from feature. If geomName is null then the * feature's default geometry is used. If geomName cannot be found in * feature then null is returned. * * @param feature * The feature to find the geometry in * @param geomName * The name of the geometry to find: null if the default geometry * should be used. * * @return The geometry extracted from feature or null if this proved * impossible. */ private Geometry findGeometry(final Object feature, String geomName) { Geometry geom = null; if (geomName == null) { geomName = ""; // ie default geometry } PropertyName property = ff.property(geomName); return (Geometry) property.evaluate(feature, Geometry.class); } /** * Returns the first font associated to the feature that can be found on the * current machine * * @param feature * The feature whose font is to be found * @param fonts * An array of fonts dependent of the feature, the first that is * found on the current machine is returned * * @return The first of the specified fonts found on this machine or null if * none found */ private java.awt.Font getFont(Object feature, Font[] fonts) { if (fonts != null) { for (int k = 0; k < fonts.length; k++) { String requestedFont = evalToString(fonts[k].getFontFamily(), feature, null); java.awt.Font javaFont = FontCache.getDefaultInstance() .getFont(requestedFont); if (javaFont != null) { String reqStyle = evalToString(fonts[k].getFontStyle(), feature, null); int styleCode; if (fontStyleLookup.containsKey(reqStyle)) { styleCode = ((Integer) fontStyleLookup.get(reqStyle)) .intValue(); } else { styleCode = java.awt.Font.PLAIN; } String reqWeight = evalToString(fonts[k].getFontWeight(), feature, null); if ("Bold".equalsIgnoreCase(reqWeight)) { styleCode = styleCode | java.awt.Font.BOLD; } int size = evalToInt(fonts[k].getFontSize(), feature, 10); return javaFont.deriveFont(styleCode, size); } } } // if everything else fails fall back on a default font distributed // along with the jdk (default font size is 10 pixels by spec... here we // are using points thoughts) return new java.awt.Font("Serif", java.awt.Font.PLAIN, 12); } void setScaleRange(Style style, Range scaleRange) { double min = ((Number) scaleRange.getMinValue()).doubleValue(); double max = ((Number) scaleRange.getMaxValue()).doubleValue(); style.setMinMaxScale(min, max); } // Builds an image version of the graphics with the proper size, no further // scaling will // be needed during rendering private Style2D getGraphicStroke(org.geotools.styling.Stroke stroke, Object feature, Range scaleRange) { if ((stroke == null) || (stroke.getGraphicStroke() == null)) { return null; } // sets graphic stroke if available and vector rendering is enabled return createPointStyle(feature, stroke.getGraphicStroke(), scaleRange, false); } private Stroke getStroke(org.geotools.styling.Stroke stroke, Object feature) { if (stroke == null) { return null; } // resolve join type into a join code String joinType; int joinCode; joinType = evalToString(stroke.getLineJoin(), feature, "miter"); if (joinLookup.containsKey(joinType)) { joinCode = ((Integer) joinLookup.get(joinType)).intValue(); } else { joinCode = java.awt.BasicStroke.JOIN_MITER; } // resolve cap type into a cap code String capType; int capCode; capType = evalToString(stroke.getLineCap(), feature, "square"); if (capLookup.containsKey(capType)) { capCode = ((Integer) capLookup.get(capType)).intValue(); } else { capCode = java.awt.BasicStroke.CAP_SQUARE; } // get the other properties needed for the stroke float[] dashes = stroke.getDashArray(); float width = evalToFloat(stroke.getWidth(), feature, 1); float dashOffset = evalToFloat(stroke.getDashOffset(), feature, 0); // Simple optimization: let java2d use the fast drawing path if the line // width // is small enough... if (width < 1.5 & lineOptimizationEnabled) { width = 0; } // now set up the stroke BasicStroke stroke2d; if ((dashes != null) && (dashes.length > 0)) { stroke2d = new BasicStroke(width, capCode, joinCode, 1, dashes, dashOffset); } else { stroke2d = new BasicStroke(width, capCode, joinCode, 1); } return stroke2d; } private Paint getStrokePaint(org.geotools.styling.Stroke stroke, Object feature) { if (stroke == null) { return null; } // the foreground color Paint contourPaint = evalToColor(stroke.getColor(), feature, Color.BLACK); // if a graphic fill is to be used, prepare the paint accordingly.... org.geotools.styling.Graphic gr = stroke.getGraphicFill(); if (gr != null) { contourPaint = getTexturePaint(gr, feature); } return contourPaint; } private Composite getStrokeComposite(org.geotools.styling.Stroke stroke, Object feature) { if (stroke == null) { return null; } // get the opacity and prepare the composite float opacity = evalOpacity(stroke.getOpacity(), feature); Composite composite = AlphaComposite.getInstance( AlphaComposite.SRC_OVER, opacity); return composite; } protected Paint getPaint(Fill fill, Object feature) { if (fill == null) { return null; } // get fill color Paint fillPaint = evalToColor(fill.getColor(), feature, null); // if a graphic fill is to be used, prepare the paint accordingly.... org.geotools.styling.Graphic gr = fill.getGraphicFill(); if (gr != null) { fillPaint = getTexturePaint(gr, feature); } return fillPaint; } /** * Computes the Composite equivalent to the opacity in the SLD Fill * * @param fill * @param feature * */ protected Composite getComposite(Fill fill, Object feature) { if (fill == null) { return null; } // get the opacity and prepare the composite float opacity = evalOpacity(fill.getOpacity(), feature); Composite composite = AlphaComposite.getInstance( AlphaComposite.SRC_OVER, opacity); return composite; } /** * DOCUMENT ME! * * @param gr * DOCUMENT ME! * @param feature * DOCUMENT ME! * * @return DOCUMENT ME! */ public TexturePaint getTexturePaint(org.geotools.styling.Graphic gr, Object feature) { // -1 to have the image use its natural size if none was provided by the // user double graphicSize = evalToDouble(gr.getSize(), feature, -1); GraphicStyle2D gs = null; for (ExternalGraphic eg : gr.getExternalGraphics()) { gs = getGraphicStyle(eg, feature, graphicSize, 1); if (gs != null) { break; } } int iSizeX; int iSizeY; BufferedImage image = null; if (gs != null) { image = gs.getImage(); iSizeX = image.getWidth() - gs.getBorder(); iSizeY = image.getHeight() - gs.getBorder(); if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("got an image in graphic fill"); } } else { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("going for the mark from graphic fill"); } org.geotools.styling.Mark mark = getMark(gr, feature); if (mark == null) { return null; } // we need the shape to get to the aspect ratio information, since // this info isnt' on the mark. Shape shape = getShape(mark, feature); if (shape == null) { return null; } Rectangle2D shapeBounds = shape.getBounds2D(); // The aspect ratio is the relation between the width and height of // this mark (x width units per y height units or width/height). The // aspect ratio is used to render non isometric sized marks (where // width != height). To discover the <code>width</code> of a non // isometric // mark, simply calculate <code>height * aspectRatio</code>, where // height is given by getSize(). double shapeAspectRatio = (shapeBounds.getHeight() > 0 && shapeBounds .getWidth() > 0) ? shapeBounds.getWidth() / shapeBounds.getHeight() : 1.0; int size = evalToInt(gr.getSize(), feature, 16); final double sizeX = size * shapeAspectRatio; // apply the aspect // ratio to fix the // sample's width. final double sizeY = size; image = new BufferedImage((int) Math.ceil(sizeX * 3), (int) Math .ceil(sizeY * 3), BufferedImage.TYPE_INT_ARGB); Graphics2D g2d = image.createGraphics(); g2d.setRenderingHints(renderingHints); double rotation = evalToDouble(gr.getRotation(), feature, 0.0); for (int i = -1; i < 2; i++) { for (int j = -1; j < 2; j++) { double tx = sizeX * 1.5 + sizeX * i; double ty = sizeY * 1.5 + sizeY * j; fillDrawMark(g2d, tx, ty, mark, size, rotation, feature); } } g2d.dispose(); iSizeX = (int) Math.floor(sizeX); iSizeY = (int) Math.floor(sizeY); image = image.getSubimage(iSizeX, iSizeY, iSizeX + 1, iSizeY + 1); // updated // to // use // the // new // sizes } Rectangle2D.Double rect = new Rectangle2D.Double(0.0, 0.0, iSizeX, iSizeY); TexturePaint imagePaint = new TexturePaint(image, rect); if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("applied TexturePaint " + imagePaint); } return imagePaint; } /** * Tries to parse the provided external graphic into a BufferedImage. * * @param eg * @param feature * @param size * @return the image, or null if the external graphics could not be * interpreted */ private GraphicStyle2D getGraphicStyle(ExternalGraphic eg, Object feature, double size, int border) { Icon icon = getIcon(eg, feature, toImageSize(size)); if (icon != null) { // optimization, if this is an IconImage based on a BufferedImage, // just return the // wrapped one if (icon instanceof ImageIcon) { ImageIcon img = (ImageIcon) icon; if (img.getImage() instanceof BufferedImage) { // return the image as is, no border BufferedImage image = (BufferedImage) img.getImage(); return new GraphicStyle2D(image, 0, 0); } } // otherwise have the icon draw itself on a BufferedImage BufferedImage result = new BufferedImage(icon.getIconWidth() + border * 2, icon.getIconHeight() + border * 2, BufferedImage.TYPE_4BYTE_ABGR); Graphics2D g = (Graphics2D) result.getGraphics(); // we paint it once, make it look good g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); icon.paintIcon(null, g, 1, 1); g.dispose(); return new GraphicStyle2D(result, 0, 0, border); } return null; } /** * Tries to parse the provided external graphic into an Icon * * @param eg * @param feature * @param size * @return the image, or null if the external graphics could not be * interpreted */ private Icon getIcon(ExternalGraphic eg, Object feature, double size) { if (eg == null) return null; // extract the url String strLocation; try { strLocation = eg.getLocation().toExternalForm(); } catch (MalformedURLException e) { LOGGER.log(Level.INFO, "Malformed URL processing external graphic", e); return null; } // parse the eventual ${cqlExpression} embedded in the URL Expression location; try { location = ExpressionExtractor.extractCqlExpressions(strLocation); } catch (IllegalArgumentException e) { // in the unlikely event that a URL is using one of the chars // reserved for ${cqlExpression} // let's try and use the location as a literal if (LOGGER.isLoggable(Level.FINE)) LOGGER .log(Level.FINE, "Could not parse cql expressions out of " + strLocation, e); location = ff.literal(strLocation); } // scan the external graphic factories and see which one can be used Iterator<ExternalGraphicFactory> it = DynamicSymbolFactoryFinder .getExternalGraphicFactories(); while (it.hasNext()) { try { Expression formatExpression = ExpressionExtractor.extractCqlExpressions(eg.getFormat()); String format = formatExpression.evaluate(feature, String.class); Icon icon = it.next().getIcon((Feature) feature, location, format, toImageSize(size)); if (icon != null) { return icon; } } catch (Exception e) { LOGGER.log(Level.FINE, "Error occurred evaluating external graphic", e); } } return null; } /** * Looks ups the marks included in the graphics and returns the one that can * be drawn by at least one mark factory * * @param graphic * @param feature * @return */ private Mark getMark(Graphic graphic, Object feature) { if (graphic == null) return null; Mark[] marks = graphic.getMarks(); for (int i = 0; i < marks.length; i++) { final Mark mark = marks[i]; Shape shape = getShape(mark, feature); if (shape != null) return mark; } // if nothing worked, we return a square return null; } /** * Given a mark and a feature, returns the Shape provided by the first * {@link MarkFactory} that was able to handle the Mark * * @param mark * @param feature * @return */ private Shape getShape(Mark mark, Object feature) { if (mark == null) return null; Expression name = mark.getWellKnownName(); // expand eventual cql expressions embedded in the name if (name instanceof Literal) { String expression = evalToString(name, null, null); if (expression != null) name = ExpressionExtractor.extractCqlExpressions(expression); } Iterator<MarkFactory> it = DynamicSymbolFactoryFinder .getMarkFactories(); while (it.hasNext()) { MarkFactory factory = it.next(); try { Shape shape = factory.getShape(null, name, (Feature) feature); if (shape != null) return shape; } catch (Exception e) { LOGGER.log(Level.FINE, "Exception while scanning for " + "the appropriate mark factory", e); } } return null; } private void fillDrawMark(Graphics2D g2d, double tx, double ty, Mark mark, double size, double rotation, Object feature) { if (mark == null) return; Shape originalShape = getShape(mark, feature); // rescale and reposition the original shape so it's centered at tx, ty // and has the desired size AffineTransform markAT = new AffineTransform(); markAT.translate(tx, ty); markAT.rotate(rotation); markAT.scale(size, -size); // resize/rotate/rescale the shape Shape shape = markAT.createTransformedShape(originalShape); // we draw it once, make it look nice g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); if (mark.getFill() != null) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("applying fill to mark"); } g2d.setPaint(getPaint(mark.getFill(), feature)); g2d.setComposite(getComposite(mark.getFill(), feature)); g2d.fill(shape); } if (mark.getStroke() != null) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("applying stroke to mark"); } g2d.setPaint(getStrokePaint(mark.getStroke(), feature)); g2d.setComposite(getStrokeComposite(mark.getStroke(), feature)); g2d.setStroke(getStroke(mark.getStroke(), feature)); g2d.draw(shape); } } /** * DOCUMENT ME! * * @param joinType * DOCUMENT ME! * * @return DOCUMENT ME! */ public static int lookUpJoin(String joinType) { if (SLDStyleFactory.joinLookup.containsKey(joinType)) { return ((Integer) joinLookup.get(joinType)).intValue(); } else { return java.awt.BasicStroke.JOIN_MITER; } } /** * DOCUMENT ME! * * @param capType * DOCUMENT ME! * * @return DOCUMENT ME! */ public static int lookUpCap(String capType) { if (SLDStyleFactory.capLookup.containsKey(capType)) { return ((Integer) capLookup.get(capType)).intValue(); } else { return java.awt.BasicStroke.CAP_SQUARE; } } /** * Getter for property mapScaleDenominator. * * @return Value of property mapScaleDenominator. */ public double getMapScaleDenominator() { return this.mapScaleDenominator; } /** * Setter for property mapScaleDenominator. * * @param mapScaleDenominator * New value of property mapScaleDenominator. */ public void setMapScaleDenominator(double mapScaleDenominator) { this.mapScaleDenominator = mapScaleDenominator; } /** * Simple key used to cache Style2D objects based on the originating * symbolizer and scale range. Will compare symbolizers by identity, * avoiding a possibly very long comparison * * @author aaime */ static class SymbolizerKey { private Symbolizer symbolizer; private double minScale; private double maxScale; public SymbolizerKey(Symbolizer symbolizer, Range scaleRange) { this.symbolizer = symbolizer; minScale = ((Number) scaleRange.getMinValue()).doubleValue(); maxScale = ((Number) scaleRange.getMaxValue()).doubleValue(); } /** * @see java.lang.Object#equals(java.lang.Object) */ public boolean equals(Object obj) { if (!(obj instanceof SymbolizerKey)) { return false; } SymbolizerKey other = (SymbolizerKey) obj; return (other.symbolizer == symbolizer) && (other.minScale == minScale) && (other.maxScale == maxScale); } /** * @see java.lang.Object#hashCode() */ public int hashCode() { return ((((17 + System.identityHashCode(symbolizer)) * 37) + doubleHash(minScale)) * 37) + doubleHash(maxScale); } private int doubleHash(double value) { long bits = Double.doubleToLongBits(value); return (int) (bits ^ (bits >>> 32)); } } private String evalToString(Expression exp, Object f, String fallback) { if (exp == null) { return fallback; } String s = exp.evaluate(f, String.class); if (s != null) { return s; } return fallback; } private float evalToFloat(Expression exp, Object f, float fallback) { if (exp == null) { return fallback; } Float fo = (Float) exp.evaluate(f, Float.class); if (fo != null) { return fo.floatValue(); } return fallback; } private double evalToDouble(Expression exp, Object f, double fallback) { if (exp == null) { return fallback; } Double d = exp.evaluate(f, Double.class); if (d != null) { return d.doubleValue(); } return fallback; } private int evalToInt(Expression exp, Object f, int fallback) { if (exp == null) { return fallback; } Integer i = exp.evaluate(f, Integer.class); if (i != null) { return i.intValue(); } return fallback; } private Color evalToColor(Expression exp, Object f, Color fallback) { if (exp == null) { return fallback; } Color color = exp.evaluate(f, Color.class); if (color != null) { return color; } return fallback; } private float evalOpacity(Expression e, Object f) { return evalToFloat(e, f, 1); } }