CesiumLite-一行代码让你在Cesium中实现标绘测量

🎨 Cesium 标绘测量太复杂?CesiumLite 让你一行代码搞定!

本文深入介绍 CesiumLite 的标绘测量模块,从传统开发痛点到模块化封装原理,再到实战应用,教你如何优雅地实现三维地图的交互式绘制和精确测量。

📌 前言

在三维地图应用开发中,标绘 (Drawing)和测量(Measure)是两个极其常见的需求:

  • 标绘功能:用户在地图上绘制点、线、面等几何图形,用于标注位置、规划路线、圈定区域等场景
  • 测量功能:计算两点间的距离、区域面积,用于空间分析、工程测算等场景

这两个功能看似简单,但使用 Cesium 原生 API 实现时,却需要处理大量细节:鼠标事件监听、坐标转换、动态绘制、结果计算等。本文将展示 CesiumLite 如何通过模块化封装,让标绘测量变得简单高效。

🎨 在线演示

项目提供了完整的标绘测量演示页面,你可以访问以下链接体验实际效果:

标绘测量演示

项目地址

演示页面包含以下功能:

  • 🎯 绘制功能:点、线、面
  • 📏 测量功能:距离测量、面积测量
  • 🎭 交互反馈:实时预览、动态显示
  • 🗑️ 清除管理:一键清除所有标绘测量结果

🚫 开发痛点分析

痛点 1:鼠标事件处理复杂

使用 Cesium 原生 API 实现一个简单的绘制线功能,需要处理多种鼠标事件:

javascript 复制代码
// 传统方式:需要手动管理大量事件和状态
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
const positions = [];
let activeEntity = null;

