Cesium中实现在地图上移动/旋转点、线、面

一、上/下/左/右整体移动

复制代码
<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cesium 点线面整体平移</title>
    <!-- 引入Cesium CDN -->
    <script src="https://cesium.com/downloads/cesiumjs/releases/1.108/Build/Cesium/Cesium.js"></script>
    <link href="https://cesium.com/downloads/cesiumjs/releases/1.108/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        #cesiumContainer {
            width: 100vw;
            height: 100vh;
            position: relative;
        }

        /* 平移按钮样式 */
        .move-controls {
            position: absolute;
            top: 20px;
            left: 20px;
            z-index: 100;
            display: grid;
            grid-template-columns: repeat(3, 40px);
            grid-template-rows: repeat(3, 40px);
            gap: 5px;
        }

        .move-btn {
            border: none;
            background: rgba(0, 123, 255, 0.8);
            color: white;
            font-size: 16px;
            border-radius: 4px;
            cursor: pointer;
            transition: background 0.3s;
        }

        .move-btn:hover {
            background: rgba(0, 86, 179, 0.8);
        }

        /* 按钮布局:上、下、左、右居中 */
        .btn-up {
            grid-area: 1 / 2 / 2 / 3;
        }

        .btn-down {
            grid-area: 3 / 2 / 4 / 3;
        }

        .btn-left {
            grid-area: 2 / 1 / 3 / 2;
        }

        .btn-right {
            grid-area: 2 / 3 / 3 / 4;
        }
    </style>
</head>

