/*

 ============================================================================
                   The Apache Software License, Version 1.1
 ============================================================================

 Copyright (C) 1999-2003 The Apache Software Foundation. All rights reserved.

 Redistribution and use in source and binary forms, with or without modifica-
 tion, are permitted provided that the following conditions are met:

 1. Redistributions of  source code must  retain the above copyright  notice,
    this list of conditions and the following disclaimer.

 2. Redistributions in binary form must reproduce the above copyright notice,
    this list of conditions and the following disclaimer in the documentation
    and/or other materials provided with the distribution.

 3. The end-user documentation included with the redistribution, if any, must
    include  the following  acknowledgment:  "This product includes  software
    developed  by the  Apache Software Foundation  (http://www.apache.org/)."
    Alternately, this  acknowledgment may  appear in the software itself,  if
    and wherever such third-party acknowledgments normally appear.

 4. The names "Batik" and  "Apache Software Foundation" must  not  be
    used to  endorse or promote  products derived from  this software without
    prior written permission. For written permission, please contact
    apache@apache.org.

 5. Products  derived from this software may not  be called "Apache", nor may
    "Apache" appear  in their name,  without prior written permission  of the
    Apache Software Foundation.

 THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 FITNESS  FOR A PARTICULAR  PURPOSE ARE  DISCLAIMED.  IN NO  EVENT SHALL  THE
 APACHE SOFTWARE  FOUNDATION  OR ITS CONTRIBUTORS  BE LIABLE FOR  ANY DIRECT,
 INDIRECT, INCIDENTAL, SPECIAL,  EXEMPLARY, OR CONSEQUENTIAL  DAMAGES (INCLU-
 DING, BUT NOT LIMITED TO, PROCUREMENT  OF SUBSTITUTE GOODS OR SERVICES; LOSS
 OF USE, DATA, OR  PROFITS; OR BUSINESS  INTERRUPTION)  HOWEVER CAUSED AND ON
 ANY  THEORY OF LIABILITY,  WHETHER  IN CONTRACT,  STRICT LIABILITY,  OR TORT
 (INCLUDING  NEGLIGENCE OR  OTHERWISE) ARISING IN  ANY WAY OUT OF THE  USE OF
 THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

 This software  consists of voluntary contributions made  by many individuals
 on  behalf of the Apache Software  Foundation. For more  information on the
 Apache Software Foundation, please see <http://www.apache.org/>.

*/

package org.apache.batik.gvt;

import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.List;
import java.util.Vector;

import org.apache.batik.ext.awt.geom.ExtendedGeneralPath;
import org.apache.batik.ext.awt.geom.ExtendedPathIterator;
import org.apache.batik.ext.awt.geom.ExtendedShape;
import org.apache.batik.ext.awt.geom.ShapeExtender;

/**
 * A shape painter that can be used to paint markers on a shape.
 *
 * @author <a href="vincent.hardy@eng.sun.com">Vincent Hardy</a>
 * @version $Id: MarkerShapePainter.java,v 1.15 2003/10/23 01:01:55 deweese Exp $
 */
public class MarkerShapePainter implements ShapePainter {

    /** 
     * The Shape to be painted.
     */
    protected ExtendedShape extShape;

    /**
     * Start Marker
     */
    protected Marker startMarker;
    
    /**
     * Middle Marker
     */
    protected Marker middleMarker;

    /**
     * End Marker
     */
    protected Marker endMarker;

    /**
     * Start marker proxy.
     */
    private ProxyGraphicsNode startMarkerProxy;

    /**
     * Middle marker proxy.
     */
    private ProxyGraphicsNode middleMarkerProxies[];

    /**
     * End marker proxy.
     */
    private ProxyGraphicsNode endMarkerProxy;

    /**
     * Contains the various marker proxies.
     */
    private CompositeGraphicsNode markerGroup;

    /**
     * Internal Cache: Primitive bounds
     */
    private Rectangle2D dPrimitiveBounds;

    /**
     * Internal Cache: Geometry bounds
     */
    private Rectangle2D dGeometryBounds;

    /**
     * Constructs a new <tt>MarkerShapePainter</tt> that can be used to markers
     * on top of a shape.
     *
     * @param shape Shape to be painted by this painter.
     * Should not be null 
     */
    public MarkerShapePainter(Shape shape) {
        if (shape == null) {
            throw new IllegalArgumentException();
        }
        if (shape instanceof ExtendedShape) {
            this.extShape = (ExtendedShape)shape;
        } else {
            this.extShape = new ShapeExtender(shape);
        }
    }

