Cesium中绘制扇形有多难?会勾股定理就够了!

813 阅读7分钟

大家好,我是日拱一卒的攻城师不浪,致力于技术与艺术的融合。这是2024年输出的第48/100篇文章。

前言

应广大群友要求,今天我们来看一下在Cesium中如何绘制一个扇形

扇形在很多领域都有重要的应用,例如军事领域的态势图雷达扫描与监控范围卫星通信与信号覆盖等领域都发挥着重要的指示作用。

初始化

先初始化一个类

class DrawFanShape {
    constructor(viewer) {
        this._viewer = viewer;
    }
}

在cesium封装场景功能,基本上都要接收一个viewer基本参数,因为无论是entity,还是primitive,最终都要装进viewer中。

功能分析

在封装一个功能之前,我们先不要着急下手,一个优秀的工程师都会事先把整体场景的结构和流程都先在脑子里过一遍,这样有助于对整体的把控以及防止事后返工甚至是推翻整个流程,事倍功半;

  • 首先,扇形是一个特殊的面实体,所以我们可以使用entity实体类里的polygon去渲染;

  • 参数分析,看下图

一个扇形需要由以下几个要素组成:

  • 中心点:确定扇形位置;

  • 角度:确定扇形张口大小;

  • 背景色

  • 半径

  • 还有一个不好画:也就是扇形的朝向

以上这几个参数是核心要素,缺一不可;

OK,整体功能点我们都分析的差不多了,接下来开始最开心的环节,撸起来;

实体管理

鉴于我们在真实项目当中,可能不止绘制一个扇形,所以最好创建一个实体管理CustomDataSource方便统一管理;

constructor(viewer, sourceName) {
    //...
    // 构造函数第二个参数接收一个实体管理名称,方便后期直接从实体列表中获取对应实体
    let dataSourceList = viewer.dataSources.getByName(sourceName);
    if (!dataSourceList || dataSourceList.length === 0) {
        this._dataSource = new Cesium.CustomDataSource(sourceName);
        viewer.dataSources.add(this._dataSource); // 为扇形数据建立一个自定义数据源
    } else {
        this._dataSource = dataSourceList[0];
    }
    //...
}

绘制实体

创建绘制函数,并接收我们上边分析的必须要传递的参数

/**
 * @description 画扇形(从正北开始顺时针旋转)
 * @param {String} id 扇形ID
 * @param {Object} position 中心点位置
 * @param {Object} heading 航向
 * @param {Object} color 扇形颜色
 * @param {Number} radius 扇形半径
 * @param {Number} angle 角度大小
 * @param {String} type 类别-用于区分是否是同一个目标的扇形
 */
drawSector(params) {
    // 通过圆心(经纬度)、航偏角度d1、d2(d1<d2)、半径
    // d1、d2 可以通过此方法获取:假设当前目标航向角度A,d1 = A - 自定义扇形角度/2;d2 = A + 自定义扇形角度/2;
    let { id, position, heading, angle, color, type, radius, label } = params;
    let A = Cesium.Math.toDegrees(heading); // 弧度转角度
    let d1 = A - angle / 2; // 扇形第一个边的角度
    let d2 = A + angle / 2; // 扇形第二个边的角度
    let pos = this.cartesian2Degrees(position);
    let { lon, lat, height } = pos;

    let box = this._dataSource.entities.add({
        id: id,
        polygon: {
            show: true,
            hierarchy: this.generateHierarchy(lon, lat, height, d1, d2, radius),
            material: color ? color.withAlpha(0.5) : this._config.color.withAlpha(0.5),
            outline: true,
            outlineWidth: 1,
            outlineColor: color ? color : this._config.color,
            zIndex: Math.floor(1000 / radius),
        },
        //。。。。
    });
    return box;
}

理解各参数

先理解 heading、d1、d2 和 angle 之间的关系:

  1. heading:这是扇形的中心方向角,用弧度表示,通过 Cesium.Math.toDegrees(heading) 转为角度。以正北为参考点,顺时针为正方向。

  2. angle:扇形的总开角度(以度为单位),表示扇形的范围。

  3. d1d2:分别是扇形的左边界右边界角度。

  • d1 是由 heading 减去半个 angle 得到的,表示扇形左边界的方向。

  • d2 是由 heading 加上半个 angle 得到的,表示扇形右边界的方向。

我们先分别赋一个值,然后画一个图例,就能很清晰的理解:

heading=90°,angle=80°

