cesium 鼠标动态绘制多边形面、贴地面、水面、材质特效

2,180 阅读5分钟

实现在cesium中基于鼠标动态绘制面功能,包括颜色模式,贴图模式,水面模式,贴地模式

1.基本架构设计

1.1封装 PolygonPrimitive 动态绘制多边形面操作类

class PolygonPrimitive {
  private app: any;
  private _handler: ScreenSpaceEventHandler;
  private _handler2: ScreenSpaceEventHandler;
  private _polygonConfigList: Map<any, any>;
  private _polygonEntityList: Map<any, any>;
  private _primitiveChecked: boolean;

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

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

    this._polygonConfigList = new Map();
    this._polygonEntityList = new Map();

    this._primitiveChecked = false;
  }

  /**
   * 根据已知数据添加一个多边形面
   * @param config 多边形面的配置项
   */
  add(config: PolygonConfig) {}

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

  /**
   * 改变多边形面的样式
   * @param config 多边形面的配置项
   * @returns
   */
  changeStyle(config: PolygonConfig): Boolean {}

  /**
   * 创建材质
   * @param config 多边形面的配置项
   * @returns
   */
  private createMaterial(config: PolygonConfig): Material {}

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

  /**
   * 开启绘制多边形面
   * @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 封装 PolygonConfig 多边形面的配置项

/**
 * PolygonConfig 多边形面的配置项
 */
class PolygonConfig {
  /** id区分多边形面 */
  id: string;
  /** 多边形面的位置数据 */
  positions: Cartesian3[];
  /** 多边形面的样式 */
  material: MaterialColor | MaterialWater | MaterialImage;
  /** 是否贴地 */
  clampToGround: boolean;

  constructor() {
    this.id = '';
    this.positions = [];
    this.material = new MaterialColor();
    this.clampToGround = false;
  }
}

/**
 * 颜色材质
 */
class MaterialColor {
  materialType: string;
  materialOptions: {
    /** 颜色值 */
    color: string;
  };

  constructor() {
    this.materialType = 'Color';
    this.materialOptions = {
      color: '#fff'
    };
  }
}

/**
 * 水面材质
 */
class MaterialWater {
  materialType: string;
  materialOptions: {
    /** 水面法线贴图 */
    normalMap: string;
    /** 水面波纹数量 */
    frequency: number;
    /** 水面波纹流动速度 */
    animationSpeed: number;
  };

  constructor() {
    this.materialType = 'Water';
    this.materialOptions = {
      normalMap: '',
      frequency: 1000.0,
      animationSpeed: 0.05
    };
  }
}

/**
 * 图片材质
 */
class MaterialImage {
  materialType: string;
  materialOptions: {
    /** 图片地址 */
    image: string;
    /** X轴方向贴图重复次数 */
    repeatX: number;
    /** Y轴方向贴图重复次数 */
    repeatY: number;
  };

  constructor() {
    this.materialType = 'Image';
    this.materialOptions = {
      image: '',
      repeatX: 1.0,
      repeatY: 1.0
    };
  }
}

封装配置项的作用是为了方便使用者配置多边形面效果

2. 关键代码实现

