Cesium常用方法封装

Cesium常用方法封装

关于cesium的webpack配置,可查看:https://www.cnblogs.com/sanhuamao/p/18027139

源码:https://gitee.com/chenxiangzhi/cesium_webpack/tree/util/

这里列举了三个例子:

  • 摄像头视角切换
  • 经纬度选择器
  • 管理不同类型的坐标点

基本使用

import MapManager from '@/mapManager';

// 传入容器id与选项(包括viewer选项和扩展选项)
const mapManager = new MapManager('root', {
    showCurrentPoint: true, // 在中心位置显示图标
    homeButton: true,  // 显示 回归中心按钮
});

// 初始化地图
mapManager.initMap()

下面是基本的封装:

import * as Cesium from 'cesium';

// 扩展类型
type ExtentionOption = {
   position?: [lng: number, lat: number], // 设置中心位置
   showCurrentPoint?: boolean // 是否显示当前坐标点
}
type Point = {
   id: string,
   lng: number,
   lat: number,
   [key: string]: any
}


class MapManager {
    container: string; // 容器ID
    options: Options // 扩展配置项
    viewerOptions: Cesium.Viewer.ConstructorOptions // Viewer配置项
    Viewer: Cesium.Viewer; // viewer视图
    current: string // 默认点位id
   
    constructor(container: string, options:  Cesium.Viewer.ConstructorOptions & ExtentionOption = {}) {
          super();
          this.container = container;
          const { position = POSITION, showCurrentPoint = false, ...rest } = options

          this.options = { position, showCurrentPoint } // 扩展选项
          this.viewerOptions = rest // 除了扩展选项 其他的都是viewer的选项
   }
   
   // 初始化地图
   initMap() {
      this.Viewer = new Cesium.Viewer(this.container, {
         ...VIEWER_DEFAULT_OPTIONS,
         ...this.viewerOptions,
      });
      this.initHomeButton() // 定义回归原点的事件
      this.options.showCurrentPoint && this.updateCurrentPoint(...this.options.position) // 如果需要显示坐标,就将其绘制出来
      this.setCameraView() // 初始化摄像头视角
      return this.Viewer
   }
   
   // 绘制默认点位
   updateCurrentPoint(lng: number, lat: number) {
      // 这个点位只有一个,所以每次绘制时都得清除上一个
      if (this.current) {
         this.Viewer.entities.removeById(this.current);
      }
      const pointId = this.genId(PointType.DEFAULT);  // 生成一个携带类型的uuid,这里的id是针对地图点位的,后面会说明原因
      this.current = pointId;
      this.addDefaultPoint(pointId, { id: generateUUID(), lng, lat }) 
   }
   
   // 添加点位:添加默认样式的点位
   addDefaultPoint = (pointId: string, data: Point) => {
      let { lng, lat } = data;
      this.Viewer.entities.add({
         id: pointId,
         position: Cesium.Cartesian3.fromDegrees(lng, lat, 10),
         billboard: {
            image: IMAGE_URL + 'position.png', // 定义的图片
            verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
            scale: 1.2
         }
      });
   }

   initHomeButton() {
      if (this.viewerOptions.homeButton) {
         this.Viewer.homeButton.viewModel.command.beforeExecute.addEventListener((e) => {
            e.cancel = true;
            this.setCameraView()
         });
      }
   }
   
   setCameraView(){
       // ...下面补充
   }
}

用到的常量如下:

// 默认配置

// 经纬度
export const POSITION: [lng: number, lat: number] = [116.39092423227684, 39.91215502466225]
// CESIUM Viewer选项
export const VIEWER_DEFAULT_OPTIONS = {
    animation: false, //左下角的动画仪表盘
    baseLayerPicker: false, //右上角的图层选择按钮
    geocoder: false, //搜索框
    homeButton: true, //home按钮
    sceneModePicker: false, //模式切换按钮
    timeline: false, //底部的时间轴
    navigationHelpButton: false, //右上角的帮助按钮,
    fullscreenButton: false, //右下角的全屏按钮
    infoBox: false, //信息框
    selectionIndicator: false, // 不要点击后的绿色框
};

1. 摄像头视角切换

import MapManager from '@/mapManager';
import { createButton } from '@/renderDemos/helper'
import { CAMERA_PITCH } from '@/mapManager/config'

const mapManager = new MapManager('root', {
    // 重置地图中心位置
    position: [116.39093217487861, 39.914163409923376],
    showCurrentPoint: true,
    homeButton: true
});