    /**
     * Paints the specified shape using the specified Graphics2D.
     *
     * @param shape the shape to paint
     * @param g2d the Graphics2D to use
     */
     public void paint(Graphics2D g2d) {
	 if (markerGroup == null) {
	     buildMarkerGroup();
	 }
         if (markerGroup.getChildren().size() > 0) {
             markerGroup.paint(g2d);
         }
     }

    /**
     * Returns the area painted by this shape painter.
     */
    public Shape getPaintedArea(){
	 if (markerGroup == null) {
	     buildMarkerGroup();
	 }
        return markerGroup.getOutline();
    }

    /**
     * Returns the bounds of the area painted by this shape painter
     */
    public Rectangle2D getPaintedBounds2D(){
	 if (markerGroup == null) {
	     buildMarkerGroup();
	 }
         return markerGroup.getPrimitiveBounds();
    }

    /**
     * Returns true if pt is in the area painted by this shape painter
     */
    public boolean inPaintedArea(Point2D pt){
	 if (markerGroup == null) {
	     buildMarkerGroup();
	 }
         GraphicsNode gn = markerGroup.nodeHitAt(pt);
         return (gn != null);
    }

    /**
     * Returns the area covered by this shape painter (even if not painted).
     * This is always null for Markers.
     */
    public Shape getSensitiveArea() { return null; }

    /**
     * Returns the bounds of the area covered by this shape painte
     * (even if not painted). This is always null for Markers.
     */
    public Rectangle2D getSensitiveBounds2D() { return null; }

    /**
     * Returns true if pt is in the sensitive area.
     * This is always false for Markers.
     */
    public boolean inSensitiveArea(Point2D pt) { return false; }


    /**
     * Sets the Shape this shape painter is associated with.
     *
     * @param shape new shape this painter should be associated with.
     * Should not be null.
     */
    public void setShape(Shape shape){
        if (shape == null) {
            throw new IllegalArgumentException();
        }
        if (shape instanceof ExtendedShape) {
            this.extShape = (ExtendedShape)shape;
        } else {
            this.extShape = new ShapeExtender(shape);
        }

        this.startMarkerProxy = null;
        this.middleMarkerProxies = null;
        this.endMarkerProxy = null;
	this.markerGroup = null;
    }

    /**
     * Gets the Shape this shape painter is associated with as an
     * Extended Shape.
     *
     * @return shape associated with this painter */
    public ExtendedShape getExtShape(){
        return extShape;
    }

    /**
     * Gets the Shape this shape painter is associated with.
     *
     * @return shape associated with this painter
     */
    public Shape getShape(){
        return extShape;
    }

    /**
     * Returns the marker that shall be drawn at the first vertex of the given
     * shape.
     */
    public Marker getStartMarker(){
        return startMarker;
    }

    /**
     * Sets the marker that shall be drawn at the first vertex of the given
     * shape.
     *
     * @param startMarker the start marker 
     */
    public void setStartMarker(Marker startMarker){
        this.startMarker = startMarker;
        this.startMarkerProxy = null;
	this.markerGroup = null;
    }

    /**
     * Returns the marker that shall be drawn at every other vertex (not the
     * first or the last one) of the given shape.
     */
    public Marker getMiddleMarker(){
        return middleMarker;
    }

    /**
     * Sets the marker that shall be drawn at every other vertex (not the first
     * or the last one) of the given shape.
     *
     * @param middleMarker the middle marker
     */
    public void setMiddleMarker(Marker middleMarker){
        this.middleMarker = middleMarker;
        this.middleMarkerProxies = null;
	this.markerGroup = null;
    }

    /**
     * Returns the marker that shall be drawn at the last vertex of the given
     * shape.
     */
    public Marker getEndMarker(){
        return endMarker;
    }

    /**
     * Sets the marker that shall be drawn at the last vertex of the given
     * shape.
     *
     * @param endMarker the end marker 
     */
    public void setEndMarker(Marker endMarker){
        this.endMarker = endMarker;
        this.endMarkerProxy = null;
	this.markerGroup = null;
    }

    // ---------------------------------------------------------------------
    // Internal methods to build GraphicsNode according to the Marker
    // ---------------------------------------------------------------------

