/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2004-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.lite;
import static java.lang.Math.*;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.Arrays;
import javax.imageio.ImageIO;
/**
* A shape wrapper that generates a stroked version of the shape without actually holding it all in
* memory (it is streamed through the path iterator)
*
* @author Andrea Aime - OpenGeo
*
* @source $URL: http://svn.osgeo.org/geotools/branches/2.7.x/build/maven/javadoc/../../../modules/library/render/src/main/java/org/geotools/renderer/lite/DashedShape.java $
*/
public class DashedShape implements Shape {
Shape shape;
float[] dashArray;
float dashPhase;
public DashedShape(Shape shape, float[] dashArray, float dashPhase) {
this.shape = shape;
this.dashArray = dashArray;
this.dashPhase = dashPhase;
}
public boolean contains(double x, double y, double w, double h) {
return shape.contains(x, y, w, h);
}
public boolean contains(double x, double y) {
return shape.contains(x, y);
}
public boolean contains(Point2D p) {
return shape.contains(p);
}
public boolean contains(Rectangle2D r) {
return shape.contains(r);
}
public Rectangle getBounds() {
return shape.getBounds();
}
public Rectangle2D getBounds2D() {
return shape.getBounds2D();
}
public boolean intersects(double x, double y, double w, double h) {
return shape.intersects(x, y, w, h);
}
public boolean intersects(Rectangle2D r) {
return shape.intersects(r);
}
public PathIterator getPathIterator(AffineTransform at) {
// we need to work against a flattened iterator, the dashed iterator
// cannot handle curved segments
return new DashedIterator(shape.getPathIterator(at, 1), dashArray, dashPhase);
}
public PathIterator getPathIterator(AffineTransform at, double flatness) {
// we need to work against a flattened iterator, the dashed iterator
// cannot handle curved segments
if (flatness < 1) {
flatness = 1;
}
return new DashedIterator(shape.getPathIterator(at, flatness), dashArray, dashPhase);
}
/**
* The iterator that generates the dashed segments in a streaming fashion
* @author Andrea Aime - OpenGeo
*/
public class DashedIterator implements PathIterator {
static final float EPS = 1e-3f;
/**
* The original iterator
*/
PathIterator delegate;
/**
* The offsets at which each dash segment ends compared to the beginning of the sequence
*/
float[] dashOffsets;
/**
* The current dash offset
*/
int dashIndex = 0;
/**
* The offset from the beginning of the sequence
*/
float dashOffset;
/**
* The previous coordinates
*/
float[] prevCoords = new float[2];
/**
* The current coordinates
*/
float[] currCoords = new float[2];
/**
* The length of the current segment
*/
float segmentLength;
/**
* The current position from the beginning of the current segment
*/
float segmentOffset;
/**
* The segment type returned by the last call to the delegates currentSegment(...)
*/
int lastType;
/**
* The coordinate of the next dash segment returned
*/
float[] dashedSegment = new float[2];
/**
* The type of the next dash segment returned
*/
int dashedType;
/**
* Are we done?
*/
boolean done;
/**
* Delta X of curr and prev coordinates
*/
private float dy;
/**
* Delta Y of the curr and prev coordinates
*/
private float dx;
/**
* Both used to reset the dash state when doing a MOVE_TO
*/
float dashPhase;
int baseDashIndex;
public DashedIterator(PathIterator delegate, float[] dashArray, float dashPhase) {
this.delegate = delegate;
dashOffsets = new float[dashArray.length];
dashOffsets[0] = dashArray[0];
for (int i = 1; i < dashArray.length; i++) {
dashOffsets[i] = dashOffsets[i - 1] + dashArray[i];
}
// adjust the phase so that it's not longer than the dash array itself
dashPhase = dashPhase % dashOffsets[dashOffsets.length - 1];
// adjust the current index in the dash array
// so that we start dashing at the requested phase
for (int i = 0; i < dashArray.length && dashPhase > dashArray[i]; i++) {
dashIndex++;
}
this.baseDashIndex = dashIndex;
this.dashPhase = dashPhase;
this.dashOffset = dashPhase;
if (delegate.isDone()) {
done = true;
} else {
dashedType = delegate.currentSegment(dashedSegment);
currCoords[0] = dashedSegment[0];
currCoords[1] = dashedSegment[1];
delegate.next();
}
}
public int currentSegment(float[] coords) {
coords[0] = dashedSegment[0];
coords[1] = dashedSegment[1];
return dashedType;
}
public int currentSegment(double[] coords) {
float[] fcoord = new float[2];
int retval = currentSegment(fcoord);
coords[0] = fcoord[0];
coords[1] = fcoord[1];
return retval;
}
public int getWindingRule() {
return delegate.getWindingRule();
}
public boolean isDone() {
return done;
}
public void next() {
// have we exhausted the previous segment?
if (segmentLength == 0) {
if(!delegate.isDone()) {
prevCoords[0] = currCoords[0];
prevCoords[1] = currCoords[1];
lastType = delegate.currentSegment(currCoords);
if(lastType == PathIterator.SEG_MOVETO) {
// start over and move to the next value
segmentOffset = 0;
dashOffset = dashPhase;
dashIndex = baseDashIndex;
dashedSegment[0] = currCoords[0];
dashedSegment[1] = currCoords[1];
dashedType = PathIterator.SEG_MOVETO;
dx = 0;
dy = 0;
delegate.next();
// if no segment after move we're done
done = delegate.isDone();
} else {
// prepare for the next round of dash array application
dx = currCoords[0] - prevCoords[0];
dy = currCoords[1] - prevCoords[1];
segmentLength = (float) sqrt(pow(dx, 2) + pow(dy, 2));
segmentOffset = 0;
delegate.next();
}
} else {
done = true;
}
}
// if not done move along the dash array
if (!done && lastType != PathIterator.SEG_MOVETO) {
float dashResidual = dashOffsets[dashIndex] - dashOffset;
float segmentResidual = segmentLength - segmentOffset;
float residual = min(dashResidual, segmentResidual);
if (abs(segmentLength) > EPS) {
dashedSegment[0] = dashedSegment[0] + dx * residual / segmentLength;
dashedSegment[1] = dashedSegment[1] + dy * residual / segmentLength;
}
// if the lastType is a line to we need to decide if we're pen down or pen
// up depending on what of the dash segments we're in
if (lastType == PathIterator.SEG_LINETO) {
dashedType = dashIndex % 2 == 0 ? PathIterator.SEG_LINETO
: PathIterator.SEG_MOVETO;
} else {
dashedType = lastType;
}
// move forward
dashOffset += residual;
segmentOffset += residual;
// move forward along the dash array
if (abs(dashOffsets[dashIndex] - dashOffset) < EPS) {
dashIndex++;
if (dashIndex >= dashOffsets.length) {
dashIndex = 0;
dashOffset = 0;
}
}
// more forward along the segment
if (abs(segmentOffset - segmentLength) < EPS) {
segmentLength = 0;
segmentOffset = 0;
}
}
}
}
// a small main useful for interactive testing
public static void main(String[] args) throws Exception {
BufferedImage image = new BufferedImage(800, 600, BufferedImage.TYPE_3BYTE_BGR);
Graphics2D graphics = image.createGraphics();
Shape stroked = new DashedShape(new Rectangle2D.Double(0, 0, 4, 4),
new float[] { 2, 2}, 0);
graphics.draw(stroked);
graphics.dispose();
ImageIO.write(image, "png", new File("/tmp/image.png"));
PathIterator pi = stroked.getPathIterator(new AffineTransform());
float[] point = new float[2];
while (!pi.isDone()) {
int type = pi.currentSegment(point);
System.out.println(type + " - " + Arrays.toString(point));
pi.next();
}
}
}