/* * @(#)MeasuredShape.java * * $Date: 2014-04-06 05:02:15 +0200 (V, 06 ápr. 2014) $ * * Copyright (c) 2011 by Jeremy Wood. * All rights reserved. * * The copyright of this software is owned by Jeremy Wood. * You may not use, copy or modify this software, except in * accordance with the license agreement you entered into with * Jeremy Wood. For details see accompanying license terms. * * This software is probably, but not necessarily, discussed here: * https://javagraphics.java.net/ * * That site should also contain the most recent official version * of this software. (See the SVN repository for more details.) */ package com.bric.geom; import java.awt.Shape; import java.awt.geom.GeneralPath; import java.awt.geom.Path2D; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.io.Serializable; import java.util.Vector; /** This represents a single closed path. * <P>This object can trace arbitrary amounts of itself using the * <code>writeShape()</code> methods. * */ public class MeasuredShape implements Serializable { private static final long serialVersionUID = 1L; /** Because a MeasuredShape must be exactly 1 subpath, this method * will safely break up a path into separate subpaths and create one * MeasuredShape for each. * * @param s a path, possibly containing multiple subpaths * @return a MeasuredShape object for each subpath in <code>i</code> */ public static MeasuredShape[] getSubpaths(Shape s) { return getSubpaths(s.getPathIterator(null),DEFAULT_SPACING); } /** Because a MeasuredShape must be exactly 1 subpath, this method * will safely break up a path into separate subpaths and create one * MeasuredShape for each. * * @param s a path, possibly containing multiple subpaths * @param spacing the spacing to be used for each <code>MeasuredShape</code> * @return a MeasuredShape object for each subpath in <code>i</code> */ public static MeasuredShape[] getSubpaths(Shape s,float spacing) { return getSubpaths(s.getPathIterator(null),spacing); } /** Because a MeasuredShape must be exactly 1 subpath, this method * will safely break up a path into separate subpaths and create one * MeasuredShape for each. * * @param i a path, possibly containing multiple subpaths * @return a MeasuredShape object for each subpath in <code>i</code> */ public static MeasuredShape[] getSubpaths(PathIterator i) { return getSubpaths(i,DEFAULT_SPACING); } /** Because a MeasuredShape must be exactly 1 subpath, this method * will safely break up a path into separate subpaths and create one * MeasuredShape for each. * * @param i a path, possibly containing multiple subpaths * @return a MeasuredShape object for each subpath in <code>i</code> */ public static MeasuredShape[] getSubpaths(PathIterator i,float spacing) { Vector<MeasuredShape> v = new Vector<MeasuredShape>(); GeneralPath path = null; float[] coords = new float[6]; while(i.isDone()==false) { int k = i.currentSegment(coords); if(k==PathIterator.SEG_MOVETO) { if(path!=null) { v.add(new MeasuredShape(path,spacing)); path = null; } path = new GeneralPath(); path.moveTo(coords[0],coords[1]); } else if(k==PathIterator.SEG_LINETO) { path.lineTo(coords[0],coords[1]); } else if(k==PathIterator.SEG_QUADTO) { path.quadTo(coords[0],coords[1],coords[2],coords[3]); } else if(k==PathIterator.SEG_CUBICTO) { path.curveTo(coords[0],coords[1],coords[2],coords[3],coords[4],coords[5]); } else if(k==PathIterator.SEG_CLOSE) { path.closePath(); } i.next(); } if(path!=null) { v.add(new MeasuredShape(path,spacing)); path = null; } return v.toArray(new MeasuredShape[v.size()]); } static class Segment implements Serializable { private static final long serialVersionUID = 1L; int type; float[] data; float realDistance; float normalizedDistance; public void write(PathWriter path,float t0,float t1) { if(t0==0 && t1==1) { if(type==PathIterator.SEG_MOVETO) { path.moveTo(data[0], data[1]); } else if(type==PathIterator.SEG_LINETO) { path.lineTo(data[2], data[3]); } else if(type==PathIterator.SEG_QUADTO) { path.quadTo(data[2],data[3],data[4],data[5]); } else if(type==PathIterator.SEG_CUBICTO) { path.curveTo(data[2],data[3],data[4],data[5],data[6],data[7]); } else { throw new RuntimeException(); } return; } else if(t0==1 && t1==0) { if(type==PathIterator.SEG_MOVETO) { path.moveTo(data[0], data[1]); } else if(type==PathIterator.SEG_LINETO) { path.lineTo(data[0], data[1]); } else if(type==PathIterator.SEG_QUADTO) { path.quadTo(data[2],data[3],data[0],data[1]); } else if(type==PathIterator.SEG_CUBICTO) { path.curveTo(data[4],data[5],data[2],data[3],data[0],data[1]); } else { throw new RuntimeException(); } return; } if(type==PathIterator.SEG_MOVETO) { path.moveTo(data[0], data[1]); //not sure what this means? } else if(type==PathIterator.SEG_LINETO) { path.lineTo(getX(t1),getY(t1)); } else if(type==PathIterator.SEG_QUADTO) { float ax = data[0]-2*data[2]+data[4]; float bx = -2*data[0]+2*data[2]; float cx = data[0]; float ay = data[1]-2*data[3]+data[5]; float by = -2*data[1]+2*data[3]; float cy = data[1]; PathWriter.quadTo(path, t0, t1, ax, bx, cx, ay, by, cy); } else if(type==PathIterator.SEG_CUBICTO) { float ax = -data[0]+3*data[2]-3*data[4]+data[6]; float bx = 3*data[0]-6*data[2]+3*data[4]; float cx = -3*data[0]+3*data[2]; float dx = data[0]; float ay = -data[1]+3*data[3]-3*data[5]+data[7]; float by = 3*data[1]-6*data[3]+3*data[5]; float cy = -3*data[1]+3*data[3]; float dy = data[1]; PathWriter.cubicTo(path, t0, t1, ax, bx, cx, dx, ay, by, cy, dy); } else if(type==PathIterator.SEG_CLOSE) { path.closePath(); } else { throw new RuntimeException(); } } public float getTangentSlope(float t) { if(type==PathIterator.SEG_LINETO) { float ax = data[2]-data[0]; float ay = data[3]-data[1]; return (float)Math.atan2(ay, ax); } else if(type==PathIterator.SEG_QUADTO) { float ax = data[0]-2*data[2]+data[4]; float bx = -2*data[0]+2*data[2]; float ay = data[1]-2*data[3]+data[5]; float by = -2*data[1]+2*data[3]; return (float)Math.atan2( 2*ay*t+by, 2*ax*t+bx ); } else if(type==PathIterator.SEG_CUBICTO) { float ax = -data[0]+3*data[2]-3*data[4]+data[6]; float bx = 3*data[0]-6*data[2]+3*data[4]; float cx = -3*data[0]+3*data[2]; float ay = -data[1]+3*data[3]-3*data[5]+data[7]; float by = 3*data[1]-6*data[3]+3*data[5]; float cy = -3*data[1]+3*data[3]; return (float)Math.atan2( 3*ay*t*t+2*by*t+cy, 3*ax*t*t+2*bx*t+cx ); } else if(type==PathIterator.SEG_MOVETO) { return data[0]; } else if(type==PathIterator.SEG_CLOSE) { throw new RuntimeException(); } else { throw new RuntimeException(); } } public float getX(float t) { if(type==PathIterator.SEG_LINETO) { float ax = data[2]-data[0]; return ax*t+data[0]; } else if(type==PathIterator.SEG_QUADTO) { float ax = data[0]-2*data[2]+data[4]; float bx = -2*data[0]+2*data[2]; float cx = data[0]; return (ax*t+bx)*t+cx; } else if(type==PathIterator.SEG_CUBICTO) { float ax = -data[0]+3*data[2]-3*data[4]+data[6]; float bx = 3*data[0]-6*data[2]+3*data[4]; float cx = -3*data[0]+3*data[2]; float dx = data[0]; return ((ax*t+bx)*t+cx)*t+dx; } else if(type==PathIterator.SEG_MOVETO) { return data[0]; } else if(type==PathIterator.SEG_CLOSE) { throw new RuntimeException(); } else { throw new RuntimeException(); } } public float getY(float t) { if(type==PathIterator.SEG_LINETO) { float ay = data[3]-data[1]; return ay*t+data[1]; } else if(type==PathIterator.SEG_QUADTO) { float ay = data[1]-2*data[3]+data[5]; float by = -2*data[1]+2*data[3]; float cy = data[1]; return (ay*t+by)*t+cy; } else if(type==PathIterator.SEG_CUBICTO) { float ay = -data[1]+3*data[3]-3*data[5]+data[7]; float by = 3*data[1]-6*data[3]+3*data[5]; float cy = -3*data[1]+3*data[3]; float dy = data[1]; return ((ay*t+by)*t+cy)*t+dy; } else if(type==PathIterator.SEG_MOVETO) { return data[1]; } else if(type==PathIterator.SEG_CLOSE) { throw new RuntimeException(); } else { throw new RuntimeException(); } } public Segment(int type,float lastX,float lastY,float[] coords,float spacing) { this.type = type; if(type==PathIterator.SEG_MOVETO) { data = new float[] {coords[0],coords[1]}; realDistance = 0; } else if(type==PathIterator.SEG_LINETO) { data = new float[] {lastX, lastY, coords[0],coords[1]}; realDistance = (float)(Math.sqrt( (coords[0]-lastX)*(coords[0]-lastX) + (coords[1]-lastY)*(coords[1]-lastY) )); } else if(type==PathIterator.SEG_CLOSE) { data = new float[0]; } else { double ax, bx, cx, dx, ay, by, cy, dy; if(type==PathIterator.SEG_QUADTO) { ay = 0; by = lastY-2*coords[1]+coords[3]; cy = -2*lastY+2*coords[1]; dy = lastY; ax = 0; bx = lastX-2*coords[0]+coords[2]; cx = -2*lastX+2*coords[0]; dx = lastX; data = new float[] {lastX, lastY, coords[0], coords[1], coords[2], coords[3]}; } else if(type==PathIterator.SEG_CUBICTO) { ay = -lastY+3*coords[1]-3*coords[3]+coords[5]; by = 3*lastY-6*coords[1]+3*coords[3]; cy = -3*lastY+3*coords[1]; dy = lastY; ax = -lastX+3*coords[0]-3*coords[2]+coords[4]; bx = 3*lastX-6*coords[0]+3*coords[2]; cx = -3*lastX+3*coords[0]; dx = lastX; data = new float[] {lastX, lastY, coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]}; } else { throw new RuntimeException("Unrecognized type: "+type); } realDistance = calculateDistance(ax,bx,cx,dx,ay,by,cy,dy,spacing); } } private float calculateDistance(double ax,double bx,double cx,double dx, double ay,double by,double cy,double dy,float spacing) { double x0 = dx; double y0 = dy; double x1, y1; double sum = 0; for(double t = spacing; t<1; t+=spacing) { x1 = ((ax*t+bx)*t+cx)*t+dx; y1 = ((ay*t+by)*t+cy)*t+dy; sum += Math.sqrt( (x0-x1)*(x0-x1)+(y0-y1)*(y0-y1) ); x0 = x1; y0 = y1; } return (float)sum; } } /** This is the increments t goes throw as each shape segment is * traversed. For quadratic and cubic curves, this affects * how the shape distance is measured. The default value is .05, * meaning quadratic and cubic curves are converted to linear segments * connecting at t = 0, t = .05, t = .1, ... t = .95, t = 1. */ public static final float DEFAULT_SPACING = .05f; Segment[] segments; float closedDistance = 0; float originalDistance; /** Construct a <code>MeasuredShape</code> from a <code>Shape</code>, * using the default spacing. * * @param s the shape data * @throws IllegalArgumentException if the shape has more than 1 path. */ public MeasuredShape(Shape s) { this(s.getPathIterator(null),DEFAULT_SPACING); } /** Construct a <code>MeasuredShape</code> from a <code>Shape</code>. * * @param s the shape data to create * @param spacing the value to increment t as each segment is traversed. * The default value is .05. * @throws IllegalArgumentException if the shape has more than 1 path. */ public MeasuredShape(Shape s,float spacing) { this(s.getPathIterator(null),spacing); } /** Construct a <code>MeasuredShape</code> from a <code>PathIterator</code> * using the default spacing. * * @param i the shape data to create * @throws IllegalArgumentException if the shape has more than 1 path. */ public MeasuredShape(PathIterator i) { this(i,DEFAULT_SPACING); } /** Construct a <code>MeasuredShape</code> from a <code>PathIterator</code>. * * @param i the shape data to create * @param spacing the value to increment t as each segment is traversed. * The default value is .05. * @throws IllegalArgumentException if the shape has more than 1 path. */ public MeasuredShape(PathIterator i,float spacing) { Vector<Segment> v = new Vector<Segment>(); float lastX = 0; float lastY = 0; float moveX = 0; float moveY = 0; int pathCount = 0; boolean closed = false; float[] coords = new float[6]; while(i.isDone()==false) { int k = i.currentSegment(coords); if(k==PathIterator.SEG_CLOSE) { closed = true; } else if(k==PathIterator.SEG_MOVETO) { if(pathCount==1) throw new IllegalArgumentException("this object can only contain 1 subpath"); moveX = coords[0]; moveY = coords[1]; lastX = moveX; lastY = moveY; pathCount++; } else if(k==PathIterator.SEG_LINETO || k==PathIterator.SEG_QUADTO || k==PathIterator.SEG_CUBICTO) { if(pathCount!=1) throw new IllegalArgumentException("this shape data did not begin with a moveTo"); Segment s = new Segment(k,lastX,lastY,coords,spacing); lastX = s.data[s.data.length-2]; lastY = s.data[s.data.length-1]; v.add(s); closedDistance += s.realDistance; } i.next(); } float t = closedDistance; if(v.size()>0) { Segment last = v.get(v.size()-1); if(Math.abs(last.data[last.data.length-2]-moveX)>.001 || Math.abs(last.data[last.data.length-1]-moveY)>.001) { coords[0] = moveX; coords[1] = moveY; Segment s = new Segment(PathIterator.SEG_LINETO,lastX,lastY,coords,spacing); v.add(s); closedDistance += s.realDistance; } } if(!closed) { originalDistance = t; } else { originalDistance = closedDistance; } segments = v.toArray(new Segment[v.size()]); //normalize everything: for(int a = 0; a<segments.length; a++) { segments[a].normalizedDistance = segments[a].realDistance/closedDistance; } } /** Writes the entire shape * @param w the destination to write to */ public void writeShape(PathWriter w) { w.moveTo(segments[0].getX(0), segments[0].getY(0) ); for(int a = 0; a<segments.length; a++) { segments[a].write(w,0,1); } w.closePath(); } /** The distance of this shape, assuming that the path is closed. * This will be greater than or equal to <code>getOriginalDistance()</code>. * * @see #getOriginalDistance() */ public float getClosedDistance() { return closedDistance; } /** The distance of the shape used to construct this * <code>MeasuredShape</code>. * <p>This will be less than or equal to <code>getClosedDistance()</code>. * * @return The distance this path covered when the shape was constructed. * * @see #getClosedDistance() */ public float getOriginalDistance() { return originalDistance; } /** Writes the entire shape backwards * @param w the destination to write to */ public void writeShapeBackwards(PathWriter w) { w.moveTo(segments[segments.length-1].getX(1), segments[segments.length-1].getY(1) ); for(int a = segments.length-1; a>=0; a--) { segments[a].write(w,1,0); } w.closePath(); } /** Returns the x-value of where this path begins. * <P>Because a <code>MeasuredShape</code> can only be one * path, there is only possible <code>moveTo()</code>. * * @return the x-value of where this path begins. */ public float getMoveToX() { Segment s = segments[0]; return s.getX(0); } /** Returns the y-value of where this path begins. * <P>Because a <code>MeasuredShape</code> can only be one * path, there is only possible <code>moveTo()</code>. * * @return the y-value of where this path begins. */ public float getMoveToY() { Segment s = segments[0]; return s.getY(0); } /** Trace the shape. * * @param position a fraction from zero to one indicating where to start tracing * @param length a fraction from negative one to one indicating how much to trace. * If this value is negative then the shape will be traced backwards. * @param w the destination to write to */ public void writeShape(float position,float length,PathWriter w) { writeShape(position,length,w,true); } /** Trace the shape. * * @param position a fraction from zero to one indicating where to start tracing * @param length a fraction from negative one to one indicating how much to trace. * If this value is negative then the shape will be traced backwards. * @param w the destination to write to * @param includeMoveTo this controls whether a moveTo is the first thing * written to the path. * Note setting this to <code>false</code> means its the caller's responsibility * to make sure the path is in the correct position. */ public void writeShape(float position,float length,PathWriter w,boolean includeMoveTo) { if(length>=.999999f) { writeShape(w); return; } else if(length<=-.999999f) { writeShapeBackwards(w); return; } else if(length<.000001 && length>-.000001) { return; } Position i1 = getIndexOfPosition(position); Position i2 = getIndexOfPosition(position+length); if(includeMoveTo) { w.moveTo(segments[i1.i].getX(i1.innerPosition), segments[i1.i].getY(i1.innerPosition)); } if(i1.i==i2.i && ((length>0 && i2.innerPosition>i1.innerPosition) || (length<0 && i2.innerPosition<i1.innerPosition) )) { segments[i1.i].write(w,i1.innerPosition,i2.innerPosition); } else { if(length>0) { segments[i1.i].write(w,i1.innerPosition,1); int i = i1.i+1; if(i>=segments.length) i = 0; while(i!=i2.i) { segments[i].write(w,0,1); i++; if(i>=segments.length) i = 0; } segments[i2.i].write(w,0,i2.innerPosition); } else { segments[i1.i].write(w,i1.innerPosition,0); int i = i1.i-1; if(i<0) i = segments.length-1; while(i!=i2.i) { segments[i].write(w,1,0); i--; if(i<0) i = segments.length-1; } segments[i2.i].write(w,1,i2.innerPosition); } } } /** Returns the point at a certain distance from the beginning of this shape. * * @param distance the distance from the beginning of this shape to measure * @param dest the destination to store the result in. (If this is null a new * Point2D will be constructed.) * @return the point at a certain distance from the beginning of this shape. * Note this will be <code>dest</code> if <code>dest</code> is non-null. */ public Point2D getPoint(float distance,Point2D dest) { if(distance<0) throw new IllegalArgumentException("distance ("+distance+") must not be negative"); if(distance>closedDistance) throw new IllegalArgumentException("distance ("+distance+") must not be greater than the total distance of this shape ("+closedDistance+")"); if(dest==null) dest = new Point2D.Float(); for(int a = 0; a<segments.length; a++) { float t = distance/segments[a].realDistance; if(t>=1) { distance = distance - segments[a].realDistance; } else { dest.setLocation(segments[a].getX(t),segments[a].getY(t)); return dest; } } dest.setLocation(segments[0].getX(0),segments[0].getY(0)); //a fluke case, where we're basically at the end of the shape return dest; } /** Returns the tangent slope at a certain distance from the beginning of this shape. * The behavior of this method when the point you request falls exactly on an edge * (that is, when two bordering segments don't have a continuous slope) is undefined. * * @param distance the distance from the beginning of this shape to measure * @return the tangent slope (in radians) at a specific position */ public float getTangentSlope(float distance) { if(distance<0) throw new IllegalArgumentException("distance ("+distance+") must not be negative"); if(distance>closedDistance) throw new IllegalArgumentException("distance ("+distance+") must not be greater than the total distance of this shape ("+closedDistance+")"); for(int a = 0; a<segments.length; a++) { float t = distance/segments[a].realDistance; if(t>=1) { distance = distance - segments[a].realDistance; } else { return segments[a].getTangentSlope(t); } } return segments[0].getTangentSlope(0); //a fluke case, where we're basically at the end of the shape } private static boolean equal(float f1,float f2) { float d = f1-f2; if(d<0) d = -d; return d<.0001; } /** Returns the length that this shape has in common with the argument. * This assumes the two shapes begin at the same point, and in the same * direction. * @param s */ public float getCommonDistance(MeasuredShape s) { float distance = 0; int m = Math.min(segments.length, s.segments.length); for(int a = 0; a<m; a++) { if(segments[a].type!=PathIterator.SEG_MOVETO && s.segments[a].type!=PathIterator.SEG_MOVETO) { if(equal(segments[a].data[0],s.segments[a].data[0]) && equal(segments[a].data[1],s.segments[a].data[1]) && equal(segments[a].data[segments[a].data.length-2], s.segments[a].data[s.segments[a].data.length-2]) && equal(segments[a].data[segments[a].data.length-1], s.segments[a].data[s.segments[a].data.length-1]) && equal(segments[a].realDistance, s.segments[a].realDistance)) { distance += segments[a].realDistance; } else { return distance; } } else if(segments[a].type==PathIterator.SEG_MOVETO && s.segments[a].type==PathIterator.SEG_MOVETO) { //skip } else { return distance; } } return distance; } /** Trace the shape. * * @param position a fraction from zero to one indicating where to start tracing * @param length a fraction from negative one to one indicating how much to trace. * If this value is negative then the shape will be traced backwards. * @return a new path */ public GeneralPath getShape(float position,float length) { GeneralPath dest = new GeneralPath(Path2D.WIND_NON_ZERO); PathWriter w = new GeneralPathWriter(dest); writeShape(position,length,w,true); return dest; } static class Position { int i; float innerPosition; public Position(int segmentIndex,float p) { this.i = segmentIndex; this.innerPosition = p; } @Override public String toString() { return "Position[ i="+i+" t="+innerPosition+"]"; } } private Position getIndexOfPosition(float p) { while(p<0) p+=1; while(p>1) p-=1; if(p>.99999f) p = 0; int i = 0; float original = p; while(i<segments.length) { if(p<=segments[i].normalizedDistance && segments[i].normalizedDistance!=0) { return new Position(i,p/segments[i].normalizedDistance); } p-=segments[i].normalizedDistance; i++; } System.err.println("p = "+p); throw new RuntimeException("the position "+original+" could not be found."); } }