拉拉杂杂的数学算法实现
二维数学算法实现
二维-向量法计算延长后的新点
思路:计算延长线方向的单位向量
/**
* 根据向量计算延长后的新点,起点为 p1,终点为 p2,延长距离为 distance
* @param {Feature<Point>} p1 起点
* @param {Feature<Point>} p2 终点
* @param {number} distance 延长距离
* @returns 延长后的新点
*/
function getDestinationByVector(p1: Feature<Point>, p2: Feature<Point>, distance: number): Position {
// 计算 p1->p2 方向延长 distance 后的新点
const p1Coords = getCoords(p1);
const p2Coords = getCoords(p2);
// p1->p2,计算方向向量 V 的分量
const deltaX = p2Coords[0] - p1Coords[0];
const deltaY = p2Coords[1] - p1Coords[1];
// 计算向量的长度(模)的平方
const length = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// 健壮性检查:如果 p1 和 p2 是同一个点,长度为0,无法确定方向。
// 在这种情况下,无法延长,直接返回 p2。
if (length === 0) {
return p2Coords; // 返回 p2 的一个副本
}
// 计算单位向量 U 的分量
const unitX = deltaX / length;
const unitY = deltaY / length;
// 计算延长后的新点
const newX = p2Coords[0] + unitX * distance;
const newY = p2Coords[1] + unitY * distance;
return [newX, newY];
}二维-计算两点之间的方位角
/**
* 计算两点之间的方位角
* @param p1 起点
* @param p2 终点
* @returns 方位角
*/
function get2DBearing(p1: Feature<Point>, p2: Feature<Point>): number {
const p1Coords = getCoords(p1);
const p2Coords = getCoords(p2);
const deltaX = p2Coords[0] - p1Coords[0];
const deltaY = p2Coords[1] - p1Coords[1];
// 使用 atan2 计算从X轴正方向(东方)逆时针的弧度
const angleRad = Math.atan2(deltaY, deltaX);
// 将弧度转换为度
const angleDeg = angleRad * (180 / Math.PI);
// 将数学角度转换为地理方位角(0度为正北,顺时针)
// 90 - angleDeg 是关键转换
let bearing = 90 - angleDeg;
// 标准化结果到 0-360 范围
if (bearing < 0) {
bearing += 360;
}
return bearing;
}二维-求两线的交点坐标
函数实现基于线性代数和参数方程。
思路
第一步:将线段表示为参数方程。
我们可以用参数 t 和 s 来分别表示线段 A 和线段 B 上的任意一点。t 和 s 的范围在 [0, 1] 之间时,点就落在线段上。
- 线段 A (
p1->p2):- 起点:
p1 = (x1, y1) - 方向向量:
vecA = (x2 - x1, y2 - y1) - 参数方程:
P = p1 + t * vecA - 展开成坐标形式:
x = x1 + t * (x2 - x1)y = y1 + t * (y2 - y1)
t = 0对应p1,t = 1对应p2。
- 起点:
- 线段 B (
p3->p4):- 起点:
p3 = (x3, y3) - 方向向量:
vecB = (x4 - x3, y4 - y3) - 参数方程:
Q = p3 + s * vecB - 展开成坐标形式:
x = x3 + s * (x4 - x3)y = y3 + s * (y4 - y3)
s = 0对应p3,s = 1对应p4。
- 起点:
第二步:求解交点
如果两条线段相交,那么必定存在一个点 I,它同时在两条线段上。这意味着在某个特定的 t 和 s 值下,P(t) 和 Q(s) 的坐标完全相等。
于是,我们得到了一个二元一次方程组:
x1 + t * (x2 - x1) = x3 + s * (x4 - x3)y1 + t * (y2 - y1) = y3 + s * (y4 - y3)
现在,我们的目标就是求解这个方程组中的未知数 t 和 s。
推导 t 和 s 的公式(解析代码中的变量)
这是一个标准的线性方程组,我们可以使用代数方法(比如消元法)来求解。
- 整理方程组:
t * (x2 - x1) - s * (x4 - x3) = x3 - x1t * (y2 - y1) - s * (y4 - y3) = y3 - y1
- 这个方程组可以用克莱姆法则来解。我们需要计算一个分母行列式
D。这个D也就是代码中的denominator。
D = (y2 - y1) * (x4 - x3) - (x2 - x1) * (y4 - y3) 这在代码中对应:
const denominator = (y2 - y1) * (x4 - x3) - (x2 - x1) * (y4 - y3);
- 判断是否相交(平行或重合):
- 如果
D = 0,这意味着什么?从几何上看,它表示两条线段的方向向量vecA和vecB是平行的(叉积为零)。平行线段可能永不相交,也可能完全重合。 - 代码中没有处理重合的情况(这会更复杂),而是简单地认为只要
D接近于 0,就不存在唯一交点。 if (Math.abs(denominator) < EPS):这里使用一个很小的常量EPS(Epsilon) 来进行浮点数比较,这是处理浮点数精度问题的标准做法,避免因微小计算误差导致误判。- 如果是平行线,直接返回
null。
- 如果
- 计算
t和s的分子:- 如果
D不为 0,我们就可以继续求解t和s。根据克莱姆法则,我们可以推导出t和s的分子Nt和Ns。 - 推导
t的分子Nt:Nt = (x4 - x3) * (y3 - y1) - (y4 - y3) * (x3 - x1)这在代码中对应tNumera(t 的 numerator, 分子):const tNumera = (x4 - x3) * (y3 - y1) - (y4 - y3) * (x3 - x1); - 推导
s的分子Ns:Ns = (x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1)这在代码中对应sNumera(s 的 numerator, 分子):const sNumera = (x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1);
- 如果
- 最终求得
t和s的值:const t = tNumera / denominator; const s = sNumera / denominator;
第三步:判断交点是否在线段上
我们现在得到了 t 和 s 的值。请记住,t 和 s 是我们参数方程中的参数。
- 如果
t的值在[0, 1]区间内,说明交点在线段 A 上。 - 如果
s的值在[0, 1]区间内,说明交点在线段 B 上。
只有当 t 和 s 同时在 [0, 1] 区间内时,这个交点才是两条线段的交点,而不仅仅是两条直线的交点。
if (t >= 0 && t <= 1 && s >= 0 && s <= 1) {
// ... 计算返回交点坐标 ...
}
return null;第四步:计算并返回交点坐标
一旦我们确认 t 和 s 都在有效范围内,我们就可以用其中任意一个参数方程来计算出交点的坐标。代码中选择了用线段 A 的方程和参数 t 来计算。
从 x = x1 + t * (x2 - x1) 和 y = y1 + t * (y2 - y1),我们直接得到最终的返回值:
return [x1 + t * (x2 - x1), y1 + t * (y2 - y1)];| 代码步骤 | 对应的逻辑和数学操作 |
|---|---|
| 解构坐标 | 从输入点中提取 x, y 坐标,提高代码可读性。 |
计算 denominator | 计算方向向量的叉积,判断两条线段是否平行。 |
| 判断平行 | 如果 denominator 接近 0,说明线段平行或重合,无唯一交点,返回 null。 |
计算 t 和 s 的分子 | 为计算参数 t 和 s 做准备,本质上是求解线性方程组。 |
求解 t 和 s | t = Nt / D, s = Ns / D。 |
判断 t 和 s 的范围 | 这是最关键的一步——判断直线交点是否落在两个线段的实际范围内。 |
| 计算并返回交点 | 如果满足条件,利用 t (或 s) 和参数方程计算出最终的交点坐标并返回。 |
返回 null | 如果交点不在线段上,返回 null 表示无交点。 |
/**
* 求点1、点2 和 点3、店4组成的线段的交点坐标
* @param {point} p1
* @param {point} p2
* @param {point} p3
* @param {point} p4
* @returns point
*/
function getIntersection(p1: Position, p2: Position, p3: Position, p4: Position) {
const x1 = p1[0],
y1 = p1[1],
x2 = p2[0],
y2 = p2[1],
x3 = p3[0],
y3 = p3[1],
x4 = p4[0],
y4 = p4[1];
const denominator = (y2 - y1) * (x4 - x3) - (x2 - x1) * (y4 - y3);
if (Math.abs(denominator) < EPS) return null;
const tNumera = (x4 - x3) * (y3 - y1) - (y4 - y3) * (x3 - x1);
const sNumera = (x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1);
const t = tNumera / denominator;
const s = sNumera / denominator;
if (t >= 0 && t <= 1 && s >= 0 && s <= 1) {
return [x1 + t * (x2 - x1), y1 + t * (y2 - y1)];
}
return null;
}二维-求垂直于两点的方向向量(单位法向量)
三角函数
// EPS 通常是一个极小的值(如 1e-6 或 Number.EPSILON),用于处理浮点数的精度问题。
const EPS = 1e-10;
/**
* 求垂直于两点的方向向量(单位法向量)
* @param {point} p1
* @param {point} p2
* @returns number[]
*/
function getNormal(p1, p2) {
// 特殊情况处理:垂直线
if (Math.abs(p1[0] - p2[0]) < EPS) {
// 返回一个指向右方的单位向量
return [1, 0];
}
// 特殊情况处理:水平线
if (Math.abs(p1[1] - p2[1]) < EPS) {
// 返回一个指向上方的单位向量
return [0, 1];
}
const k = (p1[1] - p2[1]) / (p1[0] - p2[0]); // 斜率
// 计算单位法向量
return [k / Math.sqrt(Math.pow(k, 2) + 1), -1 / Math.sqrt(Math.pow(k, 2) + 1)];
}向量法
/**
* 求垂直于两点的方向向量(单位法向量)— 向量法
* @param {point} p1
* @param {point} p2
* @returns number[]
*/
function getNormalVector(p1, p2) {
// p1->p2
const dx = p2[0] - p1[0];
const dy = p2[1] - p1[1];
// 法向量是 [-dy, dx],然后进行单位化
// 计算向量的长度(模)的平方
const lenSq = dx * dx + dy * dy;
// 如果两点非常接近或重合,无法定义一个唯一的法线
// 使用一个极小值来避免浮点数精度问题
if (lenSq < 1e-10) {
return null;
}
const len = Math.sqrt(lenSq);
// 返回其中一个法线方向,另一个方向是 [dy/len, -dx/len]
// 法向量 n 始终在前进向量 v 的逆时针方向
return [-dy / len, dx / len];
}二维-将两条线拼接成一个多边形
/**
* 将两条线拼接成一个多边形,采用 Turf 库计算
* @param {Feature<LineString>} line1 第一条线
* @param {Feature<LineString>} line2 第二条线
* @returns {Feature<Polygon>} 拼接后的多边形
*/
function linesToPolygon(
line1: LineString | Feature<LineString>,
line2: LineString | Feature<LineString>
): Feature<Polygon> {
const line1Coords = getCoords(line1);
const line2Coords = getCoords(line2);
if (line1Coords.length < 2 || line2Coords.length < 2) {
throw new Error("输入的线必须至少包含两个点。");
}
// 提取四个端点
const start1 = line1Coords[0];
const end1 = line1Coords[line1Coords.length - 1];
const start2 = line2Coords[0];
const end2 = line2Coords[line2Coords.length - 1];
// 计算所有端点组合的距离
const connections = [
{ case: "end1-start2", dist: distance(TurfInitPoint(end1), TurfInitPoint(start2)), p1: end1, p2: start2 },
{ case: "end1-end2", dist: distance(TurfInitPoint(end1), TurfInitPoint(end2)), p1: end1, p2: end2 },
{ case: "start1-start2", dist: distance(TurfInitPoint(start1), TurfInitPoint(start2)), p1: start1, p2: start2 },
{ case: "start1-end2", dist: distance(TurfInitPoint(start1), TurfInitPoint(end2)), p1: start1, p2: end2 },
];
// 找到距离最短的最佳连接方式
const bestConnection = connections.reduce((prev, curr) => (curr.dist < prev.dist ? curr : prev));
let orderedCoords: Position[] = [];
// 根据最佳连接方式,将两条线的坐标按顺序拼接,其中一条可能需要反转
switch (bestConnection.case) {
case "end1-start2":
// 连接 end1 和 start2, 封口 start1 和 end2
// 顺序: line1 -> line2 -> 封口
orderedCoords = [...line1Coords, ...line2Coords];
break;
case "end1-end2":
// 连接 end1 和 end2, 封口 start1 和 start2
// 顺序: line1 -> 反向line2 -> 封口
orderedCoords = [...line1Coords, ...[...line2Coords].reverse()];
break;
case "start1-start2":
// 连接 start1 和 start2, 封口 end1 和 end2
// 顺序: 反向line1 -> line2 -> 封口
orderedCoords = [...[...line1Coords].reverse(), ...line2Coords];
break;
case "start1-end2":
// 连接 start1 和 end2, 封口 end1 和 start2
// 顺序: line1 (从end1到start1) -> line2 (从start2到end2) -> 封口
// 这是一个更直接的拼接方式: line2 -> line1
orderedCoords = [...line2Coords, ...line1Coords];
break;
}
// 5. 闭合环:确保最后一个点与第一个点相同
const firstPoint = orderedCoords[0];
const lastPoint = orderedCoords[orderedCoords.length - 1];
if (firstPoint[0] !== lastPoint[0] || firstPoint[1] !== lastPoint[1]) {
orderedCoords.push(firstPoint);
}
// 6. 创建并返回 Polygon 特征
// turfPolygon 需要一个环数组的数组,我们这里只有一个外环
return TurfInitPolygon([orderedCoords]);
}