    /**
     * Builds a new marker group with the current set of markers.
     */
    protected void buildMarkerGroup(){
        if (startMarker != null && startMarkerProxy == null) {
            startMarkerProxy = buildStartMarkerProxy();
        }

        if (middleMarker != null && middleMarkerProxies == null) {
            middleMarkerProxies = buildMiddleMarkerProxies();
        }

        if (endMarker != null && endMarkerProxy == null) {
            endMarkerProxy = buildEndMarkerProxy();
        }

        CompositeGraphicsNode group = new CompositeGraphicsNode();
        List children = group.getChildren();
        if (startMarkerProxy != null) {
            children.add(startMarkerProxy);
        }
	
        if (middleMarkerProxies != null) {
            for(int i=0; i<middleMarkerProxies.length; i++){
                children.add(middleMarkerProxies[i]);
            }
        }
	
        if (endMarkerProxy != null) {
            children.add(endMarkerProxy);
        }

        markerGroup = group;
    }

    /**
     * Builds a proxy <tt>GraphicsNode</tt> for the input <tt>Marker</tt> to be
     * drawn at the start position 
     */
    protected ProxyGraphicsNode buildStartMarkerProxy() {

        ExtendedPathIterator iter = getExtShape().getExtendedPathIterator();

        // Get initial point on the path
        double coords[] = new double[7];
        int segType = 0;

        if (iter.isDone()) {
            return null;
        }

        segType = iter.currentSegment(coords);
        if (segType != ExtendedPathIterator.SEG_MOVETO) {
            return null;
        }
        iter.next();

        Point2D markerPosition = new Point2D.Double(coords[0], coords[1]);

        // If the marker's orient property is NaN,
        // the slope needs to be computed
        double rotation = startMarker.getOrient();
        if (Double.isNaN(rotation)) {
            if (!iter.isDone()) {
                double next[] = new double[7];
                int nextSegType = 0;
                nextSegType = iter.currentSegment(next);
                if(nextSegType == PathIterator.SEG_CLOSE){
                    nextSegType = PathIterator.SEG_LINETO;
                    next[0] = coords[0];
                    next[1] = coords[1];
                }
                rotation = computeRotation(null, 0,  // no previous seg.
                                           coords, segType,    // segment ending on start point
                                           next, nextSegType); // segment out of start point
                
            }
        }
        
        // Now, compute the marker's proxy transform
        AffineTransform markerTxf = computeMarkerTransform(startMarker,
							   markerPosition,
							   rotation);
                                   
        ProxyGraphicsNode gn = new ProxyGraphicsNode();

        gn.setSource(startMarker.getMarkerNode());
        gn.setTransform(markerTxf);
	
        return gn;
    }

    /**
     * Builds a proxy <tt>GraphicsNode</tt> for the input <tt>Marker</tt> to be
     * drawn at the end position.
     */
    protected ProxyGraphicsNode buildEndMarkerProxy() {

        ExtendedPathIterator iter = getExtShape().getExtendedPathIterator();

        int nPoints = 0;

        // Get first point, in case the last segment on the
        // path is a close
        if (iter.isDone()) {
            return null;
        }
	
        double coords[] = new double[7];
        double moveTo[] = new double[2];
        int segType = 0;
        segType = iter.currentSegment(coords);
        if (segType != ExtendedPathIterator.SEG_MOVETO) {
            return null;
        }
        nPoints++;
        moveTo[0] = coords[0];
        moveTo[1] = coords[1];

        iter.next();
        
        // Now, get the last two points on the path
        double[] lastButOne = new double[7];
        double[] last = {coords[0], coords[1], coords[2],
                         coords[3], coords[4], coords[5], coords[6] };
        double[] tmp = null;
        int lastSegType = segType;
        int lastButOneSegType = 0;

        while (!iter.isDone()) {
            tmp = lastButOne;
            lastButOne = last;
            last = tmp;
            lastButOneSegType = lastSegType;

            lastSegType = iter.currentSegment(last);

            if (lastSegType == PathIterator.SEG_MOVETO) {
                moveTo[0] = last[0];
                moveTo[1] = last[1];
            } else if (lastSegType == PathIterator.SEG_CLOSE) {
                lastSegType = PathIterator.SEG_LINETO;
                last[0] = moveTo[0];
                last[1] = moveTo[1];
            }

            iter.next();
            nPoints++;
        }

        if (nPoints < 2) {
            return null;
        }

        // Turn the last segment into a position
        Point2D markerPosition = getSegmentTerminatingPoint(last, lastSegType);

        // If the marker's orient property is NaN,
        // the slope needs to be computed
        double rotation = endMarker.getOrient();
        if (Double.isNaN(rotation)) {
            rotation = computeRotation(lastButOne, 
                                       lastButOneSegType, 
                                       last, lastSegType,
                                       null, 0);
        }

        // Now, compute the marker's proxy transform
        AffineTransform markerTxf = computeMarkerTransform(endMarker,
							   markerPosition,
							   rotation);
                                   
        ProxyGraphicsNode gn = new ProxyGraphicsNode();

        gn.setSource(endMarker.getMarkerNode());
        gn.setTransform(markerTxf);

        return gn;
    }

