001/* 002 * GeomUtil -- Static utility methods used by the geometry package. 003 * 004 * Copyright (C) 2000-2025, by Joseph A. Huwaldt 005 * All rights reserved. 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Library General Public License for more details. 016 * 017 * You should have received a copy of the GNU Lesser General Public License 018 * along with this program; if not, write to the Free Software 019 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 020 * Or visit: http://www.gnu.org/licenses/lgpl.html 021 */ 022package geomss.geom; 023 024import jahuwaldt.js.param.Parameter; 025import jahuwaldt.tools.math.MathTools; 026import java.text.MessageFormat; 027import java.util.ArrayList; 028import java.util.Collection; 029import java.util.List; 030import static java.util.Objects.isNull; 031import static java.util.Objects.nonNull; 032import static java.util.Objects.requireNonNull; 033import java.util.ResourceBundle; 034import javax.measure.converter.UnitConverter; 035import javax.measure.quantity.Angle; 036import javax.measure.quantity.Dimensionless; 037import javax.measure.quantity.Length; 038import javax.measure.unit.NonSI; 039import javax.measure.unit.SI; 040import javax.measure.unit.Unit; 041import javolution.context.ConcurrentContext; 042import javolution.context.ImmortalContext; 043import javolution.context.StackContext; 044import static javolution.lang.MathLib.PI; 045import static javolution.lang.MathLib.TWO_PI; 046import static javolution.lang.MathLib.abs; 047import static javolution.lang.MathLib.sqrt; 048import javolution.util.FastTable; 049import org.apache.commons.math3.geometry.euclidean.twod.Vector2D; 050import org.apache.commons.math3.geometry.euclidean.twod.hull.AklToussaintHeuristic; 051import org.apache.commons.math3.geometry.euclidean.twod.hull.MonotoneChain; 052import org.jscience.mathematics.vector.Float64Matrix; 053import org.jscience.mathematics.vector.Float64Vector; 054 055/** 056 * A collection of static methods used by classes in the geometry package. 057 * 058 * <p> Modified by: Joseph A. Huwaldt </p> 059 * 060 * @author Joseph A. Huwaldt, Date: March 31, 2009 061 * @version February 17, 2025 062 */ 063public final class GeomUtil { 064 065 /** 066 * The resource bundle for this package. 067 */ 068 private static final ResourceBundle RESOURCES = AbstractGeomElement.RESOURCES; 069 070 /** 071 * The machine epsilon or unit roundoff for <code>double</code> in the Java 072 * environment. Machine epsilon gives an upper bound on the relative error due to 073 * rounding in floating point arithmetic. 074 */ 075 public static final double EPS = MathTools.EPS; 076 077 /** 078 * The square-root of EPS. 079 * 080 * @see #EPS 081 */ 082 public static final double SQRT_EPS = MathTools.SQRT_EPS; 083 084 // A default geometry tolerance. 085 private static final Parameter<Length> GTOL; 086 087 static { 088 ImmortalContext.enter(); 089 try { 090 GTOL = Parameter.valueOf(1e-8, SI.METER); 091 } finally { 092 ImmortalContext.exit(); 093 } 094 } 095 096 /** 097 * Compute the average of an array of points (returning the mid-point between them all). 098 * 099 * @param pnts The array of points to average. There must be at least one point. 100 * @return The average of the input points. 101 */ 102 public static Point averagePoints(GeomPoint... pnts) { 103 StackContext.enter(); 104 try { 105 106 Point pavg = pnts[0].immutable(); 107 int numPnts = pnts.length; 108 for (int i=1; i < numPnts; ++i) { 109 pavg = pavg.plus(pnts[i]); 110 } 111 pavg = pavg.divide(numPnts); 112 return StackContext.outerCopy(pavg); 113 114 } finally { 115 StackContext.exit(); 116 } 117 } 118 119 /** 120 * Compute the average of a list of points (returning the mid-point between them all). 121 * 122 * @param pnts The list of points to average. There must be at least one point. 123 * @return The average of the input points. 124 */ 125 public static Point averagePoints(List<? extends GeomPoint> pnts) { 126 StackContext.enter(); 127 try { 128 129 Point avg = pnts.get(0).immutable(); 130 int numPnts = pnts.size(); 131 for (int i=1; i < numPnts; ++i) { 132 avg = avg.plus(pnts.get(i)); 133 } 134 avg = avg.divide(numPnts); 135 return StackContext.outerCopy(avg); 136 137 } finally { 138 StackContext.exit(); 139 } 140 } 141 142 /** 143 * Returns the total transformation represented by an entire chain of GeomTransform 144 * objects below the one provided. 145 * 146 * @param tElem The transformed geometry element to have the total TM returned for. 147 * May not be null. 148 * @return The transformation matrix (TM) that represents the entire chain of 149 * transformations contained in a nested set of transformed geometry elements. 150 */ 151 public static GTransform getTotalTransform(GeomTransform tElem) { 152 StackContext.enter(); 153 try { 154 155 GTransform T = tElem.getTransform(); 156 while (true) { 157 Transformable elem = tElem.getChild(); 158 if (!(elem instanceof GeomTransform)) 159 break; 160 tElem = (GeomTransform)elem; 161 T = T.times(tElem.getTransform()); 162 } 163 return StackContext.outerCopy(T); 164 165 } finally { 166 StackContext.exit(); 167 } 168 } 169 170 /** 171 * Returns the input angle (in radians) bounded to the range 0 - 2*PI. 172 * 173 * @param angle The angle (in radians) to be bounded. 174 * @return The input angle bounded to the range 0 to 2*PI (radians). 175 */ 176 public static double bound2Pi(double angle) { 177 while (angle > TWO_PI) 178 angle -= TWO_PI; 179 while (angle < 0) 180 angle += TWO_PI; 181 return angle; 182 } 183 184 /** 185 * Returns the input angle bounded to the range 0 - 2*PI. 186 * 187 * @param angle The angle to be bounded. May not be null. 188 * @return The input angle bounded to the range 0 to 2*PI. 189 */ 190 public static Parameter<Angle> bound2Pi(Parameter<Angle> angle) { 191 StackContext.enter(); 192 try { 193 194 while (angle.isGreaterThan(Parameter.TWOPI_ANGLE)) { 195 angle = angle.minus(Parameter.TWOPI_ANGLE); 196 } 197 while (angle.isLessThan(Parameter.ZERO_ANGLE)) { 198 angle = angle.plus(Parameter.TWOPI_ANGLE); 199 } 200 return StackContext.outerCopy(angle); 201 202 } finally { 203 StackContext.exit(); 204 } 205 } 206 207 /** 208 * Returns the input angle (in radians) bounded to the range +/-PI. 209 * 210 * @param angle The angle (in radians) to be bounded. 211 * @return The input angle bounded to the range -PI to +PI (radians). 212 */ 213 public static double boundPi(double angle) { 214 while (angle > PI) 215 angle -= TWO_PI; 216 while (angle < -PI) 217 angle += TWO_PI; 218 return angle; 219 } 220 221 /** 222 * Returns the input angle bounded to the range +/-PI (+/- 180 deg). 223 * 224 * @param angle The angle to be bounded. May not be null. 225 * @return The input angle bounded to the range -PI to +PI. 226 */ 227 public static Parameter<Angle> boundPi(Parameter<Angle> angle) { 228 StackContext.enter(); 229 try { 230 231 while (angle.isGreaterThan(Parameter.PI_ANGLE)) { 232 angle = angle.minus(Parameter.TWOPI_ANGLE); 233 } 234 Parameter NEG_PI_ANGLE = Parameter.PI_ANGLE.opposite(); 235 while (angle.isLessThan(NEG_PI_ANGLE)) { 236 angle = angle.plus(Parameter.TWOPI_ANGLE); 237 } 238 return StackContext.outerCopy(angle); 239 240 } finally { 241 StackContext.exit(); 242 } 243 } 244 245 /** 246 * Return the maximum physical dimension among a collection of geometry elements. 247 * 248 * @param elements The array of geometry elements to query. May not be null. 249 * @return The maximum physical dimension among the input list of geometry elements. 250 */ 251 public static int maxPhyDimension(GeomElement... elements) { 252 int numDims = 0; 253 int size = elements.length; 254 for (int i = size - 1; i >= 0; --i) 255 numDims = Math.max(numDims, elements[i].getPhyDimension()); 256 return numDims; 257 } 258 259 /** 260 * Return the maximum physical dimension among a collection of geometry elements. 261 * 262 * @param elements The collection of geometry elements to query. May not be null. 263 * @return The maximum physical dimension among the input list of geometry elements. 264 */ 265 public static int maxPhyDimension(Collection<GeomElement> elements) { 266 int numDims = 0; 267 for (GeomElement item : elements) 268 numDims = Math.max(numDims, item.getPhyDimension()); 269 return numDims; 270 } 271 272 /** 273 * Return a unit vector, of <code>nDims</code> dimensions, in the X (dimension = 0) 274 * direction. 275 * 276 * @param numDims The number of dimensions for the unit vector. 277 * @return A unit vector, with the given dimensions, in the "X" direction. 278 */ 279 public static Vector<Dimensionless> makeXVector(int numDims) { 280 FastTable<Parameter<Dimensionless>> tmpList = FastTable.newInstance(); 281 282 tmpList.add(Parameter.ONE); 283 for (int i = 1; i < numDims; ++i) 284 tmpList.add(Parameter.ZERO); 285 Vector<Dimensionless> V = Vector.valueOf(tmpList); 286 287 FastTable.recycle(tmpList); 288 return V; 289 } 290 291 /** 292 * Return a unit vector, of <code>nDims</code> dimensions, in the Y (dimension = 1) 293 * direction. If the number of dimensions is less than two, this will return null. 294 * 295 * @param numDims The number of dimensions for the unit vector. 296 * @return A unit vector, with the given dimensions, in the "Y" direction. 297 */ 298 public static Vector<Dimensionless> makeYVector(int numDims) { 299 if (numDims < 2) 300 return null; 301 302 FastTable<Parameter<Dimensionless>> tmpList = FastTable.newInstance(); 303 304 for (int i = 0; i < numDims; ++i) 305 tmpList.add(Parameter.ZERO); 306 tmpList.set(1, Parameter.ONE); 307 Vector<Dimensionless> V = Vector.valueOf(tmpList); 308 309 FastTable.recycle(tmpList); 310 return V; 311 } 312 313 /** 314 * Return a unit vector, of <code>nDims</code> dimensions, in the Z (dimension = 2) 315 * direction. If the number of dimensions is less than three, then this will return 316 * null. 317 * 318 * @param numDims The number of dimensions for the unit vector. 319 * @return A unit vector, with the given dimensions, in the "Z" direction. 320 */ 321 public static Vector<Dimensionless> makeZVector(int numDims) { 322 if (numDims < 3) 323 return null; 324 325 FastTable<Parameter<Dimensionless>> tmpList = FastTable.newInstance(); 326 327 for (int i = 0; i < numDims; ++i) 328 tmpList.add(Parameter.ZERO); 329 tmpList.set(2, Parameter.ONE); 330 Vector<Dimensionless> V = Vector.valueOf(tmpList); 331 332 FastTable.recycle(tmpList); 333 return V; 334 } 335 336 /** 337 * Return the yhat vector (a Y axis orthogonal to the specified plane normal). The 338 * returned vector will have the origin set to zero. 339 * 340 * @param n A unit plane normal vector for the plane containing the new axis. Pass 341 * <code>null</code> for 2D geometry. 342 * @return A unit vector orthogonal to the specified plane normal vector. 343 */ 344 public static Vector<Dimensionless> calcYHat(GeomVector<Dimensionless> n) { 345 StackContext.enter(); 346 try { 347 Vector<Dimensionless> yhat; 348 349 if (isNull(n)) { 350 // A 2D curve must be in the 2D plane. 351 yhat = Vector.valueOf(0, 1); 352 353 } else { 354 int numDims = n.getPhyDimension(); 355 356 // Create a yhat axis that is orthogonal to the normal vector. 357 if (MathTools.isApproxEqual(n.getValue(0), 1)) // n.getValue(0) == 1.0 358 // Normal vector is along the X axis (parallel to X). 359 yhat = GeomUtil.makeYVector(numDims); 360 361 else if (MathTools.isApproxEqual(n.getValue(0), -1)) // n.getValue(0) == -1.0 362 yhat = GeomUtil.makeYVector(numDims).opposite(); 363 364 else { 365 Vector<Dimensionless> xV = GeomUtil.makeXVector(numDims); // Create a vector along the X axis. 366 if (n.getValue(2) < 0) 367 yhat = xV.cross(n).toUnitVector(); 368 else { 369 yhat = n.cross(xV).toUnitVector(); 370 yhat.setOrigin(Point.newInstance(numDims)); 371 } 372 } 373 } 374 375 return StackContext.outerCopy(yhat); 376 377 } finally { 378 StackContext.exit(); 379 } 380 } 381 382 /** 383 * Return the xhat vector (an X axis orthogonal to the specified normal and Y-axis 384 * vectors). 385 * 386 * @param n A unit plane normal vector for the plane containing the new axis. Pass 387 * null for 2D geometry. 388 * @param yhat The unit direction of the Y axis in the plane containing the new axis. 389 * @return A unit vector orthogonal to the specified normal and Y-axis vectors. 390 */ 391 public static Vector<Dimensionless> calcXHat(GeomVector<Dimensionless> n, 392 GeomVector<Dimensionless> yhat) { 393 Vector<Dimensionless> xhat; 394 395 if (isNull(n)) 396 // A 2D axis must be in the 2D plane. 397 xhat = Vector.valueOf(1, 0); 398 else 399 // Create an xhat axis that is orthogonal to yhat and the normal vector. 400 xhat = (Vector<Dimensionless>)yhat.cross(n); 401 402 return xhat; 403 } 404 405 /** 406 * Return a vector that is the in-plane normal to the input 2D vector. 407 * 408 * @param u The 2D vector that the normal vector is to be determined for. 409 * @return The 2D vector normal of the input 2D vector. 410 */ 411 public static Vector normal2D(GeomVector u) { 412 if (u.getPhyDimension() != 2) 413 throw new IllegalArgumentException("Input vector must be 2D."); 414 415 Vector output = Vector.valueOf(u.get(Point.Y).opposite(), u.get(Point.X)); 416 417 return output; 418 } 419 420 /** 421 * Return the vector or cross product area value for two vectors. The magnitude of the 422 * vector or cross product results in a scalar that represents the the area of a 423 * parallelogram with the vectors for sides. This is also the magnitude of the 424 * determinant of the two vectors. For 2D vectors, a signed area will be returned with 425 * positive area indicating a normal vector that is pointing "out of the page" and a 426 * negative area indicating a normal vector pointing "into the page". 427 * 428 * @param v1 The 1st vector to be multiplied. May not be null. 429 * @param v2 The 2nd vector multiplier. May not be null. 430 * @return The vector or cross product between the input vectors (v1 X v2) as a scalar 431 * area. 432 */ 433 public static Parameter crossArea(GeomVector v1, GeomVector v2) { 434 // Convert to highest dimension between input vectors. 435 int numDims = Math.max(v1.getPhyDimension(), v2.getPhyDimension()); 436 v1 = v1.toDimension(numDims); 437 v2 = v2.toDimension(numDims); 438 439 if (numDims < 3) { 440 // Do a 2D cross product: area = (v1.X*v2.Y) - (v1.Y*v2.X); 441 Parameter area = v1.get(Vector.X).times(v2.get(Vector.Y)).minus(v1.get(Vector.Y).times(v2.get(Vector.X))); 442 return area; 443 } 444 445 // Do a regular cross product and extract the area. 446 GeomVector n = v1.cross(v2); 447 return n.norm(); 448 } 449 450 /** 451 * Finds the shortest distance from a point to an infinite line. 452 * 453 * @param p The point being compared to the line to find the shortest distance. May 454 * not be null. 455 * @param a A point on the line. May not be null. 456 * @param u The unit direction vector for the line. May not be null. 457 * @return The distance from the point to the line. 458 */ 459 public static Parameter<Length> pointLineDistance(GeomPoint p, GeomPoint a, GeomVector<Dimensionless> u) { 460 // Reference: 461 // http://softsurfer.com/Archive/algorithm_0102/algorithm_0102.htm#Distance%20to%202-Point%20Line 462 463 int numDims = maxPhyDimension(p, a, u); 464 if (numDims < 3) 465 numDims = 3; 466 467 StackContext.enter(); 468 try { 469 // Convert all the points to the highest dimension between them. 470 p = p.toDimension(numDims); 471 a = a.toDimension(numDims); 472 u = u.toDimension(numDims); 473 474 // Form a vector from the point on the line to the target point. 475 Vector<Length> w = p.minus(a).toGeomVector(); 476 477 // Cross-product of unit vector with point-line vector gives vector with magnitude that is distance. 478 GeomVector<Length> uxw = (GeomVector<Length>)u.cross(w); 479 Parameter<Length> mag = uxw.mag(); 480 481 return StackContext.outerCopy(mag); 482 483 } finally { 484 StackContext.exit(); 485 } 486 } 487 488 /** 489 * Finds the shortest distance from a point to an finite line segment. 490 * 491 * @param p The point being compared to the segment to find the shortest distance. 492 * May not be null. 493 * @param p0 The start of the line segment. May not be null. 494 * @param p1 The end of the line segment. May not be null. 495 * @return The shortest distance from the point to the line segment. 496 */ 497 public static Parameter<Length> pointLineSegDistance(GeomPoint p, GeomPoint p0, GeomPoint p1) { 498 // Reference: 499 // http://www.softsurfer.com/Archive/algorithm_0102/algorithm_0102.htm#Distance%20to%20Ray%20or%20Segment 500 501 int numDims = maxPhyDimension(p, p0, p1); 502 503 StackContext.enter(); 504 try { 505 // Convert all the points to the highest dimension between them. 506 p = p.toDimension(numDims); 507 p0 = p0.toDimension(numDims); 508 p1 = p1.toDimension(numDims); 509 510 // Form a vector along the line. 511 Vector<Length> v = p1.minus(p0).toGeomVector(); 512 513 // Form a vector from the start of the line to the target point. 514 Vector<Length> w = p.minus(p0).toGeomVector(); 515 516 // Projection of w onto v. 517 Parameter c1 = w.dot(v); 518 519 if (c1.getValue() <= 0) 520 // Closest to start. 521 return StackContext.outerCopy(p0.distance(p)); 522 523 // Magnitude of v squared. 524 Parameter c2 = v.dot(v); 525 if (c2.compareTo(c1) <= 0) 526 // Closest to the end. 527 return StackContext.outerCopy(p1.distance(p)); 528 529 Parameter<Dimensionless> b = c1.divide(c2); 530 Point pb = p0.plus(Point.valueOf(v.times(b))); 531 532 return StackContext.outerCopy(p.distance(pb)); 533 534 } finally { 535 StackContext.exit(); 536 } 537 } 538 539 /** 540 * Finds the point on an infinite line that is closest to the specified point. 541 * 542 * @param p The point being compared to the line to find the closest point on the 543 * line. May not be null. 544 * @param a A point on the line. May not be null. 545 * @param u The unit direction vector for the line. May not be null. 546 * @return The point on the line closest to the point "p". 547 */ 548 public static Point pointLineClosest(GeomPoint p, GeomPoint a, GeomVector<Dimensionless> u) { 549 550 int numDims = maxPhyDimension(p, a, u); 551 if (numDims < 2) 552 numDims = 2; 553 554 StackContext.enter(); 555 try { 556 // Convert all the points to the highest dimension between them. 557 p = p.toDimension(numDims); 558 a = a.toDimension(numDims); 559 u = u.toDimension(numDims); 560 561 // Form a vector from the point on the line to the target point. 562 Vector<Length> w = p.minus(a).toGeomVector(); 563 564 // Calculate vector from the end of the line to the closest point. 565 // Project w onto u. 566 GeomVector<Length> cpV = (GeomVector<Length>)u.times(u.dot(w)); 567 568 // Calculate the end point of the cpV vector. 569 Point pOut = Point.valueOf(cpV).plus(a); 570 571 return StackContext.outerCopy(pOut); 572 573 } finally { 574 StackContext.exit(); 575 } 576 } 577 578 /** 579 * Finds the shortest signed distance from a point to an infinite plane. 580 * 581 * @param p The point being compared to the plane to find the closest point on the 582 * plane. May not be null. 583 * @param a Any point in the plane. May not be null. 584 * @param n The unit normal vector for the plane. May not be null. 585 * @return The signed shortest distance from the input point "p" to the plane. The 586 * distance is positive for points on the side of the plane pointed to by the 587 * normal vector "n", and negative for points on the other side of the plane. 588 * @throws DimensionException if input normal vector physical dimension is not at 589 * least 3 dimensional. 590 */ 591 public static Parameter<Length> pointPlaneDistance(GeomPoint p, GeomPoint a, GeomVector<Dimensionless> n) throws DimensionException { 592 requireNonNull(p); 593 requireNonNull(a); 594 595 // Convert all the points to the highest dimension between them. 596 int numDims = n.getPhyDimension(); 597 if (numDims < 3) 598 throw new DimensionException( 599 MessageFormat.format(RESOURCES.getString("dimensionNotAtLeast3"), 600 "input normal vector", numDims)); 601 602 numDims = Math.max(numDims, Math.max(a.getPhyDimension(), p.getPhyDimension())); 603 604 StackContext.enter(); 605 try { 606 p = p.toDimension(numDims); 607 a = a.toDimension(numDims); 608 n = n.toDimension(numDims); 609 610 // Vector from point in plane to input point. 611 Vector<Length> v = p.minus(a).toGeomVector(); 612 613 // The signed distance is projection of v onto the normal vector. 614 Parameter<Length> dist = (Parameter)v.dot(n); 615 616 return StackContext.outerCopy(dist); 617 618 } finally { 619 StackContext.exit(); 620 } 621 } 622 623 /** 624 * Finds the point on an infinite plane that is closest to the specified point. The 625 * distance to the plane is simply: 626 * <code>p.minus(pointPlaneClosest(p,...)).norm();</code>. 627 * 628 * @param p The point being compared to the plane to find the closest point on the 629 * plane. May not be null. 630 * @param a Any point in the plane. May not be null. 631 * @param n The unit normal vector for the plane. May not be null. 632 * @return The point on the plane closest to the point "p". May return the input point 633 * "p" if it is in the plane. 634 * @throws DimensionException if input normal vector physical dimension is not at 635 * least 3 dimensional. 636 */ 637 public static Point pointPlaneClosest(GeomPoint p, GeomPoint a, GeomVector<Dimensionless> n) throws DimensionException { 638 requireNonNull(p); 639 requireNonNull(a); 640 641 // Convert all the points to the highest dimension between them. 642 int numDims = n.getPhyDimension(); 643 if (numDims < 3) 644 throw new DimensionException( 645 MessageFormat.format(RESOURCES.getString("dimensionNotAtLeast3"), 646 "input normal vector", numDims)); 647 648 numDims = Math.max(numDims, Math.max(a.getPhyDimension(), p.getPhyDimension())); 649 650 StackContext.enter(); 651 try { 652 p = p.toDimension(numDims); 653 a = a.toDimension(numDims); 654 n = n.toDimension(numDims); 655 656 // Find the signed distance to the plane from the point. 657 Parameter dist = pointPlaneDistance(p, a, n); 658 659 // If distance is zero, then the input point is already in the plane. 660 if (abs(dist.getValue()) <= MathTools.SQRT_EPS) 661 return StackContext.outerCopy(p.immutable()); 662 663 // dv is parallel to normal vector and "dist" long. 664 GeomVector dv = n.times(dist); 665 666 // Subtract dv from the input point to get point projected onto the plane. 667 Point projPoint = p.minus(Point.valueOf(dv)); 668 669 return StackContext.outerCopy(projPoint); 670 671 } finally { 672 StackContext.exit(); 673 } 674 } 675 676 /** 677 * Find a circle that passes through the supplied (not co-linear) points. 678 * 679 * @param p1 The 1st of the points that define the circle (must not be colinear with 680 * the other two). May not be null. 681 * @param p2 The 2nd of the points that define the circle (must not be colinear with 682 * the other two). May not be null. 683 * @param p3 The 3rd of the points that define the circle (must not be colinear with 684 * the other two). May not be null. 685 * @return A CircleInfo record containing the geometry of the circle. The "xhat" 686 * parameter points to the first point supplied. The "yhat" parameter 687 * indicates the direction to the next point. The "angle" parameter gives the 688 * angle swept out from the 1st point to the 3rd point. 689 * @throws DimensionException if one of the 3 points does not have at least 2 physical 690 * dimensions. 691 * @throws IllegalArgumentException if the input points are co-linear. 692 */ 693 public static CircleInfo threePointCircle(GeomPoint p1, GeomPoint p2, GeomPoint p3) throws DimensionException { 694 // Reference: http://en.wikipedia.org/wiki/Circumscribed_circle#Barycentric_coordinates_as_a_function_of_the_side_lengths 695 // Also: http://stackoverflow.com/a/13992781 696 697 // Convert all the points to the highest dimension between them. 698 int numDims = maxPhyDimension(p1, p2, p3); 699 if (numDims < 2) 700 throw new DimensionException( 701 MessageFormat.format(RESOURCES.getString("dimensionNotAtLeast2"), "input points", numDims)); 702 703 // Up-cast the geometry to 3D if necessary to make some of the math easier. 704 int saveDims = numDims; 705 if (numDims < 3) 706 numDims = 3; 707 708 Parameter<Length> radius; 709 Point Pc; 710 Vector<Dimensionless> xhat, yhat; 711 Parameter<Angle> angle; 712 StackContext.enter(); 713 try { 714 p1 = p1.toDimension(numDims); 715 p2 = p2.toDimension(numDims); 716 p3 = p3.toDimension(numDims); 717 Unit<Length> unit = p1.getUnit(); 718 719 // Calculate edges of the triangle being circumscribed. 720 Vector<Length> t = p2.minus(p1).toGeomVector(); 721 Vector<Length> u = p3.minus(p1).toGeomVector(); 722 Vector<Length> v = p3.minus(p2).toGeomVector(); 723 724 // Are the points colinear? 725 double tDOTv = t.dot(v).getValue() / (t.normValue() * v.normValue()); 726 if (abs(tDOTv) > 1.0 - 1.e-6) 727 throw new IllegalArgumentException(RESOURCES.getString("pointsColinearErr")); 728 729 // Compute the triangle normal. 730 Vector w = t.cross(u); 731 Parameter wmag = w.mag(); 732 Vector<Dimensionless> nhat = w.toUnitVector(); 733 734 // Compute some dot products we will need. 735 Parameter tt = t.dot(t); 736 Parameter uu = u.dot(u); 737 738 // Compute the circle radius: r = sqrt(tt*uu*(v*v))/(2*wmag) 739 radius = tt.times(uu).times(v.dot(v)).sqrt().divide(2).divide(wmag).to(unit); 740 741 // Compute the circle center: Pc = p1 + (u*tt*(u*v) - t*uu*(t*v))/(2*wmag^2) 742 Parameter wmag2 = wmag.pow(2); 743 Parameter uv = u.dot(v); 744 Parameter tv = t.dot(v); 745 Vector<Length> Pcv = u.times(tt).times(uv).minus((Vector)t.times(uu).times(tv)).divide(2).divide(wmag2).to(unit); 746 Pc = p1.plus(Point.valueOf(Pcv)); 747 748 // The xhat vector is the vector from the center to the 1st point. 749 xhat = p1.minus(Pc).toGeomVector().toUnitVector(); 750 xhat.setOrigin(Point.newInstance(numDims)); 751 752 // The yhat vector is perpendicular to the xhat and normal vectors. 753 yhat = nhat.cross(xhat).toUnitVector(); 754 yhat.setOrigin(Point.newInstance(numDims)); 755 756 // Calculate the angle between the 1st and last points. 757 Vector<Dimensionless> p2V = p2.minus(Pc).toGeomVector().toUnitVector(); 758 Parameter dot = xhat.dot(p2V); 759 angle = Parameter.acos(dot); 760 Vector<Dimensionless> p3V = p3.minus(Pc).toGeomVector().toUnitVector(); 761 dot = p2V.dot(p3V); 762 angle = angle.plus(Parameter.acos(dot)); 763 764 if (saveDims == 2) { 765 // Convert back to 2D geometry on output. 766 Pc = Pc.toDimension(2); 767 xhat = xhat.toDimension(2); 768 yhat = yhat.toDimension(2); 769 } 770 771 // Copy the things we need out of the stack context. 772 radius = StackContext.outerCopy(radius); 773 Pc = StackContext.outerCopy(Pc); 774 xhat = StackContext.outerCopy(xhat); 775 yhat = StackContext.outerCopy(yhat); 776 angle = StackContext.outerCopy(angle); 777 778 } finally { 779 StackContext.exit(); 780 } 781 782 // Create and return a circle record. 783 CircleInfo circle = new CircleInfo(); 784 circle.radius = radius; 785 circle.center = Pc; 786 circle.xhat = xhat; 787 circle.yhat = yhat; 788 circle.angle = angle; 789 790 return circle; 791 } 792 793 /** 794 * Finds a circle that is approximately centered on the specified origin point and 795 * passes through the two input points. The origin point is used to determine the 796 * plane that the circle lies in and is used to approximate the center of the circle. 797 * The true origin/center of the circle is calculated to ensure that the circle passes 798 * through the supplied edge points. 799 * 800 * @param o Approximate origin or center to create the full-circle around. May not be 801 * null. 802 * @param p1 The 1st of the points that define the circle (must not be colinear with 803 * the other two). May not be null. 804 * @param p2 The 2nd of the points that define the circle (must not be colinear with 805 * the other two). May not be null. 806 * @return A CircleInfo record containing the geometry of the circle. THe "xhat" 807 * parameter points to the first point supplied. The "yhat" parameter 808 * indicates the direction to the 2nd point. The "angle" parameter gives the 809 * angle swept out from the 1st point to the 2nd point. 810 * @throws DimensionException if the origin or 2 points do not have at least 2 811 * physical dimensions. 812 */ 813 public static CircleInfo centerTwoPointCircle(GeomPoint o, GeomPoint p1, GeomPoint p2) throws DimensionException { 814 815 // Convert all the points to the highest dimension between them. 816 int numDims = maxPhyDimension(o, p1, p2); 817 if (numDims < 2) 818 throw new DimensionException( 819 MessageFormat.format(RESOURCES.getString("dimensionNotAtLeast2"), "input points", numDims)); 820 821 // Up-cast the geometry to 3D if necessary to make some of the math easier. 822 int saveDims = numDims; 823 if (numDims < 3) 824 numDims = 3; 825 826 o = o.toDimension(numDims); 827 p1 = p1.toDimension(numDims); 828 p2 = p2.toDimension(numDims); 829 830 // Find the true center of the circle. 831 Vector<Length> a = p2.minus(p1).toGeomVector(); 832 833 // Find the bisection point of the line segment "a". 834 Point aB = p1.plus(p2).divide(2); // aB = (p1 + p2)/2 835 836 // Find normal vector and direction perpendicular to vector "a". 837 // Get vectors from the guess at the circle center to the end points. 838 GeomVector<Length> p1v = p1.minus(o).toGeomVector(); 839 GeomVector<Length> p2v = p2.minus(o).toGeomVector(); 840 841 // Determine the normal vector for the plane containing the circle. 842 Vector<Dimensionless> n = p1v.cross(p2v).toUnitVector(); 843 844 // Determine the perpendicular vector. 845 Vector<Dimensionless> taB = a.cross(n).toUnitVector(); 846 847 // The center is the nearest point to the user supplied center on the line that 848 // bisects the line between p1 and p2. 849 Point c = pointLineClosest(o, aB, taB); 850 851 // Construct a circle information record from the center, two end points, and normal vector. 852 CircleInfo circInfo = makeCircleInfo(c, p1, p2, n); 853 if (saveDims == 2) { 854 // Convert back to 2D geometry on output. 855 circInfo.center = circInfo.center.toDimension(2); 856 circInfo.xhat = circInfo.xhat.toDimension(2); 857 circInfo.yhat = circInfo.yhat.toDimension(2); 858 } 859 860 return circInfo; 861 } 862 863 /** 864 * Finds a circle that passes through the two specified points with the specified 865 * tangent vector at the start. 866 * 867 * @param p1 The 1st of the points that define the circle. May not be null. 868 * @param t1 The tangent vector at p1 (must not point directly at p2). May not be null. 869 * @param p2 The 2nd of the points that define the circle. May not be null. 870 * @return A CircleInfo record containing the geometry of the circle. THe "xhat" 871 * parameter points to the first point supplied. The "yhat" parameter 872 * indicates the direction to the 2nd point. The "angle" parameter gives the 873 * angle swept out from the 1st point to the 2nd point. 874 * @throws DimensionException if the 2 points do not have at least 2 physical 875 * dimensions. 876 */ 877 public static CircleInfo twoPointTangentCircle(GeomPoint p1, GeomVector t1, GeomPoint p2) throws DimensionException { 878 879 // Convert all the points to the highest dimension between them. 880 int numDims = maxPhyDimension(p1, t1, p2); 881 if (numDims < 2) 882 throw new DimensionException( 883 MessageFormat.format(RESOURCES.getString("dimensionNotAtLeast2"), "input points", numDims)); 884 885 // Up-cast the geometry to 3D if necessary to make some of the math easier. 886 int saveDims = numDims; 887 if (numDims < 3) 888 numDims = 3; 889 p1 = p1.toDimension(numDims); 890 p2 = p2.toDimension(numDims); 891 t1 = t1.toDimension(numDims); 892 893 // "a" is the vector between the two end points. 894 Vector<Length> a = p2.minus(p1).toGeomVector(); 895 896 // Make sure tangent vector is valid. If parallel to p2-p1 then dot product will equal 1. 897 if (t1.dot(a).divide(t1.mag()).divide(a.mag()).minus(Parameter.ONE).abs().isApproxZero()) 898 throw new IllegalArgumentException(RESOURCES.getString("t1ParallelTop2p1Err")); 899 900 // Find the bisection point of the line segment "a". 901 Point aB = p1.plus(p2).divide(2); // aB = (p1 + p2)/2 902 903 // Find normal vector and direction perpendicular to vector "a". 904 // Determine the normal vector for the plane containing the circle. 905 Vector<Dimensionless> n = t1.cross(a).toUnitVector(); 906 907 // Determine the perpendicular vector. 908 Vector<Dimensionless> taB = a.cross(n).toUnitVector(); 909 910 // Form vector perpendicular to t1 and the normal vector 911 // (points toward the center of the circle). 912 Vector<Dimensionless> t1hat = n.cross(t1).toUnitVector(); 913 914 // The center/origin is where the p1/t1hat and perpendicular bisector 915 // lines intersect (or come closest to intersecting). 916 MutablePoint c = MutablePoint.newInstance(numDims, p1.getUnit()); 917 lineLineIntersect(p1, t1hat, aB, taB, GTOL, null, null, c); 918 919 // Construct a circle information record from the center, two end points, and normal vector. 920 CircleInfo circInfo = makeCircleInfo(c, p1, p2, n); 921 if (saveDims == 2) { 922 // Convert back to 2D geometry on output. 923 circInfo.center = circInfo.center.toDimension(2); 924 circInfo.xhat = circInfo.xhat.toDimension(2); 925 circInfo.yhat = circInfo.yhat.toDimension(2); 926 } 927 928 return circInfo; 929 } 930 931 /** 932 * Return a CircleInfo record from the circle center, end points and normal vector. 933 * All input geometry must be at least 3D. No error checking is performed. All points 934 * & vectors must be the same dimension and compatible with a circle. 935 * 936 * @param c The center of the circle. 937 * @param p1 The 1st end point of the circle or circular arc. 938 * @param p2 The 2nd end point of the circle or circular arc. 939 * @param n The normal vector for the circle. 940 * @return A CircleInfo record containing the geometry of the circle. The "xhat" 941 * parameter points to the first point supplied. The "yhat" parameter 942 * indicates the direction to the next point. The "angle" parameter gives the 943 * angle swept out from the 1st point to the 2nd point. 944 */ 945 private static CircleInfo makeCircleInfo(GeomPoint c, GeomPoint p1, GeomPoint p2, GeomVector n) { 946 int numDims = c.getPhyDimension(); 947 948 // The radius is the distance from the center point to any one of the edge points. 949 Parameter<Length> r = c.distance(p1); 950 951 // The xhat vector is the vector from the center to the 1st point. 952 Vector<Dimensionless> xhat = p1.minus(c).toGeomVector().toUnitVector(); 953 xhat.setOrigin(Point.newInstance(numDims)); 954 955 // The yhat vector is perpendicular to the xhat and normal vectors. 956 Vector<Dimensionless> yhat = n.cross(xhat).toUnitVector(); 957 yhat.setOrigin(Point.newInstance(numDims)); 958 959 // Calculate the angle between the 1st and last points. 960 Vector<Dimensionless> p2V = p2.minus(c).toGeomVector().toUnitVector(); 961 Parameter dot = xhat.dot(p2V); 962 Parameter<Angle> angle = Parameter.acos(dot); 963 964 // Create and return a circle record. 965 CircleInfo circle = new CircleInfo(); 966 circle.radius = r.to(p1.getUnit()); 967 circle.center = c.to(p1.getUnit()); 968 circle.xhat = xhat; 969 circle.yhat = yhat; 970 circle.angle = angle; 971 972 return circle; 973 } 974 975 /** 976 * Returns the default units for this system. The default unit is based on the user's 977 * locale. If the user's locale is the United States, then a value of NonSI.INCH is 978 * returned, otherwise a value of SI.CENTIMETER is returned. 979 * 980 * @return The default units for this system. 981 */ 982 public static Unit<Length> getDefaultUnit() { 983 Unit<Length> unit = SI.CENTIMETER; 984 if (java.util.Locale.getDefault().equals(java.util.Locale.US)) 985 unit = NonSI.INCH; 986 return unit; 987 } 988 989 /** 990 * Finds the intersection point between two lines, or the point of closest approach if 991 * the lines do not intersect, and returns the point on each line. The output data is 992 * returned by modifying the input <code>sOut</code>, <code>out1</code> and 993 * <code>out2</code> points. 994 * 995 * @param p1 A point on the 1st line. May not be null. 996 * @param t1 The direction vector for the 1st line (does not have to be a unit 997 * vector). May not be null. 998 * @param p2 A point on the 2nd line. May not be null. 999 * @param t2 The direction vector for the 2nd line (MUST be similar to t1; unit 1000 * vector if t1 is unit vector and dimension vector if t1 is a dimensional 1001 * vector). Do NOT mix dimensional and unit vectors in the same call. May 1002 * not be null. 1003 * @param tol Tolerance on how close the lines must be to be considered intersecting. 1004 * A value of null indicates that an exact intersection is required. 1005 * @param sOut The pre-defined 2D output parametric positions on the input lines that 1006 * will be filled in to represent the points where the lines intersect. If 1007 * the input direction vectors are dimensionless (unit vectors), then the 1008 * output point will be scaled to the units of p1 (indicating the distance 1009 * that the intersection point is away from the origin in p1 units). If 1010 * the input direction vectors are dimensional, then the point will be 1011 * scaled such that 1.0 m represents the direction vector's length along 1012 * the line. If there is no intersection, the point is not modified. If 1013 * <code>null</code> is passed, then the parametric point is not 1014 * calculated. 1015 * @param out1 The pre-defined output point on the 1st line representing the 1016 * intersection or closest approach of the two lines (may be null if not 1017 * required). 1018 * @param out2 The pre-defined output point on the 2nd line representing the 1019 * intersection or closest approach of the two lines (may be null if not 1020 * required). 1021 * @return DISJOINT if the lines do not intersect or INTERSECT if they do. 1022 * @throws DimensionException if the output points do not have at least as many 1023 * dimensions as the input points. 1024 */ 1025 public static IntersectType lineLineIntersect(GeomPoint p1, GeomVector t1, 1026 GeomPoint p2, GeomVector t2, Parameter<Length> tol, 1027 MutablePoint sOut, MutablePoint out1, MutablePoint out2) throws DimensionException { 1028 // For algorithm see: http://softsurfer.com/Archive/algorithm_0106/algorithm_0106.htm 1029 1030 int numDims = maxPhyDimension(p1, t1, p2, t2); 1031 if (numDims < 2) 1032 numDims = 2; 1033 1034 boolean parallel = false; 1035 Parameter mu1, mu2; 1036 Vector<Length> v1, v2; 1037 StackContext.enter(); 1038 try { 1039 // Convert all the points to the highest dimension between them. 1040 p1 = p1.toDimension(numDims); 1041 t1 = t1.toDimension(numDims); 1042 p2 = p2.toDimension(numDims); 1043 t2 = t2.toDimension(numDims); 1044 1045 Vector<Length> v12 = p1.minus(p2).toGeomVector(); 1046 1047 Parameter a = t1.dot(t1); 1048 Parameter b = t1.dot(t2); 1049 Parameter c = t2.dot(t2); 1050 Parameter d = t1.dot(v12); 1051 Parameter e = t2.dot(v12); 1052 Parameter denom = a.times(c).minus(b.times(b)); // denom = a*c - b^2 1053 1054 if (denom.isApproxZero()) { 1055 // The lines are parallel. 1056 parallel = true; 1057 mu1 = Parameter.valueOf(0, d.getUnit().divide(b.getUnit())); // mu1 = 0; 1058 mu2 = b.isGreaterThan(c) ? d.divide(b) : e.divide(c); // b > c ? d / b : e / c; 1059 1060 } else { 1061 mu1 = b.times(e).minus(c.times(d)).divide(denom); // mu1 = (b * e - c * d) / denom; 1062 mu2 = a.times(e).minus(b.times(d)).divide(denom); // mu2 = (a * e - b * d) / denom; 1063 } 1064 1065 // Compute the closest points on each line. 1066 v1 = t1.times(mu1); 1067 v1 = v1.plus(p1.toGeomVector()); 1068 v2 = t2.times(mu2); 1069 v2 = v2.plus(p2.toGeomVector()); 1070 1071 // Copy the things we need to the outer context. 1072 if (nonNull(sOut)) { 1073 mu1 = StackContext.outerCopy(mu1); 1074 mu2 = StackContext.outerCopy(mu2); 1075 } 1076 v1 = StackContext.outerCopy(v1); 1077 v2 = StackContext.outerCopy(v2); 1078 1079 } finally { 1080 StackContext.exit(); 1081 } 1082 1083 if (nonNull(sOut)) { 1084 if (mu1.getUnit().isCompatible(Dimensionless.UNIT)) { 1085 UnitConverter cvtr = SI.METER.getConverterTo(sOut.getUnit()); 1086 sOut.setValue(0, cvtr.convert(mu1.getValue(Dimensionless.UNIT))); 1087 sOut.setValue(1, cvtr.convert(mu2.getValue(Dimensionless.UNIT))); 1088 } else { 1089 sOut.set(0, mu1); 1090 sOut.set(1, mu2); 1091 } 1092 } 1093 1094 if (nonNull(out1)) { 1095 out1.set(v1); 1096 } 1097 1098 if (nonNull(out2)) { 1099 out2.set(v2); 1100 } 1101 1102 if (v2.minus(v1).norm().isLessThan(tol)) { 1103 if (parallel) 1104 return IntersectType.COINCIDENT; 1105 return IntersectType.INTERSECT; 1106 } 1107 return IntersectType.DISJOINT; 1108 } 1109 1110 /** 1111 * Finds the intersection point between a line and a plane (if there is an 1112 * intersection). If the line and the plane are parallel, then IntersectType.DISJOINT 1113 * is output, if there is an intersection, then 1 is output and the intersection point 1114 * is returned in the input <code>out</code> point. Finally, if the line is coincident 1115 * with the plane, then 2 is output and out is set to the L0 point. 1116 * 1117 * @param L0 A point on the line (origin of the line). May not be null. 1118 * @param Ldir The direction vector for the line (does not have to be a unit vector). 1119 * May not be null. 1120 * @param plane The plane being tested for intersection. May not be null. 1121 * @param out The pre-defined output point on the line that will be filled in to 1122 * represent the intersection of the line and the plane (not modified if 1123 * there is no intersection). If <code>null</code> is passed, then the 1124 * point is not calculated, but the return codes of this method are still 1125 * valid. 1126 * @return DISJOINT if the line and plane are disjoint (parallel), INTERSECT if there 1127 * is a unique intersection and COINCIDENT if the line is coincident with the 1128 * plane. 1129 * @throws DimensionException if the output point does not have at least as many 1130 * dimensions as the input line and plane. 1131 */ 1132 public static IntersectType linePlaneIntersect(GeomPoint L0, GeomVector Ldir, 1133 GeomPlane plane, MutablePoint out) throws DimensionException { 1134 // For algorithm see: http://www.softsurfer.com/Archive/algorithm_0104/algorithm_0104B.htm 1135 1136 // Convert all the input data to the highest dimension between them. 1137 int numDims = maxPhyDimension(L0, Ldir, plane); 1138 L0 = L0.toDimension(numDims); 1139 Vector<Dimensionless> Lu = Ldir.toDimension(numDims).toUnitVector(); 1140 plane = plane.toDimension(numDims); 1141 1142 // Get data we need. 1143 GeomPoint V0 = plane.getRefPoint(); 1144 Vector nw = V0.minus(L0).toGeomVector(); 1145 GeomVector<Dimensionless> n = plane.getNormal(); 1146 1147 // Check for the line parallel to the plane. 1148 Parameter ndotu = n.dot(Lu); 1149 if (ndotu.isApproxZero()) { 1150 Parameter N = n.dot(nw); 1151 if (N.isApproxZero()) { 1152 if (out != null) 1153 out.set(L0); 1154 return IntersectType.COINCIDENT; 1155 } 1156 return IntersectType.DISJOINT; 1157 } 1158 1159 if (nonNull(out)) { 1160 // Find the intersection point. 1161 Parameter N = n.dot(nw); 1162 Parameter si = N.divide(ndotu); 1163 Vector v = Lu.times(si); 1164 v = v.plus(L0.toGeomVector()); 1165 1166 out.set(v); 1167 } 1168 1169 return IntersectType.INTERSECT; // A unique intersection. 1170 } 1171 1172 /** 1173 * Finds the intersection point between a line and a plane (if there is an 1174 * intersection). If the line and the plane are parallel, then IntersectType.DISJOINT 1175 * is output, if there is an intersection, then 1 is output and the intersection point 1176 * is returned in the input <code>out</code> point. Finally, if the line is coincident 1177 * with the plane, then 2 is output and out is set to the L0 point. 1178 * 1179 * @param L0 A point on the line (origin of the line). May not be null. 1180 * @param Ldir The direction vector for the line (does not have to be a unit vector). 1181 * May not be null. 1182 * @param plane The plane being tested for intersection. May not be null. 1183 * @param out The pre-defined output point on the line that will be filled in to 1184 * represent the intersection of the line and the plane (not modified if 1185 * there is no intersection). If <code>null</code> is passed, then the 1186 * point is not calculated, but the return codes of this method are still 1187 * valid. 1188 * @param tol The fractional tolerance on determining if the line and plane are 1189 * coincident or disjoint. 1190 * @return DISJOINT if the line and plane are disjoint (parallel), INTERSECT if there 1191 * is a unique intersection and COINCIDENT if the line is coincident with the 1192 * plane. 1193 * @throws DimensionException if the output point does not have at least as many 1194 * dimensions as the input line and plane. 1195 */ 1196 public static IntersectType linePlaneIntersect(GeomPoint L0, GeomVector Ldir, 1197 GeomPlane plane, MutablePoint out, double tol) throws DimensionException { 1198 // For algorithm see: http://www.softsurfer.com/Archive/algorithm_0104/algorithm_0104B.htm 1199 1200 // Convert all the input data to the highest dimension between them. 1201 int numDims = maxPhyDimension(L0, Ldir, plane); 1202 L0 = L0.toDimension(numDims); 1203 Vector<Dimensionless> Lu = Ldir.toDimension(numDims).toUnitVector(); 1204 plane = plane.toDimension(numDims); 1205 1206 // Get data we need. 1207 GeomPoint V0 = plane.getRefPoint(); 1208 Vector nw = V0.minus(L0).toGeomVector(); 1209 GeomVector<Dimensionless> n = plane.getNormal(); 1210 1211 // Check for the line parallel to the plane. 1212 Parameter ptol = Parameter.valueOf(tol, Dimensionless.UNIT); 1213 Parameter ndotu = n.dot(Lu); 1214 if (!ndotu.isLargerThan(ptol)) { 1215 Parameter N = n.dot(nw.toUnitVector()); 1216 if (!N.isLargerThan(ptol)) { 1217 if (out != null) 1218 out.set(L0); 1219 return IntersectType.COINCIDENT; 1220 } 1221 return IntersectType.DISJOINT; 1222 } 1223 1224 if (nonNull(out)) { 1225 // Find the intersection point. 1226 Parameter N = n.dot(nw); 1227 Parameter si = N.divide(ndotu); 1228 Vector v = Lu.times(si); 1229 v = v.plus(L0.toGeomVector()); 1230 1231 out.set(v); 1232 } 1233 1234 return IntersectType.INTERSECT; // A unique intersection. 1235 } 1236 1237 /** 1238 * Finds the intersection line between two planes (if there is an intersection). If 1239 * the two planes are parallel, then IntersectType.DISJOINT is output, if there is an 1240 * intersection, then IntersectType.INTERSECT is output and a point on the 1241 * intersection line is returned in the input <code>out</code> point and the direction 1242 * vector of the intersection line is returned in the input <code>dir</code> vector. 1243 * Finally, if the two planes are coincident, then IntersectType.COINCIDENT is output. 1244 * 1245 * @param plane1 The 1st plane being tested for intersection. May not be null. 1246 * @param plane2 The 2nd plane being tested for intersection. May not be null. 1247 * @param out The pre-defined output point on the intersection line that will be 1248 * filled in to represent the intersection of the two planes (not 1249 * modified if there is no intersection or if the planes are 1250 * coincident). If <code>null</code> is passed, then the intersection 1251 * line point is not calculated, but the return codes of this method are 1252 * still valid. 1253 * @param dir The pre-defined output unit direction vector of the intersection line 1254 * that will be filled in to represent the intersection of the two 1255 * planes (not modified if there is no intersection or if the planes are 1256 * coincident). If <code>null</code> is passed, then the intersection 1257 * line direction is not calculated, but the return codes of this method 1258 * are still valid. 1259 * @return DISJOINT if the line and plane are disjoint (parallel), INTERSECT if there 1260 * is a unique intersection and COINCIDENT if the line is coincident with the 1261 * plane. 1262 * @throws DimensionException if the output point and plane do not have at least as 1263 * many dimensions as the input planes. 1264 */ 1265 public static IntersectType planePlaneIntersect(GeomPlane plane1, 1266 GeomPlane plane2, MutablePoint out, MutableVector<Dimensionless> dir) throws DimensionException { 1267 // Reference: http://mathworld.wolfram.com/Plane-PlaneIntersection.html 1268 1269 // Convert all the input data to the highest dimension between them. 1270 int numDims = Math.max(plane1.getPhyDimension(), plane2.getPhyDimension()); 1271 1272 StackContext.enter(); 1273 try { 1274 // Convert to the highest dimension input object. 1275 Unit<Length> unit = plane1.getUnit(); 1276 plane1 = plane1.toDimension(numDims); 1277 plane2 = plane2.toDimension(numDims).to(unit); 1278 1279 // Get data we need. 1280 GeomPoint P1 = plane1.getRefPoint(); 1281 GeomVector<Dimensionless> n1 = plane1.getNormal(); 1282 GeomPoint P2 = plane2.getRefPoint(); 1283 GeomVector<Dimensionless> n2 = plane2.getNormal(); 1284 Vector nw = P2.minus(P1).toGeomVector(); 1285 1286 // Check for the two planes being parallel. 1287 double n1dotn2 = n1.dot(n2).getValue(); 1288 if (MathTools.isApproxEqual(n1dotn2, 1)) { // n1dotn2 == 1 1289 // Are the two planes also coincident? 1290 Parameter N = n1.dot(nw); 1291 if (N.isApproxZero()) { 1292 return IntersectType.COINCIDENT; 1293 } 1294 return IntersectType.DISJOINT; 1295 } 1296 1297 if (nonNull(dir)) { 1298 // Find the intersection line direction vector. 1299 Vector<Dimensionless> a = n1.cross(n2).toUnitVector(); 1300 // The following works because MutableVector.set() is context safe when provided 1301 // a "Vector" instance, but ONLY in that case. 1302 dir.set(StackContext.outerCopy(a)); 1303 } 1304 1305 if (nonNull(out)) { 1306 // Find the intersection point. 1307 1308 // Create elements of the "b" matrix. 1309 double np1 = plane1.getConstant().getValue(); // -p1 1310 double np2 = plane2.getConstant().getValue(); // -p2 1311 Float64Vector b = Float64Vector.valueOf(np1, np2); 1312 1313 // Create the "M" matrix. 1314 Float64Matrix M = Float64Matrix.valueOf(n1.toFloat64Vector(), n2.toFloat64Vector()); 1315 M = M.transpose(); 1316 1317 // Solve for the "x0" vector: x0 = M^-1 * b. 1318 Float64Vector x0 = Float64Vector.valueOf(M.pseudoInverse().transpose().times(b)); 1319 1320 // Store the newly calculated point in x0. 1321 Point x0p = Point.valueOf(x0, unit); 1322 // The following works because MutablePoint.set() is context safe when provided 1323 // a "Point" instance, but ONLY in that case. 1324 out.set(StackContext.outerCopy(x0p)); 1325 } 1326 1327 } finally { 1328 StackContext.exit(); 1329 } 1330 1331 // We have an intersection if we have gotten here. 1332 if (nonNull(out) && nonNull(dir)) { 1333 // If the line direction is requested, store the intersection point in it. 1334 dir.setOrigin(out.immutable()); 1335 } 1336 1337 return IntersectType.INTERSECT; // A unique intersection. 1338 } 1339 1340 /** 1341 * Returns true if the bounding boxes of two geometry objects intersect or overlap. 1342 * This is an aligned axis bounding box (AABB) test. 1343 * 1344 * @param b1 The first geometric object to test for overlap. May not be null. 1345 * @param b2 The 2nd geometric object to test for overlap. May not be null. 1346 * @return <code>true</code> if the bounding boxes of the two input geometry objects 1347 * intersect or overlap (including one box contained completely inside of the 1348 * other). 1349 */ 1350 public static boolean boundsIntersect(GeomElement b1, GeomElement b2) { 1351 // Reference: 1352 // http://www.gamasutra.com/view/feature/3383/simple_intersection_tests_for_games.php?page=3 1353 1354 StackContext.enter(); 1355 try { 1356 // Make sure both geometry elements have the same units. 1357 Unit<Length> unit = b1.getUnit(); 1358 1359 // Get physical dimension of each element. 1360 int dim1 = b1.getPhyDimension(); 1361 int dim2 = b2.getPhyDimension(); 1362 1363 // Get the bounding boxes of each geometry element. 1364 Point b1min = b1.getBoundsMin().to(unit); 1365 Point b1max = b1.getBoundsMax().to(unit); 1366 Point b2min = b2.getBoundsMin().to(unit); 1367 Point b2max = b2.getBoundsMax().to(unit); 1368 1369 // Make sure points are all the same dimension 1370 // by making both have the dimension of the largest. 1371 if (dim1 > dim2) { 1372 b2min = b2min.toDimension(dim1); 1373 b2max = b2max.toDimension(dim1); 1374 1375 } else if (dim1 != dim2) { // dim2 > dim1 1376 b1min = b1min.toDimension(dim2); 1377 b1max = b1max.toDimension(dim2); 1378 } 1379 1380 // Find center of each bounding box. 1381 Point p1 = b1min.plus(b1max).divide(2); 1382 Point p2 = b2min.plus(b2max).divide(2); 1383 1384 // Find extents of each bounding box. 1385 Point E1 = p1.minus(b1min); 1386 Point E2 = p2.minus(b2min); 1387 1388 // Find the distance between the centers of each box. 1389 Point T = p2.minus(p1); 1390 1391 // Do the overlap test. 1392 for (int i = 0; i < dim1; ++i) { // Loop over each axis. 1393 double Tv = abs(T.getValue(i)); // Distance between centers along this axis. 1394 double E1v = abs(E1.getValue(i)); // Bounding box extents along this axis. 1395 double E2v = abs(E2.getValue(i)); 1396 if (Tv > E1v + E2v) { 1397 // Found a separating axis where 1398 // distance along this axis is > than box sizes along this axis. 1399 // So, the boxes must not overlap. 1400 return false; 1401 } 1402 } 1403 1404 return true; 1405 1406 } finally { 1407 StackContext.exit(); 1408 } 1409 } 1410 1411 /** 1412 * Returns true if the specified infinite line and bounding box of the specified 1413 * geometry element intersect. This is an infinite line - aligned axis bounding box 1414 * (AABB) test. 1415 * 1416 * @param L0 A point on the line (origin of the line). May not be null. 1417 * @param Ldir The direction vector for the line (does not have to be a unit vector). 1418 * May not be null. 1419 * @param box The geometry element to extract the bounding box from. May not be null. 1420 * @param sOut The pre-defined 2D output parametric position on the line (relative to 1421 * the origin) that will be filled in to represent the points where the 1422 * line enters and leaves the bounding box. If there is no intersection, 1423 * the point is not modified. If <code>null</code> is passed, then the 1424 * parametric point is not calculated. 1425 * @return <code>true</code> if the line and geometry element bounding box intersects. 1426 */ 1427 public static boolean lineBoundsIntersect(GeomPoint L0, GeomVector Ldir, GeomElement box, MutablePoint sOut) { 1428 // Reference: 1429 // http://ggt.sourceforge.net/gmtlReferenceGuide-0.6.1-html/namespacegmtl.html#a5e30c7bc7cdaec04e04276eea798264f 1430 double tIn = -Double.MAX_VALUE; 1431 double tOut = Double.MAX_VALUE; 1432 1433 int numDims = maxPhyDimension(L0, Ldir, box); 1434 if (numDims < 2) 1435 numDims = 2; 1436 1437 StackContext.enter(); 1438 try { 1439 // Make sure everything uses the same units. 1440 Unit<Length> unit = box.getUnit(); 1441 1442 // Make sure everything has the dimensions of the largest input object. 1443 L0 = L0.toDimension(numDims); 1444 Ldir = Ldir.toDimension(numDims); 1445 if (!Dimensionless.UNIT.equals(Ldir.getUnit())) 1446 Ldir = (GeomVector)Ldir.toDimension(numDims).to(unit); 1447 1448 // Get the bounding box of the geometry element. 1449 Point bmin = box.getBoundsMin().toDimension(numDims).minus(GTOL); 1450 Point bmax = box.getBoundsMax().toDimension(numDims).plus(GTOL); 1451 1452 // Do any of the principal axes form a separating axis? 1453 for (int i = 0; i < numDims; ++i) { // Loop over each axis. 1454 double ui = Ldir.getValue(i); // Component of the line this axis direction. 1455 1456 // Is the line parallel to this axis? 1457 if (MathTools.isApproxZero(ui)) { 1458 double L0i = L0.getValue(i, unit); 1459 if (L0i < bmin.getValue(i, unit) || L0i > bmax.getValue(i, unit)) 1460 return false; 1461 } 1462 } 1463 1464 // Check to see if there are any intersections with the bounding box. 1465 for (int i = 0; i < numDims; ++i) { 1466 double ui = Ldir.getValue(i); 1467 if (MathTools.isApproxZero(ui)) 1468 continue; 1469 double L0i = L0.getValue(i, unit); 1470 1471 double t0 = (bmin.getValue(i, unit) - L0i) / ui; // t0 = (bmin[i] - p0[i]) / u[i]; 1472 double t1 = (bmax.getValue(i, unit) - L0i) / ui; // t1 = (bmax[i] - p0[i]) / u[i]; 1473 if (t0 > t1) { 1474 // Swap 1475 double tmp = t0; 1476 t0 = t1; 1477 t1 = tmp; 1478 } 1479 1480 if (t0 > tIn) 1481 tIn = t0; 1482 if (t1 < tOut) 1483 tOut = t1; 1484 if (tIn > tOut || tOut < 0) 1485 return false; 1486 } 1487 1488 } finally { 1489 StackContext.exit(); 1490 } 1491 1492 if (nonNull(sOut)) { 1493 sOut.setValue(0, tIn); 1494 sOut.setValue(1, tOut); 1495 } 1496 1497 return true; 1498 } 1499 1500 /** 1501 * Returns true if the specified line segment and bounding box of the specified 1502 * geometry element intersect or overlap. This is a line segment aligned axis bounding 1503 * box (AABB) test. 1504 * 1505 * @param p0 One end of the line segment. May not be null. 1506 * @param p1 The other end of the line segment. May not be null. 1507 * @param box The geometry element to extract the bounding box from. May not be null. 1508 * @return <code>true</code> if the line segment and geometry element bounding box 1509 * intersects or overlaps (including the line segment contained completely 1510 * inside of the box). 1511 */ 1512 public static boolean lineSegBoundsIntersect(GeomPoint p0, GeomPoint p1, GeomElement box) { 1513 // Reference: 1514 // http://ggt.sourceforge.net/gmtlReferenceGuide-0.6.1-html/namespacegmtl.html#a4c99e51116eb3c68a52091d6a2480522 1515 requireNonNull(p0); 1516 requireNonNull(p1); 1517 requireNonNull(box); 1518 1519 // Define a point that represents the parametric position on the line where 1520 // the intersection occurs. 1521 MutablePoint pt = MutablePoint.valueOf(-Double.MAX_VALUE, Double.MAX_VALUE); 1522 try { 1523 boolean result = lineBoundsIntersect(p0, p1.minus(p0).toGeomVector(), box, pt); 1524 if (!result) 1525 return false; 1526 1527 double tIn = pt.getValue(0); 1528 double tOut = pt.getValue(1); 1529 if (tIn > 1.0) 1530 return false; 1531 if (tOut < 0.0) 1532 return false; 1533 1534 } finally { 1535 MutablePoint.recycle(pt); 1536 } 1537 1538 return true; 1539 } 1540 1541 /** 1542 * Returns true if the specified infinite plane and bounding box of the specified 1543 * geometry element intersect. This is a aligned axis bounding box (AABB) - infinite 1544 * plane test. 1545 * 1546 * @param plane The infinite plane being used for the intersection test. May not be null. 1547 * @param box The geometry element to extract the bounding box from. May not be null. 1548 * @return <code>true</code> if the plane and geometry element bounding box 1549 * intersects. 1550 */ 1551 public static boolean planeBoundsIntersect(GeomPlane plane, GeomElement box) { 1552 // Reference: 1553 // http://www.gamasutra.com/view/feature/131790/simple_intersection_tests_for_games.php?page=7 1554 // http://www8.cs.umu.se/kurser/5DV058/VT09/lectures/Lecture7.pdf 1555 1556 int numDims = Math.max(plane.getPhyDimension(), box.getPhyDimension()); 1557 if (numDims < 3) 1558 numDims = 3; 1559 1560 StackContext.enter(); 1561 try { 1562 // Make sure everything uses the same units. 1563 Unit<Length> unit = box.getUnit(); 1564 1565 // Make sure everything has the dimensions of the largest input object. 1566 GeomPoint P0 = plane.getRefPoint().toDimension(numDims).to(unit); 1567 GeomVector<Dimensionless> nhat = plane.getNormal().toDimension(numDims); 1568 1569 // Get the bounding box of the geometry element. 1570 Point bmin = box.getBoundsMin().toDimension(numDims).to(unit); 1571 Point bmax = box.getBoundsMax().toDimension(numDims).to(unit); 1572 1573 // Find center of the bounding box. 1574 Point p1 = bmin.plus(bmax).divide(2); 1575 1576 // Find extents of the bounding box. 1577 Point E1 = p1.minus(bmin); 1578 1579 // Find the shortest distance between the center of the box and the plane. 1580 double d = pointPlaneClosest(p1, P0, nhat).distanceValue(p1); 1581 1582 // Do the overlap test. 1583 double r = 0; 1584 for (int i = 0; i < numDims; ++i) { // Loop over each axis. 1585 double E1v = abs(E1.getValue(i)); // Bounding box extents along this axis. 1586 double ni = abs(nhat.getValue(i)); // Component of the plane normal vector. 1587 r += E1v * ni; 1588 } 1589 return d <= r; 1590 1591 } finally { 1592 StackContext.exit(); 1593 } 1594 } 1595 1596 /** 1597 * Return the approximate normal vector for a quadrilateral panel. Quadrilateral 1598 * panels are not necessarily planar, but in practical applications usually are close. 1599 * This method returns the cross-product of the diagonal vectors of the quadrilateral 1600 * panel which approximates the normal vector of the quadrilateral panel. 1601 * 1602 * @param quadA 1st corner point of a quadrilateral panel. May not be null. 1603 * @param quadB 2nd corner point in a counter-clockwise direction. May not be null. 1604 * @param quadC 3rd corner point in a counter-clockwise direction. May not be null. 1605 * @param quadD 4th corner point in a counter-clockwise direction. May not be null. 1606 * @return The normal vector formed by the cross-product of the diagonal vectors of 1607 * the quadrilateral panel. Will be at least 3-dimensional no matter the 1608 * inputs. 1609 */ 1610 public static Vector<Dimensionless> quadNormal(GeomPoint quadA, GeomPoint quadB, GeomPoint quadC, GeomPoint quadD) { 1611 1612 int numDims = maxPhyDimension(quadA, quadB, quadC, quadD); 1613 if (numDims < 3) 1614 numDims = 3; 1615 1616 StackContext.enter(); 1617 try { 1618 // Make everything the same dimension. 1619 quadA = quadA.toDimension(numDims); 1620 quadB = quadB.toDimension(numDims); 1621 quadC = quadC.toDimension(numDims); 1622 quadD = quadD.toDimension(numDims); 1623 1624 // Compute diagonal vectors. 1625 Vector<Length> T1 = quadC.minus(quadA).toGeomVector(); 1626 Vector<Length> T2 = quadD.minus(quadB).toGeomVector(); 1627 1628 // Compute normal vector as cross product: N = T1 X T2 1629 Vector norm = T1.cross(T2); 1630 1631 // Compute average point. 1632 Point avg = quadA.plus(quadB).plus(quadC).plus(quadD).divide(4); 1633 norm.setOrigin(avg); 1634 1635 // Make the normal vector unit length. 1636 Vector<Dimensionless> n = norm.toUnitVector(); 1637 return StackContext.outerCopy(n); 1638 1639 } finally { 1640 StackContext.exit(); 1641 } 1642 } 1643 1644 /** 1645 * Test to see if a line segment intersects a triangle. 1646 * 1647 * @param L0 The start point on the line segment (origin of the line). May not be 1648 * null. 1649 * @param L1 The end point on the line segment. May not be null. 1650 * @param V0 1st corner point of a triangle. May not be null. 1651 * @param V1 2nd corner point in a counter-clockwise direction. May not be null. 1652 * @param V2 3rd corner point in a counter-clockwise direction. May not be null. 1653 * @param pOut A point that will be filled in with the intersection point in this 1654 * triangle. If there is no intersection with this triangle, then pOut is 1655 * not modified. Pass <code>null</code> if the intersection point is not 1656 * required. 1657 * @return DISJOINT if the line and triangle are disjoint (do not intersect), 1658 * INTERSECT if there is a unique intersection and COINCIDENT if the line is 1659 * coincident with the plane of the triangle. 1660 */ 1661 public static IntersectType lineSegTriIntersect(GeomPoint L0, GeomPoint L1, 1662 GeomPoint V0, GeomPoint V1, GeomPoint V2, MutablePoint pOut) { 1663 // Reference: http://geomalgorithms.com/a06-_intersect-2.html#intersect3D_RayTriangle() 1664 1665 int numDims = maxPhyDimension(L0, L1, V0, V1, V2); 1666 if (numDims < 3) 1667 numDims = 3; 1668 1669 StackContext.enter(); 1670 try { 1671 // Make sure all the geometry is the same dimension and units. 1672 Unit<Length> unit = L0.getUnit(); 1673 if (nonNull(pOut) && pOut.getPhyDimension() != numDims) 1674 throw new DimensionException(RESOURCES.getString("pOutDimErr")); 1675 L0 = L0.toDimension(numDims); 1676 L1 = L1.toDimension(numDims).to(unit); 1677 V0 = V0.toDimension(numDims).to(unit); 1678 V1 = V1.toDimension(numDims).to(unit); 1679 V2 = V2.toDimension(numDims).to(unit); 1680 1681 // Determine the triangle normal vector. 1682 Vector<Length> u = V1.minus(V0).toGeomVector(); 1683 Vector<Length> v = V2.minus(V0).toGeomVector(); 1684 Vector n = u.cross(v); 1685 if (n.magValue() < MathTools.SQRT_EPS) { 1686 // Triangle is degenerate (a line or point). 1687 double um = u.magValue(); 1688 double vm = v.magValue(); 1689 if (um < MathTools.SQRT_EPS && vm < MathTools.SQRT_EPS) { 1690 // Triangle is actually a single point. 1691 // Does the input line segment cross this single point? 1692 Parameter D = pointLineSegDistance(V0, L0, L1); 1693 if (D.isApproxZero()) { 1694 if (nonNull(pOut)) { 1695 Point V0p = V0.immutable(); 1696 pOut.set(StackContext.outerCopy(V0p)); 1697 } 1698 return IntersectType.INTERSECT; 1699 } else 1700 return IntersectType.DISJOINT; 1701 } 1702 1703 // The triangle is a straight line segment. 1704 if (um > vm) 1705 v = u; 1706 Vector<Length> dir = L1.minus(L0).toGeomVector(); 1707 MutablePoint sOut = MutablePoint.newInstance(2); 1708 IntersectType type = lineLineIntersect(L0, dir, V0, v, GTOL, sOut, null, null); 1709 if (type == IntersectType.INTERSECT) { 1710 double s1 = sOut.getValue(0); 1711 double s2 = sOut.getValue(1); 1712 if (s1 > 0 && s1 < 1 && s2 > 0 && s2 < 1) { 1713 // The input line segment intersected the collapsed triangle line. 1714 if (nonNull(pOut)) { 1715 Point p = L0.plus(Point.valueOf(dir.times(s1))); 1716 pOut.set(StackContext.outerCopy(p)); 1717 } 1718 return IntersectType.INTERSECT; 1719 } 1720 } 1721 return IntersectType.DISJOINT; 1722 } 1723 Vector<Dimensionless> nhat = n.toUnitVector(); 1724 1725 Vector<Length> dir = L1.minus(L0).toGeomVector(); 1726 Vector<Length> w0 = L0.minus(V0).toGeomVector(); 1727 double a = -nhat.dot(w0).getValue(); 1728 double b = nhat.dot(dir).getValue(); 1729 if (abs(b) < MathTools.SQRT_EPS) { 1730 // Line is parallel to triangle plane 1731 if (abs(a) < MathTools.SQRT_EPS) 1732 // Line lies in triangle plane 1733 return IntersectType.COINCIDENT; 1734 else 1735 // Line disjoint from plane 1736 return IntersectType.DISJOINT; 1737 } 1738 1739 // Get intersect point of line with triangle plane 1740 double rv = a / b; 1741 if (rv < 0.0) { 1742 // Line goes away from triangle (=> no intersect) 1743 return IntersectType.DISJOINT; 1744 } else if (rv > 1.0) { 1745 // Line does not reach the triangle (=> no intersect) 1746 return IntersectType.DISJOINT; 1747 } 1748 1749 // Intersect point of line and triangle 1750 Point I = L0.plus(Point.valueOf(dir.times(rv))); // I = L0 + r * dir 1751 1752 // Is I inside T? 1753 double uu = u.dot(u).getValue(); 1754 double uv = u.dot(v).getValue(); 1755 double vv = v.dot(v).getValue(); 1756 Vector<Length> w = I.minus(V0).toGeomVector(); 1757 double wu = w.dot(u).getValue(); 1758 double wv = w.dot(v).getValue(); 1759 double D = uv * uv - uu * vv; 1760 1761 // Get and test parametric coords 1762 double s = (uv * wv - vv * wu) / D; 1763 if (s < 0.0 || s > 1.0) { 1764 // I is outside T 1765 return IntersectType.DISJOINT; 1766 } 1767 double t = (uv * wu - uu * wv) / D; 1768 if (t < 0.0 || (s + t) > 1.0) { 1769 // I is outside T 1770 return IntersectType.DISJOINT; 1771 } 1772 1773 // Change the output point if we get this far. 1774 if (nonNull(pOut)) { 1775 pOut.set(StackContext.outerCopy(I)); 1776 } 1777 1778 return IntersectType.INTERSECT; 1779 1780 } finally { 1781 StackContext.exit(); 1782 } 1783 } 1784 1785 /** 1786 * Test to see if an infinite line intersects a quadrilateral panel. 1787 * 1788 * @param L0 A point on the line (origin of the line). May not be null. 1789 * @param Ldir The direction vector for the line (does not have to be a unit vector). 1790 * May not be null. 1791 * @param quadA 1st corner point of a quadrilateral panel. May not be null. 1792 * @param quadB 2nd corner point in a counter-clockwise direction. May not be null. 1793 * @param quadC 3rd corner point in a counter-clockwise direction. May not be null. 1794 * @param quadD 4th corner point in a counter-clockwise direction. May not be null. 1795 * @param nhat The normal vector for the quadrilateral panel. May not be null. 1796 * @param sOut A 2D point that will be filled in with the parametric position of the 1797 * intersection point in this quad (s, t) where "s" is the fractional 1798 * distance between corners AB (or DC) and "t" is the fractional distance 1799 * between corners AD (or BC). If there is no intersection with this 1800 * panel, then sOut is not modified. Pass <code>null</code> if the 1801 * parametric position is not required. 1802 * @return <code>true</code> if the line and quadrilateral panel intersect. 1803 */ 1804 public static boolean lineQuadIntersect(GeomPoint L0, GeomVector Ldir, 1805 GeomPoint quadA, GeomPoint quadB, GeomPoint quadC, GeomPoint quadD, 1806 GeomVector nhat, MutablePoint sOut) { 1807 // Reference: 1808 // Graphic Gems V: http://tog.acm.org/resources/GraphicsGems/gemsv/ch5-2/quad_gg.c 1809 1810 // Make sure all the geometry is the same dimension and units. 1811 Unit<Length> unit = L0.getUnit(); 1812 int numDims = maxPhyDimension(L0, Ldir, quadA, quadB, quadC, quadD, nhat); 1813 if (numDims < 3) 1814 numDims = 3; 1815 Ldir = Ldir.toDimension(numDims).toUnitVector(); 1816 nhat = nhat.toDimension(numDims).toUnitVector(); 1817 1818 // If the line is parallel to the facet, there is no intersection. 1819 Parameter D = Ldir.dot(nhat); 1820 if (D.isApproxZero()) 1821 return false; 1822 1823 L0 = L0.toDimension(numDims); 1824 quadA = quadA.toDimension(numDims).to(unit); 1825 quadB = quadB.toDimension(numDims).to(unit); 1826 quadC = quadC.toDimension(numDims).to(unit); 1827 quadD = quadD.toDimension(numDims).to(unit); 1828 1829 // Compute line intersection with the plane of the facet 1830 Plane qPln = Plane.valueOf(nhat, averagePoints(quadA,quadB,quadC,quadD)); 1831 MutablePoint hit = MutablePoint.newInstance(numDims); 1832 IntersectType intersection = linePlaneIntersect(L0, Ldir, qPln, hit); 1833 Plane.recycle(qPln); 1834 if (intersection != IntersectType.INTERSECT) { 1835 MutablePoint.recycle(hit); 1836 return false; 1837 } 1838 1839 // Is the intersection point inside the facet 1840 boolean inside = pointInQuad(quadA, quadB, quadC, quadD, nhat, hit, sOut); 1841 MutablePoint.recycle(hit); 1842 1843 return inside; 1844 } 1845 1846 /** 1847 * Tests if the specified point, in the plane of an approximately planar quadrilateral 1848 * panel, is inside the boundaries of the quadrilateral panel or not. 1849 */ 1850 private static boolean pointInQuad(GeomPoint quadA, GeomPoint quadB, GeomPoint quadC, GeomPoint quadD, 1851 GeomVector nhat, GeomPoint hit, MutablePoint sOut) { 1852 // Reference: 1853 // Graphic Gems V: http://tog.acm.org/resources/GraphicsGems/gemsv/ch5-2/quad_gg.c 1854 1855 double u = 0, v = 0; 1856 boolean isInside = false; 1857 1858 StackContext.enter(); 1859 try { 1860 // Projection on the plane that is most parallel to the facet 1861 int largestComp = largestComponent(nhat); 1862 1863 Unit<Length> unit = quadA.getUnit(); 1864 MutablePoint A = MutablePoint.newInstance(2, unit); // Projected vertices 1865 MutablePoint B = MutablePoint.newInstance(2, unit); 1866 MutablePoint C = MutablePoint.newInstance(2, unit); 1867 MutablePoint D = MutablePoint.newInstance(2, unit); 1868 MutablePoint M = MutablePoint.newInstance(2, unit); // Projected intersection point. 1869 if (largestComp == 0) { // X 1870 A.set(0, quadA.get(1)); // A.x = Quad->A.y; 1871 B.set(0, quadB.get(1)); // B.x = Quad->B.y; 1872 C.set(0, quadC.get(1)); // C.x = Quad->C.y; 1873 D.set(0, quadD.get(1)); // D.x = Quad->D.y; 1874 M.set(0, hit.get(1)); // M.x = hit.y 1875 } else { 1876 A.set(0, quadA.get(0)); // A.x = Quad->A.x; 1877 B.set(0, quadB.get(0)); // B.x = Quad->B.x; 1878 C.set(0, quadC.get(0)); // C.x = Quad->C.x; 1879 D.set(0, quadD.get(0)); // D.x = Quad->D.x; 1880 M.set(0, hit.get(0)); // M.x = hit.x; 1881 } 1882 1883 if (largestComp == 2) { // Z 1884 A.set(1, quadA.get(1)); // A.y = Quad->A.y; 1885 B.set(1, quadB.get(1)); // B.y = Quad->B.y; 1886 C.set(1, quadC.get(1)); // C.y = Quad->C.y; 1887 D.set(1, quadD.get(1)); // D.y = Quad->D.y; 1888 M.set(1, hit.get(1)); // M.y = Hit->Point.y; 1889 1890 } else { 1891 A.set(1, quadA.get(2)); // A.y = Quad->A.z; 1892 B.set(1, quadB.get(2)); // B.y = Quad->B.z; 1893 C.set(1, quadC.get(2)); // C.y = Quad->C.z; 1894 D.set(1, quadD.get(2)); // D.y = Quad->D.z; 1895 M.set(1, hit.get(2)); // M.y = Hit->Point.z; 1896 } 1897 1898 Vector AB = B.minus(A).toGeomVector(); 1899 Vector BC = C.minus(B).toGeomVector(); 1900 Vector CD = D.minus(C).toGeomVector(); 1901 Vector AD = D.minus(A).toGeomVector(); 1902 Vector AE = CD.plus(AB).opposite(); // AE = DC - AB = DA - CB 1903 Vector AM = M.minus(A).toGeomVector(); 1904 1905 if (abs(determinant2D(AB, CD)) < SQRT_EPS) { 1906 // case AB // CD 1907 Vector vec = AB.minus(CD); 1908 v = determinant2D(AM, vec) / determinant2D(AD, vec); 1909 if ((v >= -SQRT_EPS) && (v <= 1.0 + SQRT_EPS)) { 1910 double b = determinant2D(AB, AD) - determinant2D(AM, AE); 1911 double c = determinant2D(AM, AD); 1912 u = abs(b) <= EPS ? -1 : c / b; 1913 isInside = ((u >= -SQRT_EPS) && (u <= 1.0 + SQRT_EPS)); 1914 } 1915 1916 } else if (abs(determinant2D(BC, AD)) < SQRT_EPS) { 1917 // case AD // BC 1918 Vector vec = AD.plus(BC); 1919 u = determinant2D(AM, vec) / determinant2D(AB, vec); 1920 if ((u >= -SQRT_EPS) && (u <= 1.0 + SQRT_EPS)) { 1921 double b = determinant2D(AD, AB) - determinant2D(AM, AE); 1922 double c = determinant2D(AM, AB); 1923 v = abs(b) <= EPS ? -1 : c / b; 1924 isInside = ((v >= -SQRT_EPS) && (v <= 1.0 + SQRT_EPS)); 1925 } 1926 1927 } else { 1928 // General case 1929 double a = determinant2D(AB, AE); 1930 double c = -determinant2D(AM, AD); 1931 double b = determinant2D(AB, AD) - determinant2D(AM, AE); 1932 a = -0.5 / a; 1933 b *= a; 1934 c *= (a + a); 1935 double SqrtDelta = b * b + c; 1936 if (SqrtDelta >= 0.0) { 1937 SqrtDelta = sqrt(SqrtDelta); 1938 u = b - SqrtDelta; 1939 if ((u < -SQRT_EPS) || (u > 1.0 + SQRT_EPS)) // to choose u between 0 and 1 1940 u = b + SqrtDelta; 1941 if ((u >= -SQRT_EPS) && (u <= 1.0 + SQRT_EPS)) { 1942 v = AD.getValue(0) + u * AE.getValue(0); 1943 if (abs(v) <= EPS) 1944 v = (AM.getValue(1) - u * AB.getValue(1)) / (AD.getValue(1) + u * AE.getValue(1)); 1945 else 1946 v = (AM.getValue(0) - u * AB.getValue(0)) / v; 1947 isInside = ((v >= -SQRT_EPS) && (v <= 1.0 + SQRT_EPS)); 1948 } 1949 } 1950 } 1951 1952 } finally { 1953 StackContext.exit(); 1954 } 1955 1956 if (isInside && nonNull(sOut) && sOut.getPhyDimension() > 1) { 1957 sOut.setValue(0, u); 1958 sOut.setValue(1, v); 1959 } 1960 1961 return isInside; 1962 } 1963 1964 private static int largestComponent(GeomVector A) { 1965 int maxDim = 0; 1966 double maxValue = -1; 1967 int numDims = A.getPhyDimension(); 1968 for (int i = 0; i < numDims; ++i) { 1969 double v = abs(A.getValue(i)); 1970 if (v > maxValue) { 1971 maxValue = v; 1972 maxDim = i; 1973 } 1974 } 1975 return maxDim; 1976 } 1977 1978 private static double determinant2D(Vector A, Vector B) { 1979 return A.getValue(Vector.X) * B.getValue(Vector.Y) - A.getValue(Vector.Y) * B.getValue(Vector.X); 1980 } 1981 1982 /** 1983 * Test to see if an infinite plane intersects a quadrilateral panel. 1984 * 1985 * @param P0 A reference point in the plane to test for intersection with a 1986 * quadrilateral panel. May not be null. 1987 * @param nhat The unit normal vector for the plane. May not be null. 1988 * @param quadA 1st corner point of a quadrilateral panel. May not be null. 1989 * @param quadB 2nd corner point in a counter-clockwise direction. May not be null. 1990 * @param quadC 3rd corner point in a counter-clockwise direction. May not be null. 1991 * @param quadD 4th corner point in a counter-clockwise direction. May not be null. 1992 * @return <code>true</code> if the plane and quadrilateral panel intersect. 1993 */ 1994 public static boolean planeQuadIntersect(GeomPoint P0, GeomVector<Dimensionless> nhat, 1995 GeomPoint quadA, GeomPoint quadB, GeomPoint quadC, GeomPoint quadD) { 1996 1997 int numDims = maxPhyDimension(P0, nhat, quadA, quadB, quadC, quadD); 1998 1999 StackContext.enter(); 2000 try { 2001 // Convert everything to the same physical dimensions and units. 2002 Unit<Length> unit = quadA.getUnit(); 2003 P0 = P0.toDimension(numDims).to(unit); 2004 nhat = nhat.toDimension(numDims); 2005 quadA = quadA.toDimension(numDims).to(unit); 2006 quadB = quadB.toDimension(numDims).to(unit); 2007 quadC = quadC.toDimension(numDims).to(unit); 2008 quadD = quadD.toDimension(numDims).to(unit); 2009 2010 boolean intersects = false; 2011 2012 // Edge 1 2013 Parameter<Length> d0 = pointPlaneDistance(quadA, P0, nhat); 2014 Parameter<Length> d1 = pointPlaneDistance(quadB, P0, nhat); 2015 double d0v = d0.getValue(); 2016 if (d0v * d1.getValue() < 0) { 2017 // An intersection on this patch edge exists. 2018 intersects = true; 2019 2020 } else { 2021 // Edge 4 2022 Parameter<Length> d2 = pointPlaneDistance(quadD, P0, nhat); 2023 if (d0v * d2.getValue() < 0) { 2024 // An intersection on this patch edge exists. 2025 intersects = true; 2026 2027 } else { 2028 // Edge 3 2029 d0 = GeomUtil.pointPlaneDistance(quadC, P0, nhat); 2030 d0v = d0.getValue(); 2031 if (d0v * d2.getValue() < 0) { 2032 intersects = true; 2033 } else { 2034 // Edge 2 2035 if (d0v * d1.getValue() < 0) { 2036 intersects = true; 2037 } 2038 } 2039 } 2040 } 2041 2042 return intersects; 2043 2044 } finally { 2045 StackContext.exit(); 2046 } 2047 } 2048 2049 /** 2050 * <p> 2051 * Method that detects corners in a a list of 2D points which represent a planar 2052 * curve. The input array "sValues" is filled in with the sharpness of a corner 2053 * detected at each point. If no corner is detected at a point, that entry in 2054 * "sValues" is filled in with Math.PI. The opening angle of a triangle fit to each 2055 * corner can be calculated as: <code>alpha[i] = Math.PI - sValues[i];</code>. 2056 * </p> 2057 * 2058 * @param points A list of at least 2D GeomPoint objects making up a planar curve. If 2059 * the input points are greater than 2D, then the higher dimensions are 2060 * simply ignored. 2061 * @param sValues An existing array of doubles of size points.size(). The sharpness 2062 * (in radians) of a corner applied to each point will be stored here. 2063 * @param dmin The minimum distance to consider (any points closer together than 2064 * this are ignored. 2065 * @param dmax The maximum distance to consider (any points further apart than this 2066 * are ignored). 2067 * @param amax The maximum opening angle to consider as a corner. 2068 * @return A count of the number of corners (sharper than <code>amax</code>) detected 2069 * in the curve. 2070 */ 2071 public static int detectCorners(List<? extends GeomPoint> points, double[] sValues, 2072 Parameter<Length> dmin, Parameter<Length> dmax, Parameter<Angle> amax) { 2073 /* 2074 Uses the IPAN99 algorithm described in: Chetverinkov, D., Szabo, Z., 2075 "A Simple and Efficient Algorithm for Detection of High Curvature Points 2076 in Planar Curves", Proc. 23rd Workshop of Austrian Pattern Recognition Group, 2077 Steyr, pp. 175-184, 1999. 2078 */ 2079 2080 // Convert the input geometry to 2D. 2081 PointString pointsLst; 2082 if (points instanceof PointString) 2083 pointsLst = (PointString)points; 2084 else { 2085 pointsLst = PointString.newInstance(); 2086 pointsLst.addAll(points); 2087 } 2088 if (pointsLst.getPhyDimension() < 2) { 2089 throw new IllegalArgumentException( 2090 MessageFormat.format(RESOURCES.getString("dimensionNotAtLeast2"), 2091 "input points", pointsLst.getPhyDimension())); 2092 } 2093 pointsLst = pointsLst.toDimension(2); 2094 2095 // Extract some things we need. 2096 Unit<Length> units = pointsLst.getUnit(); 2097 double dminv = dmin.getValue(units); 2098 double dminv2 = dminv*dminv; 2099 double dmaxv = dmax.getValue(units); 2100 double dmaxv2 = dmaxv*dmaxv; 2101 double amaxv = amax.getValue(SI.RADIAN); 2102 2103 // Loop over all the points in this curve and 2104 // calculate the opening angle of the sharpest triangle 2105 // that will fit the curve at that point. 2106 int numPntsm1 = pointsLst.size() - 1; 2107 for (int j = 1; j < numPntsm1; ++j) { 2108 // Return the sharpness of the sharpest triangle fit 2109 double sharpness = fitTriangle(pointsLst, j, dminv2, dmaxv2, amaxv); 2110 sValues[j] = sharpness; 2111 } 2112 2113 // Remove spurious corners. 2114 int cornerCount = removeAdjacentCorners(pointsLst, sValues, dminv2); 2115 2116 return cornerCount; 2117 } 2118 2119 /** 2120 * Method that fits a series of triangles to a list of points 2121 * at the specified index and returns the sharpness of the sharpest 2122 * triangle. 2123 * 2124 * @param points A list of 2D GeomPoint objects making up a planar curve. 2125 * @param ip Index to the point for the tip of the triangle. Can not be 2126 * outside range 1 to points.length-2. 2127 * @param dmin2 The minimum distance to consider squared. 2128 * @param dmax2 The maximum distance to consider squared. 2129 * @param amax The maximum opening angle to consider (in radians). 2130 * @return The sharpness of the sharpest triangle found is returned (radians). 2131 */ 2132 private static double fitTriangle(PointString<GeomPoint> points, int ip, double dmin2, double dmax2, double amax) { 2133 double sharpest = Math.PI; 2134 2135 GeomPoint p = points.get(ip); 2136 int ipm = ip - 1; 2137 int ipp = ip + 1; 2138 boolean firstPass = true; 2139 boolean notDone = true; 2140 while (notDone) { 2141 // Get end points of the triangle. 2142 GeomPoint pm = points.get(ipm); 2143 GeomPoint pp = points.get(ipp); 2144 double a2 = p.distanceSqValue(pp); 2145 double b2 = p.distanceSqValue(pm); 2146 2147 // Check criteria. 2148 boolean c2 = (a2 < dmax2) || firstPass; 2149 boolean c4 = (b2 < dmax2) || firstPass; 2150 if (c2 && c4 && a2 > dmin2 && b2 > dmin2) { 2151 firstPass = false; 2152 2153 // Calculate the opening angle of the triangle. 2154 double alpha = calcAlpha(a2, b2, pp.distanceSqValue(pm)); 2155 2156 if (alpha < amax) { 2157 // Calculate sharpness for this triangle. 2158 double sharpness = Math.PI - Math.abs(alpha); 2159 if (sharpness < sharpest) 2160 sharpest = sharpness; 2161 } else 2162 notDone = false; 2163 } 2164 2165 // If we are not further away than the tolerance, then try another triangle. 2166 if (c2 && c4) { 2167 ++ipp; 2168 --ipm; 2169 if (ipp > points.size() - 1) 2170 ipp = points.size() - 1; 2171 if (ipm < 0) 2172 notDone = false; 2173 } else { 2174 notDone = false; 2175 } 2176 } 2177 2178 return sharpest; 2179 } 2180 2181 /** 2182 * Method that calculates a triangle opening angle (the angle at the tip of 2183 * the triangle that has legs of length "a", and "b" and a base of length 2184 * "c"). Returns the opening angle in radians. 2185 */ 2186 private static double calcAlpha(double a2, double b2, double c2) { 2187 double ab = Math.sqrt(a2 * b2); 2188 double den = 2 * ab; 2189 double num = a2 + b2 - c2; 2190 return Math.acos(num / den); 2191 } 2192 2193 /** 2194 * Method that removes adjacent corners. It searches through the provided array of 2195 * corners and removes any that are closer together than <code>dmin2</code>. 2196 * 2197 * @param points A list of 2D GeomPoint objects representing the points in a planar curve. 2198 * @param sValues An array of sharpness values for each point in the point list 2199 * (radians). 2200 * @param tol2 The square of the tolerance to use on determining if corners are 2201 * adjacent. 2202 * @return A count of the number of corners remaining in the curve. 2203 */ 2204 private static int removeAdjacentCorners(PointString<GeomPoint> points, double[] sValues, double tol2) { 2205 int cornerCount = 0; 2206 int numPntsm1 = points.size() - 1; 2207 2208 for (int j = 1; j < numPntsm1; ++j) { 2209 double sharpness = sValues[j]; 2210 if (sharpness < Math.PI) { 2211 // Potential corner found. 2212 ++cornerCount; 2213 2214 // Does it have any sharper neighbors. 2215 int pp = j + 1; 2216 GeomPoint pointj = points.get(j); 2217 2218 while (pp < numPntsm1) { 2219 // Make sure we aren't to far away from "j". 2220 double d2 = pointj.distanceSqValue(points.get(pp)); 2221 if (d2 >= tol2) 2222 break; 2223 2224 if (sValues[pp] < sharpness) { 2225 // Sharper neighbor found, discard corner "j". 2226 sValues[j] = Math.PI; 2227 --cornerCount; 2228 break; 2229 2230 } else // If it is not sharper than "j", then discard it. 2231 sValues[pp] = Math.PI; 2232 2233 // Move on to the next point. 2234 ++pp; 2235 } 2236 } 2237 } 2238 2239 return cornerCount; 2240 } 2241 2242 /** 2243 * Return an ordered list of 2D points that represent the convex hull of a 2244 * collection of unordered 2D points. 2245 * 2246 * @param points A collection of 2D points to find the convex hull of. If 2247 * the input points have dimensions > 2, then only the 1st two dimensions 2248 * are used (higher dimensions are dropped). 2249 * @return The ordered 2D points representing the convex hull of the input 2250 * collection of points. 2251 */ 2252 public static PointString<Point> convexHull(Collection<? extends GeomPoint> points) { 2253 if (points.isEmpty()) 2254 return PointString.newInstance(); 2255 2256 // Convert the GeomSS points into a set of apache commons 2D vectors. 2257 Unit<Length> units = null; 2258 boolean firstPass = true; 2259 Collection<Vector2D> points2d = new ArrayList(); 2260 for (GeomPoint p : points) { 2261 if (firstPass) { 2262 firstPass = false; 2263 units = p.getUnit(); 2264 } 2265 Vector2D v2d = new Vector2D(p.getValue(Point.X, units), p.getValue(Point.Y, units)); 2266 points2d.add(v2d); 2267 } 2268 2269 // Create a convex hull from the 2D points. 2270 points2d = AklToussaintHeuristic.reducePoints(points2d); 2271 MonotoneChain mchain = new MonotoneChain(); 2272 List<Vector2D> vertices2d = new ArrayList(mchain.findHullVertices(points2d)); 2273 2274 // Convert the 2D apache commons verticies into 2D GeomSS Point objects. 2275 PointString<Point> chullPnts = PointString.newInstance(); 2276 for (Vector2D v2d : vertices2d) { 2277 chullPnts.add(Point.valueOf(v2d.getX(),v2d.getY(),units)); 2278 } 2279 2280 return chullPnts; 2281 } 2282 2283 /** 2284 * Return a list of strings of ordered 2D points that represent the 2D 2285 * convex hulls of each input collection of 2D points. The convex hull for 2286 * each collection of points in the list will be run concurrently. 2287 * 2288 * @param listOfPointCollections The list of collections of 2D points to 2289 * find the convex hull of. If the input points have dimensions > 2, then 2290 * only the 1st two dimensions are used (higher dimensions are dropped). 2291 * @return A list of strings of ordered 2D points that represent the convex 2292 * hull of the input collections of points. 2293 */ 2294 public static GeomList<PointString<Point>> convexHullList(List<Collection<? extends GeomPoint>> listOfPointCollections) { 2295 if (listOfPointCollections.isEmpty()) 2296 return GeomList.newInstance(); 2297 2298 // Create an array to hold the output strings of points. 2299 PointString<Point>[] outputArr = new PointString[listOfPointCollections.size()]; 2300 2301 // Use a ConcurrentContext to run each convex hull in parallel. 2302 ConcurrentContext.enter(); 2303 try { 2304 int size = listOfPointCollections.size(); 2305 for (int i=0; i < size; ++i) { 2306 Collection<? extends GeomPoint> pointCollection = listOfPointCollections.get(i); 2307 2308 // Run each convex hull concurrently on its own thread. 2309 Runnable runner = new ConvexHullRunner(pointCollection, outputArr, i); 2310 ConcurrentContext.execute(runner); 2311 } 2312 } finally { 2313 ConcurrentContext.exit(); 2314 } 2315 2316 // Convert the outputs from an array to a GeomList. 2317 GeomList<PointString<Point>> output = GeomList.newInstance(); 2318 output.addAll(outputArr); 2319 2320 return output; 2321 } 2322 2323 /** 2324 * A Runnable for use with computing the convex hull of a collection of 2D points. 2325 */ 2326 private static class ConvexHullRunner implements Runnable { 2327 private final Collection<? extends GeomPoint> points; 2328 private final PointString<Point>[] output; 2329 private final int idx; 2330 2331 public ConvexHullRunner(Collection<? extends GeomPoint> points, 2332 PointString<Point>[] outputArr, int outIdx) { 2333 this.points = points; 2334 this.output = outputArr; 2335 this.idx = outIdx; 2336 } 2337 2338 @Override 2339 public void run() { 2340 // Run the convex hull on this collection of points and store the outputs. 2341 output[idx] = convexHull(points); 2342 } 2343 } 2344}