<body>
    <div id="cesiumContainer">
        <!-- 平移控制按钮 -->
        <div class="move-controls">
            <button class="move-btn btn-up">↑</button>
            <button class="move-btn btn-down">↓</button>
            <button class="move-btn btn-left">←</button>
            <button class="move-btn btn-right">→</button>
        </div>
    </div>

    <script>
        // 1. 初始化Cesium场景
        Cesium.Ion.defaultAccessToken = 'Access Token'

        const viewer = new Cesium.Viewer("cesiumContainer", {
            // terrainProvider: Cesium.createWorldTerrain(),
            timeline: false,
            animation: false
        });
        viewer.camera.setView({
            destination: Cesium.Cartesian3.fromDegrees(116.403963, 39.915112, 1000) // 聚焦北京天安门附近
        });

        // 2. 定义平移增量(每次点击移动的距离,单位:度(经纬度)、米(高度))
        // 注:1度纬度≈111公里,此处设置0.001度≈111米,平移效果更直观
        const MOVE_OFFSET = {
            lon: 0.001,  // 经度增量(左右移动)
            lat: 0.001,  // 纬度增量(上下移动)
            height: 0    // 高度增量(可按需调整,实现垂直升降)
        };

        // 3. 创建测试实体(点、线、面)
        let testEntities = [];

        // 3.1 创建点实体
        const pointEntity = viewer.entities.add({
            name: "测试点",
            position: Cesium.Cartesian3.fromDegrees(116.403963, 39.915112, 100),
            point: {
                pixelSize: 10,
                color: Cesium.Color.RED,
                outlineColor: Cesium.Color.WHITE,
                outlineWidth: 2
            }
        });

        // 3.2 创建线实体
        const polylineEntity = viewer.entities.add({
            name: "测试线",
            polyline: {
                positions: Cesium.Cartesian3.fromDegreesArray([
                    116.402963, 39.914112,
                    116.404963, 39.915112,
                    116.403963, 39.916112
                ]),
                width: 5,
                material: Cesium.Color.BLUE,
                clampToGround: false
            }
        });

        // 3.3 创建面实体
        const polygonEntity = viewer.entities.add({
            name: "测试面",
            polygon: {
                hierarchy: Cesium.Cartesian3.fromDegreesArray([
                    116.400963, 39.913112,
                    116.405963, 39.913112,
                    116.405963, 39.917112,
                    116.400963, 39.917112
                ]),
                material: Cesium.Color.GREEN.withAlpha(0.5),
                outline: true,
                outlineColor: Cesium.Color.DARK_GREEN,
                outlineWidth: 2,
                clampToGround: false
            }
        });

        // 收集所有测试实体
        testEntities = [pointEntity, polylineEntity, polygonEntity];

        // 4. 核心:平移实体的通用方法
        /**
         * 平移Cesium实体(支持点、线、面)
         * @param {Cesium.Entity} entity - 要平移的实体
         * @param {number} lonOffset - 经度偏移量(正值右移,负值左移)
         * @param {number} latOffset - 纬度偏移量(正值上移,负值下移)
         * @param {number} heightOffset - 高度偏移量(正值上升,负值下降)
         */
        function translateEntity(entity, lonOffset, latOffset, heightOffset) {
            // 情况1:点实体(拥有position属性)
            if (entity.position) {
                // 获取当前笛卡尔坐标
                const currentCartesian = entity.position.getValue(Cesium.JulianDate.now());
                if (!currentCartesian) return;

                // 转换为地理坐标(经纬度+高度)
                const currentCartographic = Cesium.Cartographic.fromCartesian(currentCartesian);

                // 计算新的地理坐标
                const newLon = currentCartographic.longitude + Cesium.Math.toRadians(lonOffset);
                const newLat = currentCartographic.latitude + Cesium.Math.toRadians(latOffset);
                const newHeight = currentCartographic.height + heightOffset;

                // 转换回笛卡尔坐标并更新实体位置
                const newCartesian = Cesium.Cartesian3.fromRadians(newLon, newLat, newHeight);
                entity.position = newCartesian;
            }

            // 情况2:线/面实体(拥有polyline.positions 或 polygon.hierarchy)
            // 提取所有需要平移的坐标点
            let positions = null;
            if (entity.polyline && entity.polyline.positions) {
                positions = entity.polyline.positions.getValue(Cesium.JulianDate.now());
            } else if (entity.polygon && entity.polygon.hierarchy) {
                const hierarchy = entity.polygon.hierarchy.getValue(Cesium.JulianDate.now());
                positions = hierarchy ? hierarchy.positions : null;
            }

            if (positions && positions.length > 0) {
                const newPositions = [];
                for (let i = 0; i < positions.length; i++) {
                    // 单个坐标点的平移逻辑(与点实体一致)
                    const currentCartographic = Cesium.Cartographic.fromCartesian(positions[i]);
                    const newLon = currentCartographic.longitude + Cesium.Math.toRadians(lonOffset);
                    const newLat = currentCartographic.latitude + Cesium.Math.toRadians(latOffset);
                    const newHeight = currentCartographic.height + heightOffset;
                    const newCartesian = Cesium.Cartesian3.fromRadians(newLon, newLat, newHeight);
                    newPositions.push(newCartesian);
                }

                // 更新线/面的坐标集合
                if (entity.polyline) {
                    entity.polyline.positions = newPositions;
                } else if (entity.polygon) {
                    entity.polygon.hierarchy = new Cesium.PolygonHierarchy(newPositions);
                }
            }
        }

        // 5. 绑定按钮点击事件,实现上下左右平移
        document.querySelector(".btn-up").addEventListener("click", () => {
            // 上移:纬度增加(北纬越大,位置越北)
            testEntities.forEach(entity => {
                translateEntity(entity, 0, MOVE_OFFSET.lat, MOVE_OFFSET.height);
            });
        });

        document.querySelector(".btn-down").addEventListener("click", () => {
            // 下移:纬度减少
            testEntities.forEach(entity => {
                translateEntity(entity, 0, -MOVE_OFFSET.lat, MOVE_OFFSET.height);
            });
        });

        document.querySelector(".btn-left").addEventListener("click", () => {
            // 左移:经度减少(东经越大,位置越东,左移即经度减小)
            testEntities.forEach(entity => {
                translateEntity(entity, -MOVE_OFFSET.lon, 0, MOVE_OFFSET.height);
            });
        });

        document.querySelector(".btn-right").addEventListener("click", () => {
            // 右移:经度增加
            testEntities.forEach(entity => {
                translateEntity(entity, MOVE_OFFSET.lon, 0, MOVE_OFFSET.height);
            });
        });
    </script>
</body>

</html>

核心方法解析(translateEntity):

该方法是实现平移的核心,兼容点、线、面三种实体类型,核心流程为:获取当前Cartesian3坐标 → 转换为Cartographic地理坐标 → 叠加平移增量 → 转换回Cartesian3坐标 → 更新实体位置。

线 / 面实体的处理逻辑是遍历其所有顶点坐标,对每个顶点执行相同的平移操作,从而实现整体移动,保证形状不发生变化。

平移增量调整:

MOVE_OFFSET中定义了经纬度和高度的增量,可根据需求调整:

  • 增大lon/lat可提高平移速度,减小则更精细。
  • 调整height可实现实体的垂直升降(如设置为 10,每次点击上升 10 米)。

二、旋转

保持形状、大小不变

复制代码
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <script src="https://cesium.com/downloads/cesiumjs/releases/1.110/Build/Cesium/Cesium.js"></script>
    <link href="https://cesium.com/downloads/cesiumjs/releases/1.110/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
</head>

