计算点到直线上的最近点(Cesium、Canvas演示)

250 阅读3分钟

前言

笔者最近接触到了一个功能场景,需要在有高度差且错综的巷道中(基于 Cesium 引擎)展示人员实时位置。但因为巷道宽度有限且人员定位有一定误差,这可能导致人员出现在巷道之外。因此需要对人员位置手动纠偏,也就是计算人员到巷道的最近点。在此记录计算过程。

在二维中计算

定义直线与待纠偏点的坐标

这里采用canvas中的坐标系,以左上角为原点。

const line = [
    [50, 50],
    [200, 250],
];
const point = [50, 150];

计算所需向量

  • vecStartToEnd\overrightarrow{vecStartToEnd} 线条起点到终点的向量

  • vecStartToPoint\overrightarrow{vecStartToPoint} 线条起点到待纠偏点的向量

const vecStartToEnd = [
    line[1][0] - line[0][0],
    line[1][1] - line[0][1],
]
const vecStartToPoint = [
    point[0] - line[0][0],
    point[1] - line[0][1],
]

归一化vecStartToEnd\overrightarrow{vecStartToEnd}

const len = Math.sqrt(vecStartToEnd[0] ** 2 + vecStartToEnd[1] ** 2);
vecStartToEnd[0] /= len;
vecStartToEnd[1] /= len;

计算vecStartToPoint\overrightarrow{vecStartToPoint}vecStartToEnd\overrightarrow{vecStartToEnd}上的投影长度

projba=abb|\text{proj}_{\boldsymbol{b}} \boldsymbol{a}| = \frac{|\vec{a} \cdot \vec{b}|}{|\vec{b}|}
projba=acosθ|\text{proj}_{\boldsymbol{b}} \boldsymbol{a}| =|\vec{a}| \cdot |\cos\theta|

计算向量投影长度的公式如上,其中avecStartToPointbvecStartToEndθa与b的夹角 此时已知向量的点积与模长,因此使用第一个公式。

vecStartToEnd\overrightarrow{vecStartToEnd}已在上一步归一化,因此直接计算两个向量的点积就可以得到a\overrightarrow{a}b\overrightarrow{b}上的投影长度:

const projectionLength = vecStartToPoint[0] * vecStartToEnd[0] + vecStartToPoint[1] * vecStartToEnd[1];

计算最近点(垂足)

此时已知投影长度vecStartToPoint\overrightarrow{vecStartToPoint}vecStartToEnd\overrightarrow{vecStartToEnd}上的投影长度)和单位向量(归一化后的vecStartToEnd\overrightarrow{vecStartToEnd}),具备了计算最近点的所有条件。

最近点坐标=投影长度×单位向量+线条起点坐标最近点坐标 = 投影长度 × 单位向量 + 线条起点坐标

其中投影长度 × 单位向量代表着把单位向量(长度 1)拉长 / 缩短到投影长度,得到从线起点到最近点的向量,再加上线条的起点便能得到最近点的坐标。

const nearestPoint = [
   vecStartToEnd[0] * projectionLength + line[0][0],
   vecStartToEnd[1] * projectionLength + line[0][1],
]

考虑最近点在直线延长线上的情况

至此计算已接近完成,最后还需考虑待纠偏点到直线上的垂线的垂足在直线延长线以外的情况。可以操作文章最前的示例,拖拽待纠偏点查看。

image.png

// 点F到直线起点的距离
let distancePointToStart = calculateDistance(line[0], nearestPoint);
// 点F到直线终点的距离
let distancePointToEnd = calculateDistance(line[1], nearestPoint);
// 直线长度
let lineDistance = calculateDistance(line[0], line[1]);
// 最近点在延长线上
if (Math.abs(lineDistance - (distancePointToStart + distancePointToEnd)) > 0.001) {
    if (distancePointToStart > distancePointToEnd) {//点F距离直线终点更近
        nearestPoint[0] = line[1][0];
        nearestPoint[1] = line[1][1];
    } else {//点F距离直线起点更近
        nearestPoint[0] = line[0][0];
        nearestPoint[1] = line[0][1];
    }
}
// 计算两点的欧几里得距离
function calculateDistance([x1, y1], [x2, y2]) {
    const dxSquared = (x2 - x1) ** 2;
    const dySquared = (y2 - y1) ** 2;
    return Math.sqrt(dxSquared + dySquared);
}

image.png

至此计算完成。

完整代码