注意:所有的角度都是从正北顺时针为正方向开始画

基于以上这个图例,就能够清晰的看到扇形的角度是由绿箭头和蓝箭头组成的,也就是最终的angle=d2 - d1

最重要的hierarchy参数

大家可能注意到了,我们本来是想画一个扇形,为什么要用polygon这个几何实体,因为它具备hierarchy这个参数,它是PolygonHierarchy的实例

也就是它能够根据我们提供的点位坐标去形成一个闭环的面。

所以,就引出了我们在代码中自定义的generateHierarchy函数的作用

generateHierarchy

它就是为了获取到扇形的边界点位

/**
 * 生成polygon线性环
 */
generateHierarchy(lon, lat, height, d1, d2, radius) {
    let list = [Number(lon), Number(lat), Number(height)];
    //获取 航偏角d1 至 航偏角d2 弧段的点位信息
    for (let i = d1; i < d2; i += 1) {
        let point = this.getPointByProjection(lon, lat, height, (90 - i) * (Math.PI / 180), radius);
        list.push(Number(point[0]));
        list.push(Number(point[1]));
        list.push(height);
    }
    list.push(Number(lon));
    list.push(Number(lat));
    list.push(Number(height));
    return Cesium.Cartesian3.fromDegreesArrayHeights(list);
}

因为d1<d2,所以我们从d1开始,每次加,一直循环到d2,拿到扇形弧上的点位分布,最终连接成一个面。

紧接着,自定义了getPointByProjection函数

getPointByProjection

我们倒推一下,这个函数最终返回的是经纬度两个数值,而扇形那个弧线上的经纬度是要通过勾股定理进行计算的

getPointByProjection(lon, lat, height, direction, radius) {
    // 观察点
    let cartesian = Cesium.Cartesian3.fromDegrees(lon, lat, height);
    // 世界坐标转为投影坐标
    // ...省略
    // 计算目标点
    let toPoint = new Cesium.Cartesian3(
        viewPointWebMercator.x + radius * Math.cos(direction),
        viewPointWebMercator.y + radius * Math.sin(direction),
        height
    );
    // 投影坐标转为世界坐标
    // 。。。省略
    return point;
}

这里最关键的就是计算目标点这一环,我们先用其中一个点进行举例,来画个图

我们就要计算图中红色点的x,y,所以,这里需要运用勾股定理去求,而direction就是弧形上的每个点对应的角度,最终得出的x和y再分别与中心点的x和y相加,得到最终的定位。

所以,现在关键点就是如何去获取这个direction,根据代码我们给出了答案:

(90 - i) * (Math.PI / 180)

这行代码的核心是将角度 i 转换为适用于笛卡尔坐标的弧度值 direction,并且通过 (90 - i) 的公式调整方向,使其与地理坐标系的定义相匹配。

看到这里可能会有点懵,为什么要这么算,这就又引申出了另外一个知识点,请往下看

地理坐标系和笛卡尔坐标系

方向定义的差异

  • 地理坐标系
    • 地理角度以正北为基准,顺时针增加。例如:
      • 0° 表示正北。
      • 90° 表示正东。
      • 180° 表示正南。
      • 270° 表示正西。

  • 笛卡尔坐标系
    • 通常以**正X轴(右侧)**为基准,逆时针增加。例如:
      • 0° 表示正东。
      • π/2(90°)表示正北。
      • π(180°)表示正西。
      • 3π/2(270°)表示正南。

(90 - i) 的作用

地理角度 i 转换为适合笛卡尔坐标系计算的角度,应用以下公式:

direction = 90∘ − i

角度转弧度

在 JavaScript 中,Math.sin() 接收的参数是弧度,而不是角度。所以需要将角度转弧度,公式如下:

角度 * (Math.PI / 180

为什么是Math.PI / 180,再复习下数学知识:

OK,经过以上对弧形上点位的计算获取,我们已经拿到了绘制扇形面所需要的点位坐标,最后让点位首尾相连就成功绘制出一个扇形啦~

最后

如果想系统学习Cesium,可以了解下我的Cesium系列教程《Cesium从入门到实战》,将Cesium的知识点进行串联,让不了解Cesium的小伙伴拥有一个完整的学习路线,并最终完成一个智慧城市的完整项目,课程也在不断更新迭代中,想了解+作者:brown_7778(备注来意)了解教程细节。

有需要进可视化&Webgis交流群可以加我:brown_7778(备注来意),另外也可接项目合作。