    /**
     * Builds a proxy <tt>GraphicsNode</tt> for the input
     * <tt>Marker</tt> to be drawn at the middle positions 
     */
    protected ProxyGraphicsNode[] buildMiddleMarkerProxies() {

        ExtendedPathIterator iter = getExtShape().getExtendedPathIterator();

        double[] prev = new double[7];
        double[] curr = new double[7];
        double[] next = new double[7], tmp = null;
        int prevSegType = 0, currSegType = 0, nextSegType = 0;

        // Get the first three points on the path
        if (iter.isDone()) {
            return null;
        }

        prevSegType = iter.currentSegment(prev);
        double[] moveTo = new double[2];

        if (prevSegType != PathIterator.SEG_MOVETO) {
            return null;
        }

        moveTo[0] = prev[0];
        moveTo[1] = prev[1];
        iter.next();

        if (iter.isDone()) {
            return null;
        }
	
        currSegType = iter.currentSegment(curr);

        if (currSegType == PathIterator.SEG_MOVETO) {
            moveTo[0] = curr[0];
            moveTo[1] = curr[1];
        } else if (currSegType == PathIterator.SEG_CLOSE) {
            currSegType = PathIterator.SEG_LINETO;
            curr[0] = moveTo[0];
            curr[1] = moveTo[1];
        }

        iter.next();

        Vector proxies = new Vector();
        while (!iter.isDone()) {
            nextSegType = iter.currentSegment(next);

            if (nextSegType == PathIterator.SEG_MOVETO) {
                moveTo[0] = next[0];
                moveTo[1] = next[1];
            } else if (nextSegType == PathIterator.SEG_CLOSE) {
                nextSegType = PathIterator.SEG_LINETO;
                next[0] = moveTo[0];
                next[1] = moveTo[1];
            }
	    
            proxies.addElement(createMiddleMarker(prev, prevSegType,
                                                  curr, currSegType,
                                                  next, nextSegType));
            
            tmp = prev;
            prev = curr;
            prevSegType = currSegType;
            curr = next;
            currSegType = nextSegType;
            next = tmp;
            
            iter.next();
        }

        ProxyGraphicsNode [] gn = new ProxyGraphicsNode[proxies.size()];
        proxies.copyInto(gn);

        return gn;
    }

    /**
     * Creates a ProxyGraphicsNode for a middle marker.
     */
    private ProxyGraphicsNode createMiddleMarker
        (double[] prev, int prevSegType,
         double[] curr, int currSegType,
         double[] next, int nextSegType) {

        // Turn the curr segment into a position
        Point2D markerPosition = getSegmentTerminatingPoint(curr, currSegType);

        // If the marker's orient property is NaN,
        // the slope needs to be computed
        double rotation = middleMarker.getOrient();
        if (Double.isNaN(rotation)) {
            rotation = computeRotation(prev, prevSegType,
                                       curr, currSegType,
                                       next, nextSegType);
        }
	
        // Now, compute the marker's proxy transform
        AffineTransform markerTxf = computeMarkerTransform(middleMarker,
							   markerPosition,
							   rotation);
                                   
        ProxyGraphicsNode gn = new ProxyGraphicsNode();

        gn.setSource(middleMarker.getMarkerNode());
        gn.setTransform(markerTxf);

        return gn;
    }

