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);
总结:

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

相关推荐
SoaringHeart2 小时前
Flutter组件封装:页面点击事件拦截
前端·flutter
杨天天.2 小时前
小程序原生实现音频播放器,下一首上一首切换,拖动进度条等功能
前端·javascript·小程序·音视频
Dragon Wu2 小时前
React state在setInterval里未获取最新值的问题
前端·javascript·react.js·前端框架
Jinuss2 小时前
Vue3源码reactivity响应式篇之watch实现
前端·vue3
YU大宗师2 小时前
React面试题
前端·javascript·react.js
木兮xg2 小时前
react基础篇
前端·react.js·前端框架
ssshooter3 小时前
你知道怎么用 pnpm 临时给某个库打补丁吗?
前端·面试·npm
IT利刃出鞘3 小时前
HTML--最简的二级菜单页面
前端·html
yume_sibai3 小时前
HTML HTML基础(4)
前端·html
给月亮点灯|4 小时前
Vue基础知识-Vue集成 Element UI全量引入与按需引入
前端·javascript·vue.js