1
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;
19 private static final double EPSILON = 1.0 * Math.pow(2, -MAXDEPTH-1);
20 private static final int DEGREE = 3;
21 private static final int W_DEGREE = 5;
22
23 private NearestPoint() {
24
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];
66 Point2D[] v = {
67 c.getP1(), c.getCtrlP1(), c.getCtrlP2(), c.getP2()
68 };
69
70
71 Point2D[] w = convertToBezierForm(v, pa);
72
73
74 int nSolutions = findRoots(w, W_DEGREE, tCandidate, 0);
75
76
77
78 double minDistance = pa.distanceSq(c.getP1());
79 double t = 0.0;
80
81
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
92 double distance = pa.distanceSq(c.getP2());
93 if (distance < minDistance) {
94 minDistance = distance;
95 t = 1.0;
96 }
97
98
99
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 : {
116 return 0;
117 }
118 case 1 : {
119
120
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
134
135 Point2D[] left = new Point2D.Double[W_DEGREE+1];
136 Point2D[] right = new Point2D.Double[W_DEGREE+1];
137 double[] leftT = new double[W_DEGREE+1];
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
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
153 return leftCount+rightCount;
154 }
155
156 private static final double[][] cubicZ = {
157
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];
172 Point2D[] d = new Point2D.Double[DEGREE];
173 double[][] cdTable = new double[3][4];
174 Point2D[] w = new Point2D.Double[W_DEGREE+1];
175
176
177
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
183
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
190
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
198
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
239
240
241
242
243 private static boolean controlPolygonFlatEnough(Point2D[] v, int degree) {
244
245
246
247
248
249
250
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];
257
258 for (int i = 1; i < degree; i++) {
259
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
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
283 double a1 = 0.0;
284 double b1 = 1.0;
285 double c1 = 0.0;
286
287
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
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
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
320
321
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
340 Point2D[][] p = new Point2D.Double[W_DEGREE+1][W_DEGREE+1];
341
342
343 for (int j=0; j <= degree; j++) {
344 p[0][j].setLocation(c[j]);
345 }
346
347
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 }