// 左键点击添加点
handler.setInputAction((event) => {
  const earthPosition = viewer.scene.pickPosition(event.position);
  if (Cesium.defined(earthPosition)) {
    positions.push(earthPosition);

    // 移除旧实体
    if (activeEntity) {
      viewer.entities.remove(activeEntity);
    }

    // 创建新实体
    activeEntity = viewer.entities.add({
      polyline: {
        positions: new Cesium.CallbackProperty(() => {
          return positions.concat(mousePosition || []);
        }, false),
        width: 3,
        material: Cesium.Color.YELLOW,
        clampToGround: true
      }
    });
  }
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

// 鼠标移动实时预览
let mousePosition = null;
handler.setInputAction((event) => {
  const earthPosition = viewer.scene.pickPosition(event.endPosition);
  if (Cesium.defined(earthPosition)) {
    mousePosition = earthPosition;
  }
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

// 右键完成绘制
handler.setInputAction((event) => {
  handler.destroy();
  // 还需要清理状态、处理回调...
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);

// 还需要处理双击事件、深度测试、光标样式...

问题在于:

  • 需要手动管理多种鼠标事件(点击、移动、右键、双击)
  • 状态管理复杂(临时点集合、移动点、当前实体等)
  • 需要处理实时预览的动态效果
  • 事件清理和资源释放容易遗漏
  • 代码重复率高,每种图形都要重写一遍

痛点 2:测量计算繁琐

实现距离和面积测量需要复杂的地理计算:

javascript 复制代码
// 传统方式:手动实现距离计算
function calculateDistance(positions) {
  let totalDistance = 0;

  for (let i = 0; i < positions.length - 1; i++) {
    const start = positions[i];
    const end = positions[i + 1];

    // 转换为经纬度
    const startCartographic = Cesium.Cartographic.fromCartesian(start);
    const endCartographic = Cesium.Cartographic.fromCartesian(end);

    const startLon = Cesium.Math.toDegrees(startCartographic.longitude);
    const startLat = Cesium.Math.toDegrees(startCartographic.latitude);
    const endLon = Cesium.Math.toDegrees(endCartographic.longitude);
    const endLat = Cesium.Math.toDegrees(endCartographic.latitude);

    // Haversine 公式计算球面距离
    const R = 6371; // 地球半径(公里)
    const dLat = (endLat - startLat) * Math.PI / 180;
    const dLon = (endLon - startLon) * Math.PI / 180;
    const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
             Math.cos(startLat * Math.PI / 180) * Math.cos(endLat * Math.PI / 180) *
             Math.sin(dLon/2) * Math.sin(dLon/2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
    totalDistance += R * c;
  }

  return totalDistance;
}

// 面积计算更复杂,需要考虑球面几何...
function calculateArea(positions) {
  // 需要实现球面多边形面积计算
  // 坐标转换、面积公式、单位换算...
  // 代码省略 50+ 行...
}

// 还需要创建标签显示结果
viewer.entities.add({
  position: lastPosition,
  label: {
    text: `${distance.toFixed(2)} 公里`,
    font: '14px sans-serif',
    fillColor: Cesium.Color.WHITE,
    outlineColor: Cesium.Color.BLACK,
    outlineWidth: 2,
    // 还有一堆样式配置...
  }
});

问题在于:

  • 地理计算公式复杂(Haversine 公式、球面几何)
  • 坐标系转换繁琐(笛卡尔坐标 ↔ 经纬度)
  • 需要手动创建和管理测量标签
  • 单位换算容易出错
  • 结果展示样式需要大量配置

痛点 3:生命周期管理混乱

绘制和测量功能需要严格的生命周期管理:

javascript 复制代码
// 需要手动管理各种资源
const drawHandlers = [];
const drawEntities = [];
const measureLabels = [];

// 开始新的绘制前需要清理
function clearAllDrawing() {
  // 清理事件监听器
  drawHandlers.forEach(handler => {
    handler.destroy();
  });
  drawHandlers.length = 0;

  // 清理实体
  drawEntities.forEach(entity => {
    viewer.entities.remove(entity);
  });
  drawEntities.length = 0;

  // 清理标签
  measureLabels.forEach(label => {
    viewer.entities.remove(label);
  });
  measureLabels.length = 0;

  // 重置光标样式
  viewer.container.style.cursor = 'default';

  // 还要重置各种状态变量...
}

// 如果忘记清理,会导致内存泄漏和意外行为

问题在于:

  • 资源清理逻辑分散,容易遗漏
  • 事件监听器未及时销毁导致内存泄漏
  • 状态重置不完整导致功能异常
  • 光标样式、深度测试等副作用未恢复
  • 缺乏统一的管理接口

痛点 4:样式配置重复

每次绘制都要配置相同的样式:

javascript 复制代码
// 每个项目都要重复配置样式
const lineStyle = {
  width: 3,
  material: new Cesium.PolylineDashMaterialProperty({
    color: Cesium.Color.YELLOW
  }),
  clampToGround: true,
  depthFailMaterial: new Cesium.PolylineDashMaterialProperty({
    color: Cesium.Color.YELLOW
  })
};

const polygonStyle = {
  material: Cesium.Color.RED.withAlpha(0.4),
  outline: true,
  outlineColor: Cesium.Color.RED,
  outlineWidth: 3,
  clampToGround: true,
  // ...
};

// 在不同地方使用时又要重新配置一遍

问题在于:

  • 样式配置分散,难以统一管理
  • 修改样式需要找到所有使用位置
  • 缺乏默认样式和自定义样式机制
  • 代码重复率高

💡 CesiumLite 的解决方案

核心设计思路

CesiumLite 的标绘测量模块采用了以下设计思路:

  1. 双工具分离架构DrawTool(绘制工具)+ MeasureTool(测量工具)
  2. 事件生命周期管理:自动注册、清理事件,防止内存泄漏
  3. 样式配置分层:默认样式 + 自定义样式,支持全局和局部配置
  4. 回调驱动设计:通过回调函数返回绘制结果,解耦业务逻辑
  5. 状态封装隔离:内部管理所有状态,外部只需调用简单接口

架构设计图

复制代码
┌─────────────────────────────────────────────────────┐
│             CesiumLite 核心类                        │
│                                                      │
│  ┌────────────────────┐      ┌──────────────────┐  │
│  │   DrawTool 绘制    │      │ MeasureTool 测量 │  │
│  │                    │      │                  │  │
│  │  - 点/线/面绘制    │◄─────┤  - 距离测量      │  │
│  │  - 鼠标事件管理    │      │  - 面积测量      │  │
│  │  - 实时预览        │      │  - 结果标注      │  │
│  │  - 样式配置        │      │  - 地理计算      │  │
│  │  - 回调通知        │      │                  │  │
│  └────────────────────┘      └──────────────────┘  │
│           ↓                            ↓             │
│  ┌──────────────────────────────────────────────┐  │
│  │          Cesium Viewer 实例                   │  │
│  │  - CustomDataSource(数据源隔离)            │  │
│  │  - ScreenSpaceEventHandler(事件处理)       │  │
│  └──────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────┘

🔧 核心代码实现

1. DrawTool:标绘工具核心

DrawTool 负责处理所有绘制逻辑,是标绘功能的核心:

javascript 复制代码
import {
    CustomDataSource,
    ScreenSpaceEventHandler,
    Cartesian3,
    Color,
    CallbackProperty,
    PolylineDashMaterialProperty,
    HeightReference,
    PolygonHierarchy,
    ScreenSpaceEventType
} from "cesium";

class DrawTool {
    constructor(viewer, options = {}) {
        this.viewer = viewer;
        this._drawHandler = null; // 事件处理器
        this._dataSource = null; // 数据源
        this._tempPositions = []; // 临时点集合
        this._mousePos = null; // 鼠标移动点
        this._drawType = null; // 绘制类型
        this._originalCursor = null; // 原始光标样式

        // 默认样式配置
        this.defaultStyles = {
            point: {
                color: Color.RED,
                pixelSize: 10,
                outlineColor: Color.YELLOW,
                outlineWidth: 2
            },
            polyline: {
                color: Color.RED,
                width: 3,
                material: new PolylineDashMaterialProperty({
                    color: Color.YELLOW
                })
            },
            polygon: {
                color: Color.RED,
                width: 3,
                fillColor: Color.RED.withAlpha(0.4),
                material: new PolylineDashMaterialProperty({
                    color: Color.YELLOW
                })
            }
        };

        // 合并用户配置
        this.options = {
            styles: {
                point: { ...this.defaultStyles.point, ...(options.styles?.point || {}) },
                polyline: { ...this.defaultStyles.polyline, ...(options.styles?.polyline || {}) },
                polygon: { ...this.defaultStyles.polygon, ...(options.styles?.polygon || {}) }
            }
        };
    }

    /**
     * 开始绘制
     * @param {string} drawType - 绘制类型:Point/Polyline/Polygon
     * @param {Function} callback - 完成回调
     */
    draw(drawType, callback) {
        this.clearAll(); // 清除之前的绘制
        this._drawType = drawType;
        this._dataSource = new CustomDataSource("_dataSource");
        this.viewer.dataSources.add(this._dataSource);

        // 更改光标为十字准星
        this._originalCursor = this.viewer.container.style.cursor;
        this.viewer.container.style.cursor = 'crosshair';

        this._registerEvents(callback); // 注册事件
    }

    /**
     * 注册鼠标事件
     */
    _registerEvents(callback) {
        this._drawHandler = new ScreenSpaceEventHandler(this.viewer.scene.canvas);
        this.viewer.scene.globe.depthTestAgainstTerrain = true; // 开启深度测试

        switch (this._drawType) {
            case "Point":
                this._leftClickEventForPoint();
                break;
            case "Polyline":
                this._leftClickEventForPolyline();
                this._mouseMoveEventForPolyline();
                this._rightClickEventForPolyline(callback);
                this._doubleClickEventForPolyline(callback);
                break;
            case "Polygon":
                this._leftClickEventForPolygon();
                this._mouseMoveEventForPolygon();
                this._rightClickEventForPolygon(callback);
                this._doubleClickEventForPolygon(callback);
                break;
        }
    }

    /**
     * 绘制多边形(核心实现)
     */
    _addPolygon() {
        if (this._tempPositions.length == 1) {
            // 一个顶点:绘制线
            this._dataSource.entities.add({
                polyline: {
                    positions: new CallbackProperty(() => {
                        let c = Array.from(this._tempPositions);
                        if (this._mousePos) {
                            c.push(this._mousePos);
                        }
                        return c;
                    }, false),
                    clampToGround: true,
                    ...this.options.styles.polygon
                }
            });
        } else {
            // 两个及以上顶点:绘制多边形
            this._dataSource.entities.removeAll();
            this._dataSource.entities.add({
                polygon: {
                    hierarchy: new CallbackProperty(() => {
                        let poss = Array.from(this._tempPositions);
                        if (this._mousePos) {
                            poss.push(this._mousePos);
                        }
                        return new PolygonHierarchy(poss);
                    }, false),
                    extrudedHeightReference: HeightReference.CLAMP_TO_GROUND,
                    material: this.options.styles.polygon.fillColor,
                    clampToGround: true
                },
                polyline: {
                    positions: new CallbackProperty(() => {
                        let c = Array.from(this._tempPositions);
                        if (this._mousePos) {
                            c.push(this._mousePos);
                            c.push(c[0]); // 首尾相连
                        }
                        return c;
                    }, false),
                    clampToGround: true,
                    ...this.options.styles.polygon
                }
            });
        }
    }

    /**
     * 清除所有绘制
     */
    clearAll() {
        this._removeAllEvent(); // 清除事件监听
        this._resetParams(); // 重置参数
    }

    /**
     * 移除所有事件监听
     */
    _removeAllEvent() {
        if (this._drawHandler) {
            this._drawHandler.removeInputAction(ScreenSpaceEventType.LEFT_CLICK);
            this._drawHandler.removeInputAction(ScreenSpaceEventType.MOUSE_MOVE);
            this._drawHandler.removeInputAction(ScreenSpaceEventType.RIGHT_CLICK);
            this._drawHandler.destroy();
            this._drawHandler = null;
        }

        // 恢复原始光标样式
        if (this._originalCursor !== null) {
            this.viewer.container.style.cursor = this._originalCursor;
            this._originalCursor = null;
        }
    }

    /**
     * 重置参数
     */
    _resetParams() {
        if (this._dataSource != null) {
            this._dataSource.entities.removeAll();
            this.viewer.dataSources.remove(this._dataSource);
        }
        this._dataSource = null;
        this._tempPositions = [];
        this._mousePos = null;
        this._drawType = null;
    }
}

export default DrawTool;

设计亮点:

  • 事件自动管理:自动注册、销毁事件,防止内存泄漏
  • 实时预览 :使用 CallbackProperty 实现动态效果
  • 样式分层:默认样式 + 用户自定义,灵活配置
  • 光标管理:自动切换光标样式,提升用户体验
  • 回调解耦:通过回调函数返回结果,业务逻辑解耦

2. MeasureTool:测量工具核心

MeasureTool 基于 DrawTool,提供距离和面积测量功能:

javascript 复制代码
import { Cartesian3, Cartographic, Math as CesiumMath, Color, Cartesian2 } from "cesium";
import DrawTool from "./draw";

class MeasureTool {
    constructor(viewer, options = {}) {
        this.viewer = viewer;
        this.drawTool = new DrawTool(viewer, options);
        this._measureEntities = new Map(); // 存储测量实体
        this._measureLabels = new Map(); // 存储测量标签
        this._currentMeasureId = null; // 当前测量ID
        this._measureCount = 0; // 测量计数器
    }

    /**
     * 开始测距
     * @param {Function} callback 测量完成回调
     */
    measureDistance(callback) {
        this.clearAll();
        this._measureCount++;
        this._currentMeasureId = `distance_${this._measureCount}`;

        this.drawTool.draw('Polyline', (entity) => {
            const positions = entity.polyline.positions.getValue();
            const totalDistance = this._calculateDistance(positions);

            // 存储测量实体和标签
            this._measureEntities.set(this._currentMeasureId, entity);

            // 在线的最后一个点添加标签
            const lastPosition = positions[positions.length - 1];
            const labelEntity = this._createLabel([lastPosition], totalDistance);
            this._measureLabels.set(this._currentMeasureId, labelEntity);

            if (callback) {
                callback({
                    id: this._currentMeasureId,
                    distance: totalDistance,
                    positions: positions
                });
            }
        });
    }

    /**
     * 开始测面
     * @param {Function} callback 测量完成回调
     */
    measureArea(callback) {
        this._measureCount++;
        this._currentMeasureId = `area_${this._measureCount}`;

        this.drawTool.draw('Polygon', (entity) => {
            const positions = entity.polygon.hierarchy.getValue().positions;
            const area = this._calculateArea(positions);

            // 存储测量实体
            this._measureEntities.set(this._currentMeasureId, entity);

            // 在面的中心点添加标签
            const center = this._calculatePolygonCenter(positions);
            const labelEntity = this._createLabel([center], area, true);
            this._measureLabels.set(this._currentMeasureId, labelEntity);

            if (callback) {
                callback({
                    id: this._currentMeasureId,
                    area: area,
                    positions: positions
                });
            }
        });
    }

    /**
     * 计算距离(Haversine 公式)
     */
    _calculateDistance(positions) {
        let totalDistance = 0;
        for (let i = 0; i < positions.length - 1; i++) {
            const start = positions[i];
            const end = positions[i + 1];
            totalDistance += this._getDistance(start, end);
        }
        return totalDistance;
    }

    /**
     * 使用 Haversine 公式计算两点间距离
     */
    _getDistance(start, end) {
        const startCartographic = Cartographic.fromCartesian(start);
        const endCartographic = Cartographic.fromCartesian(end);

        const startLon = CesiumMath.toDegrees(startCartographic.longitude);
        const startLat = CesiumMath.toDegrees(startCartographic.latitude);
        const endLon = CesiumMath.toDegrees(endCartographic.longitude);
        const endLat = CesiumMath.toDegrees(endCartographic.latitude);

        // Haversine 公式
        const R = 6371; // 地球半径(公里)
        const dLat = (endLat - startLat) * Math.PI / 180;
        const dLon = (endLon - startLon) * Math.PI / 180;
        const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
                 Math.cos(startLat * Math.PI / 180) * Math.cos(endLat * Math.PI / 180) *
                 Math.sin(dLon/2) * Math.sin(dLon/2);
        const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
        return R * c;
    }

    /**
     * 计算多边形面积(球面几何)
     */
    _calculateArea(positions) {
        let area = 0;
        const points = positions.map(pos => {
            const cartographic = Cartographic.fromCartesian(pos);
            return {
                longitude: CesiumMath.toDegrees(cartographic.longitude),
                latitude: CesiumMath.toDegrees(cartographic.latitude)
            };
        });

        for (let i = 0; i < points.length; i++) {
            const j = (i + 1) % points.length;
            area += points[i].longitude * points[j].latitude;
            area -= points[j].longitude * points[i].latitude;
        }

        // 转换为平方公里
        area = Math.abs(area) * 111.32 * 111.32 * Math.cos(points[0].latitude * Math.PI / 180) / 2;
        return area;
    }

    /**
     * 创建测量结果标签
     */
    _createLabel(positions, value, isArea = false) {
        const position = positions[0];
        const displayValue = isArea ?
            `${value.toFixed(2)} 平方公里` :
            `${value.toFixed(2)} 公里`;

        return this.viewer.entities.add({
            position: position,
            label: {
                text: displayValue,
                font: '14px sans-serif',
                fillColor: Color.WHITE,
                outlineColor: Color.BLACK,
                outlineWidth: 2,
                verticalOrigin: 1,
                pixelOffset: new Cartesian2(0, -10),
                showBackground: true,
                backgroundColor: new Color(0.165, 0.165, 0.165, 0.8),
                backgroundPadding: new Cartesian2(7, 5),
                disableDepthTestDistance: Number.POSITIVE_INFINITY
            }
        });
    }

    /**
     * 计算多边形中心点(质心)
     */
    _calculatePolygonCenter(positions) {
        let sumX = 0, sumY = 0, sumZ = 0;
        positions.forEach(pos => {
            sumX += pos.x;
            sumY += pos.y;
            sumZ += pos.z;
        });

        const center = new Cartesian3(
            sumX / positions.length,
            sumY / positions.length,
            sumZ / positions.length
        );

        const cartographic = Cartographic.fromCartesian(center);
        return Cartesian3.fromRadians(
            cartographic.longitude,
            cartographic.latitude,
            cartographic.height
        );
    }

    /**
     * 清除所有测量
     */
    clearAll() {
        this._measureEntities.forEach(entity => {
            this.viewer.entities.remove(entity);
        });
        this._measureEntities.clear();

        this._measureLabels.forEach(label => {
            this.viewer.entities.remove(label);
        });
        this._measureLabels.clear();

        this.drawTool.clearAll();
        this._currentMeasureId = null;
        this._measureCount = 0;
    }
}

export default MeasureTool;

设计亮点:

  • 复用绘制逻辑:基于 DrawTool,避免重复开发
  • 地理计算封装:Haversine 公式、球面几何计算
  • 结果自动标注:自动创建和定位测量标签
  • ID 管理:为每次测量分配唯一 ID,支持单独清除
  • 单位自动转换:自动转换并格式化显示单位

🎯 使用教程

基础用法

1. 初始化 CesiumLite
javascript 复制代码
const cesiumLite = new CesiumLite('cesiumContainer', {
  map: {
    baseMap: {
      id: 'imagery'
    },
    camera: {
      longitude: 116.397428,
      latitude: 39.90923,
      height: 1000000
    }
  }
});
2. 标绘功能
绘制点
javascript 复制代码
// 绘制点,并在完成时获取结果
cesiumLite.drawTool.draw('Point', (entity) => {
  console.log('绘制完成:', entity);
  const position = entity.position.getValue();
  console.log('点位坐标:', position);
});
绘制线
javascript 复制代码
// 绘制线(左键添加点,右键或双击完成)
cesiumLite.drawTool.draw('Polyline', (entity) => {
  console.log('绘制完成:', entity);
  const positions = entity.polyline.positions.getValue();
  console.log('线段坐标:', positions);
});
绘制面
javascript 复制代码
// 绘制面(左键添加点,右键或双击完成,至少3个点)
cesiumLite.drawTool.draw('Polygon', (entity) => {
  console.log('绘制完成:', entity);
  const positions = entity.polygon.hierarchy.getValue().positions;
  console.log('多边形坐标:', positions);
});
清除绘制
javascript 复制代码
// 清除所有绘制内容
cesiumLite.drawTool.clearAll();
3. 测量功能
测量距离
javascript 复制代码
// 开始测距(左键添加点,右键或双击完成)
cesiumLite.measureTool.measureDistance((result) => {
  console.log('测量ID:', result.id);
  console.log('总距离:', result.distance, '公里');
  console.log('测量点:', result.positions);
});
测量面积
javascript 复制代码
// 开始测面(左键添加点,右键或双击完成,至少3个点)
cesiumLite.measureTool.measureArea((result) => {
  console.log('测量ID:', result.id);
  console.log('总面积:', result.area, '平方公里');
  console.log('多边形顶点:', result.positions);
});
清除测量
javascript 复制代码
// 清除所有测量结果
cesiumLite.measureTool.clearAll();

// 清除指定测量
cesiumLite.measureTool.clear('distance_1');

高级用法

自定义绘制样式
javascript 复制代码
// 创建 CesiumLite 时配置全局样式
const cesiumLite = new CesiumLite('cesiumContainer', {
  map: { /* ... */ },
  drawStyles: {
    point: {
      color: Cesium.Color.BLUE,
      pixelSize: 15,
      outlineColor: Cesium.Color.WHITE,
      outlineWidth: 3
    },
    polyline: {
      color: Cesium.Color.GREEN,
      width: 5,
      material: new Cesium.PolylineDashMaterialProperty({
        color: Cesium.Color.GREEN
      })
    },
    polygon: {
      fillColor: Cesium.Color.BLUE.withAlpha(0.3),
      color: Cesium.Color.BLUE,
      width: 4
    }
  }
});
绘制结果处理
javascript 复制代码
// 保存绘制结果到业务系统
cesiumLite.drawTool.draw('Polygon', (entity) => {
  const positions = entity.polygon.hierarchy.getValue().positions;

  // 转换为经纬度数组
  const coordinates = positions.map(pos => {
    const cartographic = Cesium.Cartographic.fromCartesian(pos);
    return {
      longitude: Cesium.Math.toDegrees(cartographic.longitude),
      latitude: Cesium.Math.toDegrees(cartographic.latitude),
      height: cartographic.height
    };
  });

  // 提交到后端
  fetch('/api/save-polygon', {
    method: 'POST',
    body: JSON.stringify({ coordinates }),
    headers: { 'Content-Type': 'application/json' }
  });
});
测量结果展示
javascript 复制代码
// 在自定义UI中展示测量结果
cesiumLite.measureTool.measureDistance((result) => {
  // 更新UI显示
  document.getElementById('distance-result').innerHTML = `
    <div class="measure-result">
      <strong>测量距离:</strong>
      <span>${result.distance.toFixed(2)} 公里</span>
      <span>${(result.distance * 1000).toFixed(2)} 米</span>
    </div>
  `;

  // 保存到历史记录
  saveMeasureHistory({
    type: 'distance',
    value: result.distance,
    timestamp: new Date(),
    positions: result.positions
  });
});
结合按钮控制
javascript 复制代码
// 完整的交互示例
const drawButtons = {
  point: document.getElementById('drawPoint'),
  line: document.getElementById('drawLine'),
  polygon: document.getElementById('drawPolygon'),
  clear: document.getElementById('clearDraw')
};

const measureButtons = {
  distance: document.getElementById('measureDistance'),
  area: document.getElementById('measureArea'),
  clear: document.getElementById('clearMeasure')
};

// 移除所有按钮的激活状态
function removeActiveState() {
  Object.values({...drawButtons, ...measureButtons}).forEach(btn => {
    btn.classList.remove('active');
  });
}

// 绘制点
drawButtons.point.addEventListener('click', () => {
  removeActiveState();
  drawButtons.point.classList.add('active');
  cesiumLite.drawTool.draw('Point', (entity) => {
    console.log('点绘制完成');
    removeActiveState();
  });
});

// 绘制线
drawButtons.line.addEventListener('click', () => {
  removeActiveState();
  drawButtons.line.classList.add('active');
  cesiumLite.drawTool.draw('Polyline', (entity) => {
    console.log('线绘制完成');
    removeActiveState();
  });
});

// 绘制面
drawButtons.polygon.addEventListener('click', () => {
  removeActiveState();
  drawButtons.polygon.classList.add('active');
  cesiumLite.drawTool.draw('Polygon', (entity) => {
    console.log('面绘制完成');
    removeActiveState();
  });
});

// 清除绘制
drawButtons.clear.addEventListener('click', () => {
  removeActiveState();
  cesiumLite.drawTool.clearAll();
});

// 测距
measureButtons.distance.addEventListener('click', () => {
  removeActiveState();
  measureButtons.distance.classList.add('active');
  cesiumLite.measureTool.measureDistance((result) => {
    console.log('测距完成:', result.distance, '公里');
    removeActiveState();
  });
});

// 测面
measureButtons.area.addEventListener('click', () => {
  removeActiveState();
  measureButtons.area.classList.add('active');
  cesiumLite.measureTool.measureArea((result) => {
    console.log('测面完成:', result.area, '平方公里');
    removeActiveState();
  });
});

// 清除测量
measureButtons.clear.addEventListener('click', () => {
  removeActiveState();
  cesiumLite.measureTool.clearAll();
});

📊 对比传统开发方式

代码量对比

功能 传统方式 CesiumLite 减少代码量
绘制点 30+ 行 3 行 90%
绘制线 80+ 行 3 行 96%
绘制面 120+ 行 3 行 97%
距离测量 150+ 行 5 行 97%
面积测量 180+ 行 5 行 97%
清除功能 30+ 行 1 行 97%

功能对比

功能 传统方式 CesiumLite
基础绘制
实时预览 ❌ 需手动实现 ✅ 内置支持
事件管理 ❌ 手动管理 ✅ 自动管理
样式配置 ❌ 分散配置 ✅ 集中配置
测量计算 ❌ 需手动实现 ✅ 内置算法
结果标注 ❌ 需手动创建 ✅ 自动标注
光标管理 ❌ 需手动切换 ✅ 自动切换
内存管理 ❌ 容易泄漏 ✅ 自动清理

开发体验对比

方面 传统方式 CesiumLite
学习难度 ⭐⭐⭐⭐⭐ 需深入了解 Cesium API ⭐⭐ 简单易学
开发效率 ⭐⭐ 大量重复代码 ⭐⭐⭐⭐⭐ 开箱即用
代码维护 ⭐⭐ 分散难维护 ⭐⭐⭐⭐⭐ 集中易维护
Bug 风险 ⭐⭐⭐⭐ 事件泄漏、状态混乱 ⭐ 风险极低

🚀 快速开始

1. 安装

bash 复制代码
# NPM 安装(推荐)
npm install cesium-lite

# 或者通过 GitHub 克隆
git clone https://github.com/lukeSuperCoder/cesium-lite.git
cd cesium-lite
npm install

2. 引入使用

方式一:NPM 方式
javascript 复制代码
import CesiumLite from 'cesium-lite';
import 'cesium/Build/Cesium/Widgets/widgets.css';

const cesiumLite = new CesiumLite('cesiumContainer', {
  // 配置项
});

// 开始绘制
cesiumLite.drawTool.draw('Polygon', (result) => {
  console.log('绘制完成', result);
});

方式二:本地运行项目

bash 复制代码
# 克隆项目
git clone https://github.com/lukeSuperCoder/cesium-lite.git
cd cesium-lite

# 安装依赖
npm install

# 启动开发服务器
npm run dev

访问 http://localhost:8020/examples/draw.html 查看标绘测量示例。

💡 最佳实践建议

1. 交互提示优化

javascript 复制代码
// 添加交互提示,提升用户体验
cesiumLite.drawTool.draw('Polygon', (entity) => {
  // 绘制开始时显示提示
  showToast('左键添加点,右键或双击完成绘制');

  // 绘制完成后的处理
  showToast('绘制完成!');
});

2. 错误处理

javascript 复制代码
// 添加错误处理
try {
  cesiumLite.measureTool.measureArea((result) => {
    if (result.area < 0.01) {
      showWarning('测量面积过小,请重新绘制');
      cesiumLite.measureTool.clear(result.id);
    }
  });
} catch (error) {
  console.error('测量失败:', error);
  showError('测量失败,请重试');
}

3. 样式主题化

javascript 复制代码
// 定义多套主题样式
const themes = {
  default: {
    point: { color: Cesium.Color.RED, pixelSize: 10 },
    polyline: { color: Cesium.Color.YELLOW, width: 3 },
    polygon: { fillColor: Cesium.Color.RED.withAlpha(0.4) }
  },
  blue: {
    point: { color: Cesium.Color.BLUE, pixelSize: 10 },
    polyline: { color: Cesium.Color.CYAN, width: 3 },
    polygon: { fillColor: Cesium.Color.BLUE.withAlpha(0.4) }
  }
};

// 切换主题
function setTheme(themeName) {
  const cesiumLite = new CesiumLite('cesiumContainer', {
    drawStyles: themes[themeName]
  });
}

4. 测量历史记录

javascript 复制代码
// 保存测量历史
const measureHistory = [];

cesiumLite.measureTool.measureDistance((result) => {
  measureHistory.push({
    id: result.id,
    type: 'distance',
    value: result.distance,
    timestamp: new Date(),
    positions: result.positions
  });

  // 更新历史列表UI
  updateHistoryList(measureHistory);
});

// 重新显示历史测量
function showHistoryMeasure(id) {
  const history = measureHistory.find(h => h.id === id);
  if (history) {
    // 重新绘制测量结果
    // ...
  }
}

🔮 未来规划

标绘测量模块后续将会支持:

  • 更多几何图形(圆形、矩形、箭头等)
  • 编辑功能(移动点、修改形状)
  • 吸附功能(点吸附、边界吸附)
  • 测量单位切换(公里/米/英里)
  • 高程测量(海拔高度、相对高度)
  • 坡度坡向分析
  • 测量结果导出(GeoJSON、KML)
  • 撤销/重做功能

📚 相关资源

🙏 总结

CesiumLite 的标绘测量模块通过模块化封装和事件自动管理,有效解决了 Cesium 原生开发中的诸多痛点:

  • 极简 API:减少 90%+ 的代码量,一行代码完成绘制
  • 自动管理:事件、状态、资源全自动管理,零内存泄漏
  • 实时预览:动态效果开箱即用,交互体验优秀
  • 精确计算:内置地理计算算法,结果准确可靠
  • 样式灵活:支持全局和局部样式配置,满足不同需求

如果你正在使用 Cesium 开发标绘测量功能,CesiumLite 将是你的最佳选择,让开发效率提升 10 倍!


⭐ 如果这个项目对你有帮助,欢迎给个 Star 支持一下!

💬 有任何问题或建议,欢迎在评论区交流!

相关标签: #Cesium #三维地图 #WebGIS #标绘测量 #前端开发 #JavaScript #开源项目 #地图可视化

相关推荐
1024小神1 小时前
swiftui和uikit的桥梁:UIViewRepresentable、Coordinator
前端
岁月宁静1 小时前
从 JavaScript 到 Python:前端工程师的完全转换指南
前端·javascript·python
特严赤傲2 小时前
在 Vue 中 v-for的写法对语法高亮的影响
前端·javascript·vue.js
PBitW2 小时前
从前端工程师的角度将SEO做到极致 -- 菜鸟来实操
前端·seo
雨雨雨雨雨别下啦2 小时前
【从0开始学前端】vue3路由,Pinia,组件通信
前端·vue.js·vue
调皮LE2 小时前
前端 HTML 转 PDF
前端
Nan_Shu_6142 小时前
熟悉RuoYi-Vue-Plus-前端 (1)
前端·javascript·vue.js
23124_802 小时前
网络管理-1
运维·服务器·前端
PBitW2 小时前
Electron 初体验
前端·electron·trae