// 初始化地图
mapManager.initMap()
let currentPitch = CAMERA_PITCH

// 为了演示加的按钮
createButton('视线垂直于地面', () => {
    currentPitch = -90
    mapManager.setCameraView({
        pitch: -90
    })
})
createButton('俯视60度看向中心', () => {
    currentPitch = -60
    mapManager.setCameraView({
        pitch: -60
    })
})
createButton('俯视45度看向中心', () => {
    currentPitch = -45
    mapManager.setCameraView({
        pitch: -45
    })
})
createButton('俯视30度看向中心', () => {
    currentPitch = -30
    mapManager.setCameraView({
        pitch: -30
    })
})
createButton('转向目标点(camera.setView)', () => {
    mapManager.setCameraView({
        pitch: currentPitch,
        lng: 116.392724,
        lat: 39.917955,
        isFly: false

    })
})
createButton('飞向目标点(camera.flyTo 含过渡)', () => {
    mapManager.setCameraView({
        pitch: currentPitch,
        lng: 116.392724,
        lat: 39.917955
    })
})

这里主要用到的是setCameraView方法——切换摄像头视角,定义如下:

type CameraOption = {
   lng?: number,
   lat?: number,
   height?: number,
   pitch?: number, // 俯仰角
   isFly?: boolean, // 切换位置时是否过渡
}
const CAMERA_PITCH: number = -60 // 默认俯仰角

setCameraView(
      options: CameraOption = {}
   ) {
      let __option = {
         lng: options.lng || this.options.position[0],
         lat: options.lat || this.options.position[1],
         height: options.height || CAMERA_HEIGHT,
         pitch: options.pitch || CAMERA_PITCH,
         isFly: options.isFly === undefined ? true : options.isFly, // 切换时是否需要过渡动画
      }

      const destination = Cesium.Cartesian3.fromDegrees(__option.lng, getOffsetLat({
         lat: __option.lat,
         height: __option.height,
         pitch: __option.pitch,
      }), __option.height)
      const orientation = {
         heading: Cesium.Math.toRadians(0), // 左右倾斜角
         pitch: Cesium.Math.toRadians(__option.pitch), // 俯仰角 -60表示向下俯视60度
      }
      if (__option.isFly) {
         this.Viewer.camera.flyTo({
            destination,
            orientation,
            duration: CAMERA_FLY_DURATION
         });
      } else {
         this.Viewer.camera.setView({
            destination,
            orientation,
         });
      }
   }

上面用到了一个方法:getOffsetLat。这是因为,当摄像头经纬度不变,但是有俯仰角的话,它将不会对准目标点。所以需要计算出偏移后的经纬度,让摄像头对准目标:

// 相机偏移后的经纬度
export const getOffsetLat = (options: OffsetOption) => {
   const ONE_LAT_TO_METERS = 111 * 1000 // 1纬度对应的距离 111km

   // 如果是90或者0度,不发生偏移
   if (Math.abs(options.pitch % 90) === 0) {
      return options.lat
   }

   const latOffsetMeters = options.height / Math.tan((options.pitch * Math.PI) / 180) // tan用的是弧度,这里要将角度转为弧度
   const lat = Number((latOffsetMeters / ONE_LAT_TO_METERS).toFixed(12))
   return options.lat + lat
}

2. 经纬度选择器

import MapManager from '@/mapManager';

// 加一个元素来显示经纬度信息
const div = document.createElement('div');
document.body.appendChild(div);

const mapManager = new MapManager('root', {
    homeButton: true, 
    showCurrentPoint: true, 

    // 点击地图时,同步修改默认图标位置
    shouldCurrentPointChange: true,

    // 监听初始坐标点位置
    getInitPosition: (lng, lat) => {
        div.innerHTML = `当前位置:${lng}, ${lat}`;
    }, 
    // 也可以通过 mapManager.options.position获取
    
    // 监听点击地图位置事件
    onClickPosition: (lng, lat) => {
        div.innerHTML = `当前位置:${lng}, ${lat}`;
    } 
});

mapManager.initMap()

上面多了三个扩展选项:

type ExtentionOption = {
    // ...
   onClickPosition?: (lng: number, lat: number) => void // 点击地图时的回调,返回经纬度信息
   getInitPosition?: (lng: number, lat: number) => void // 将初始点经纬度返回
   shouldCurrentPointChange?: boolean // 点击地图时是否改变默认坐标点位置
}

