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) } : {})
}
});
}