Cesium基础(五)实体创建与拖拽

cesium实体创建与拖拽

引言:这篇文件主要介绍cesium的实体创建和拖拽,主要是部分entity实体的添加cesium场景中。常见的需求比如多边形框选、广告牌标志、文字标识、管道等等。因为实体的特性,代码主要实现实体的加载和拖拽。(官方示例)

Entity实体介绍

Entity是Cesium中的一个核心类,主要用来描述地理空间场景中的动态对象(如点、线、面、模型、标签等)。这个动态对象通常具有位置,方向,类型等属性,这些属性都是可选的,可以根据需要进行加载和调整。(API查阅点击跳转)

  1. 核心功能:
    • 统一数据容器: Entity 封装了对象的 ​位置、方向、形状、外观​ 等属性,并支持随时间变化的动态数据(通过Property系统)。
    • 跨数据源支持: 可用于 GeoJSONKMLCZML 等数据格式,也可手动创建。
  2. 常用属性:
    • id:唯一标识符(字符串或对象)
    • position:位置(PositionProperty
    • orientation:方向(四元数)
    • billboardpointlabelpolylinepolygonmodel 等图形类型(通过 XXXGraphics 类配置)
    • availability:时间范围(TimeIntervalCollection
    • description:HTML描述(点击时显示在InfoBox中)
  3. 相关核心类
    • Graphics 类族(可视化样式): 主要定义Entity具体的图形表现(如颜色、大小、轮廓等)。常见子类如下:

      • BillboaLabelGraphics:文本标签
      • PointGraphics:点
      • PolylineGraphics:折线
      • PolygonGraphics:多边形
      • ModelGraphics:3D模型(glTF格式)
      • EllipseGraphics/BoxGraphics:几何体rdGraphics:图标(2D图像)
    • Property 系统(动态属性): 处理随时间变化的属性(如移动轨迹、颜色渐变)。

      • ConstantProperty:静态值
      • SampledProperty:插值数据(如位置采样)
      • TimeIntervalCollectionProperty:分段定义不同时间段的值
      • CompositeProperty:组合多个Property
    • EntityCollection 类: 管理一组Entity,通常挂载在Viewer.entities或DataSource.entities上。核心方法如下:

      • entities.add(entity); :添加实体
      • entities.remove(entity); :删除实体
      • entities.getById('id'); :按ID查询
    • DataSource 类: 加载外部数据(如CZML、GeoJSON),自动生成Entity集合。常见子类如下:

      • CzmlDataSource
      • GeoJsonDataSource
      • KmlDataSource
    • EntityCluster 类: 当实体过多时自动聚合成图标,优化性能。

      • 启用方式:viewer.dataSourceDisplay.defaultDataSource.entities.clustering.enabled = true;
Entity实体创建

关于单个entity实体创建的代码我就不一一列举了,可以点击文章开头的官方示例链接进行查看。这里我主要实现的是一些entity实体创建和拖拽移动,这里说一下实现思路:

  1. 事件流程: 监听鼠标左键按下(LEFT_DOWN)→ 移动(MOUSE_MOVE)→ 释放(LEFT_UP)事件链。
  2. 位置更新: 拖拽时实时计算鼠标在三维场景中的坐标(Cartesian3),并更新Entity的position属性。
  3. 防干扰处理: 拖拽时禁用相机旋转/平移,避免地图移动干扰实体操作。
实现代码:
js 复制代码
<script setup>
import * as cesium from "cesium";
import { onMounted, ref } from "vue";
import { useCesium } from "@/hooks/useCesium";
import logo from '../assets/img/cesium_logo.png'
import { ElMessage } from "element-plus";
let viewer = null
let handler = null
let dragEntity = null
let isDraging = false
let originalPositions = []; // 存储原始顶点坐标
/**
 * StripeMaterialProperty:一个将条纹材质属性映射到材质uniforms的材质属性类
 * 参数:
 *      orientation:指定条纹方向的属性,可选水平或垂直。
 *      evenColor:指定第一种(偶数条纹)颜色的属性。
 *      oddColor:指定第一种(偶数条纹)颜色的属性。
 *      offset:指定材质模式起始点的偏移量(0.0从偶数颜色开始,1.0从奇数颜色开始)。
 *      repeat:指定条纹重复次数的数值属性。
 */
const stripeMaterial = new cesium.StripeMaterialProperty({
    evenColor: cesium.Color.WHITE.withAlpha(0.5),
    oddColor: cesium.Color.BLUE.withAlpha(0.5),
    repeat: 10.0,
});
// 折线体计算
const starPositions = (arms, rOuter, rInner) => {
    const angle = Math.PI / arms;
    const pos = [];
    for (let i = 0; i < 2 * arms; i++) {
        const r = i % 2 === 0 ? rOuter : rInner;
        const p = new cesium.Cartesian2(
            Math.cos(i * angle) * r,
            Math.sin(i * angle) * r,
        );
        pos.push(p);
    }
    return pos;
}
// 折线体计算
const computeCircle = (radius) => {
    const positions = [];
    for (let i = 0; i < 360; i++) {
        const radians = cesium.Math.toRadians(i);
        positions.push(
            new cesium.Cartesian2(
                radius * Math.cos(radians),
                radius * Math.sin(radians),
            ),
        );
    }
    return positions;
}

const clickBtn = (item) => {
    const entity = viewer.entities.getById(item.id)
    entity ? viewer.zoomTo(entity) : createEntity(item)
}
const createEntity = (item) => {
    const entity = { id: item.id }
    if (item.type && item.property) {
        entity[item.type] = item.property
    }
    if (item.position) {
        entity['position'] = item.position
    }
    if (item.properties) {
        entity['properties'] = item.properties
    }
    const e = viewer.entities.add(entity);
    viewer.zoomTo(e)
}
// 鼠标点击事件
const clickEntity = () => {
    handler.setInputAction(function (e) {
        const pick = viewer.scene.pick(e.position);
        if (cesium.defined(pick)) {
            if (pick.id?.properties?.isDrag._value) {
                dragEntity = pick.id
                isDraging = true
                viewer.scene.screenSpaceCameraController.enableRotate = false; // 锁定相机
            } else {
                ElMessage({ type: 'info', message: '不可拖拽' })
            }

        }
    }, cesium.ScreenSpaceEventType.LEFT_DOWN);
}
// 鼠标移动
const mouseEntity = () => {
    handler.setInputAction(function (movement) {
        const scene = viewer.scene;
        if (scene.mode !== cesium.SceneMode.MORPHING) {
            if (scene.pickPositionSupported) {
                const cartesian = viewer.scene.pickPosition(movement.endPosition);
                if (cesium.defined(cartesian) && isDraging) {
                    if (dragEntity?.properties?.flag) {
                        dragEntity.position = cartesian
                    } else {
                        originalPositions = [...dragEntity.polygon.hierarchy.getValue().positions]; // 克隆顶点数组
                        const offset = cesium.Cartesian3.subtract(cartesian, originalPositions[0], new cesium.Cartesian3());
                        const newPositions = originalPositions.map(pos =>
                            cesium.Cartesian3.add(pos, offset, new cesium.Cartesian3())
                        );
                        dragEntity.polygon.hierarchy = new cesium.PolygonHierarchy(newPositions);
                    }

                }
            }
        }

    }, cesium.ScreenSpaceEventType.MOUSE_MOVE);
}

// 鼠标松开
const clickUp = () => {
    handler.setInputAction(() => {
        isDraging = false;
        dragEntity = null;
        originalPositions = []
        viewer.scene.screenSpaceCameraController.enableRotate = true; // 解锁相机
    }, cesium.ScreenSpaceEventType.LEFT_UP);

}
// 实体创建参数,这里做了一些改变,作为参数传递到createEntity函数种创建实体
const buttons = ref([
    {
        title: '矩形',
        id: '1',
        type: 'rectangle',
        property: {
            //  Rectangle:由经度和纬度坐标指定的二维区域。
            coordinates: cesium.Rectangle.fromDegrees(-92.0, 20.0, -86.0, 27.0),
            outline: true, // 边缘线条
            outlineColor: cesium.Color.WHITE,// 边缘线条颜色
            outlineWidth: 4, // 边缘线条宽度
            stRotation: cesium.Math.toRadians(45), // 一个数字属性,用于指定矩形纹理从北侧开始逆时针旋转的角度。
            material: stripeMaterial, // 材质
        },
    },
    {
        title: '多边形',
        id: '2',
        type: 'polygon',
        property: {
            //PolygonHierarchy:定义多边形及其孔洞的线性环层次结构。这些孔洞自身也可能包含嵌套内部多边形的孔洞。
            hierarchy: new cesium.PolygonHierarchy(
                cesium.Cartesian3.fromDegreesArray([
                    -107.0, 27.0, -107.0, 22.0, -102.0, 23.0, -97.0, 21.0, -97.0, 25.0,
                ]),
            ),
            outline: true,
            outlineColor: cesium.Color.WHITE,
            outlineWidth: 4,
            material: stripeMaterial,
        },
        properties: {
            isDrag: true,
        }
    },
    {
        title: '椭圆形',
        id: '3',
        type: 'ellipse',
        position: cesium.Cartesian3.fromDegrees(-80.0, 25.0),
        property: {
            semiMinorAxis: 300000.0,// 半短轴
            semiMajorAxis: 500000.0,// 半长轴
            rotation: cesium.Math.toRadians(-40.0), // 向北旋转40°
            stRotation: cesium.Math.toRadians(22),
            material: stripeMaterial,
        },
        properties: {
            isDrag: true,
            flag: true
        }
    },
    {
        title: '圆形',
        id: '4',
        position: cesium.Cartesian3.fromDegrees(-72.0, 25.0),
        type: "ellipse",
        property: {
            semiMinorAxis: 250000.0,
            semiMajorAxis: 250000.0,
            rotation: cesium.Math.toRadians(-40.0),

            stRotation: cesium.Math.toRadians(90),
            material: stripeMaterial,
        },
        properties: {
            isDrag: true,
            flag: true
        }
    }, {
        title: '长方体',
        id: '5',
        type: 'rectangle',
        property: {
            coordinates: cesium.Rectangle.fromDegrees(-118.0, 38.0, -116.0, 40.0),
            extrudedHeight: 500000.0, // 拉伸高度
            outline: true,
            outlineColor: cesium.Color.WHITE,
            outlineWidth: 4,
            stRotation: cesium.Math.toRadians(45),
            // 使用提供的选项创建随机颜色。如需生成可重现的随机颜色,应在应用程序启动时调用一次
            material: cesium.Color.fromRandom({ alpha: 1.0 }),
        }
    },
    {
        title: '椭圆柱',
        id: '6',
        type: 'ellipse',
        position: cesium.Cartesian3.fromDegrees(-117.0, 35.0),
        property: {
            semiMinorAxis: 100000.0,
            semiMajorAxis: 200000.0,
            height: 100000.0,// 柱体高度
            extrudedHeight: 10000.0,// 离地高度
            rotation: cesium.Math.toRadians(90.0),//向南旋转90°
            outline: true,
            outlineColor: cesium.Color.WHITE,
            outlineWidth: 4,
            material: cesium.Color.fromRandom({ alpha: 1.0 })
        },
        properties: {
            isDrag: true,
            flag: true
        }
    },
    {
        title: '不规则多边体',
        id: '7',
        type: 'polygon',
        property: {
            hierarchy: new cesium.PolygonHierarchy(
                cesium.Cartesian3.fromDegreesArray([
                    -118.0, 30.0, -115.0, 30.0, -117.1, 31.1, -118.0, 33.0,
                ]),
            ),
            height: 300000.0,
            extrudedHeight: 700000.0,
            outline: true,
            outlineColor: cesium.Color.WHITE,
            outlineWidth: 4,
            material: cesium.Color.fromRandom({ alpha: 1.0 }),
        },
        properties: {
            isDrag: true,
        }
    }, {
        title: '墙体',
        id: '8',
        type: 'wall',
        position: null,
        property: {
            positions: cesium.Cartesian3.fromDegreesArray([
                -95.0, 50.0, -85.0, 50.0, -75.0, 50.0,
            ]),
            // 一个与位置平行的数组,用于指定各位置处墙体的最大高度。如果未定义,则使用每个位置的高度。
            maximumHeights: [500000, 1000000, 500000],
            // 一个与位置平行的数组,用于指定各位置处墙体的最小高度。如果未定义,则每个位置的高度为 0.0。
            minimumHeights: [0, 500000, 0],
            outline: true,
            outlineColor: cesium.Color.LIGHTGRAY,
            outlineWidth: 4,
            material: cesium.Color.fromRandom({ alpha: 0.7 }),
        }
    },
    {
        title: '层级多变形',
        id: '9',
        type: 'polygon',
        position: null,
        property: {
            //hierarchy:一个指定多边形层次结构的属性。
            hierarchy: {
                positions: cesium.Cartesian3.fromDegreesArray([
                    -109.0, 30.0, -95.0, 30.0, -95.0, 40.0, -109.0, 40.0,
                ]),
                /**
                 描述了一个由线性环层次结构定义的多边形,这些线性环构成了外部形状和所有嵌套孔洞。
                该多边形遵循地球曲率,可放置于地表或某一高度,还可选择拉伸为立体形状。
                */
                holes: [
                    {
                        positions: cesium.Cartesian3.fromDegreesArray([
                            -107.0, 31.0, -107.0, 39.0, -97.0, 39.0, -97.0, 31.0,
                        ]),
                        holes: [
                            {
                                positions: cesium.Cartesian3.fromDegreesArray([
                                    -105.0, 33.0, -99.0, 33.0, -99.0, 37.0, -105.0, 37.0,
                                ]),
                                holes: [
                                    {
                                        positions: cesium.Cartesian3.fromDegreesArray([
                                            -103.0, 34.0, -101.0, 34.0, -101.0, 36.0, -103.0, 36.0,
                                        ]),
                                    },
                                ],
                            },
                        ],
                    },
                ],
            },
            material: stripeMaterial,
        },
        properties: {
            isDrag: false
        }
    },
    {
        title: '圆锥体',
        id: '10',
        type: 'cylinder',
        position: cesium.Cartesian3.fromDegrees(-70.0, 40.0, 200000.0),
        property: {
            hierarchy: new cesium.PolygonHierarchy(
                cesium.Cartesian3.fromDegreesArray([
                    -118.0, 30.0, -115.0, 30.0, -117.1, 31.1, -118.0, 33.0,
                ]),
            ),
            length: 400000.0, // 一个指定圆柱体长度的数值属性。
            topRadius: 0.0,
            outline: true,
            bottomRadius: 200000.0, // 一个指定圆柱体底部半径的数值属性。
            material: cesium.Color.fromRandom({ alpha: 1.0 }),
            numberOfVerticalLines: 11,// 垂直线条数:一个数值属性,用于指定沿轮廓周长绘制的垂直线条数量。
        },
        properties: {
            isDrag: true,
            flag: true
        }
    },
    {
        title: '墙合拢',
        id: '11',
        type: 'wall',
        position: null,
        property: {
            positions: cesium.Cartesian3.fromDegreesArrayHeights([
                -90.0, 43.0, 100000.0, -87.5, 45.0, 100000.0, -85.0, 43.0, 100000.0, -87.5,
                41.0, 100000.0, -90.0, 43.0, 100000.0,
            ]),
            material: new cesium.CheckerboardMaterialProperty({
                repeat: new cesium.Cartesian2(20.0, 6.0),
            }),
        }
    },
    {
        title: '廊道',
        id: '12',
        type: 'corridor',
        position: null,
        property: {
            positions: cesium.Cartesian3.fromDegreesArray([
                -120.0, 45.0, -125.0, 50.0, -125.0, 55.0,
            ]),
            width: 100000,
            height: 700000,
            outline: true,
            outlineColor: cesium.Color.WHITE,
            outlineWidth: 4,
            material: cesium.Color.fromRandom({ alpha: 0.7 }),
        }
    },
    {
        title: '折线体',
        id: '13',
        type: 'polylineVolume',
        position: null,
        property: {
            positions: cesium.Cartesian3.fromDegreesArray([
                -102.0, 15.0, -105.0, 20.0, -110.0, 20.0,
            ]),
            shape: starPositions(7, 30000.0, 20000.0),
            material: cesium.Color.fromRandom({ alpha: 1.0 }),
        }
    },
    {
        title: '折线体',
        id: '14',
        type: 'polylineVolume',
        position: null,
        property: {
            positions: cesium.Cartesian3.fromDegreesArray([
                -104.0, 13.0, -107.0, 18.0, -112.0, 18.0,
            ]),
            // 一个指定笛卡尔二维坐标数组的属性,该数组定义了要拉伸的形状。
            shape: computeCircle(40000.0),
            material: cesium.Color.RED,
        }
    },
    {
        title: '广告牌',
        id: '15',
        type: 'billboard',
        position: cesium.Cartesian3.fromDegrees(-75.59777, 40.03883),
        property: {
            show: true,// 控制显隐
            image: logo,//图片地址
            scale: 2.0,// 放大倍数
        },
        properties: {
            isDrag: true,
            flag: true
        }
    }

])

const home = () => {
    viewer.camera.flyTo({
        destination: cesium.Cartesian3.fromDegrees(-75.59777, 40.03883, 10000 * 2000),
        orientation: {
            heading: 0,
            pitch: cesium.Math.toRadians(-90),
            roll: 0
        },
        duration: 1.0
    });
}

onMounted(async () => {
    const earth = document.querySelector("#earth");
    viewer = useCesium(earth);
    handler = new cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
    clickEntity()
    mouseEntity()
    clickUp()
});
</script>
<template>
    <div class="entities">
        <div class="earth" id="earth"></div>
        <div class="settings">
            <div class="flex fw-wrap ai-center">
                <el-button type="primary" class="m5" plain size="small" @click="home">视角重置</el-button>
                <el-button class="m5" v-for="item in buttons" :key="item.id" plain size="small" type="primary"
                    @click="clickBtn(item)">{{ item.title
                    }}</el-button>
            </div>
        </div>
    </div>

</template>

<style lang="scss" scoped>
.entities {
    position: relative;
    width: 100%;
    height: 100%;

    .earth {
        width: 100%;
        height: 100%;
        z-index: 9999;
    }

    .settings {
        position: absolute;
        top: 0;
        left: 0;
        width: 200px;
        height: 300px;
        // background-color: aquamarine;
    }
}
</style>
实现效果:
多边形部分拖拽:

这种功能主要时为了在地图上框选某个区域进行功能分析,实现思路,具体是为多边形每个顶点添加可拖拽标识,更新多边形顶点数组(需根据顶点ID找到对应索引),伪代码如下:

js 复制代码
handler.setInputAction((movement) => {
    if (draggedVertex) {
        const newPos = viewer.scene.pickPosition(movement.endPosition);
        if (newPos) {
            draggedVertex.position = newPos; // 更新顶点位置
            // 更新多边形顶点数组(需根据顶点ID找到对应索引)
            const polygon = draggedVertex.parent; // 假设顶点实体关联到多边形
            const index = polygon.vertexEntities.indexOf(draggedVertex);
            polygon.positions[index] = newPos;
            polygon.polygon.hierarchy = new cesium.PolygonHierarchy(polygon.positions);
        }
    }
}, cesium.ScreenSpaceEventType.MOUSE_MOVE);
总结:

文章一开始是想写一些实体加载,按照官方文档中的那样,想想还是放弃了,后来做了一些改动,但是改动不是很大,主要是将一些实体创建的参数拆到一起。有不足之处或者有好的想法欢迎大家下方留言!

相关推荐
然我1 小时前
防抖与节流:如何让频繁触发的函数 “慢下来”?
前端·javascript·html
鱼樱前端1 小时前
2025前端人一文看懂 Broadcast Channel API 通信指南
前端·vue.js
烛阴2 小时前
非空断言完全指南:解锁TypeScript/JavaScript的安全导航黑科技
前端·javascript
鱼樱前端2 小时前
2025前端人一文看懂 window.postMessage 通信
前端·vue.js
快乐点吧2 小时前
【前端】异步任务风控验证与轮询机制技术方案(通用笔记版)
前端·笔记
pe7er3 小时前
nuxtjs+git submodule的微前端有没有搞头
前端·设计模式·前端框架
七月的冰红茶3 小时前
【threejs】第一人称视角之八叉树碰撞检测
前端·threejs
爱掉发的小李3 小时前
前端开发中的输出问题
开发语言·前端·javascript
祝余呀3 小时前
HTML初学者第四天
前端·html
浮桥5 小时前
vue3实现pdf文件预览 - vue-pdf-embed
前端·vue.js·pdf