/*
* 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.Graphics;
import java.awt.Graphics2D;
import java.awt.MediaTracker;
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.TextMark;
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
*/
/*
* 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");
/** 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);
// 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();
if ((nameSet == null) || (nameSet.size() == 0)) {
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));
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
if (fill.getGraphicFill() != null && isVectorRenderingEnabled()) {
// sets graphic fill if available and vector rendering is enabled
Style2D style2DFill = createPointStyle(feature, fill.getGraphicFill(), scaleRange);
style.setGraphicFill(style2DFill);
} else {
//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));
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);
}
/**
* 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) {
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.isEmpty()){
symbols = new ArrayList<GraphicalSymbol>();
Mark square = StyleFactoryFinder.createStyleFactory().createMark();
symbols.add( square );
}
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(vectorRenderingEnabled) {
Icon icon = getIcon(eg, feature, (int) size);
if(icon == null) {
// no icon -> no image either, there is no raster fallback
continue;
} else {
retval = new IconStyle2D(icon, feature, displacementX, displacementY, rotation, composite);
break;
}
} else {
img = getImage(eg, (Feature) feature, (int) size);
if (img == null) {
continue;
} else {
retval = new GraphicStyle2D(img, rotation, opacity);
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 (symbol instanceof TextMark) {
// for the moment don't support TextMarks since they are not part
// of the SLD specification
continue;
/**
* if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("rendering text symbol");
* } flag = renderTextSymbol(geom, sldgraphic, feature, (TextMark) symbols[i]); if
* (flag) { return; }
*/
}
}
if (retval != null) {
setScaleRange(retval, scaleRange);
}
return retval;
}
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)
{
PointSymbolizer p = StyleFactoryFinder.createStyleFactory().createPointSymbolizer();
p.setGraphic(graphicShield);
Style2D shieldStyle = createPointStyle(feature, p, scaleRange);
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.getDefaultInsance().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 BufferedImage getGraphicStroke(org.geotools.styling.Stroke stroke, Object feature) {
if ((stroke == null) || (stroke.getGraphicStroke() == null)) {
return null;
}
Graphic graphicStroke = stroke.getGraphicStroke();
// default stroke size is 1 by spec
int size = evalToInt(graphicStroke.getSize(), feature, 1);
// lets see if an external image is to be used
BufferedImage image = getImage(graphicStroke, (Feature) feature, size);
if (image == null) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("going for the mark from graphic fill");
}
Mark mark = getMark(graphicStroke, feature);
image = new BufferedImage((int) size, (int) size, BufferedImage.TYPE_INT_ARGB);
Graphics2D ig2d = image.createGraphics();
ig2d.setRenderingHints(renderingHints);
double rotation = 0.0;
rotation = evalToDouble(graphicStroke.getRotation(), feature, 0.0);
rotation *= (Math.PI / 180.0);
fillDrawMark(ig2d, size / 2, size / 2, mark, (int) size, rotation, feature);
MediaTracker track = new MediaTracker(obs);
track.addImage(image, 1);
try {
track.waitForID(1);
} catch (InterruptedException e) {
LOGGER.warning(e.toString());
}
}
return image;
}
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
int graphicSize = evalToInt(gr.getSize(), feature, -1);
BufferedImage image = getImage(gr, feature, graphicSize);
int iSizeX;
int iSizeY;
if (image != null) {
iSizeX = image.getWidth();
iSizeY = image.getHeight();
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();
// try {
// ImageIO.write(image, "png", new java.io.File("c:/temp/sampler.png"));
// } catch(Exception e) {
// System.out.println(e);
// }
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
// try {
// ImageIO.write(image, "png", new java.io.File("c:/temp/texture.png"));
// } catch(Exception e) {
// System.out.println(e);
// }
}
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;
}
/**
* Scans the dynamic external graphic factories and returns the image representing
* the first external graphic that could be parsed successfully
* @param graphic
* @param feature
* @param size
* @return
*/
private BufferedImage getImage(Graphic graphic, Object feature, int size) {
if(graphic == null)
return null;
ExternalGraphic[] extgraphics = graphic.getExternalGraphics();
if (extgraphics != null) {
for (int i = 0; i < extgraphics.length; i++) {
BufferedImage image = getImage(extgraphics[i], feature, size);
if(image != null)
return image;
}
}
return null;
}
/**
* 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 BufferedImage getImage(ExternalGraphic eg, Object feature, int size) {
Icon icon = getIcon(eg, feature, size);
if(icon != null)
return rasterizeIcon(icon);
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, int 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 {
Icon icon = it.next().getIcon((Feature) feature, location, eg.getFormat(), size);
if(icon != null) {
return icon;
}
} catch(Exception e) {
LOGGER.log(Level.FINE, "Error occurred evaluating external graphic", e);
}
}
return null;
}
/**
* Turns an icon into a BufferedImage
* @param icon
* @return
*/
private BufferedImage rasterizeIcon(Icon icon) {
// 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 (BufferedImage) img.getImage();
}
// otherwise have the icon draw itself on a BufferedImage
BufferedImage result = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(),
BufferedImage.TYPE_4BYTE_ABGR);
Graphics2D g = (Graphics2D) result.getGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
icon.paintIcon(null, g, 0, 0);
g.dispose();
return result;
}
/**
* 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);
double unitSize = 1.0;
double drawSize = size / unitSize;
markAT.scale(drawSize, -drawSize);
// resize/rotate/rescale the shape
Shape shape = markAT.createTransformedShape(originalShape);
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);
}
if (mark.getFill() != null) {
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
}
}
/**
* 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;
}
Object o = exp.evaluate(f);
if(o instanceof String)
return (String) o;
String s = (String) 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;
}
Object o = exp.evaluate(f);
if(o instanceof Number)
return ((Number) o).floatValue();
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;
}
Object o = exp.evaluate(f);
if(o instanceof Number)
return ((Number) o).doubleValue();
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;
}
Object o = exp.evaluate(f);
if(o instanceof Number)
return ((Number) o).intValue();
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);
}
}