    /**
     * Returns the rotation according to the specified parameters.
     */
    private double computeRotation(double[] prev, int prevSegType,
                                   double[] curr, int currSegType,
                                   double[] next, int nextSegType){

        // Compute in slope, i.e., the slope of the segment
        // going into the current point
        double[] inSlope = computeInSlope(prev, prevSegType, 
                                          curr, currSegType);

        // Compute out slope, i.e., the slope of the segment
        // going out of the current point
        double[] outSlope = computeOutSlope(curr, currSegType, 
                                            next, nextSegType);

        if (inSlope == null) {
            inSlope = outSlope;
        }

        if (outSlope == null) {
            outSlope = inSlope;
        }

        if (inSlope == null) {
            return 0;
        }

        double dx = inSlope[0] + outSlope[0];
        double dy = inSlope[1] + outSlope[1];

        if (dx == 0 && dy == 0) {
            // The two vectors are exact opposites. There is no way to 
            // know which direction to go (+90 or -90). Choose +90
            return Math.atan2(inSlope[1], inSlope[0])*180./Math.PI + 90;
        } else {
            return Math.atan2(dy, dx)*180./Math.PI;
        }
    }

    /**
     * Returns dx/dy for the in slope.
     */
    private double[] computeInSlope(double[] prev, int prevSegType,
                                    double[] curr, int currSegType){

        // Compute point into which the slope runs
        Point2D currEndPoint = getSegmentTerminatingPoint(curr, currSegType);

        double dx = 0;
        double dy = 0;

        switch(currSegType){
        case PathIterator.SEG_LINETO: {
            // This is equivalent to a line from the previous segment's
            // terminating point and the current end point.
            Point2D prevEndPoint = 
                getSegmentTerminatingPoint(prev, prevSegType);
            dx = currEndPoint.getX() - prevEndPoint.getX();
            dy = currEndPoint.getY() - prevEndPoint.getY();
        }
            break;
        case PathIterator.SEG_QUADTO:
            // If the current segment is a line, quad or cubic curve.
            // the slope is about equal to that of the line from the
            // last control point and the curEndPoint
            dx = currEndPoint.getX() - curr[0];
            dy = currEndPoint.getY() - curr[1];
            break;
        case PathIterator.SEG_CUBICTO:
            // If the current segment is a quad or cubic curve.
            // the slope is about equal to that of the line from the
            // last control point and the curEndPoint
            dx = currEndPoint.getX() - curr[2];
            dy = currEndPoint.getY() - curr[3];
            break;
        case ExtendedPathIterator.SEG_ARCTO: {
            // If the current segment is an ARCTO then we build the
            // arc and ask for it's end angle and get the tangent there.
            Point2D prevEndPoint = 
                getSegmentTerminatingPoint(prev, prevSegType);
            boolean large   = (curr[3]!=0.);
            boolean goLeft = (curr[4]!=0.);
            Arc2D arc = ExtendedGeneralPath.computeArc
                (prevEndPoint.getX(), prevEndPoint.getY(),
                 curr[0], curr[1], curr[2],
                 large, goLeft, curr[5], curr[6]);
            double theta = arc.getAngleStart()+arc.getAngleExtent();
            theta = Math.toRadians(theta);
            dx = -arc.getWidth()/2.0*Math.sin(theta);
            dy = arc.getHeight()/2.0*Math.cos(theta);

            // System.out.println("In  Theta:  " + Math.toDegrees(theta) +  
            //                    " Dx/Dy: " + dx + "/" + dy);
            if (curr[2] != 0) {
                double ang = Math.toRadians(-curr[2]);
                double sinA = Math.sin(ang);
                double cosA = Math.cos(ang);
                double tdx = dx*cosA - dy*sinA;
                double tdy = dx*sinA + dy*cosA;
                dx = tdx;
                dy = tdy;
            }
            // System.out.println("    Rotate: " + curr[2] + 
            //                    " Dx/Dy: " + dx + "/" + dy);
            if (goLeft) {
                dx = -dx;
            } else {
                dy = -dy;
            }
            // System.out.println("    GoLeft? " + goLeft + 
            //                    " Dx/Dy: " + dx + "/" + dy);
        }
            break;
        case PathIterator.SEG_CLOSE:
            // Should not have any close at this point
            throw new Error();
        case PathIterator.SEG_MOVETO:
            // Cannot compute the slope
        default:
            return null;
        }

        if (dx == 0 && dy == 0) {
            return null;
        }

        return normalize(new double[] { dx, dy });
    }