<body>
    <div id="cesiumContainer"></div>
    <div style="position: absolute; top: 10px; left: 10px; z-index: 1000;">
        <button id="clockwiseBtn">顺时针旋转</button>
        <button id="counterclockwiseBtn">逆时针旋转</button>
        <input type="number" id="rotationAngle" value="15" min="1" max="90">
        <label for="rotationAngle">旋转角度(度)</label>
    </div>

    <script>
        Cesium.Ion.defaultAccessToken = 'Access Token'

        // 初始化Cesium Viewer
        const viewer = new Cesium.Viewer('cesiumContainer', {
            // terrainProvider: Cesium.createWorldTerrain(),
            baseLayerPicker: false,
            animation: false,
            timeline: false
        });

        // 存储原始图形数据
        let originalEntities = [];
        let isOriginalDataSaved = false;

        // 创建示例图形(点、线、面)
        function createExampleEntities() {
            // 清除现有实体
            viewer.entities.removeAll();
            originalEntities = [];

            // 示例多边形顶点(三角形)
            const polygonPositions = Cesium.Cartesian3.fromDegreesArray([
                -115.0, 37.0,
                -115.0, 32.0,
                -107.0, 33.0
            ]);

            // 创建面(多边形)
            const polygonEntity = viewer.entities.add({
                polygon: {
                    hierarchy: polygonPositions,
                    material: Cesium.Color.GREEN.withAlpha(0.5),
                    outline: true,
                    outlineColor: Cesium.Color.GREEN
                }
            });

            // 创建线(多段线)
            const polylineEntity = viewer.entities.add({
                polyline: {
                    positions: polygonPositions,
                    width: 3,
                    material: Cesium.Color.BLUE
                }
            });

            // 创建点(在顶点位置)
            const pointEntities = polygonPositions.map((position, index) => {
                return viewer.entities.add({
                    position: position,
                    point: {
                        pixelSize: 10,
                        color: Cesium.Color.RED
                    },
                    label: {
                        text: `点 ${index + 1}`,
                        font: '14px sans-serif',
                        pixelOffset: new Cesium.Cartesian2(0, 20)
                    }
                });
            });

            // 保存原始数据
            originalEntities.push({
                type: 'polygon',
                positions: polygonPositions
            });
            originalEntities.push({
                type: 'polyline',
                positions: polygonPositions
            });
            pointEntities.forEach((entity, index) => {
                originalEntities.push({
                    type: 'point',
                    position: polygonPositions[index],
                    entity: entity
                });
            });

            // 缩放到图形范围
            viewer.zoomTo(viewer.entities);

            isOriginalDataSaved = true;
        }

        // 计算图形的几何中心
        function calculateGeometricCenter(positions) {
            const cartographics = positions.map(cartesian =>
                Cesium.Cartographic.fromCartesian(cartesian)
            );

            // 计算平均经纬度和高度
            const avgLat = cartographics.reduce((sum, carto) => sum + carto.latitude, 0) / cartographics.length;
            const avgLon = cartographics.reduce((sum, carto) => sum + carto.longitude, 0) / cartographics.length;
            const avgHeight = cartographics.reduce((sum, carto) => sum + carto.height, 0) / cartographics.length;

            return Cesium.Cartesian3.fromRadians(avgLon, avgLat, avgHeight);
        }

        // 旋转函数
        function rotateEntities(angleDegrees, clockwise = true) {
            if (!isOriginalDataSaved) {
                console.log("请先创建图形");
                return;
            }

            const angle = clockwise ? -angleDegrees : angleDegrees;
            const angleRadians = Cesium.Math.toRadians(angle);

            // 计算所有原始顶点的中心点
            const allPositions = originalEntities
                .filter(entity => entity.positions || entity.position)
                .flatMap(entity => entity.positions ? entity.positions : [entity.position]);

            const center = calculateGeometricCenter(allPositions);

            // 获取当前所有实体
            const entities = viewer.entities.values;

            entities.forEach(entity => {
                if (entity.polygon) {
                    // 旋转多边形
                    const polygonPositions = entity.polygon.hierarchy.getValue(Cesium.JulianDate.now());
                    const rotatedPositions = rotatePositions(polygonPositions.positions, center, angleRadians);
                    entity.polygon.hierarchy = rotatedPositions;
                }

                if (entity.polyline) {
                    // 旋转线
                    const polylinePositions = entity.polyline.positions.getValue(Cesium.JulianDate.now());
                    const rotatedPositions = rotatePositions(polylinePositions, center, angleRadians);
                    entity.polyline.positions = rotatedPositions;
                }

                if (entity.point) {
                    // 旋转点
                    const position = entity.position.getValue(Cesium.JulianDate.now());
                    const rotatedPosition = rotatePosition(position, center, angleRadians);
                    entity.position = rotatedPosition;
                }
            });
        }

        // 旋转单个点
        function rotatePosition(position, center, angleRadians) {
            // 将笛卡尔坐标转换为经纬度
            const cartographic = Cesium.Cartographic.fromCartesian(position);
            const centerCartographic = Cesium.Cartographic.fromCartesian(center);

            // 计算相对中心点的经纬度差
            const deltaLon = cartographic.longitude - centerCartographic.longitude;
            const deltaLat = cartographic.latitude - centerCartographic.latitude;

            // 使用平面近似(在小范围内有效)
            const x = deltaLon * Math.cos(centerCartographic.latitude);
            const y = deltaLat;

            // 应用2D旋转矩阵
            const cosA = Math.cos(angleRadians);
            const sinA = Math.sin(angleRadians);

            const xRotated = x * cosA - y * sinA;
            const yRotated = x * sinA + y * cosA;

            // 转换回经纬度
            const rotatedLon = centerCartographic.longitude + xRotated / Math.cos(centerCartographic.latitude);
            const rotatedLat = centerCartographic.latitude + yRotated;

            return Cesium.Cartesian3.fromRadians(rotatedLon, rotatedLat, cartographic.height);
        }

        // 旋转位置数组
        function rotatePositions(positions, center, angleRadians) {
            const rotatedPositions = [];
            for (let i = 0; i < positions.length; i++) {
                rotatedPositions.push(rotatePosition(positions[i], center, angleRadians));
            }
            return rotatedPositions;
        }

        // 重置到原始状态
        function resetToOriginal() {
            if (!isOriginalDataSaved) return;

            viewer.entities.removeAll();

            // 使用原始数据重新创建实体
            originalEntities.forEach(original => {
                if (original.type === 'polygon') {
                    viewer.entities.add({
                        polygon: {
                            hierarchy: original.positions,
                            material: Cesium.Color.GREEN.withAlpha(0.5),
                            outline: true,
                            outlineColor: Cesium.Color.GREEN
                        }
                    });
                } else if (original.type === 'polyline') {
                    viewer.entities.add({
                        polyline: {
                            positions: original.positions,
                            width: 3,
                            material: Cesium.Color.BLUE
                        }
                    });
                }
                // 点会在后续步骤中重新创建
            });
        }

        // 事件监听
        document.getElementById('clockwiseBtn').addEventListener('click', () => {
            const angle = parseFloat(document.getElementById('rotationAngle').value);
            rotateEntities(angle, true);
        });

        document.getElementById('counterclockwiseBtn').addEventListener('click', () => {
            const angle = parseFloat(document.getElementById('rotationAngle').value);
            rotateEntities(angle, false);
        });

        // 添加重置按钮(可选)
        const resetBtn = document.createElement('button');
        resetBtn.textContent = '重置图形';
        resetBtn.style.marginLeft = '10px';
        resetBtn.addEventListener('click', resetToOriginal);
        document.querySelector('div[style]').appendChild(resetBtn);

        // 初始化创建示例图形
        createExampleEntities();
    </script>
