cesium 鼠标动态绘制线及线动效

实现在cesium中基于鼠标动态绘制线功能

1.基本架构设计

1.1封装PolylineEntity 动态绘制线操作类

ts 复制代码
import {
  ScreenSpaceEventHandler,
  ScreenSpaceEventType,
} from 'cesium';

class PolylineEntity {
  private app: any;
  private _handler: ScreenSpaceEventHandler;
  private _handler2: ScreenSpaceEventHandler;

  constructor(app: any) {
    this.app = app;

    this._handler = new ScreenSpaceEventHandler(this.app.viewerCesium.scene.canvas);
    this._handler2 = new ScreenSpaceEventHandler(this.app.viewerCesium.scene.canvas);
  }

  /**
   * 根据已知数据添加一条线
   * @param config 线的配置项
   */
  add(config: PolylineConfig) {}

  /**
   * 根据id删除对应的线
   * @param id
   * @returns
   */
  remove(id: string): Boolean {}

  /**
   * 改变线的样式
   * @param config 线的配置项
   * @returns
   */
  changeStyle(config: PolylineConfig): Boolean {}

  /**
   * 创建材质
   * @param config 线的配置项
   * @returns
   */
  private createMaterial(config: PolylineConfig) {}

  /**
   * 绘制形状,用于内部临时画线
   * @param positionData 位置数据
   * @param config 线的配置项
   * @returns
   */
  private drawShape(positionData: Cartesian3[], config?: PolylineConfig) {}