    /**
     * Returns dx/dy for the out slope.
     */
    private double[] computeOutSlope(double[] curr, int currSegType,
                                     double[] next, int nextSegType){

        Point2D currEndPoint = getSegmentTerminatingPoint(curr, currSegType);
        
        double dx = 0, dy = 0;

        switch(nextSegType){
        case PathIterator.SEG_CLOSE:
            // Should not happen at this point, because all close
            // segments have been replaced by lineTo segments.
            break;
        case PathIterator.SEG_CUBICTO:
        case PathIterator.SEG_LINETO:
        case PathIterator.SEG_QUADTO:
            // If the next segment is a line, quad or cubic curve.
            // the slope is about equal to that of the line from
            // curEndPoint and the first control point
            dx = next[0] - currEndPoint.getX();
            dy = next[1] - currEndPoint.getY();
            break;
        case ExtendedPathIterator.SEG_ARCTO: {
            // If the current segment is an ARCTO then we build the
            // arc and ask for it's end angle and get the tangent there.
            boolean large   = (next[3]!=0.);
            boolean goLeft = (next[4]!=0.);
            Arc2D arc = ExtendedGeneralPath.computeArc
                (currEndPoint.getX(), currEndPoint.getY(),
                 next[0], next[1], next[2],
                 large, goLeft, next[5], next[6]);
            double theta = arc.getAngleStart();
            theta = Math.toRadians(theta);
            dx = -arc.getWidth()/2.0*Math.sin(theta);
            dy = arc.getHeight()/2.0*Math.cos(theta);
            // System.out.println("Out Theta:  " + Math.toDegrees(theta) +  
            //                    " Dx/Dy: " + dx + "/" + dy);
            if (next[2] != 0) {
                double ang = Math.toRadians(-next[2]);
                double sinA = Math.sin(ang);
                double cosA = Math.cos(ang);
                double tdx = dx*cosA - dy*sinA;
                double tdy = dx*sinA + dy*cosA;
                dx = tdx;
                dy = tdy;
            }
            // System.out.println("    Rotate: " + next[2] + 
            //                    " Dx/Dy: " + dx + "/" + dy);

            if (goLeft) {
                dx = -dx;
            } else {
                dy = -dy;
            }
            // System.out.println("    GoLeft? " + goLeft + 
            //                    " Dx/Dy: " + dx + "/" + dy);
        }
            break;
        case PathIterator.SEG_MOVETO:
            // Cannot compute the out slope
        default:
            return null;
        }

        if (dx == 0 && dy == 0) {
            return null;
        }

        return normalize(new double[] { dx, dy });
    }

    /**
     * Normalizes the input vector. This assumes an non-zero length
     */
    public double[] normalize(double[] v) {
        double n = Math.sqrt(v[0]*v[0]+v[1]*v[1]);
        v[0] /= n;
        v[1] /= n;
        return v;
    }

    /**
     * Computes the transform for the input marker, so that it is positioned at
     * the given position with the specified rotation 
     */
    private AffineTransform computeMarkerTransform(Marker marker,
						   Point2D markerPosition,
						   double rotation) {
        Point2D ref = marker.getRef();
        /*AffineTransform txf = 
            AffineTransform.getTranslateInstance(markerPosition.getX()
                                                 - ref.getX(),
                                                 markerPosition.getY()
                                                 - ref.getY());*/
        AffineTransform txf = new AffineTransform();

        txf.translate(markerPosition.getX() - ref.getX(),
                      markerPosition.getY() - ref.getY());

        if (!Double.isNaN(rotation)) {
            txf.rotate(rotation*Math.PI/180., ref.getX(), ref.getY());
        }

        return txf;
    }

    /**
     * Extracts the terminating point, depending on the segment type.
     */
    protected Point2D getSegmentTerminatingPoint(double coords[], int segType) {
        switch(segType){
        case PathIterator.SEG_CUBICTO:
            return new Point2D.Double(coords[4], coords[5]);
        case PathIterator.SEG_LINETO:
            return new Point2D.Double(coords[0], coords[1]);
        case PathIterator.SEG_MOVETO:
            return new Point2D.Double(coords[0], coords[1]);
        case PathIterator.SEG_QUADTO:
            return new Point2D.Double(coords[2], coords[3]);
        case ExtendedPathIterator.SEG_ARCTO:
            return new Point2D.Double(coords[5], coords[6]);
        case PathIterator.SEG_CLOSE:
        default:
            throw new Error(); 
            // Should never happen: close segments are replaced with lineTo
        }
    }
}