</body>

</html>

关键特性

统一旋转中心:计算所有几何图形的几何中心作为旋转中心

保持形状大小:通过保持各点到中心的距离不变来实现

支持多种图形:同时处理点、线、面

可调旋转角度:可以通过输入框设置旋转角度

双向旋转:支持顺时针和逆时针旋转

注意事项

这个实现在小范围内(几十公里)效果较好

对于大范围的图形,需要考虑地球曲率的影响

旋转操作会修改原始图形数据,可以通过重置功能恢复

相关推荐
冥界摄政王3 天前
CesiumJS学习第四章 替换指定3D建筑模型
3d·vue·html·webgl·js·cesium
冥界摄政王5 天前
Cesium学习第二章 camera 相机
node.js·html·vue3·js·cesium
冥界摄政王6 天前
Cesium学习第一章 安装下载 基于vue3引入Cesium项目开发
vue·vue3·html5·webgl·cesium
你们瞎搞8 天前
Cesium加载20GB航测影像.tif
前端·cesium·gdal·地图切片
闲云一鹤9 天前
Cesium 使用 Turf 实现坐标点移动(偏移)
前端·gis·cesium
二狗哈9 天前
Cesium快速入门34:3dTile高级样式设置
前端·javascript·算法·3d·webgl·cesium·地图可视化
二狗哈10 天前
Cesium快速入门33:tile3d设置样式
3d·状态模式·webgl·cesium·地图可视化
闲云一鹤12 天前
Cesium 去掉默认瓦片和地形,解决网络不好时地图加载缓慢的问题
前端·cesium
闲云一鹤14 天前
将地图上的 poi 点位导出为 excel,并转换为 shp 文件
前端·cesium