  /**
   * 开启绘制线
   * @param successCallback 绘制线完成后的回调,返回线的Cartesian3[]位置数据,此时调用add添加线,并可进行业务逻辑处理
   */
  startDrawing(successCallback: Function) {
    // ...someCode...
    this._handler.setInputAction((event: any) => {}, ScreenSpaceEventType.LEFT_CLICK);
    this._handler.setInputAction((event: any) => {}, ScreenSpaceEventType.MOUSE_MOVE);
    this._handler.setInputAction(() => {}, ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
    this._handler.setInputAction(() => {}, ScreenSpaceEventType.RIGHT_CLICK);
  }

  /**
   * 关闭绘制线
   */
  endDrawing() {
    this._handler.removeInputAction(ScreenSpaceEventType.LEFT_CLICK);
    this._handler.removeInputAction(ScreenSpaceEventType.MOUSE_MOVE);
    this._handler.removeInputAction(ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
    this._handler.removeInputAction(ScreenSpaceEventType.RIGHT_CLICK);
  }

  /**
   * 开启线选中
   * @param successCallback 选中线的回调,返回这条线的config相关数据
   */
  openSelection(successCallback: Function) {
    this._handler2.setInputAction((event: any) => {}, ScreenSpaceEventType.MOUSE_MOVE);
    this._handler2.setInputAction((event: any) => {}, ScreenSpaceEventType.LEFT_CLICK);
  }

  /**
   * 关闭线选中
   */
  closeSelection() {
    this._handler2.removeInputAction(ScreenSpaceEventType.MOUSE_MOVE);
    this._handler2.removeInputAction(ScreenSpaceEventType.LEFT_CLICK);
  }
}

以上是一些基本的操作函数,具体的实现视业务而定

1.2封装PolylineConfig 线的配置项

ts 复制代码
interface StyleInterface {
  /** 线的宽度 */
  width: number;
  /** 线的颜色 */
  color: string;
  /** 是否贴地 */
  clampToGround: Boolean;
  /** 粒子效果 */
  particle: {
    /** 是否开启使用 */
    used: boolean;
    /** 自定义图片 */
    image: string;
    /** 运动方向,true正向,false反向 */
    forward: boolean;
    /** 速度 */
    speed: number;
    /** 贴图重复次数 */
    repeatX: number;
  };
}

class PolylineConfig {
  /** id区分线 */
  id: string;
  /** 线的位置数据 */
  positions: Cartesian3[];
  /** 线的样式 */
  style: StyleInterface;

  constructor() {
    this.id = '';
    this.positions = [];

    this.style = {
      width: 3.0,
      color: '#fff',
      clampToGround: false,
      particle: {
        used: false,
        image: '',
        forward: true,
        speed: 1.0,
        repeatX: 1.0
      }
    };
  }
}

封装配置项的作用是为了方便使用者配置线效果

2.关键代码实现

2.1绘制线交互相关事件

ts 复制代码
  /**
   * 开启绘制线
   * @param successCallback 绘制线完成后的回调,返回线的Cartesian3[]位置数据,此时调用add添加线,并可进行业务逻辑处理
   */
  startDrawing(successCallback: Function) {
    // 预防多次调用这个函数,多次绑定事件,所以先解绑事件
    this.endDrawing();

    // -----------文字提示--------------
    textEntity = new Entity({
      position: new Cartesian3(),
      label: {
        show: false,
        text: '单击画线',
        font: '14px',
        scale: 0.8,
        showBackground: true,
        disableDepthTestDistance: Number.POSITIVE_INFINITY,
        pixelOffset: new Cartesian2(0.0, 30.0)
      }
    });
    this.app.viewerCesium.entities.add(textEntity);
    // -----------文字提示--------------

    let activeShapePoints: Cartesian3[] = [];
    let activeShape: Entity | null;
    let dynamicPositions: any;

    // 鼠标左键点击事件
    this._handler.setInputAction((event: any) => {
      if (this._lineChecked) return;

      const ray = this.app.viewerCesium.camera.getPickRay(event.position);
      const earthPosition = this.app.viewerCesium.scene.globe.pick(ray, this.app.viewerCesium.scene);

      if (defined(earthPosition)) {
        if (activeShapePoints.length === 0) {
          activeShapePoints.push(earthPosition);
          // 使用CallbackProperty来实时画线,当activeShapePoints改变时,会自动计算位置实时绘制新的线
          dynamicPositions = new CallbackProperty(() => {
            return activeShapePoints;
          }, false);

          activeShape = this.drawShape(dynamicPositions);
        }

        if (activeShapePoints.length === 1) {
          // @ts-ignore
          textEntity.label.text = '左键双击结束,右键撤销';
        }

        activeShapePoints.push(earthPosition);
      }
    }, ScreenSpaceEventType.LEFT_CLICK);

    // 鼠标移动事件(实现画线,位置更新)
    this._handler.setInputAction((event: any) => {
      const ray = this.app.viewerCesium.camera.getPickRay(event.endPosition);
      const earthPosition = this.app.viewerCesium.scene.globe.pick(ray, this.app.viewerCesium.scene);

      //@ts-ignore
      if (!textEntity.label.show._value) {
        // @ts-ignore
        textEntity.label.show = true;
      }

      textEntity.position = earthPosition;

      if (activeShapePoints.length > 0) {
        if (defined(earthPosition)) {
          activeShapePoints.pop();
          activeShapePoints.push(earthPosition);
        }
      }
    }, ScreenSpaceEventType.MOUSE_MOVE);

    // 鼠标左键双击结束绘制
    this._handler.setInputAction(() => {
      activeShapePoints.pop();
      activeShapePoints.pop();
      this.app.viewerCesium.entities.remove(activeShape);

      // 坐标转换成经纬度高度
      let geographyCoords = [];
      for (let i = 0; i < activeShapePoints.length; i++) {
        const cartographic = Cartographic.fromCartesian(activeShapePoints[i]);
        const longitude = CesiumMath.toDegrees(cartographic.longitude);
        const latitude = CesiumMath.toDegrees(cartographic.latitude);
        const height = cartographic.height;
        geographyCoords.push({
          longitude,
          latitude,
          height
        });
      }

      // 返回相关数据供业务端使用
      successCallback &&
        successCallback({
          gc: geographyCoords,
          cartesian3: activeShapePoints
        });

      activeShape = null;
      activeShapePoints = [];
      dynamicPositions = null;

      // @ts-ignore
      textEntity.label.text = '单击画线';
    }, ScreenSpaceEventType.LEFT_DOUBLE_CLICK);

    // 右键点击事件撤销上一步操作
    this._handler.setInputAction(() => {
      activeShapePoints.pop();
      if (activeShapePoints.length === 1) {
        // @ts-ignore
        textEntity.label.text = '单击画线';
      }
    }, ScreenSpaceEventType.RIGHT_CLICK);
  }

  /**
   * 绘制形状,用于内部临时画线
   * @param positionData 位置数据
   * @param config 线的配置项
   * @returns
   */
  private drawShape(positionData: Cartesian3[], config?: PolylineConfig) {
    const plConfig = config || new PolylineConfig();
    const material = this.createMaterial(plConfig);

    const shape = this.app.viewerCesium.entities.add({
      polyline: {
        positions: positionData,
        width: plConfig.style.width,
        material: material,
        clampToGround: plConfig.style.clampToGround
      }
    });
    return shape;
  }

2.2创建材质相关

ts 复制代码
  /**
   * 创建材质
   * @param config 线的配置项
   * @returns
   */
  private createMaterial(config: PolylineConfig) {
    let material = new ColorMaterialProperty(Color.fromCssColorString(config.style.color));
    if (config.style.particle.used) {
      material = new PolylineFlowMaterialProperty({
        image: config.style.particle.image,
        forward: config.style.particle.forward ? 1.0 : -1.0,
        speed: config.style.particle.speed,
        repeat: new Cartesian2(config.style.particle.repeatX, 1.0)
      });
    }

    return material;
  }

创建PolylineFlowMaterialProperty.js(具体为何如此请看这篇文章,cesium自定义材质 juejin.cn/post/728795...

js 复制代码
import { Color, defaultValue, defined, Property, createPropertyDescriptor, Material, Event, Cartesian2 } from 'cesium';

const defaultColor = Color.TRANSPARENT;
import defaultImage from '../../../assets/images/effect/line-color-yellow.png';
const defaultForward = 1;
const defaultSpeed = 1;
const defaultRepeat = new Cartesian2(1.0, 1.0);

class PolylineFlowMaterialProperty {
  constructor(options) {
    options = defaultValue(options, defaultValue.EMPTY_OBJECT);

    this._definitionChanged = new Event();
    // 定义材质变量
    this._color = undefined;
    this._colorSubscription = undefined;
    this._image = undefined;
    this._imageSubscription = undefined;
    this._forward = undefined;
    this._forwardSubscription = undefined;
    this._speed = undefined;
    this._speedSubscription = undefined;
    this._repeat = undefined;
    this._repeatSubscription = undefined;
    // 变量初始化
    this.color = options.color || defaultColor; //颜色
    this.image = options.image || defaultImage; //材质图片
    this.forward = options.forward || defaultForward;
    this.speed = options.speed || defaultSpeed;
    this.repeat = options.repeat || defaultRepeat;
  }

  // 材质类型
  getType() {
    return 'PolylineFlow';
  }

  // 这个方法在每次渲染时被调用,result的参数会传入glsl中。
  getValue(time, result) {
    if (!defined(result)) {
      result = {};
    }

    result.color = Property.getValueOrClonedDefault(this._color, time, defaultColor, result.color);
    result.image = Property.getValueOrClonedDefault(this._image, time, defaultImage, result.image);
    result.forward = Property.getValueOrClonedDefault(this._forward, time, defaultForward, result.forward);
    result.speed = Property.getValueOrClonedDefault(this._speed, time, defaultSpeed, result.speed);
    result.repeat = Property.getValueOrClonedDefault(this._repeat, time, defaultRepeat, result.repeat);

    return result;
  }

  equals(other) {
    return (
      this === other ||
      (other instanceof PolylineFlowMaterialProperty &&
        Property.equals(this._color, other._color) &&
        Property.equals(this._image, other._image) &&
        Property.equals(this._forward, other._forward) &&
        Property.equals(this._speed, other._speed) &&
        Property.equals(this._repeat, other._repeat))
    );
  }
}

Object.defineProperties(PolylineFlowMaterialProperty.prototype, {
  isConstant: {
    get: function get() {
      return (
        Property.isConstant(this._color) &&
        Property.isConstant(this._image) &&
        Property.isConstant(this._forward) &&
        Property.isConstant(this._speed) &&
        Property.isConstant(this._repeat)
      );
    }
  },

  definitionChanged: {
    get: function get() {
      return this._definitionChanged;
    }
  },

  color: createPropertyDescriptor('color'),
  image: createPropertyDescriptor('image'),
  forward: createPropertyDescriptor('forward'),
  speed: createPropertyDescriptor('speed'),
  repeat: createPropertyDescriptor('repeat')
});

Material.PolylineFlowType = 'PolylineFlow';
Material._materialCache.addMaterial(Material.PolylineFlowType, {
  fabric: {
    type: Material.PolylineFlowType,
    uniforms: {
      // uniforms参数跟我们上面定义的参数以及getValue方法中返回的result对应,这里值是默认值
      color: defaultColor,
      image: defaultImage,
      forward: defaultForward,
      speed: defaultSpeed,
      repeat: defaultRepeat
    },
    // source编写glsl,可以使用uniforms参数,值来自getValue方法的result
    source: `czm_material czm_getMaterial(czm_materialInput materialInput)
    {
      czm_material material = czm_getDefaultMaterial(materialInput);

      vec2 st = materialInput.st;
      vec4 fragColor = texture(image, fract(vec2(st.s - speed*czm_frameNumber*0.005*forward, st.t)*repeat));

      material.emission = fragColor.rgb;
      material.alpha = fragColor.a;

      return material;
    }`
  },
  translucent: true
});

export { PolylineFlowMaterialProperty };

2.3添加polyline实体

ts 复制代码
  /**
   * 根据已知数据添加一条线
   * @param config 线的配置项
   */
  add(config: PolylineConfig) {
    const configCopy = cloneDeep(config);

    const positions = configCopy.positions;

    const material = this.createMaterial(configCopy);

    let distance = new DistanceDisplayCondition();
    if (configCopy.distanceDisplayCondition) {
      distance = new DistanceDisplayCondition(
        configCopy.distanceDisplayCondition.near,
        configCopy.distanceDisplayCondition.far
      );
    }

    this.app.viewerCesium.entities.add({
      id: 'polylineEntity_' + configCopy.id,
      polyline: {
        positions: positions,
        width: configCopy.style.width,
        material: material,
        distanceDisplayCondition: distance,
        clampToGround: configCopy.style.clampToGround
      }
    });

    this._polylineConfigList.set('polylineEntity_' + config.id, config);
  }

3.业务端调用

js 复制代码
let polylineEntity = new Plugins.Polyline.PolylineEntity(gisApp);
polylineEntity.startDrawing((data) => {
  console.log(data);
  let config = new Plugins.Polyline.PolylineConfig();

  const id = ++polylineId;
  polylineConfigMap.set(id, config);

  config.id = id; // id必传且不能重复
  config.positions = data.cartesian3;
  polylineEntity.add(config);
});

// polylineEntity.endDrawing();
// polylineEntity.changeStyle(config);

这里的设计,将Polyline模块放在Plugins下,并对外提供接口,由业务端组装使用功能。

4.效果

相关推荐
崔庆才丨静觅2 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax