前言: 本文使用ts对cesium进行封装,实现了随地图拖拽而移动的浮动弹窗效果。对实体的信息展示、点击实体交互等业务功能友好。
使用API文档:
Popup:返回浮动弹窗实例。
new Popup(options);
参数(options):
Name | Require | Default | Description |
lng:number | true | -- | 浮动弹窗定位位置经度。 |
lat:number | true | -- | 浮动弹窗定位位置纬度。 |
alt:number | true | -- | 浮动弹窗定位位置高度。 |
offset: { top:number, bottom:number, left:number, right:number } | false | -- | 浮动弹窗相对于定位位置,调整的距离。 e.g. top:10, 弹窗向上移动10px。 |
map:map | true | -- | 浮动弹窗挂载的Cesium地图实例。 |
html: string | DOM | false | '' | 浮动弹窗内部的html片段。 |
className:string | false | '' | 添加到浮动弹窗的css类名。 |
实例方法:
setHtml(html: string | HTMLElement); //修改浮动弹窗内部html
setLngLatAlt(lngLatAlt: LngLatAlt); //修改浮动弹窗定位的经纬度
remove(); //销毁浮动弹窗实例
Popup实现方法:
import { BaseObject } from "./lib/BaseObject";
import { LngLatAlt } from "./types";
import { HKMap } from "./HKMap";
import { PopupOptions, Offset } from "./types/popup";
import "./style/index.less";
export class Popup extends BaseObject {
private _map: HKMap;
private _offset: Offset;
private _container: HTMLElement;
private _popupContent: HTMLElement;
private _lngLatAlt: LngLatAlt;
private _html: HTMLElement | string;
constructor(options: PopupOptions) {
super();
const { lng, lat, alt, offset, map, html } = options;
this._lngLatAlt = this._disposePosition(lng, lat, alt);
this._html = html;
this._offset = offset;
this._map = map;
this._init(options);
}
_init(opts: PopupOptions) {
const { className } = opts;
if (!this._map) return;
if (!this._container) {
const target = this._map.getContainer() as HTMLElement;
target.style.overflow = "hidden";
this._container = this._createDOM("div", "hkmap-popup", target);
this._createDOM("div", "hkmap-popup-tip", this._container);
this._popupContent = this._createDOM(
"div",
"hkmap-popup-content",
this._container
);
if (this._html) this._setHtmlToDom(this._popupContent, this._html);
if (className) {
className.split(" ").forEach((v) => this._container.classList.add(v));
}
}
this._map.onCameraChange(() => this._update());
this._update();
}
/**
* 更新浮窗html
*
* @param {(string | HTMLElement)} html
* @return {*}
* @memberof Popup
*/
setHtml(html: string | HTMLElement) {
if (!html) return;
this._html = html;
if (this._popupContent && this._popupContent.parentNode) {
this._popupContent.parentNode.removeChild(this._popupContent);
}
this._popupContent = this._createDOM(
"div",
"hkmap-popup-content",
this._container
);
this._setHtmlToDom(this._popupContent, this._html);
this._update();
}
/**
* 更新浮窗位置
*
* @param {LngLatAlt} lngLatAlt
* @return {*}
* @memberof Popup
*/
setLngLatAlt(lngLatAlt: LngLatAlt) {
if (!lngLatAlt) return;
this._lngLatAlt = lngLatAlt;
this._update();
}
/**
* 销毁Popup对象
*
* @memberof Popup
*/
remove() {
if (this._popupContent && this._popupContent.parentNode) {
this._popupContent.parentNode.removeChild(this._popupContent);
}
if (this._container && this._container.parentNode) {
this._container.parentNode.removeChild(this._container);
}
if (this._map) {
this._map.offCameraChange(() => this._update());
}
}
/**
* 更新弹窗
*
* @memberof Popup
*/
_update() {
const { lng, lat, alt } = this._lngLatAlt;
const windowPos = Cesium.SceneTransforms.wgs84ToWindowCoordinates(
this._map.viewer.scene,
Cesium.Cartesian3.fromDegrees(lng, lat, alt)
);
const transPos = this._getPopTranslate(windowPos);
this._setTransformCss(
this._container,
`translate(-50%, -100%) translate(${transPos.x}px,${transPos.y}px) translateZ(0)`
);
}
_createDOM(
tagName: string,
className?: string,
container?: Element
): HTMLElement {
const el = document.createElement(tagName);
if (className !== undefined) el.className = className;
if (container) container.appendChild(el);
return el;
}
_getPopTranslate({ x, y }) {
let tansX: number = x;
let tansY: number = y;
if (this._offset) {
const { left, right, bottom, top } = this._offset;
if (left) tansX = x - left;
if (right) tansX = x + right;
if (top) tansY = y - top;
if (bottom) tansY = y + bottom;
}
return { x: Math.round(tansX), y: Math.round(tansY) };
}
//dom位置转化
_setTransformCss(el: HTMLElement, value: string) {
const transformProp = this._getCssProps(["transform", "WebkitTransform"]);
el.style[transformProp] = value;
}
_setHtmlToDom(target: HTMLElement, child): void {
if (typeof child === "string") {
target.innerHTML = child;
} else {
target.appendChild(child);
}
}
_getCssProps(props) {
const docStyle = window.document.documentElement.style;
if (!docStyle) return props[0];
for (let i = 0; i < props.length; i++) {
if (props[i] in docStyle) return props[i];
}
return props[0];
}
_disposePosition(lng: number, lat: number, alt?: number) {
const position = { lng, lat, alt };
if (!(typeof lng === "number") || !(typeof lat === "number")) {
throw "无效的经纬度";
}
if (!alt) position.alt = 0;
return position;
}
}
需要引入css样式:
.hkmap-popup {
position: absolute;
top: 0;
left: 0;
display: -webkit-flex;
display: flex;
will-change: transform;
pointer-events: none;
-webkit-flex-direction: column-reverse;
flex-direction: column-reverse;
z-index: 1;
zoom: 1.001;
.hkmap-popup-tip {
border: 10px solid transparent;
z-index: 1;
align-self: center;
border-bottom: none;
border-top-color: #fff;
}
.hkmap-popup-content {
min-width: 100px;
min-height: 40px;
background-color: #fff;
}
}
使用示例:
const pop = new Popup({
lng: 120.12087707148685,
lat: 30.25188693213902,
alt: 0,
map: this.map,
offset: { top: 10 },
html: `<div className="ttt-div">测试</div>`,
});
//修改html:
const newHtml = `<div className="ttt-div">新的html</div>`;
pop.setHtml(newHtml);
//移除popup
pop.remove();
//修改经纬度
pop.setLngLatAlt({
lng: 120.1308770,
lat: 30.23888693213902,
alt: 100,
})
效果展示:
缺点:
当使用很多弹窗时,需要创建多个popup实例,而每个实例内都对map的cameraChange事件进行了监听,会导致注册过多监听事件,这在设计上是不够完善的。
改进思路=>不单独构建Popup实例,将Popup挂载在map实例的方法内。