在初始化地图时,需要初始化地图点击事件:

initMap() {
  // ...
  this.initEvent()
  // 将初始点位返回
  this.options.getInitPosition && this.options.getInitPosition(...this.options.position)

  return this.Viewer
}

// 注册事件
initEvent() {
  // 点击事件
  const handler = new Cesium.ScreenSpaceEventHandler(this.Viewer.scene.canvas);
  handler.setInputAction((e: Cesium.ScreenSpaceEventHandler.PositionedEvent) => {
     this.initPositionClickEvent(e)
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
}

// 返回经纬度的点击事件
initPositionClickEvent(e: Cesium.ScreenSpaceEventHandler.PositionedEvent) {
  var ray = this.Viewer.camera.getPickRay(e.position);
  var cartesian = this.Viewer.scene.globe.pick(ray, this.Viewer.scene);
  var cartographic = Cesium.Cartographic.fromCartesian(cartesian);
  var lng = Cesium.Math.toDegrees(cartographic.longitude); //经度值
  var lat = Cesium.Math.toDegrees(cartographic.latitude); //纬度值

  // 只有 showCurrentPoint 为 true 且允许修改点位的的情况下,才会触发修改当前点位事件
  if (this.options.showCurrentPoint && this.options.shouldCurrentPointChange) {
     // 绘制点位
     this.updateCurrentPoint(lng, lat)
     // 摄像头视角跟随点击移动
     this.setCameraView({
        lng,
        lat,
        height: this.Viewer.camera.positionCartographic.height
     })
  }
  // onClickPosition:将经纬度返回
  this.options.onClickPosition && this.options.onClickPosition(lng, lat)
}

3. 管理不同类型的坐标点

假设这里有两种类型的点位:默认样式的、设备类型的(又区分广播与摄像头,并且有不同的状态)

import MapManager, { Point, DevicePoint, DeviceStatus, PointType, DeviceType } from '@/mapManager';
import { createButton } from '@/renderDemos/helper'


const pointInfoEle = document.createElement('div');

// Device类型的点位
const deviceList: Array<DevicePoint> = [
    {
        id:'1',
        lng: 116.391925,
        lat: 39.9122550,
        status: DeviceStatus.OFFLINE,
        type: DeviceType.CAMERA
    },
    {
        id:'2',
        lng: 116.391925,
        lat: 39.9142550,
        status: DeviceStatus.UNKNOWN,
        type: DeviceType.CAMERA
    },
    {
        id:'3',
        lng: 116.390905,
        lat: 39.9141579,
        status: DeviceStatus.ONLINE,
        type: DeviceType.BROADCAST
    },
    {
        id:'4',
        lng: 116.391985,
        lat: 39.9151599,
        status: DeviceStatus.PLAYING,
        type: DeviceType.BROADCAST
    }
]

// 其他点位:使用默认样式
const defaultPoints: Array<Point> = [
    {
        id:'1',
        lng: 116.390735,
        lat: 39.9141359
    },
    {
        id:'2',
        lng: 116.391835,
        lat: 39.9151459
    }
]

const mapManager = new MapManager('root',{
    position: [116.390935, 39.9141559], // 覆盖地图中心点位

    // 监听点击点位事件,显示具体信息(或者打开弹框之类的操作)
    onClickPoint: (pointId, pointType) => {
        if(pointType === PointType.DEVICE){
            const result = deviceList.find(item => item.id === pointId)
            pointInfoEle.innerHTML = JSON.stringify(result)
        }

        if(pointType === PointType.DEFAULT){
            const result = defaultPoints.find(item => item.id === pointId)
            pointInfoEle.innerHTML = JSON.stringify(result)
        }
    }
});

const viewer = mapManager.initMap()

// 绘制边界
MapManager.loadMapOutline(viewer)

// 为了测试写的一个方法,无关紧要
const addDevices = (type?: DeviceType) => {
    // 默认添加全部点位
    if (type === undefined) {
        deviceList.forEach(item => {
            mapManager.addPoint(item.id, item, PointType.DEVICE)
        })
    } else {
        deviceList.filter(item => item.type === type).forEach(item => {
            mapManager.addPoint(item.id, item, PointType.DEVICE)
        })
    }

}
// 添加默认样式的点位
const addDefaultPoints = () => {
    defaultPoints.forEach(item => {
        mapManager.addPoint(item.id, item)
    })
}

// 默认添加全部点位
addDevices()
addDefaultPoints()


createButton('清除设备', () => {
    mapManager.removePointByGroup(PointType.DEVICE)
})

createButton('清除坐标点', () => {
    mapManager.removePointByGroup()
})

createButton('显示全部', () => {
    mapManager.removeAllPoints()
    addDevices()
    addDefaultPoints()
})

createButton('显示设备', () => {
    mapManager.removeAllPoints()
    addDevices()
})

createButton('显示坐标点', () => {
    mapManager.removeAllPoints()
    addDefaultPoints()
})

createButton('显示摄像头', () => {
    mapManager.removeAllPoints()
    addDevices(DeviceType.CAMERA)
})

createButton('显示广播', () => {
    mapManager.removeAllPoints()
    addDevices(DeviceType.BROADCAST)
})
document.body.appendChild(pointInfoEle);

现在又多了:

  • (选项)onClickPoint:监听点击点位的事件
  • (实例方法)addPoint(原始id, 数据, 类型?):添加点位
  • (实例方法)removePointByGroup(类型?):删除某个组别的点位
  • (实例方法)removeAllPoints():删除所有点位

说明一下,一个点位携带的信息是有限的,不可能把所有信息都放在上面。所以这里携带的仅仅是一个字符串id,通过id再去原本的数据中找到具体信息。

然而不同类型的数据,可能包含相同的id(例如上面的deviceList和defaultPoints),这就不能保证点位id的唯一性。这里采用的方法是:给id前面加上个类型组成一个新的id。因此封装了以下两种方法来管理id:

export enum PointType {
   DEFAULT = 'DEFAULT',
   DEVICE = 'DEVICE'
}

// 这里的key是数据最原始的id
genId = (type: PointType, key: string = generateUUID()) => {
   // 1. 生成包含类型的id信息
   const pointId = `${type}_${key}`

   // 2. 初始化点位分组
   if (!this.pointGroup[type]) {
      this.pointGroup[type] = []
   }
   this.pointGroup[type].push(pointId)
   return pointId
}

getIdInfo = (id: string) => {
   let infos = id.split("_")
   return {
      type: infos[0] as PointType,
      id: infos[1],
   }
}

上面还多了一个属性pointGroup,这是为了方便管理点位的显示与清除的。

首先是注册点击点位的事件:onClickPoint

initEvent() {
   // 点击事件
   const handler = new Cesium.ScreenSpaceEventHandler(this.Viewer.scene.canvas);
   handler.setInputAction((e: Cesium.ScreenSpaceEventHandler.PositionedEvent) => {
      this.initPositionClickEvent(e)

      // 下面是新增部分
      const pick = this.Viewer.scene.pick(e.position);
      if (pick && pick.id) {
         const pointId = pick.id._id
         // 这里的id是经过特殊处理的
         const { id, type } = this.getIdInfo(pointId)
         this.options.onClickPoint && this.options.onClickPoint(id, type)
      }
   }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
}

添加点位:addPoint

type DevicePoint = Point & {
   status: DeviceStatus,
   type: DeviceType
}

addPoint(id: string, data: Point, type?: PointType) {
   const _type = type || PointType.DEFAULT

   // 1. 根据分组生成ID
   const pointId = this.genId(_type, id)

   // 2. 根据不同类型采用不用的点位添加方式
   switch (_type) {
      case PointType.DEVICE:
         this.addDevicePoint(pointId, data as DevicePoint)
         break
      default:
         this.addDefaultPoint(pointId, data)
   }
}

// 添加设备类型点位
addDevicePoint = (pointId: string, data: DevicePoint) => {
   let { lng, lat, status, type } = data;
   let image = IMAGE_URL + type
   // 如果是播放状态,加上闪烁
   let isFlash = status === DeviceStatus.PLAYING
   image += `_${status}.png`
   this.Viewer.entities.add({
      id: pointId,
      position: Cesium.Cartesian3.fromDegrees(lng, lat, 10),
      billboard: {
         scale: 1,
         image,
         verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
         heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
         // pixelOffset : new Cesium.Cartesian2(0, -30),// 图标偏移
         ...(isFlash ? { show: new Cesium.CallbackProperty(cesiumFlash(0.05), false) } : {})
      }
   });
}
posted @ 2024-04-11 00:39  sanhuamao  阅读(435)  评论(0编辑  收藏  举报