主要思路:根据给定的经纬位置,在对应的屏幕位置上创建一个div,在地球转动的时候,更新div位置
// 整理了一下cesium弹框,主要是写了一个容器,把位置监听和边缘超出部分裁剪加进去了
import * as Cesium from 'cesium'
import {Cartesian2, Cartesian3, Cartographic, Event, Viewer} from 'cesium'
import {v1 as uuidv1} from 'uuid'
export interface popupOptions {
id?: string
position?: Cartesian3 // 弹出地理坐标位置
anchor?: 'bottom-left' | 'bottom' | 'bottom-right' | 'top-left' | 'center' //附着点
show?: boolean
windowCoordinate?: Cartesian2 // 屏幕坐标
cartographicCoordinate?: Cartographic // 地理坐标
cartesian3Coordinate?: Cartesian3
className?: "earth-popup-default" | 'earth-popup-message' // 根据className 可以设置不同的外层样式,目前就一个
htmlText?: string // 弹框内部显示的东西
updatePosition?: boolean
visibleRange?: number[]
properties?: any //属性
offset?: number[]
}
export class Popup {
private viewer: Viewer
public _popupOptions: popupOptions
private _show?: boolean
public popupContainer: HTMLElement // 盛放要显示的内容 外层的div
private _htmlText?: string // 需显示的内容
public id?: string
private updatePosition?: boolean
private updateEvent?: Event.RemoveCallback
private enableVisibleRange?: boolean
private offset: number[]
constructor(viewer: Viewer, options: popupOptions) {
this._popupOptions = options
this.viewer = viewer
this._htmlText = options.htmlText
this.offset = options.offset ? options.offset : [0, 0]
this.popupContainer = this.initPopup()
if (this.popupContainer) {
const {show, htmlText, updatePosition} = this.popupOptions
this.show = show !== false;
if (htmlText)
this.setContent(htmlText)
this.updatePosition = updatePosition !== false;
if (this.updatePosition) {
this.update()
}
}
}
// 是否显示
set show(value: boolean) {
if (value) {
this.showPopup()
} else {
this.hidePopup()
}
this._show = value
}
get show() {
return this._show ? this._show : false
}
set htmlText(value: string | undefined) {
this._htmlText = value
this.setContent(value)
}
get htmlText() {
return this._htmlText
}
set popupOptions(value: popupOptions) {
this._popupOptions = value
}
get popupOptions() {
return this._popupOptions
}
addEventListener(type: keyof HTMLElementEventMap, listener: any) {
this.popupContainer?.addEventListener(type, listener)
}
/* 添加到html界面上 */
private initPopup() {
const id = this.popupOptions.id ? this.popupOptions.id : uuidv1()
let container = document.getElementById(id)
if (container) {
throw new Error(`id为${id}的div已存在`)
} else {
container = this.createPopupContainer(id)
this.id = id
}
return container
}
/* 创建最外面的div来放定义的内容 */
private createPopupContainer(id: string) {
const mapContainer = this.viewer?.container
const {className} = this.popupOptions
const container = document.createElement("div");
container.id = id;
container.className = className ? className : 'earth-popup-default'
mapContainer?.appendChild(container)
return container
}
/* 文本转成 html 并附着在container上 */
private setContent(htmlText: string | undefined) {
if (htmlText) {
const {popupContainer} = this
while (popupContainer?.hasChildNodes() && popupContainer?.firstChild) //当div下还存在子节点时 循环继续
{
popupContainer.removeChild(popupContainer?.firstChild);
}
const html = document.createRange().createContextualFragment(htmlText)
popupContainer?.appendChild(html)
}
}
/* 添加监听 更新屏幕上popup的位置,避免出现漂移 还必须监听。。。*/
private update() {
const scene = this.viewer?.scene
this.updateEvent = this.viewer.scene.postRender.addEventListener(() => {
if (!this.show) return
const {windowCoordinate, cartesian3Coordinate} = this.popupOptions
if (!cartesian3Coordinate) {
this.hidePopup()
return
}
this.showPopup()
const newWinCoord = Cesium.SceneTransforms.wgs84ToWindowCoordinates(scene, cartesian3Coordinate)
if (newWinCoord) {
if (newWinCoord?.x !== windowCoordinate?.x || newWinCoord?.y !== windowCoordinate?.y) {
this._popupOptions.windowCoordinate = newWinCoord
this.setPosition(newWinCoord)
}
}
// 一定距离范围内可见
const {visibleRange} = this.popupOptions
if (visibleRange && cartesian3Coordinate) {
const cameraPosition = this.viewer?.camera.position
const distance = Cartesian3.distance(cameraPosition, cartesian3Coordinate)
if (cameraPosition && (distance > visibleRange[1] || distance < visibleRange[0])) {
this.hidePopup()
} else {
this.showPopup()
}
}
})
}
/* 根据屏幕坐标 设置弹框出现的位置 */
private setPosition(windowCoord: Cartesian2) {
let x = 0.0 // 弹框位置 左上角的
let y = 0.0
const {popupContainer} = this
const mapContainer = this.viewer.container as HTMLDivElement
// 避免地图div不是填充整个屏幕
const {offsetLeft, offsetTop} = mapContainer //地图容器左、上边缘距离
const position = mapContainer.style.position // 地图容器的定位方式
const {clientWidth, clientHeight} = popupContainer as HTMLDivElement // 弹框容器的长宽
if (position === 'absolute' || position === 'fixed' || position === 'relative') {
x = windowCoord.x
y = windowCoord.y
} else {
x = windowCoord.x + offsetLeft
y = windowCoord.y + offsetTop
}
const anchor = this.popupOptions.anchor ? this.popupOptions.anchor : "bottom"
switch (anchor) {
// 左上角 屏幕的x、y直接设置成div的x、y
case "top-left":
break
// 左下角 div往上移div的高度 x不变
case "bottom-left":
y = y - clientHeight
break
// 正下 div向上移div的高度 x左移一半
case "bottom":
x = x - clientWidth / 2
y = y - clientHeight
break
// 右下 div向上移div的高度 x左移div的宽度
case "bottom-right":
x = x - clientWidth
y = y - clientHeight
break
// 正中间 均移一半
case "center":
x = x - clientWidth / 2
y = y - clientHeight / 2
break
}
popupContainer.style.left = `${x + this.offset[0]}px`
popupContainer.style.top = `${y + this.offset[1]}px`
this.clipContainer(x, y)
}
clipContainer(x: number, y: number) {
const mapContainer = this.viewer.container as HTMLDivElement
const {
offsetLeft: mOffsetLeft,
offsetTop: mOffsetTop,
offsetWidth: mOffsetWidth,
offsetHeight: mOffsetHeight
} = mapContainer//地图div距浏览器的左边距
const position = mapContainer.style.position //获取地图容器的定位方式
//弹框与浏览器的边距
const {
offsetLeft: cOffsetLeft,
offsetTop: cOffsetTop,
offsetWidth: cOffsetWidth,
offsetHeight: cOffsetHeight
} = this.popupContainer//地图div距浏览器的左边距
let topLength = '0px ' //上方溢出距离 默认是0
let rightLength = cOffsetWidth + 'px ' //右留下距离 默认是整个弹框宽度
let bottomLength = cOffsetHeight + 'px ' //下方留下距离 默认是整个弹框高度(加上尖尖宽度)
let leftLength = '0px ' //左边溢出距离
//放地图的div的定位方式不同,是否溢出的判断方式也不同,溢出范围的计算也不同
if (position === 'static' || position === '') {
const overflowTop = y < mOffsetTop //上方是否溢出
const overflowRight = mOffsetLeft + mOffsetWidth - x < cOffsetWidth //右
const overflowBottom = mOffsetTop + mOffsetHeight - y < cOffsetHeight //下
const overflowLeft = x < mOffsetLeft //左
if (overflowTop) {
topLength = mOffsetTop - cOffsetTop + 'px ' //上方溢出距离
}
if (overflowRight) {
rightLength = mOffsetLeft + mOffsetWidth - cOffsetLeft + 'px ' //右留下距离
}
if (overflowBottom) {
bottomLength = mOffsetTop + mOffsetHeight - cOffsetTop + 'px ' //下方留下距离
}
if (overflowLeft) {
leftLength = mOffsetLeft - cOffsetLeft + 'px ' //左边溢出距离
}
} else {
const overflowTop = y < mOffsetTop //上方是否溢出
const overflowRight = mOffsetWidth - x < cOffsetWidth //右
const overflowBottom = mOffsetHeight - y < cOffsetHeight //下
const overflowLeft = x < mOffsetLeft //左
if (overflowTop) {
topLength = -cOffsetTop + 'px ' //上方溢出距离
}
if (overflowRight) {
rightLength = mOffsetWidth - cOffsetLeft + 'px ' //右留下距离
}
if (overflowBottom) {
bottomLength = mOffsetHeight - cOffsetTop + 'px ' //下方留下距离
}
if (overflowLeft) {
leftLength = -cOffsetLeft + 'px ' //左边溢出距离
}
}
const {popupContainer} = this
popupContainer.style.clip = 'rect(' + topLength + rightLength + bottomLength + leftLength + ')'
}
// 移除
destroyPopup() {
const {popupContainer, updateEvent} = this
const {container} = this.viewer
if (popupContainer && updateEvent) {
popupContainer.style.display = 'none'
container.removeChild(popupContainer)
this.viewer.scene.postRender.removeEventListener(updateEvent)
return true
} else return false
}
// 隐藏
hidePopup() {
const {popupContainer} = this
if (popupContainer) {
popupContainer.style.display = 'none'
}
}
// 显示
showPopup() {
const {popupContainer} = this
if (popupContainer) {
popupContainer.style.display = 'block'
}
}
}
为了方便管理还写了一个popupcollection
这样就可以批量管理了
import { Viewer } from "cesium";
import { popupOptions, Popup } from "./cesiumPopup";
/* 把弹框管理起来 */
export class PopupCollection {
collectionMap: any
collectionArray: any[]
_length: number
_show?: boolean
constructor() {
this.collectionMap = new Map()
this.collectionArray = []
this._length = 0
}
set show(value: boolean) {
const { collectionMap } = this
collectionMap.forEach((popup: Popup, id: string) => {
popup.show = value
})
this._show = value
}
get show() {
return this._show ? this._show : false
}
get length() {
const length = this.collectionArray.length
this._length = length
return this._length
}
add(viewer: Viewer, options: popupOptions) {
const popObj = new Popup(viewer, options)
const { id } = popObj
this.collectionMap.set(id, popObj)
this.updateCollectionArray()
return popObj
}
get(id: string) {
// 有id就返回特定的 没有就返回所有
const { collectionMap } = this
if (collectionMap.has(id)) {
const popup = collectionMap.get(id)
return [popup]
} else throw new Error(`id为${id}的弹框不存在`)
}
getAll() {
return this.collectionArray
}
updateCollectionArray() {
const collectionArray = [...this.collectionMap.values()]
this.collectionArray = collectionArray
}
remove(id: string): boolean {
// 有id就移除特定的 没有就移除所有
const { collectionMap } = this
if (collectionMap.has(id)) {
const popup = collectionMap.get(id)
if (popup.destoryPopup()) {
collectionMap.delete(id)
}
this.updateCollectionArray()
} else throw new Error(`id为${id}的弹框不存在`)
return false
}
removeAll(): boolean {
this.collectionMap.forEach((id: string, popup: Popup) => {
if (!popup.destroyPopup()) {
return false
}
})
this.updateCollectionArray()
return true
}
}
使用
// 使用popup
import * as Cesium from 'cesium'
import { popupOptions } from "./cesiumPopup"
import { PopupCollection } from "./popupCollection"
import { getPosition, feature } from "../dataPreparation";
export function addByPopup(viewer: Cesium.Viewer, feature: feature, popupCollection: PopupCollection) {
const { text, top_heig_3 } = feature.properties
const coordinate = getPosition(feature)
const cartographicCoordinate = Cesium.Cartographic.fromDegrees(coordinate[0], coordinate[1])
const cartesians: Cesium.Cartesian3 = Cesium.Cartesian3.fromRadians(cartographicCoordinate.longitude, cartographicCoordinate.latitude, top_heig_3)
const htmlText = `<div class="sy-gis-label-one"><span class="label">${text}</span></div>`
const popupOptions: popupOptions = {
cartographicCoordinate: cartographicCoordinate,
htmlText: htmlText,
updatePositon: true,
visibleRange: [0, 15000],
cartesian3Coordinate: cartesians,
properties: feature.properties
}
const addedObj = popupCollection.add(viewer, popupOptions)
return addedObj
}
/* 根据类型不同,获取标签显示的坐标位置
*这里用了turf.js需要下载包*/
export function getPosition(feature: feature) {
let coordinate: number[] = []
let positions: turf.Position[] | any = undefined
let turfFeature = undefined
let centerFeature = undefined
if (feature?.geometry?.type) {
const type = feature.geometry.type
switch (type) {
case 'Point':
coordinate = (feature.geometry as Point).coordinates
break
case 'MultiPolygon':
if ((feature.geometry as MultiPoint).coordinates.length <= 0) {
return
} else {
positions = (feature.geometry as MultiPoint).coordinates[0][0]
}
turfFeature = turf.points(positions)
centerFeature = turf.center(turfFeature)
coordinate = centerFeature.geometry.coordinates
break
case 'Polygon':
positions = (feature.geometry as MultiPoint).coordinates[0]
turfFeature = turf.points(positions)
centerFeature = turf.center(turfFeature)
coordinate = centerFeature.geometry.coordinates
break
case 'MultiLineString':
break
}
}
return coordinate
}
---------------------------------------------------------------------------华丽的分割线-----------------------------------------------------------------------------------------
之前都给整复杂了。。。
直接用html转成图片(这里用的是canvas),然后赋值给billboard就可以了。
比如:
// 创建Canvas
const canvas = document.createElement('canvas');
const scale = 2 // 模糊的话,就把canvas绘制大一点,然后在添加billboard的时候缩小就会增加清晰度
canvas.width = 131 * scale;
canvas.height = 85 * scale;
const ctx = canvas.getContext('2d');
// 绘制HTML样式内容
ctx.fillStyle = 'white';
ctx.font = 'bold 32px OPPOsans';
// 添加图片
const img = new Image();
img.onload = function () {
ctx.drawImage(img, 0, 0);
ctx.fillText(item.labelName, ((100 - 16 * item.labelName.length) / 2 + 30) * scale, 44); // 文字在右侧框中居中显示
billbordPOI(item, canvas)
};
img.src = new URL(`../assets/home/newPoi/${index + 1}.png`, import.meta.url) // vue的文件路径处理
// 添加billboard函数
function billbordPOI(item, canvas) {
const billLableEntity = new Cesium.Entity({
id: item.adcd,
position: Cesium.Cartesian3.fromDegrees(item.lgtd, item.lttd),
name: item.name,
billboard: {
scale: 0.5,
image: canvas.toDataURL(),
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
eyeOffset: new Cesium.Cartesian3(0, 0, 0)
},
})
viewer.value.entities.add(billLableEntity)
}