/* * @(#)EnhancedPath.java * * Copyright (c) 2007 The authors and contributors of JHotDraw. * You may not use, copy or modify this file, except in compliance with the * accompanying license terms. */ package org.jhotdraw.samples.odg.geom; import javax.annotation.Nullable; import java.awt.Rectangle; import java.awt.Shape; import java.awt.geom.*; import java.util.ArrayList; import java.util.Arrays; /** * Represents an ODG Enhanced Path. * <p> * The coordinates of a EnhancedPath.Segment can reference a * formula or a modifier. * * @author Werner Randelshofer * @version $Id$ */ public class EnhancedPath extends ArrayList<EnhancedPath.Segment> implements Shape { private static final long serialVersionUID = 1L; public enum SegType { /* moveto x y */ MOVETO(2), /* lineto x y */ LINETO(2), /* curveto x1 y1 x2 y2 x y */ CURVETO(6), /* quadto x1 y1 x y */ QUADTO(4), /* closepath */ CLOSE(0), /* ellipseto x y w h t0 t1 */ ELLIPSETO(6), /* (counter-clockwise) arcto x1 y1 x2 y2 x3 y3 x y */ ARCTO(8), /* clockwise arcto x1 y1 x2 y2 x3 y3 x y */ CLOCKWISE_ARCTO(8), /* elliptical-quadrantx x y */ QUADRANT_XTO(2), /* elliptical-quadranty x y */ QUADRANT_YTO(2); /** * len is the number of parameters needed by a segment. */ private int len; SegType(int len) { this.len = len; } int getLen() { return len; } } /** * We cache a Path2D.Double instance to speed up Shape operations. */ @Nullable private transient Path2D.Double generalPath; /** * We cache a Rectangle2D.Double instance to speed up getBounds operations. */ @Nullable private transient Rectangle2D.Double bounds; /** * The winding rule for filling the bezier path. */ private int windingRule = Path2D.Double.WIND_EVEN_ODD; @Override public Rectangle getBounds() { return getBounds2D().getBounds(); } @Override public Rectangle2D getBounds2D() { if (bounds == null) { } return (Rectangle2D.Double) bounds.clone(); } @Override public boolean contains(double x, double y, double w, double h) { validatePath(); return generalPath.contains(x, y, w, h); } @Override public boolean contains(Point2D p) { validatePath(); return generalPath.contains(p); } @Override public boolean contains(double x, double y) { validatePath(); return generalPath.contains(x, y); } @Override public boolean contains(Rectangle2D r) { validatePath(); return generalPath.contains(r); } @Override public boolean intersects(Rectangle2D r) { validatePath(); return generalPath.intersects(r); } @Override public boolean intersects(double x, double y, double w, double h) { validatePath(); return generalPath.intersects(x, y, w, h); } @Override public PathIterator getPathIterator(AffineTransform at) { validatePath(); return generalPath.getPathIterator(at); } @Override public PathIterator getPathIterator(AffineTransform at, double flatness) { validatePath(); return generalPath.getPathIterator(at, flatness); } /** * Defines a vertex (node) of the bezier path. * <p> * A vertex consists of three control points: C0, C1 and C2. * <ul> * <li>The bezier path always passes through C0.</li> * <li>C1 is used to control the curve towards C0. * </li> * <li>C2 is used to control the curve going away from C0.</li> * </ul> */ public static class Segment implements Cloneable { /** * The type of the segment. */ public SegType type; /** Control points x and y coordinates. */ public double[] coords = new double[8]; /** Modifiers and formulas. */ public String[] modifiers = new String[8]; public Segment() { type = SegType.LINETO; } /** * Creates a segment. */ public Segment(SegType type, Object... coordOrModifier) { this.type = type; for (int i = 0; i < coordOrModifier.length; i++) { if (coordOrModifier[i] instanceof Double) { coords[i] = (Double) coordOrModifier[i]; } else { modifiers[i] = (String) coordOrModifier[i]; } } } public Segment(Segment that) { setTo(that); } public void setTo(Segment that) { this.type = that.type; System.arraycopy(that.coords, 0, this.coords, 0, that.type.getLen()); System.arraycopy(that.modifiers, 0, this.modifiers, 0, that.type.getLen()); } @Override public Object clone() { try { Segment that = (Segment) super.clone(); that.coords = this.coords.clone(); that.modifiers = this.modifiers.clone(); return that; } catch (CloneNotSupportedException e) { InternalError error = new InternalError(); error.initCause(e); throw error; } } @Override public int hashCode() { return (type.hashCode() << 24) | Arrays.hashCode(coords) & 0x0fff0000 | Arrays.hashCode(modifiers) & 0xffff; } @Override public boolean equals(Object o) { if (o instanceof EnhancedPath.Segment) { EnhancedPath.Segment that = (EnhancedPath.Segment) o; return that.type == this.type && Arrays.equals(that.coords, this.coords) && Arrays.equals(that.modifiers, this.modifiers); } return false; } } /** * Recomputes the EnhancedPath, if it is invalid. */ public void validatePath() { if (generalPath == null) { generalPath = toGeneralPath(); } } /** * This must be called after the EnhancedPath has been changed. */ public void invalidatePath() { generalPath = null; bounds = null; } /** Converts the EnhancedPath into a Path2D.Double. */ public Path2D.Double toGeneralPath() { Path2D.Double gp = new Path2D.Double(); // XXX implement me return gp; } /** * Opens a new path segment at the specified position. */ public void moveTo(Object xm1, Object ym1) { add(new Segment(SegType.MOVETO, xm1, ym1)); } /** * Adds a line to the current path segment. * This is only allowed, when the current path segment is open. */ public void lineTo(Object x1, Object y1) { if (size() == 0 || get(size() - 1).type == SegType.CLOSE) { throw new IllegalPathStateException("lineTo is only allowed when a path segment is open"); } add(new Segment(SegType.LINETO, x1, y1)); } /** * Closes the current path segment. * This is only allowed, when the current path segment is open. */ public void close() { if (size() == 0 || get(size() - 1).type == SegType.CLOSE) { throw new IllegalPathStateException("close is only allowed when a path segment is open"); } add(new Segment(SegType.CLOSE)); } /** * Adds a quadratic curve to the current path segment. * This is only allowed, when the current path segment is open. */ public void quadTo(Object x1, Object y1, Object x2, Object y2) { if (size() == 0 || get(size() - 1).type == SegType.CLOSE) { throw new IllegalPathStateException("quadTo is only allowed when a path segment is open"); } add(new Segment(SegType.QUADTO, x1, y1, x2, y2)); } /** * Adds a cubic curve to the current path segment. * This is only allowed, when the current path segment is open. */ public void curveTo(Object x1, Object y1, Object x2, Object y2, Object x3, Object y3) { if (size() == 0 || get(size() - 1).type == SegType.CLOSE) { throw new IllegalPathStateException("curveTo is only allowed when a path segment is open"); } add(new Segment(SegType.CURVETO, x1, y1, x2, y2, x3, y3)); } /** * (x1, y1) and (x2, y2) is defining the bounding * box of a ellipse. A line is then drawn from the * current point to the start angle of the arc that is * specified by the radial vector of point (x3, y3) * and then counter clockwise to the end-angle * that is specified by point (x4, y4). */ public void arcTo(Object x1, Object y1, Object x2, Object y2, Object x3, Object y3, Object x4, Object y4) { if (size() == 0) { throw new IllegalPathStateException("arcTo only allowed when not empty"); } add(new Segment(SegType.ARCTO, x1, y1, x2, y2, x3, y3, x4, y4)); } public void clockwiseArcTo(Object x1, Object y1, Object x2, Object y2, Object x3, Object y3, Object x4, Object y4) { if (size() == 0) { throw new IllegalPathStateException("clockwiseArcTo only allowed when not empty"); } add(new Segment(SegType.CLOCKWISE_ARCTO, x1, y1, x2, y2, x3, y3, x4, y4)); } /** * Draws a segment of an ellipse. The ellipse is specified by the * center(x, y), the size(w, h) and the start-angle t0 and end-angle t1. */ public void ellipseTo(Object x, Object y, Object w, Object h, Object t0, Object t1) { if (size() == 0 || get(size() - 1).type == SegType.CLOSE) { throw new IllegalPathStateException("ellipseTo is only allowed when a path segment is open"); } add(new Segment(SegType.ELLIPSETO, x, y, w, h, t0, t1)); } public void quadrantXTo(Object x, Object y) { if (size() == 0 || get(size() - 1).type == SegType.CLOSE) { throw new IllegalPathStateException("quadrantXTo is only allowed when a path segment is open"); } add(new Segment(SegType.QUADRANT_XTO, x, y)); } public void quadrantYTo(Object x, Object y) { if (size() == 0 || get(size() - 1).type == SegType.CLOSE) { throw new IllegalPathStateException("quadrantYTo is only allowed when a path segment is open"); } add(new Segment(SegType.QUADRANT_YTO, x, y)); } /** * Sets winding rule for filling the bezier path. * @param newValue Must be Path2D.Double.WIND_EVEN_ODD or Path2D.Double.WIND_NON_ZERO. */ public void setWindingRule(int newValue) { if (newValue != windingRule) { invalidatePath(); int oldValue = windingRule; this.windingRule = newValue; } } /** * Gets winding rule for filling the bezier path. * @return Path2D.Double.WIND_EVEN_ODD or Path2D.Double.WIND_NON_ZERO. */ public int getWindingRule() { return windingRule; } }