View Javadoc

1   // Copyright 2004, FreeHEP.
2   package hep.wired.heprep.util;
3   
4   import java.awt.geom.*;
5   
6   /***
7    * Calculates the nearest point on a straight line or on a cubic bezier curve.
8    *
9    * Calculations for the Bezier parts come from:
10   * "Solving the Nearest Point-on-Curve Problem" and "A Bezier Curve-Based Root-Finder"
11   * by Philip J. Schneider from "Graphics Gems", Academic Press, 1990.
12   *
13   * @author Mark Donszelmann
14   * @version $Id: NearestPoint.java 258 2004-06-08 06:27:49Z duns $
15   */ 
16  public class NearestPoint {
17      
18      private static final int MAXDEPTH = 64;                                 // Maximum depth for recursion
19      private static final double EPSILON  = 1.0 * Math.pow(2, -MAXDEPTH-1);  // Flatness control value
20      private static final int DEGREE = 3;                                    // Cubic Bezier curve
21      private static final int W_DEGREE = 5;                                  // Degree of eqn to find roots of
22  
23      private NearestPoint() {
24          // only static methods
25      }
26  
27      /***
28       * Returns the nearest point (pn) on line p1 - p2 nearest to point pa.
29       *
30       * @param p1 start point of line
31       * @param p2 end point of line
32       * @param pa arbitrary point
33       * @param pn nearest point (return param)
34       * @return distance squared between pa and nearest point (pn)
35       */ 
36      public static double onLine(Point2D p1, Point2D p2, Point2D pa, Point2D pn) {
37          double dx = p2.getX() - p1.getX();
38          double dy = p2.getY() - p1.getY();
39          double dsq = dx*dx + dy*dy;
40          if (dsq == 0) {
41              pn.setLocation(p1);
42          } else {
43              double u = ((pa.getX()-p1.getX())*dx + (pa.getY()-p1.getY())*dy)/dsq;
44              if (u <= 0) {
45                  pn.setLocation(p1);
46              } else if (u >= 1) {
47                  pn.setLocation(p2);
48              } else {
49                  pn.setLocation(p1.getX() + u*dx, p1.getY() + u*dy);
50              }
51          }
52          return pn.distanceSq(pa);
53      }
54  
55      /***
56       * Return the nearest point (pn) on cubic bezier curve c nearest to point pa.
57       *
58       * @param c cubice curve
59       * @param pa arbitrary point
60       * @param pn nearest point found (return param)
61       * @return distance squared between pa and nearest point (pn)
62       */    
63      public static double onCurve(CubicCurve2D c, Point2D pa, Point2D pn) {
64                                      
65          double[] tCandidate = new double[W_DEGREE];     // Possible roots
66          Point2D[] v = {
67              c.getP1(), c.getCtrlP1(), c.getCtrlP2(), c.getP2()
68          };
69  
70          // Convert problem to 5th-degree Bezier form
71          Point2D[] w = convertToBezierForm(v, pa);
72  
73          // Find all possible roots of 5th-degree equation
74          int nSolutions = findRoots(w, W_DEGREE, tCandidate, 0);
75  
76          // Compare distances of P5 to all candidates, and to t=0, and t=1
77          // Check distance to beginning of curve, where t = 0
78          double minDistance = pa.distanceSq(c.getP1());
79          double t = 0.0;
80  
81          // Find distances for candidate points
82          for (int i = 0; i < nSolutions; i++) {
83              Point2D p = bezier(v, DEGREE, tCandidate[i], null, null);
84              double distance = pa.distanceSq(p);
85              if (distance < minDistance) {
86                  minDistance = distance;
87                  t = tCandidate[i];
88              }
89          }
90  
91          // Finally, look at distance to end point, where t = 1.0
92          double distance = pa.distanceSq(c.getP2());
93          if (distance < minDistance) {
94              minDistance = distance;
95              t = 1.0;
96          }
97  
98  
99          //  Return the point on the curve at parameter value t
100         System.out.println("t: "+t);
101         pn.setLocation(bezier(v, DEGREE, t, null, null));
102 
103         return pn.distanceSq(pa);
104     }
105 
106     /***
107      *  FindRoots :
108      *  Given a 5th-degree equation in Bernstein-Bezier form, find
109      *  all of the roots in the interval [0, 1].  Return the number
110      *  of roots found.
111      */
112     private static int findRoots(Point2D[] w, int degree, double[] t, int depth) {  
113 
114         switch (crossingCount(w, degree)) {
115             case 0 : { // No solutions here
116                 return 0;   
117             }
118             case 1 : { // Unique solution
119                 // Stop recursion when the tree is deep enough
120                 // if deep enough, return 1 solution at midpoint
121                 if (depth >= MAXDEPTH) {
122                     t[0] = (w[0].getX() + w[W_DEGREE].getX()) / 2.0;
123                     return 1;
124                 }
125                 if (controlPolygonFlatEnough(w, degree)) {
126                     t[0] = computeXIntercept(w, degree);
127                     return 1;
128                 }
129                 break;
130             }
131         }
132 
133         // Otherwise, solve recursively after
134         // subdividing control polygon
135         Point2D[] left = new Point2D.Double[W_DEGREE+1];    // New left and right
136         Point2D[] right = new Point2D.Double[W_DEGREE+1];   // control polygons
137         double[] leftT = new double[W_DEGREE+1];            // Solutions from kids
138         double[] rightT = new double[W_DEGREE+1];
139         
140         bezier(w, degree, 0.5, left, right);
141         int leftCount  = findRoots(left,  degree, leftT, depth+1);
142         int rightCount = findRoots(right, degree, rightT, depth+1);
143     
144         // Gather solutions together
145         for (int i = 0; i < leftCount; i++) {
146             t[i] = leftT[i];
147         }
148         for (int i = 0; i < rightCount; i++) {
149             t[i+leftCount] = rightT[i];
150         }
151     
152         // Send back total number of solutions  */
153         return leftCount+rightCount;
154     }
155 
156     private static final double[][] cubicZ = {  
157         /* Precomputed "z" for cubics   */
158         {1.0, 0.6, 0.3, 0.1},
159         {0.4, 0.6, 0.6, 0.4},
160         {0.1, 0.3, 0.6, 1.0},
161     };
162 
163     /***
164      *  ConvertToBezierForm :
165      *      Given a point and a Bezier curve, generate a 5th-degree
166      *      Bezier-format equation whose solution finds the point on the
167      *      curve nearest the user-defined point.
168      */
169     private static Point2D[] convertToBezierForm(Point2D[] v, Point2D pa) {
170 
171         Point2D[]   c = new Point2D.Double[DEGREE+1];   // v(i) - pa
172         Point2D[]   d = new Point2D.Double[DEGREE];     // v(i+1) - v(i)
173         double[][]  cdTable = new double[3][4];         // Dot product of c, d
174         Point2D[]   w = new Point2D.Double[W_DEGREE+1]; // Ctl pts of 5th-degree curve
175 
176         // Determine the c's -- these are vectors created by subtracting
177         // point pa from each of the control points
178         for (int i = 0; i <= DEGREE; i++) {
179             c[i].setLocation(v[i].getX() - pa.getX(), v[i].getY() - pa.getY());
180         }
181 
182         // Determine the d's -- these are vectors created by subtracting
183         // each control point from the next
184         double s = 3;
185         for (int i = 0; i <= DEGREE - 1; i++) {       
186             d[i].setLocation(s * (v[i+1].getX() - v[i].getX()), s * (v[i+1].getY() - v[i].getY()));
187         }
188         
189         // Create the c,d table -- this is a table of dot products of the
190         // c's and d's                          */
191         for (int row = 0; row <= DEGREE - 1; row++) {
192             for (int column = 0; column <= DEGREE; column++) {
193                 cdTable[row][column] = (d[row].getX() * c[column].getX()) + (d[row].getY() * c[column].getY());
194             }
195         }
196 
197         // Now, apply the z's to the dot products, on the skew diagonal
198         // Also, set up the x-values, making these "points"
199         for (int i = 0; i <= W_DEGREE; i++) {
200             w[i].setLocation((double)(i) / W_DEGREE, 0.0);
201         }
202 
203         int n = DEGREE;
204         int m = DEGREE-1;
205         for (int k = 0; k <= n + m; k++) {
206             int lb = Math.max(0, k - m);
207             int ub = Math.min(k, n);
208             for (int i = lb; i <= ub; i++) {
209                 int j = k - i;
210                 w[i+j].setLocation(w[i+j].getX(), w[i+j].getY() + cdTable[j][i] * cubicZ[j][i]);
211             }
212         }
213 
214         return w;
215     }
216 
217     /***
218      * CrossingCount :
219      *  Count the number of times a Bezier control polygon 
220      *  crosses the 0-axis. This number is >= the number of roots.
221      *
222      */
223     private static int crossingCount(Point2D[] v, int degree) {
224         int nCrossings = 0;
225         int sign = v[0].getY() < 0 ? -1 : 1;
226         int oldSign = sign;
227         for (int i = 1; i <= degree; i++) {
228             sign = v[i].getY() < 0 ? -1 : 1;
229             if (sign != oldSign) nCrossings++;
230             oldSign = sign;
231         }
232         return nCrossings;
233     }
234     
235     
236     
237     /*
238      *  ControlPolygonFlatEnough :
239      *  Check if the control polygon of a Bezier curve is flat enough
240      *  for recursive subdivision to bottom out.
241      *
242      */
243     private static boolean controlPolygonFlatEnough(Point2D[] v, int degree) {
244 
245         // Find the  perpendicular distance
246         // from each interior control point to
247         // line connecting v[0] and v[degree]
248     
249         // Derive the implicit equation for line connecting first
250         // and last control points
251         double a = v[0].getY() - v[degree].getY();
252         double b = v[degree].getX() - v[0].getX();
253         double c = v[0].getX() * v[degree].getY() - v[degree].getX() * v[0].getY();
254     
255         double abSquared = (a * a) + (b * b);
256         double[] distance = new double[degree+1];      // Distances from pts to line
257     
258         for (int i = 1; i < degree; i++) {
259         // Compute distance from each of the points to that line
260             distance[i] = a * v[i].getX() + b * v[i].getY() + c;
261             if (distance[i] > 0.0) {
262                 distance[i] = (distance[i] * distance[i]) / abSquared;
263             }
264             if (distance[i] < 0.0) {
265                 distance[i] = -((distance[i] * distance[i]) / abSquared);
266             }
267         }
268     
269     
270         // Find the largest distance
271         double maxDistanceAbove = 0.0;
272         double maxDistanceBelow = 0.0;
273         for (int i = 1; i < degree; i++) {
274             if (distance[i] < 0.0) {
275                 maxDistanceBelow = Math.min(maxDistanceBelow, distance[i]);
276             }
277             if (distance[i] > 0.0) {
278                 maxDistanceAbove = Math.max(maxDistanceAbove, distance[i]);
279             }
280         }
281     
282         // Implicit equation for zero line
283         double a1 = 0.0;
284         double b1 = 1.0;
285         double c1 = 0.0;
286     
287         // Implicit equation for "above" line
288         double a2 = a;
289         double b2 = b;
290         double c2 = c + maxDistanceAbove;
291     
292         double det = a1 * b2 - a2 * b1;
293         double dInv = 1.0/det;
294         
295         double intercept1 = (b1 * c2 - b2 * c1) * dInv;
296     
297         //  Implicit equation for "below" line
298         a2 = a;
299         b2 = b;
300         c2 = c + maxDistanceBelow;
301         
302         det = a1 * b2 - a2 * b1;
303         dInv = 1.0/det;
304         
305         double intercept2 = (b1 * c2 - b2 * c1) * dInv;
306     
307         // Compute intercepts of bounding box
308         double leftIntercept = Math.min(intercept1, intercept2);
309         double rightIntercept = Math.max(intercept1, intercept2);
310     
311         double error = 0.5 * (rightIntercept-leftIntercept);    
312         
313         return error < EPSILON;
314     }
315     
316     
317     
318     /*
319      *  ComputeXIntercept :
320      *  Compute intersection of chord from first control point to last
321      *      with 0-axis.
322      * 
323      */
324     private static double computeXIntercept(Point2D[] v, int degree) {
325     
326         double XNM = v[degree].getX() - v[0].getX();
327         double YNM = v[degree].getY() - v[0].getY();
328         double XMK = v[0].getX();
329         double YMK = v[0].getY();
330     
331         double detInv = - 1.0/YNM;
332     
333         return (XNM*YMK - YNM*XMK) * detInv;
334     }
335     
336     
337 
338     private static Point2D bezier(Point2D[] c, int degree, double t, Point2D[] left, Point2D[] right) {
339         // FIXME WIRED-252, move outside the method and make static
340         Point2D[][] p = new Point2D.Double[W_DEGREE+1][W_DEGREE+1];
341         
342         /* Copy control points  */
343         for (int j=0; j <= degree; j++) {
344             p[0][j].setLocation(c[j]);
345         }
346             
347         /* Triangle computation */
348         for (int i = 1; i <= degree; i++) {  
349             for (int j = 0 ; j <= degree - i; j++) {
350                 p[i][j].setLocation(
351                     (1.0 - t) * p[i-1][j].getX() + t * p[i-1][j+1].getX(),
352                     (1.0 - t) * p[i-1][j].getY() + t * p[i-1][j+1].getY()
353                 );
354             }
355         }
356         
357         if (left != null) {
358             for (int j = 0; j <= degree; j++) {
359                 left[j]  = p[j][0];
360             }
361         }
362         
363         if (right != null) {
364             for (int j = 0; j <= degree; j++) {
365                 right[j] = p[degree-j][j];
366             }
367         }
368         
369         return p[degree][0];
370     }
371     
372     /***
373      * Test for onCurve
374      */
375     public static void main(String[] args) {
376         Point2D pn = new Point2D.Double();
377         CubicCurve2D c = new CubicCurve2D.Double(0, 0, 1, 2, 3, 3, 4, 2);
378         Point2D pa = new Point2D.Double(3.5, 2.0);
379         double distSq = onCurve(c, pa, pn);
380         System.out.println("Point On Curve is "+pn);
381     }
382 }