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.效果

相关推荐
Redstone Monstrosity5 分钟前
字节二面
前端·面试
东方翱翔13 分钟前
CSS的三种基本选择器
前端·css
Fan_web35 分钟前
JavaScript高级——闭包应用-自定义js模块
开发语言·前端·javascript·css·html
yanglamei196243 分钟前
基于GIKT深度知识追踪模型的习题推荐系统源代码+数据库+使用说明,后端采用flask,前端采用vue
前端·数据库·flask
千穹凌帝44 分钟前
SpinalHDL之结构(二)
开发语言·前端·fpga开发
dot.Net安全矩阵1 小时前
.NET内网实战:通过命令行解密Web.config
前端·学习·安全·web安全·矩阵·.net
Hellc0071 小时前
MacOS升级ruby版本
前端·macos·ruby
前端西瓜哥1 小时前
贝塞尔曲线算法:求贝塞尔曲线和直线的交点
前端·算法
又写了一天BUG1 小时前
npm install安装缓慢及npm更换源
前端·npm·node.js
cc蒲公英1 小时前
Vue2+vue-office/excel 实现在线加载Excel文件预览
前端·vue.js·excel