const line = [
    [50, 50],
    [200, 250],
];
const point = [50, 150];
const vecStartToEnd = [
    line[1][0] - line[0][0],
    line[1][1] - line[0][1],
]
const vecStartToPoint = [
    point[0] - line[0][0],
    point[1] - line[0][1],
]
// 归一化vecStartToEnd
const len = Math.sqrt(vecStartToEnd[0] ** 2 + vecStartToEnd[1] ** 2);
vecStartToEnd[0] /= len;
vecStartToEnd[1] /= len;
// 计算vecStartToPoint在vecStartToEnd上的投影长度
const projectionLength = vecStartToPoint[0] * vecStartToEnd[0] + vecStartToPoint[1] * vecStartToEnd[1];
// 计算最近点
const nearestPoint = [
    line[0][0] + vecStartToEnd[0] * projectionLength,
    line[0][1] + vecStartToEnd[1] * projectionLength,
]
// 最近点到直线起点的距离
let distancePointToStart = calculateDistance(line[0], nearestPoint);
// 最近点到直线终点的距离
let distancePointToEnd = calculateDistance(line[1], nearestPoint);
// 直线长度
let lineDistance = calculateDistance(line[0], line[1]);
// 最近点在延长线上
if (Math.abs(lineDistance - (distancePointToStart + distancePointToEnd)) > 0.001) {
    if (distancePointToStart > distancePointToEnd) {//最近点距离直线终点更近
        nearestPoint[0] = line[1][0];
        nearestPoint[1] = line[1][1];
    } else {//最近点距离直线起点更近
        nearestPoint[0] = line[0][0];
        nearestPoint[1] = line[0][1];
    }
}
// 计算两点的欧几里得距离
function calculateDistance([x1, y1], [x2, y2]) {
    const dxSquared = (x2 - x1) ** 2;
    const dySquared = (y2 - y1) ** 2;
    return Math.sqrt(dxSquared + dySquared);
}

在三维中计算

在Cesium三维中的计算原理与上面演示的完全一致,另外我们还可以简化操作,将向量相关的手动计算改为直接调用Cesium.Cartesian3提供的数学方法。这些方法内部的计算方式与上面演示的原理相同,只是多加了Z轴的计算。

完整代码

const viewer = new Cesium.Viewer('cesiumContainer', {
  fullscreenButton: false,
  baseLayerPicker: false,
  timeline: false,
  geocoder: false,
  selectionIndicator: false,
  sceneModePicker: false,
  infoBox: false,
  homeButton: false,
  navigationHelpButton: false,
  animation: false,
});
viewer.camera.setView({
  destination: Cesium.Cartesian3.fromRadians(1.9012177769615666, 0.5971271406508205, 15630.008051801948),
  orientation: {
    heading: 6.283185307179579,
    pitch: -1.221719470127658,
    roll: 0,
  },
});

const point = Cesium.Cartesian3.fromDegrees(108.95174797785178, 34.28394435328798, 1000)
const start = Cesium.Cartesian3.fromDegrees(108.91840952081975, 34.28584112524303, 1000)
const end = Cesium.Cartesian3.fromDegrees(108.96422890207202, 34.25740044568396, 100)

viewer.entities.add({
  position: point,
  point: {
    pixelSize: 10,
    color: Cesium.Color.WHITE,
    outlineWidth: 2,
  }
})
viewer.entities.add({
  polyline: {
    positions: [start, end],
    width: 5,
    material: Cesium.Color.WHITE,
  }
})
const vecStartToEnd = Cesium.Cartesian3.subtract(end, start, new Cesium.Cartesian3());
const vecStartToPoint = Cesium.Cartesian3.subtract(point, start, new Cesium.Cartesian3())
const vecStartToEndNormal = Cesium.Cartesian3.normalize(vecStartToEnd, new Cesium.Cartesian3());

const projectionLength = Cesium.Cartesian3.multiplyByScalar(vecStartToEndNormal, Cesium.Cartesian3.dot(vecStartToPoint, vecStartToEndNormal), new Cesium.Cartesian3());
let nearestPoint = Cesium.Cartesian3.add(start, projectionLength, new Cesium.Cartesian3());
const distancePointToStart = Cesium.Cartesian3.distance(start, nearestPoint);
const distancePointToEnd = Cesium.Cartesian3.distance(end, nearestPoint);
const lineDistance = Cesium.Cartesian3.distance(start, end);
if (Math.abs(lineDistance - (distancePointToStart + distancePointToEnd)) > 0.001) {
  if (distancePointToStart > distancePointToEnd) {
    nearestPoint = end;
  } else {
    nearestPoint = start;
  }
}
viewer.entities.add({
  position: nearestPoint,
  point: {
    pixelSize: 10,
    color: Cesium.Color.RED,
    outlineWidth: 2,
    disableDepthTestDistance: 1000000,
  }
})
// 绘制虚线
viewer.entities.add({
  polyline: {
    positions: [point, nearestPoint],
    width: 5,
    material: new Cesium.PolylineDashMaterialProperty({
      color: Cesium.Color.BLUE
    })

  }
})

image.png