2. 绘制多边形面交互相关事件

  /**
   * 开启绘制多边形面
   * @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: Primitive | null;
    let dynamicPositions: any;

    // 鼠标左键点击事件
    this._handler.setInputAction((event: any) => {
      if (this._primitiveChecked) 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 new PolygonHierarchy(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[]) {
    const shape = this.app.viewerCesium.entities.add({
      polygon: {
        hierarchy: positionData,
        perPositionHeight: true // 指定是否使用每个位置的高度(polygon的entity形式默认试贴地的,所以如果不配置成true,他会改变position位置数据,以达到贴地效果。
        //为了保证位置数据是原始拾取到的数据,这里需要配置perPositionHeight: true。
      }
    });
    return shape;
  }

2.2 创建材质相关

  /**
   * 创建材质
   * @param config 多边形面的配置项
   * @returns
   */
  private createMaterial(config: PolygonConfig): Material {
    let material = null;

    switch (config.material.materialType) {
      case 'Color':
        const materialColor = config.material as MaterialColor;

        material = new Material({
          fabric: {
            type: 'Color',
            uniforms: {
              color: Color.fromCssColorString(materialColor.materialOptions.color)
            }
          }
        });
        break;

      case 'Water':
        const materialWater = config.material as MaterialWater;

        const normalMap =
          materialWater.materialOptions.normalMap ||
          window.CcbimGisSDKLoaderConfig.staticHost +
            '/ccbimGisSDK@' +
            window.CcbimGisSDKLoaderConfig.version +
            '/ccbimGis/bimAssets/assets/images/effect/waterNormals.jpg';

        material = new Material({
          fabric: {
            type: 'Water',
            uniforms: {
              normalMap: normalMap,
              frequency: materialWater.materialOptions.frequency,
              animationSpeed: materialWater.materialOptions.animationSpeed
            }
          }
        });
        break;

      case 'Image':
        const materialImage = config.material as MaterialImage;

        const image =
          materialImage.materialOptions.image ||
          window.CcbimGisSDKLoaderConfig.staticHost +
            '/ccbimGisSDK@' +
            window.CcbimGisSDKLoaderConfig.version +
            '/ccbimGis/bimAssets/assets/images/pmlogo.jpg';

        material = new Material({
          fabric: {
            type: 'Image',
            uniforms: {
              image: image,
              repeat: new Cartesian2(materialImage.materialOptions.repeatX, materialImage.materialOptions.repeatY)
            }
          }
        });
        break;

      default:
        material = new Material({
          fabric: {
            type: 'Color',
            uniforms: {
              color: Color.fromCssColorString('#fff')
            }
          }
        });
        break;
    }

    return material;
  }

cesium在Material中实现了水面效果,所以我们使用Primitive的material,而不是Entity的Property

image.png

image.png

2.3 添加polygon实体

  /**
   * 根据已知数据添加一个多边形面
   * @param config 多边形面的配置项
   */
  add(config: PolygonConfig) {
    const configCopy = cloneDeep(config);

    const positions = configCopy.positions;

    const material = this.createMaterial(configCopy);

    let polygonPrimitive;
    if (configCopy.clampToGround) {
      // 贴地
      polygonPrimitive = new GroundPrimitive({
        geometryInstances: new GeometryInstance({
          id: 'polygonEntity_' + configCopy.id,
          geometry: new PolygonGeometry({
            polygonHierarchy: new PolygonHierarchy(positions)
          })
        }),
        appearance: new EllipsoidSurfaceAppearance({
          material: material
        })
      });
    } else {
      // 不贴地
      polygonPrimitive = new Primitive({
        geometryInstances: new GeometryInstance({
          id: 'polygonEntity_' + configCopy.id,
          geometry: new PolygonGeometry({
            polygonHierarchy: new PolygonHierarchy(positions),
            perPositionHeight: true // 使用每个位置的位置高度,而不是使用配置的高度来确定高度。如果不配置true,则会以配置的height高度来显示,即面与椭球体表面之间的距离显示,默认值0.0,会被地形遮挡看不见。
          })
        }),
        appearance: new EllipsoidSurfaceAppearance({
          material: material
        })
      });
    }

    this.app.viewerCesium.scene.primitives.add(polygonPrimitive);

    this._polygonConfigList.set('polygonEntity_' + configCopy.id, configCopy);
    this._polygonEntityList.set('polygonEntity_' + configCopy.id, polygonPrimitive);
  }

image.png

image.png

3. 业务端调用

  const polygonPrimitive = new Plugins.Polygon.PolygonPrimitive(gisApp);

  // -------------鼠标动态绘制多边形面----------------
  polygonPrimitive.startDrawing((data) => {
    console.log(data);
    let config = new Plugins.Polygon.PolygonConfig();

    const id = ++polygonId;
    polygonConfigMap.set(id, config);

    config.id = id; // id必传且不能重复
    config.positions = data.cartesian3;
    polygonPrimitive.add(config);
  });
  // -------------鼠标动态绘制多边形面----------------

  // polygonPrimitive.endDrawing();

  // -------------改变成水面样式-----------------
  let config = polygonConfigMap.get(polygonId);
  let material = new Plugins.Polygon.MaterialWater();
  config.material = material;
  polygonPrimitive.changeStyle(config);
  // -------------改变成水面样式-----------------

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

4. 效果

polygon动画.webp