/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2002-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.styling; import java.util.Arrays; import org.geotools.factory.CommonFactoryFinder; import org.geotools.factory.GeoTools; import org.geotools.util.Utilities; import org.opengis.feature.simple.SimpleFeature; import org.opengis.filter.FilterFactory; import org.opengis.filter.expression.Expression; import org.opengis.style.StyleVisitor; import org.opengis.util.Cloneable; /** * Provides a Java representation of the Stroke object in an SLD document. A * stroke defines how a line is rendered. * * @author James Macgill, CCG * * @source $URL$ * @version $Id$ */ public class StrokeImpl implements Stroke, Cloneable { private FilterFactory filterFactory; private Expression color; private float[] dashArray; private Expression dashOffset; private GraphicImpl fillGraphic; private GraphicImpl strokeGraphic; private Expression lineCap; private Expression lineJoin; private Expression opacity; private Expression width; /** * Creates a new instance of Stroke */ protected StrokeImpl() { this( CommonFactoryFinder.getFilterFactory(GeoTools.getDefaultHints())); } protected StrokeImpl(FilterFactory factory) { filterFactory = factory; } public void setFilterFactory(FilterFactory factory) { filterFactory = factory; } /** * This parameter gives the solid color that will be used for a stroke.<br> * The color value is RGB-encoded using two hexidecimal digits per * primary-color component in the order Red, Green, Blue, prefixed with * the hash (#) sign. The hexidecimal digits between A and F may be in * either upper or lower case. For example, full red is encoded as * "#ff0000" (with no quotation marks). The default color is defined to * be black ("#000000"). Note: in CSS this parameter is just called Stroke * and not Color. * * @return The color of the stroke encoded as a hexidecimal RGB value. */ public Expression getColor() { return color; } /** * This parameter sets the solid color that will be used for a stroke.<br> * The color value is RGB-encoded using two hexidecimal digits per * primary-color component in the order Red, Green, Blue, prefixed with * the hash (#) sign. The hexidecimal digits between A and F may be in * either upper or lower case. For example, full red is encoded as * "#ff0000" (with no quotation marks). The default color is defined to * be black ("#000000"). Note: in CSS this parameter is just called Stroke * and not Color. * * @param color The color of the stroke encoded as a hexidecimal RGB value. * This must not be null. * * @throws IllegalArgumentException DOCUMENT ME! */ public void setColor(Expression color) { if (this.color == color) { return; } this.color = color; } /** * This parameter sets the solid color that will be used for a stroke.<br> * The color value is RGB-encoded using two hexidecimal digits per * primary-color component in the order Red, Green, Blue, prefixed with * the hash (#) sign. The hexidecimal digits between A and F may be in * either upper or lower case. For example, full red is encoded as * "#ff0000" (with no quotation marks). The default color is defined to * be black ("#000000"). Note: in CSS this parameter is just called Stroke * and not Color. * * @param color The color of the stroke encoded as a hexidecimal RGB value. */ public void setColor(String color) { setColor(filterFactory.literal(color)); } /** * This parameter encodes the dash pattern as a series of floats.<br> * The first number gives the length in pixels of the dash to draw, the * second gives the amount of space to leave, and this pattern repeats.<br> * If an odd number of values is given, then the pattern is expanded by * repeating it twice to give an even number of values. The default is to * draw an unbroken line.<br> * For example, "2 1 3 2" would produce:<br> * <code>-- ---  -- ---  --  * ---  -- ---  -- ---  --</code> * * @return The dash pattern as an array of float values in the form * "dashlength gaplength ..." */ public float[] getDashArray() { float[] ret = null; if (dashArray != null) { ret = new float[dashArray.length]; System.arraycopy(dashArray, 0, ret, 0, dashArray.length); } else { final float[] defaultDashArray = Stroke.DEFAULT.getDashArray(); if(defaultDashArray == null) return null; ret = new float[defaultDashArray.length]; System.arraycopy(defaultDashArray, 0, ret, 0, defaultDashArray.length); } return ret; } /** * This parameter encodes the dash pattern as a series of floats.<br> * The first number gives the length in pixels of the dash to draw, the * second gives the amount of space to leave, and this pattern repeats.<br> * If an odd number of values is given, then the pattern is expanded by * repeating it twice to give an even number of values. The default is to * draw an unbroken line.<br> * * For example, "2 1 3 2" would produce:<br> * <code>-- ---  -- ---   * -- ---  -- ---  --  * ---  --</code> * * @param dashPattern The dash pattern as an array of float values in the * form "dashlength gaplength ..." */ public void setDashArray(float[] dashPattern) { dashArray = dashPattern; } /** * This param determines where the dash pattern should start from. * * @return where the dash should start from. */ public Expression getDashOffset() { if ( dashOffset == null ) { return Stroke.DEFAULT.getDashOffset(); } return dashOffset; } /** * This param determines where the dash pattern should start from. * * @param dashOffset The distance into the dash pattern that should act as * the start. */ public void setDashOffset(Expression dashOffset) { if (dashOffset == null) { return; } this.dashOffset = dashOffset; } /** * This parameter indicates that a stipple-fill repeated graphic will be * used and specifies the fill graphic to use. * * @return The graphic to use as a stipple fill. If null, then no Stipple * fill should be used. */ public GraphicImpl getGraphicFill() { return fillGraphic; } /** * This parameter indicates that a stipple-fill repeated graphic will be * used and specifies the fill graphic to use. * * @param fillGraphic The graphic to use as a stipple fill. If null, then * no Stipple fill should be used. */ public void setGraphicFill(org.opengis.style.Graphic fillGraphic) { if (this.fillGraphic == fillGraphic) { return; } this.fillGraphic = GraphicImpl.cast( fillGraphic ); } /** * This parameter indicates that a repeated-linear-graphic graphic stroke * type will be used and specifies the graphic to use. Proper stroking * with a linear graphic requires two "hot-spot" points within the space * of the graphic to indicate where the rendering line starts and stops. * In the case of raster images with no special mark-up, this line will be * assumed to be the middle pixel row of the image, starting from the * first pixel column and ending at the last pixel column. * * @return The graphic to use as a linear graphic. If null, then no graphic * stroke should be used. */ public GraphicImpl getGraphicStroke() { return strokeGraphic; } /** * This parameter indicates that a repeated-linear-graphic graphic stroke * type will be used and specifies the graphic to use. Proper stroking * with a linear graphic requires two "hot-spot" points within the space * of the graphic to indicate where the rendering line starts and stops. * In the case of raster images with no special mark-up, this line will be * assumed to be the middle pixel row of the image, starting from the * first pixel column and ending at the last pixel column. * * @param strokeGraphic The graphic to use as a linear graphic. If null, * then no graphic stroke should be used. */ public void setGraphicStroke(org.opengis.style.Graphic strokeGraphic) { if (this.strokeGraphic == strokeGraphic) { return; } this.strokeGraphic = GraphicImpl.cast(strokeGraphic); } /** * This parameter controls how line strings should be capped. * * @return The cap style. This will be one of "butt", "round" and "square" * There is no defined default. */ public Expression getLineCap() { if( lineCap == null ){ // ConstantExpression.constant("miter") return Stroke.DEFAULT.getLineCap(); } return lineCap; } /** * This parameter controls how line strings should be capped. * * @param lineCap The cap style. This can be one of "butt", "round" and * "square" There is no defined default. */ public void setLineCap(Expression lineCap) { if (lineCap == null) { return; } this.lineCap = lineCap; } /** * This parameter controls how line strings should be joined together. * * @return The join style. This will be one of "mitre", "round" and * "bevel". There is no defined default. */ public Expression getLineJoin() { if( lineCap == null ){ // ConstantExpression.constant("miter") return Stroke.DEFAULT.getLineJoin(); } return lineJoin; } /** * This parameter controls how line strings should be joined together. * * @param lineJoin The join style. This will be one of "mitre", "round" * and "bevel". There is no defined default. */ public void setLineJoin(Expression lineJoin) { if (lineJoin == null) { return; } this.lineJoin = lineJoin; } /** * This specifies the level of translucency to use when rendering the stroke.<br> * The value is encoded as a floating-point value between 0.0 and 1.0 with * 0.0 representing totally transparent and 1.0 representing totally * opaque. A linear scale of translucency is used for intermediate values.<br> * For example, "0.65" would represent 65% opacity. The default value is * 1.0 (opaque). * * @return The opacity of the stroke, where 0.0 is completely transparent * and 1.0 is completely opaque. */ public Expression getOpacity() { if( lineCap == null ){ return Stroke.DEFAULT.getOpacity(); } return opacity; } /** * This specifies the level of translucency to use when rendering the stroke.<br> * The value is encoded as a floating-point value between 0.0 and 1.0 with * 0.0 representing totally transparent and 1.0 representing totally * opaque. A linear scale of translucency is used for intermediate values.<br> * For example, "0.65" would represent 65% opacity. The default value is * 1.0 (opaque). * * @param opacity The opacity of the stroke, where 0.0 is completely * transparent and 1.0 is completely opaque. */ public void setOpacity(Expression opacity) { if (opacity == null) { return; } this.opacity = opacity; } /** * This parameter gives the absolute width (thickness) of a stroke in * pixels encoded as a float. The default is 1.0. Fractional numbers are * allowed but negative numbers are not. * * @return The width of the stroke in pixels. This may be fractional but * not negative. */ public Expression getWidth() { if( width == null ){ return filterFactory.literal(1.0); } return width; } /** * This parameter sets the absolute width (thickness) of a stroke in pixels * encoded as a float. The default is 1.0. Fractional numbers are allowed * but negative numbers are not. * * @param width The width of the stroke in pixels. This may be fractional * but not negative. */ public void setWidth(Expression width) { this.width = width; } public String toString() { StringBuffer out = new StringBuffer( "org.geotools.styling.StrokeImpl:\n"); out.append("\tColor " + this.color + "\n"); out.append("\tWidth " + this.width + "\n"); out.append("\tOpacity " + this.opacity + "\n"); out.append("\tLineCap " + this.lineCap + "\n"); out.append("\tLineJoin " + this.lineJoin + "\n"); out.append("\tDash Array " + this.dashArray + "\n"); out.append("\tDash Offset " + this.dashOffset + "\n"); out.append("\tFill Graphic " + this.fillGraphic + "\n"); out.append("\tStroke Graphic " + this.strokeGraphic); return out.toString(); } public java.awt.Color getColor(SimpleFeature feature) { return java.awt.Color.decode((String) this.getColor().evaluate(feature)); } public Object accept(StyleVisitor visitor,Object data) { return visitor.visit(this,data); } public void accept(org.geotools.styling.StyleVisitor visitor) { visitor.visit(this); } /** * Clone the StrokeImpl object. * * <p> * The clone is a deep copy of the original, except for the expression * values which are immutable. * </p> * * @see org.geotools.styling.Stroke#clone() */ public Object clone() { try { StrokeImpl clone = (StrokeImpl) super.clone(); if (dashArray != null) { clone.dashArray = new float[dashArray.length]; System.arraycopy(dashArray, 0, clone.dashArray, 0, dashArray.length); } if (fillGraphic != null && fillGraphic instanceof Cloneable) { clone.fillGraphic = (GraphicImpl) ((Cloneable) fillGraphic).clone(); } if (strokeGraphic != null && fillGraphic instanceof Cloneable ) { clone.strokeGraphic = (GraphicImpl) ((Cloneable) strokeGraphic) .clone(); } return clone; } catch (CloneNotSupportedException e) { // This will never happen throw new RuntimeException("Failed to clone StrokeImpl"); } } public int hashCode() { final int PRIME = 1000003; int result = 0; if (color != null) { result = (PRIME * result) + color.hashCode(); } if (dashOffset != null) { result = (PRIME * result) + dashOffset.hashCode(); } if (fillGraphic != null) { result = (PRIME * result) + fillGraphic.hashCode(); } if (strokeGraphic != null) { result = (PRIME * result) + strokeGraphic.hashCode(); } if (lineCap != null) { result = (PRIME * result) + lineCap.hashCode(); } if (lineJoin != null) { result = (PRIME * result) + lineJoin.hashCode(); } if (opacity != null) { result = (PRIME * result) + opacity.hashCode(); } if (width != null) { result = (PRIME * result) + width.hashCode(); } if (dashArray != null) { result = (PRIME * result) + hashCodeDashArray(dashArray); } return result; } /* * Helper method to compute the hashCode of float arrays. */ private int hashCodeDashArray(float[] a) { final int PRIME = 1000003; if (a == null) { return 0; } int result = 0; for (int i = 0; i < a.length; i++) { result = (PRIME * result) + Float.floatToIntBits(a[i]); } return result; } /** * Compares this stroke with another stroke for equality. * * @param oth The other StrokeImpl to compare * * @return True if this and oth are equal. */ public boolean equals(Object oth) { if (this == oth) { return true; } if (oth == null) { return false; } if (oth.getClass() != getClass()) { return false; } StrokeImpl other = (StrokeImpl) oth; // check the color first - most likely to change if( !Utilities.equals( getColor(), other.getColor() )){ return false; } // check the width if( !Utilities.equals( getWidth(), other.getWidth() )){ return false; } if( !Utilities.equals( getLineCap(), other.getLineCap() )){ return false; } if( !Utilities.equals( getLineJoin(), other.getLineJoin() )){ return false; } if( !Utilities.equals( getOpacity(), other.getOpacity() )){ return false; } if( !Utilities.equals( getGraphicFill(), other.getGraphicFill() )){ return false; } if( !Utilities.equals( getGraphicStroke(), other.getGraphicStroke() )){ return false; } if (!Arrays.equals(getDashArray(), other.getDashArray())) { return false; } return true; } static StrokeImpl cast(org.opengis.style.Stroke stroke) { if( stroke == null ){ return null; } else if (stroke instanceof StrokeImpl){ return (StrokeImpl) stroke; } else { StrokeImpl copy = new StrokeImpl(); copy.setColor( stroke.getColor()); if( stroke.getDashArray() != null ){ float dashArray[] = stroke.getDashArray(); float ret[] = new float[ dashArray.length ]; System.arraycopy(dashArray, 0, ret, 0, dashArray.length ); copy.setDashArray( ret ); } copy.setDashOffset(stroke.getDashOffset()); copy.setGraphicFill( GraphicImpl.cast(stroke.getGraphicFill())); copy.setGraphicStroke( GraphicImpl.cast(stroke.getGraphicStroke())); copy.setLineCap( stroke.getLineCap()); copy.setLineJoin(stroke.getLineJoin()); copy.setOpacity( stroke.getOpacity()); copy.setWidth(stroke.getWidth()); return copy; } } }