【Cesium】自定义弹框

主要思路:根据给定的经纬位置,在对应的屏幕位置上创建一个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)
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值