001/** 002 * Plane -- A concrete 2D plane in nD space. 003 * 004 * Copyright (C) 2009-2016, Joseph A. Huwaldt. All rights reserved. 005 * 006 * This library is free software; you can redistribute it and/or modify it under the terms 007 * of the GNU Lesser General Public License as published by the Free Software Foundation; 008 * either version 2.1 of the License, or (at your option) any later version. 009 * 010 * This library is distributed in the hope that it will be useful, but WITHOUT ANY 011 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 012 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. 013 * 014 * You should have received a copy of the GNU Lesser General Public License along with 015 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - 016 * Suite 330, Boston, MA 02111-1307, USA. Or visit: http://www.gnu.org/licenses/lgpl.html 017 */ 018package geomss.geom; 019 020import jahuwaldt.js.param.Parameter; 021import java.text.MessageFormat; 022import java.util.List; 023import java.util.Objects; 024import static java.util.Objects.requireNonNull; 025import javax.measure.converter.ConversionException; 026import javax.measure.quantity.Dimensionless; 027import javax.measure.quantity.Length; 028import javax.measure.unit.Unit; 029import javolution.context.ImmortalContext; 030import javolution.context.ObjectFactory; 031import javolution.lang.MathLib; 032import javolution.lang.ValueType; 033import javolution.util.FastTable; 034import javolution.xml.XMLFormat; 035import javolution.xml.stream.XMLStreamException; 036import org.apache.commons.math3.linear.MatrixUtils; 037import org.apache.commons.math3.linear.RealMatrix; 038import org.apache.commons.math3.linear.SingularValueDecomposition; 039import org.jscience.mathematics.number.Float64; 040import org.jscience.mathematics.vector.Float64Vector; 041 042/** 043 * A concrete 2D plane in n-dimensional space. 044 * 045 * <p> Modified by: Joseph A. Huwaldt </p> 046 * 047 * @author Joseph A. Huwaldt, Date: June 14, 2009 048 * @version September 12, 2016 049 */ 050@SuppressWarnings({"serial", "CloneableImplementsClone"}) 051public final class Plane extends GeomPlane implements ValueType { 052 053 // Smallest roundoff in quantities near 0, EPS, such that 0 + EPS > 0 054 private static final double EPS = Parameter.EPS; 055 private static final double SQRT_EPS = Parameter.SQRT_EPS; 056 057 /////////////////////// 058 // Factory creation. // 059 /////////////////////// 060 private Plane() { } 061 062 private static final ObjectFactory<Plane> FACTORY = new ObjectFactory<Plane>() { 063 @Override 064 protected Plane create() { 065 return new Plane(); 066 } 067 068 @Override 069 protected void cleanup(Plane obj) { 070 obj.reset(); 071 obj._n = null; 072 obj._const = null; 073 obj._refPoint = null; 074 } 075 }; 076 077 private static Plane copyOf(Plane original) { 078 Plane o = FACTORY.object(); 079 o._n = original._n.copy(); 080 o._const = original._const.copy(); 081 o._refPoint = original._refPoint.copy(); 082 original.copyState(o); 083 return o; 084 } 085 086 private static Plane newInstance() { 087 return FACTORY.object(); 088 } 089 090 /** 091 * A 3D plane that represents the YZ plane through the origin. 092 */ 093 private static final Plane YZ; 094 095 /** 096 * A 3D plane that represents the XZ plane through the origin. 097 */ 098 private static final Plane XZ; 099 100 /** 101 * A 3D plane that represents the XY plane through the origin. 102 */ 103 private static final Plane XY; 104 105 static { 106 ImmortalContext.enter(); 107 try { 108 YZ = Plane.newInstance(); 109 YZ._n = Vector.valueOf(1, 0, 0); 110 YZ._const = Parameter.ZERO_LENGTH; 111 YZ._refPoint = YZ.calcRefPoint(); 112 113 XZ = Plane.newInstance(); 114 XZ._n = Vector.valueOf(0, 1, 0); 115 XZ._const = Parameter.ZERO_LENGTH; 116 XZ._refPoint = XZ.calcRefPoint(); 117 118 XY = Plane.newInstance(); 119 XY._n = Vector.valueOf(0, 0, 1); 120 XY._const = Parameter.ZERO_LENGTH; 121 XY._refPoint = XY.calcRefPoint(); 122 } finally { 123 ImmortalContext.exit(); 124 } 125 } 126 127 /** 128 * Returns a 3D plane that represents the YZ plane through the origin. 129 * 130 * @return A 3D plane that represents the YZ plane through the origin. 131 */ 132 public static Plane getYZ() { 133 return YZ.copy(); 134 } 135 136 /** 137 * Return a 3D plane that represents the XZ plane through the origin. 138 * 139 * @return A 3D plane that represents the XZ plane through the origin. 140 */ 141 public static Plane getXZ() { 142 return XZ.copy(); 143 } 144 145 /** 146 * Return a 3D plane that represents the XY plane through the origin. 147 * 148 * @return A 3D plane that represents the XY plane through the origin. 149 */ 150 public static Plane getXY() { 151 return XY.copy(); 152 } 153 154 /** 155 * The normal vector for the plane. 156 */ 157 private GeomVector<Dimensionless> _n; 158 159 /** 160 * The plane offset. 161 */ 162 private Parameter<Length> _const; 163 164 /** 165 * A reference point for drawing this plane. 166 */ 167 private Point _refPoint; 168 169 /** 170 * Returns a <code>Plane</code> instance with the specified 3D plane equation values. 171 * The plane equation is: <code>A*x + B*y + C*z = D</code>. 172 * 173 * @param A The coefficient on the X axis term. 174 * @param B The coefficient on the Y axis term. 175 * @param C The coefficient on the Z axis term. 176 * @param D The constant term ((normal DOT p0) where p0 is a point in the plane). May 177 * not be null. 178 * @return the plane having the specified values. 179 */ 180 public static Plane valueOf(double A, double B, double C, Parameter<Length> D) { 181 requireNonNull(D); 182 Plane P = newInstance(); 183 Vector n = Vector.valueOf(A, B, C); 184 P._n = n.toUnitVector(); 185 P._const = D.divide(n.magValue()); 186 P._refPoint = P.calcRefPoint(); 187 P._n.setOrigin(P._refPoint); 188 return P; 189 } 190 191 /** 192 * Returns a <code>Plane</code> instance with the specified normal vector and plane 193 * equation constant. 194 * 195 * @param normal The unit normal of the plane. May not be null. 196 * @param constant The constant term of the plane equation ((normal DOT p0) where p0 197 * is a point in the plane). May not be null. 198 * @return the plane having the specified values. 199 * @throws DimensionException if the input normal vector is not at least 3 200 * dimensional. 201 */ 202 public static Plane valueOf(GeomVector<Dimensionless> normal, Parameter<Length> constant) { 203 requireNonNull(constant); 204 int numDims = normal.getPhyDimension(); 205 if (numDims < 3) 206 throw new DimensionException( 207 MessageFormat.format(RESOURCES.getString("dimensionNotAtLeast3"), 208 "input normal vector", numDims)); 209 210 Plane P = newInstance(); 211 P._n = normal.toUnitVector(); 212 P._const = constant; 213 P._refPoint = P.calcRefPoint(); 214 P._n.setOrigin(P._refPoint); 215 return P; 216 } 217 218 /** 219 * Returns a <code>Plane</code> instance with the specified normal vector and plane 220 * equation constant. 221 * 222 * @param normal The unit normal of the plane. May not be null. 223 * @param p A point in the plane (the reference point for the plane). May not be 224 * null. 225 * @return the plane having the specified values. 226 * @throws DimensionException if the input normal vector is not at least 3 227 * dimensional. 228 */ 229 public static Plane valueOf(GeomVector<Dimensionless> normal, Point p) { 230 int numDims = normal.getPhyDimension(); 231 if (numDims < 3) 232 throw new DimensionException( 233 MessageFormat.format(RESOURCES.getString("dimensionNotAtLeast3"), 234 "input normal vector", numDims)); 235 236 // Convert all the points to the highest dimension between them. 237 if (p.getPhyDimension() > numDims) 238 numDims = p.getPhyDimension(); 239 p = p.toDimension(numDims); 240 normal = normal.toDimension(numDims); 241 242 Plane P = newInstance(); 243 P._n = normal.toUnitVector(); 244 P._const = (Parameter<Length>)P._n.dot(p.toGeomVector()); 245 P._refPoint = p; 246 P._n.setOrigin(p); 247 248 return P; 249 } 250 251 /** 252 * Returns a <code>Plane</code> instance that contains the specified 3 independent 253 * points. 254 * 255 * @param p A point in the plane (not equal to one of the other two). May not be null. 256 * @param q A point in the plane (not equal to one of the other two). May not be null. 257 * @param r A point in the plane (not equal to one of the other two). May not be null. 258 * @return The plane containing the specified points. The plane will match the 259 * dimensions of the highest dimension point passed to this method. 260 * @throws DimensionException if one of the input points does not have at least 3 261 * physical dimensions. 262 * @throws IllegalArgumentException if any of the input points are equal to the 263 * others. 264 */ 265 public static Plane valueOf(GeomPoint p, GeomPoint q, GeomPoint r) throws DimensionException { 266 267 // Convert all the points to the highest dimension between them. 268 int numDims = GeomUtil.maxPhyDimension(p, q, r); 269 if (numDims < 3) 270 throw new DimensionException( 271 MessageFormat.format(RESOURCES.getString("dimensionNotAtLeast3"), 272 "input points", numDims)); 273 274 p = p.toDimension(numDims); 275 q = q.toDimension(numDims); 276 r = r.toDimension(numDims); 277 278 Vector<Length> PQ = q.minus(p).toGeomVector(); 279 Vector<Length> PR = r.minus(p).toGeomVector(); 280 if (PQ.magValue() < SQRT_EPS || PR.magValue() < SQRT_EPS) 281 throw new IllegalArgumentException(RESOURCES.getString("pointsEqualErr")); 282 283 Vector PQcrossPR = PQ.cross(PR); 284 if (PQcrossPR.magValue() < SQRT_EPS) 285 throw new IllegalArgumentException(RESOURCES.getString("pointsEqualErr")); 286 287 Point refP = p.immutable(); 288 Plane P = newInstance(); 289 P._n = PQcrossPR.toUnitVector(); 290 P._const = (Parameter<Length>)P._n.dot(refP.toGeomVector()); 291 P._refPoint = refP; 292 P._n.setOrigin(refP); 293 294 return P; 295 } 296 297 /** 298 * Returns a <code>Plane</code> instance that contains the specified triangle. 299 * 300 * @param tri A triangle that defines the plane. May not be null. 301 * @return The plane containing the specified triangle. The plane will match the 302 * dimensions of the triangle. 303 * @throws DimensionException if the input triangle does not have at least 3 304 * physical dimensions. 305 * @throws IllegalArgumentException if the triangle is degenerate (has two vertices 306 * equal to each other). 307 */ 308 public static Plane valueOf(GeomTriangle tri) throws DimensionException { 309 return valueOf(tri.getP1(), tri.getP2(), tri.getP3()); 310 } 311 312 /** 313 * Return a <code>Plane</code> instance that is fit through the input list of points 314 * resulting in the least-squared orthogonal error between the points and the plane. 315 * 316 * @param points The list of points to fit a plane to. 317 * @return A plane that represents the least squared error fit to the input points. 318 */ 319 public static Plane fitPoints(List<? extends GeomPoint> points) { 320 // Reference: http://mathforum.org/library/drmath/view/63765.html 321 322 int numPoints = points.size(); 323 if (numPoints < 3) 324 throw new IllegalArgumentException(MessageFormat.format( 325 RESOURCES.getString("planeNumPointsErr"), numPoints)); 326 if (numPoints == 3) 327 return Plane.valueOf(points.get(0), points.get(1), points.get(2)); 328 329 // Convert the input list of points to a PointString. 330 PointString<? extends GeomPoint> pnts; 331 if (points instanceof PointString) 332 pnts = (PointString)points; 333 else 334 pnts = PointString.valueOf(null, points); 335 int numDims = pnts.getPhyDimension(); 336 if (numDims < 3) 337 throw new IllegalArgumentException(MessageFormat.format( 338 RESOURCES.getString("dimensionNotAtLeast3"), "input points", numDims)); 339 340 // Get the units of the points. 341 Unit refUnit = pnts.getUnit(); 342 343 // Find the centroid of all the points. 344 Point centroid = pnts.getAverage(); 345 346 // Form a matrix of input points. 347 double[] tmpArr = new double[numDims]; 348 RealMatrix Mmat = MatrixUtils.createRealMatrix(numPoints, numDims); 349 for (int i = 0; i < numPoints; ++i) { 350 GeomPoint point = pnts.get(i).to(refUnit); 351 Mmat.setRow(i, point.toArray(tmpArr)); 352 } 353 354 // Calculate the singular value decomposition. 355 SingularValueDecomposition svd = new SingularValueDecomposition(Mmat); 356 357 // Get the matrix V of the decomposition. The columns of V are the singular vectors. 358 // They are sorted from the largest to the smallest singular value. 359 RealMatrix V = svd.getV(); 360 361 // The normal vector corresponds to the smallest singular value from SVD, which is always the last one. 362 Vector n = Vector.valueOf(V.getColumn(numDims - 1)); 363 364 // The best-fit plane is the one passing through the centroid with the 365 // calculated normal vector. 366 return Plane.valueOf(n, centroid); 367 } 368 369 /** 370 * Calculate and return an arbitrary reference point that is <i>any</i> point in this 371 * plane. 372 */ 373 private Point calcRefPoint() { 374 375 // Find a non-zero axis. 376 int dim = getPhyDimension(); 377 int axis = 0; 378 for (; axis < dim; ++axis) 379 if (MathLib.abs(_n.getValue(axis)) >= EPS) 380 break; 381 382 // Set all axes except the chosen one to zero and solve for the point. 383 Float64 v = Float64.valueOf(_const.getValue() / _n.getValue(axis)); 384 FastTable<Float64> values = FastTable.newInstance(); 385 386 for (int i = 0; i < dim; ++i) { 387 if (i != axis) 388 values.add(Float64.ZERO); 389 else 390 values.add(v); 391 } 392 393 Point p = Point.valueOf(Float64Vector.valueOf(values), _const.getUnit()); 394 395 return p; 396 } 397 398 /** 399 * Return an immutable version of this plane. 400 * 401 * @return An immutable version of this plane. 402 */ 403 @Override 404 public Plane immutable() { 405 return this; 406 } 407 408 /** 409 * Recycles a <code>Plane</code> instance immediately (on the stack when executing in 410 * a <code>StackContext</code>). 411 * 412 * @param instance The instance to be recycled. 413 */ 414 public static void recycle(Plane instance) { 415 FACTORY.recycle(instance); 416 } 417 418 /** 419 * Returns the number of physical dimensions of the geometry element. 420 * 421 * @return The number of physical dimensions of the geometry element. 422 */ 423 @Override 424 public int getPhyDimension() { 425 return _n.getPhyDimension(); 426 } 427 428 /** 429 * Return the equivalent of this plane converted to the specified number of physical 430 * dimensions. If the number of dimensions is greater than this element, then zeros 431 * are added to the additional dimensions. If the number of dimensions is less than 432 * this element, then the extra dimensions are simply dropped (truncated). If the new 433 * dimensions are the same as the dimension of this element, then this element is 434 * simply returned. 435 * 436 * @param newDim The dimension of the plane to return. 437 * @return A copy of this plane converted to the new dimensions. 438 */ 439 @Override 440 public Plane toDimension(int newDim) { 441 if (newDim < 3) 442 throw new DimensionException( 443 MessageFormat.format(RESOURCES.getString("dimensionNotAtLeast3"), 444 "input normal vector", newDim)); 445 int thisDim = this.getPhyDimension(); 446 if (newDim == thisDim) 447 return this; 448 449 Plane P = Plane.valueOf(getNormal().toDimension(newDim), getRefPoint().toDimension(newDim)); 450 return P; 451 } 452 453 /** 454 * Return the normal vector for the plane. The normal vector is a unit vector that is 455 * perpendicular to the plane. 456 * 457 * @return The normal vector for the plane. 458 */ 459 @Override 460 public GeomVector<Dimensionless> getNormal() { 461 return _n.copy(); 462 } 463 464 /** 465 * Return the constant term of the plane equation (e.g.: "D" for a 3D plane: 466 * <code>A*x + B*y + C*z = D</code>). 467 * 468 * @return The constant term of the plane equation for this plane. 469 */ 470 @Override 471 public Parameter<Length> getConstant() { 472 return _const; 473 } 474 475 /** 476 * Return the reference point for this plane. The reference point is an arbitrary 477 * point that is contained in the plane and is used as a reference when drawing the 478 * plane. 479 * 480 * @return The reference point for this plane. 481 */ 482 @Override 483 public Point getRefPoint() { 484 return _refPoint; 485 } 486 487 /** 488 * Returns the unit in which the geometry in this element are stated. 489 * 490 * @return The unit in which the geometry in this element are stated. 491 */ 492 @Override 493 public Unit<Length> getUnit() { 494 return _const.getUnit(); 495 } 496 497 /** 498 * Returns the equivalent to this element but stated in the specified unit. 499 * 500 * @param unit the length unit of the element to be returned. May not be null. 501 * @return an equivalent to this element but stated in the specified unit. 502 * @throws ConversionException if the the input unit is not a length unit. 503 */ 504 @Override 505 public Plane to(Unit<Length> unit) { 506 if (unit.equals(getUnit())) 507 return this; 508 Plane newPlane = Plane.valueOf(_n, _const.to(unit)); 509 //newPlane.setRefPoint(_refPoint.to(unit)); 510 copyState(newPlane); 511 return newPlane; 512 } 513 514 /** 515 * Returns a copy of this Plane instance 516 * {@link javolution.context.AllocatorContext allocated} by the calling thread 517 * (possibly on the stack). 518 * 519 * @return an identical and independent copy of this point. 520 */ 521 @Override 522 public Plane copy() { 523 return copyOf(this); 524 } 525 526 /** 527 * Return a copy of this object with any transformations or subranges removed 528 * (applied). 529 * 530 * @return A copy of this object with any transformations or subranges removed. 531 */ 532 @Override 533 public Plane copyToReal() { 534 return copy(); 535 } 536 537 /** 538 * Compares this Plane against the specified object for strict equality (same values 539 * and same units). 540 * 541 * @param obj the object to compare with. 542 * @return <code>true</code> if this object is identical to that object; 543 * <code>false</code> otherwise. 544 */ 545 @Override 546 public boolean equals(Object obj) { 547 if (this == obj) 548 return true; 549 if ((obj == null) || (obj.getClass() != this.getClass())) 550 return false; 551 552 Plane that = (Plane)obj; 553 return this._n.equals(that._n) 554 && this._const.equals(that._const) 555 && super.equals(obj); 556 } 557 558 /** 559 * Returns the hash code for this parameter. 560 * 561 * @return the hash code value. 562 */ 563 @Override 564 public int hashCode() { 565 return 31*super.hashCode() + Objects.hash(_n, _const); 566 } 567 568 /** 569 * Holds the default XML representation for this object. 570 */ 571 @SuppressWarnings("FieldNameHidesFieldInSuperclass") 572 protected static final XMLFormat<Plane> XML = new XMLFormat<Plane>(Plane.class) { 573 574 @Override 575 public Plane newInstance(Class<Plane> cls, InputElement xml) throws XMLStreamException { 576 return FACTORY.object(); 577 } 578 579 @Override 580 public void read(InputElement xml, Plane obj) throws XMLStreamException { 581 GeomPlane.XML.read(xml, obj); // Call parent read. 582 583 obj._n = (GeomVector<Dimensionless>)xml.getNext(); 584 obj._const = (Parameter)xml.getNext(); 585 586 obj._refPoint = obj._n.getOrigin(); 587 } 588 589 @Override 590 public void write(Plane obj, OutputElement xml) throws XMLStreamException { 591 GeomPlane.XML.write(obj, xml); // Call parent write. 592 593 xml.add(obj._n); 594 xml.add(obj._const); 595 596 } 597 }; 598 599}