1. 向量复习
因为本节需要向量来计算交点,如果大家对向量不是很熟悉,请见:向量复习(一)。已经很熟悉的童鞋,可以跳过这部分内容。
2. 思路解析
用向量求解两直线a和b的交点主要思路为:
- 用to left测试判断两直线是否有交点;
- 求解a两端点在b上的垂线长度,这个长度可以用向量组成的平行四边形来求解;
- 用相似三角形求解已知点到交点的向量;
- 已知点 + 向量 = 交点;
文字描述太难理解了,我们用一个图例来说明哦。现在我们给定两条直线和他们的两个端点,AB和CD,以及交点E:
如果我们要求E,运用向量的特性,我们把线段看成向量,那么有一个非常简单的思路:任意一个已知端点 + 该端点到交点的向量。比如选择C点,那么C + CE = E(加粗线段表示向量),因为我们在向量复习(一)中提到,向量的求解可以用两个坐标相减,那么已知一个向量和其中一个点,我们可以 点 + 向量 = 另一个点,如下图所示:
而且我们知道,CE和CD是有比例关系的,那么接下来我们需要思考如何求解这个比例1 / t,即CD : CE = 1 / t(后面我们会解释为什么会设成这样,而不是更一般的 CD : CE = t )。如果我们过C和D做AB的垂线,大家知道我们能得到什么信息呢:
没错你可能已经发现了,两条垂线所在的两个三角形为相似三角形:三角形DED’~ 三角形ECC’。相似的理由为:
- ∠DED’ = ∠C’EC;
- ∠DD’E = ∠EC’C;
即两个角相等的三角形相似。通过这里,我们知道只要我们求解d1和d2的比例,即可求得CE和CD的比例 1 / t。那么,现在问题转换成求解d1和d2的比例。怎么做呢?我们主要到,DD’和CC’为垂线,也就是说如果我们能找到以DD’和CC为’垂线的平行四边形,而且两个四边形的底都是AB,那么我们可以通过面积和底边来求解高,即垂线的长度,如下图所示:
到现在,我们构建了两个我们需要的平行四边形。还差最后一步:如何求得两个平行四边形的面积呢?这里我们就需要向量来帮忙了,不知道大家还记得向量的叉积么?向量叉积的模表示两个向量组成的平行四边形的面积。那么,我们可以引入两个向量:AD和AC,然后分别求AD x AB 和 AC x AB就可以求得两个平行四边形的面积,即平行四边形ABFD和平行四边形ACGB的面积。接下来就简单了,我们根据求得的d1和d2,去求解 1 / t;
3. 代码解析
讲解完思路,我们接着讲解一下代码,帮助大家梳理一下代码实现的思路。首先,我们先来看看整体的求交代码:
/**
* get line intersection point with vector, if exists
*
* Reference resource:
* https://blog.csdn.net/qq_40998706/article/details/87482435
*/
public static
Vector linesIntersect( Line line1, Line line2 ) {
// have intersection?
if ( !ifLinesIntersect( line1, line2 ) ) return null;
// yes, but the intersection point is one of the endpoints.
if ( isOverlapButHavingCommonEndPoint( line1, line2 ) )
return getOnlyCommonEndPoint( line1, line2 );
// yes, normal intersection point.
Vector base = line2.getVector();
double d1 = Math.abs( Vector.cross(
base, line1.startPoint.subtract( line2.startPoint ) ) );
double d2 = Math.abs( Vector.cross(
base, line1.endPoint.subtract( line2.startPoint ) ) );
assert !MyMath.equalZero( d1 + d2 );
double t = d1 / ( d1 + d2 );
Vector intersection = line1.getVector().multiply( t );
// the following commented-out code is correct as well,
// but with less computational accuracy because of ( 1 / t ),
// one additional division compared to the method above.
// double t = ( d1 + d2 ) / d1;
// Vector intersection = line1.endPoint.subtract(
// line1.startPoint ).multiply( 1 / t );
return line1.startPoint.add( intersection );
}
代码基本就是思路的直接实现,但这里需要注意两点:
1)判断是否有交点的方法
/**
* get the common endPoint, intersection as well,
* if two lines intersect at one of the endpoints
*/
private static
Vector getOnlyCommonEndPoint( Line line1, Line line2 ) {
if ( line1.startPoint.equalsXAndY( line2.endPoint ) ) return line1.startPoint;
return line1.endPoint;
}
/**
* if the two lines Overlap But Have Common EndPoint,
* but note that may only have one common endPoint.
*/
private static
boolean isOverlapButHavingCommonEndPoint( Line line1, Line line2 ) {
return Vector.sortByX( line2.startPoint, line2.endPoint ) > 0 &&
line1.startPoint.equalsXAndY( line2.endPoint ) ||
line1.endPoint.equalsXAndY( line2.startPoint );
}
/**
* toLeft test to check if two lines intersect
*/
private static
boolean ifLinesIntersect( Line line1, Line line2,
double res1, double res2 ) {
// parallel cases:
// case 1: overlap or on the same line.
if ( MyMath.equalZero( res1 ) &&
MyMath.equalZero( res2 ) )
return isOverlapButHavingCommonEndPoint( line1, line2 );
// case 2: parallel on the right side.
if ( res1 < 0 && res2 < 0 )
return false;
// case 3: parallel on the left side.
if ( res1 > 0 && res2 > 0 )
return false;
// intersecting cases: either intersect at
// a common point other than endpoints,
// or at one of the endpoints.
return true;
}
/**
* line1 and line2 intersects?
*/
public static
boolean ifLinesIntersect( Line line1, Line line2 ) {
if ( line1 == null || line2 == null ) return false;
// to left test based on line1.
double res1 = Triangle.areaTwo( line1.endPoint, line1.startPoint, line2.endPoint );
double res2 = Triangle.areaTwo( line1.endPoint, line1.startPoint, line2.startPoint );
// to left test based on line2.
double res3 = Triangle.areaTwo( line2.endPoint, line2.startPoint, line1.endPoint );
double res4 = Triangle.areaTwo( line2.endPoint, line2.startPoint, line1.startPoint );
// have intersection if and only if
// two endpoints of one line are
// at the opposite side of the other line.
boolean finalRes1 = ifLinesIntersect( line1, line2, res1, res2 );
boolean finalRes2 = ifLinesIntersect( line1, line2, res3, res4 );
return finalRes1 && finalRes2;
}
这里判断两直线是否有直线的方法为To Left测试,即判断另一条直线的两端点位于基准直线的哪一侧,如果两个端点都是异侧,则有交点,或交点为端点;其余情况都没有交点,即两线平行或重合(我们认为有无穷个交点的情况,不算有交点),但注意一种特殊情况:两线共线但有一个公共的端点(这个很特殊,很容易忽略!);
To Left测试会在计算几何中详细介绍,因为如果这里不检查是否有交点,直接用向量去算的话,如果两线平行(非重合),则会算出不存在的交点。大家在代码中的测试案例可以看到这样的情况。
// public static void testLinesIntersection()
System.out.println( linesIntersect( line7, line6 ) ); // null
如果觉得判断交点的代码太复杂的童鞋,可以跳过额,说实话,情况太多了,这里就不细说了,但可以参考一下注释哦。
2)为什么要设CD : CE = 1 / t,而不是CD : CE = t
以上图为例,我们先用这种方法算一下CD : CE,我们假设|CE| = m,|DE| = z,|CD| = l,如下图:
那么,我们可以得到下面的等式:
- l : m = 1 / t;
- z + m = l;
- d1 : d2 = m / z;
然后我们可以解得 t = d1 / d1 + d2,和代码里面是一样的。那么现在反过来,我们假设CD : CE = t,我们再用上面的等式解出t,t = ( d1 + d2 ) / d1,刚好和上面是反的。好像两者都可以用,但是我们注意一下这里求解交点向量的代码(公式):
Vector intersection = line1.getVector().multiply( t );
如果我们用的是CD : CE = t,那么这里就不是乘t,而是 1 / t 了,因为原来CE = t * CD,现在变成CE = CD / t。从数学的角度来看,两者都是没有什么问题的,但是从计算精度来看,这里多算了一次除法,会导致精度丢失,所以原来的假设更优。看到这里,不得不佩服原作者的设计思路哦,这一点很小,但很精妙。
最后我用代码中用的变量来标记上面的图例,方便大家理解。假设AB为line2,CD为line1:
最后需要提醒一点:严格来说,上面的代码应该算是线段和线段的交点,而不是直线和直线的交点,但是可以通过一点点的修改将原来的代码处理成直线和直线的交点,或者将直线处理成线段,然后求交,这些问题就留给大家“课后思考题”啦。如果有机会,我将会在计算几何 - BO算法中详细介绍哦~
接下来,我们将讲解另一种几何求交的情况:直线和圆的交点。
下一节:几何求交(二):直线和圆的交点
4. 附录:项目代码
1.1.3 Geometric Intersection
Description | Entry method\File |
---|---|
Line and line | Vector lineIntersect( Line l ) |
Segment and segment | Vector segmentIntersect( Line l ) |
Segment and Circle | Vector[] segmentCircle( Segment s, Circle c ) |
Line and Circle | Line lineCircleIntersect( Line line, Circle circle ) |
Brute Force | List<Vector> bruteForce( List<E> S ) |
Bentley Ottmann’s algrithom( Intersection Of segment, ray, line and Circle ) | List<EventPoint2D> findIntersection( List<IntersectionShape> S ) |
Program ( including visualization ) | CG2017 PA1-2 Crossroad |
5. 参考资料
6. 免责声明
※ 本文之中如有错误和不准确的地方,欢迎大家指正哒~
※ 此项目仅用于学习交流,请不要用于任何形式的商用用途,谢谢呢;