非科班Java出身GISer

非科班Java出身GISer

Cesium 实战 - 气泡框跟随飞行

气泡框在地图中非常常用,尤其是二维地图中;而在三维地图中经常会用广告牌、标牌等作为气泡框使用。

广告牌(billboard)虽然方便,但是不支持富文本,样式比较一般,因此很多情况还是需要气泡框来实现。

普通的气泡框比较容易,互联网搜索很容易搜到完整代码,这里放上作者参考的博客地址

后来,在实际应用中,想要展示移动中模型的信息,于是对气泡框组件进行修改,实现气泡框跟随飞行。

本文包括气泡框三部分。


Cesium 气泡框

作者基于以下工具类进行修改,实现气泡框功能:


var BaseEvent = function () {
  this.handles = {}
  this.cached = []
}
BaseEvent.prototype.on = function (eventName, callback) {
  if (typeof callback !== 'function') return

  if (!this.handles[eventName]) {
    this.handles[eventName] = []
  }
  this.handles[eventName].push(callback)

  if (this.cached[eventName] instanceof Array) {
    //说明有缓存的 可以执行
    callback.apply(null, this.cached[eventName])
  }
}

BaseEvent.prototype.emit = function () {
  if (this.handles[arguments[0]] instanceof Array) {
    for (let i = 0; i < this.handles[arguments[0]].length; i++) {
      this.handles[arguments[0]][i](arguments[1])
    }
  }
  //默认缓存
  this.cached[arguments[0]] = Array.prototype.slice.call(arguments, 1)
}

// 气泡框类
var CesiumPopup = (function () {
  // 容器
  var _panelContainer = null
  var _contentContainer = null
  var _closeBtn = null

  var _renderListener = null
  var _viewer = null
  
  var CesiumPopup = function (options) {
    //继承
    BaseEvent.call(this)

    this.className = options.className || ''
    this.title = options.title || ''
    this.offset = options.offset || [0, 0]

    // this.render = this.render.bind(this)
    this.closeHander = this.closeHander.bind(this)
  }

  CesiumPopup.prototype = new BaseEvent()
  CesiumPopup.prototype.constrctor = CesiumPopup
  
  // 添加气泡框,并且开启后处理改变气泡框位置
  CesiumPopup.prototype.addTo = function (viewer) {
    if (_viewer) this.remove()
    _viewer = viewer
    this.initPanle()
    //关闭按钮
    _closeBtn.addEventListener('click', this.closeHander, false)
    if (this.position) {
      _panelContainer.style.display = 'block'
      _renderListener = _viewer.scene.postRender.addEventListener(this.render, this)
    }

    return this
  }

  // 初始化气泡框
  CesiumPopup.prototype.initPanle = function () {
    var closeBtnIcon =
      '<svg t="1603334792546" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1328" width="32" height="32"><path d="M568.922 508.232L868.29 208.807a39.139 39.139 0 0 0 0-55.145l-1.64-1.64a39.139 39.139 0 0 0-55.09 0l-299.367 299.82-299.425-299.934a39.139 39.139 0 0 0-55.088 0l-1.697 1.64a38.46 38.46 0 0 0 0 55.09l299.48 299.594-299.424 299.48a39.139 39.139 0 0 0 0 55.09l1.64 1.696a39.139 39.139 0 0 0 55.09 0l299.424-299.48L811.56 864.441a39.139 39.139 0 0 0 55.089 0l1.696-1.64a39.139 39.139 0 0 0 0-55.09l-299.48-299.537z" p-id="1329"></path></svg>'

    _panelContainer = document.createElement('div')
    _panelContainer.classList.add('cesium-popup-panel')
    if (this.className && this.className !== '') {
      _panelContainer.classList.add(this.className)
    }

    _closeBtn = document.createElement('div')
    _closeBtn.classList.add('cesium-popup-close-btn')

    _closeBtn.innerHTML = closeBtnIcon

    // header container
    var headerContainer = document.createElement('div')
    headerContainer.classList.add('cesium-popup-header-panel')

    this.headerTitle = document.createElement('div')
    this.headerTitle.classList.add('cesium-poput-header-title')
    this.headerTitle.innerHTML = this.title

    headerContainer.appendChild(this.headerTitle)
    _panelContainer.appendChild(_closeBtn)

    _panelContainer.appendChild(headerContainer)

    // content container

    _contentContainer = document.createElement('div')
    _contentContainer.classList.add('cesium-popup-content-panel')
    _contentContainer.innerHTML = this.content

    _panelContainer.appendChild(_contentContainer)

    //tip container
    var tipContaienr = document.createElement('div')
    tipContaienr.classList.add('cesium-popup-tip-panel')

    var tipDiv = document.createElement('div')
    tipDiv.classList.add('cesium-popup-tip-bottom')

    tipContaienr.appendChild(tipDiv)

    _panelContainer.appendChild(tipContaienr)

    _panelContainer.style.display = 'none'
    // add to Viewer Container
    _viewer.cesiumWidget.container.appendChild(_panelContainer)
    this.emit('open')
  }

  // 设置气泡框内容
  CesiumPopup.prototype.setHTML = function (html) {
    if (_contentContainer) {
      _contentContainer.innerHTML = html
    }
    this.content = html
    return this
  }

  // 渲染气泡框
  CesiumPopup.prototype.render = function () {
    var geometry = this.position
    if (!geometry) return
    var position = Cesium.SceneTransforms.wgs84ToWindowCoordinates(_viewer.scene, geometry)
    if (!position) {
      return
    }
    if (_panelContainer) {
      _panelContainer.style.left = position.x - _panelContainer.offsetWidth / 2 + this.offset[0] + 'px'
      _panelContainer.style.top = position.y - _panelContainer.offsetHeight - 10 + this.offset[1] + 'px'
    }
  }

  // 设置气泡框位置
  CesiumPopup.prototype.setPosition = function (cartesian3) {
    this.position = cartesian3
    return this
  }

  // 修改气泡框样式
  CesiumPopup.prototype.addClassName = function (className) {
    if (_panelContainer) {
      _panelContainer.classList.add(className)
    }
    return this
  }

  // 移除气泡框样式
  CesiumPopup.prototype.removeClass = function (className) {
    if (_panelContainer) {
      _panelContainer.classList.remove(className)
    }
    return this
  }

  // 设置气泡框标题
  CesiumPopup.prototype.setTitle = function (title) {
    this.headerTitle.innerHTML = title

    return this
  }

  // 气泡框偏移
  CesiumPopup.prototype.setOffset = function (offset) {
    this.offset = offset
    return this
  }

  CesiumPopup.prototype.closeHander = function () {
    this.remove()
  }

  // 移除气泡框
  CesiumPopup.prototype.remove = function () {
    _closeBtn.removeEventListener('click', this.closeHander, false)

    if (_closeBtn) {
      _closeBtn.parentNode.removeChild(_closeBtn)
      _closeBtn = null
    }

    if (_contentContainer) {
      _contentContainer.parentNode.removeChild(_contentContainer)
      _contentContainer = null
    }

    if (_panelContainer) {
      _panelContainer.parentNode.removeChild(_panelContainer)
      _panelContainer = null
    }

    if (_renderListener) {
      _renderListener()
      _renderListener = null
    }

    if (_viewer) {
      _viewer = null
    }
    this.emit('close')
  }

  return CesiumPopup
})()


css 样式代码:

/* pop框css*/
.cesium-popup-panel {
  opacity: 0.8;
  width: 312px;
  position: absolute;
  z-index: 999;
  color: #00fcf9;

  background: rgba(23, 50, 108, 0.6);
  border: 1px solid #4674d6;
}

.cesium-popup-tip-panel {
  width: 40px;
  height: 20px;
  position: absolute;
  left: 50%;
  bottom: -20px;
  margin-left: -20px;
  overflow: hidden;
  pointer-events: none;
  opacity: 0.8;
}

.cesium-popup-tip-bottom {
  width: 17px;
  background: rgba(23, 50, 108, 0.8);
  border-bottom: 1px solid #4674d6;
  height: 17px;
  padding: 1px;
  margin: -10px auto 0;
  -webkit-transform: rotate(45deg);
  -moz-transform: rotate(45deg);
  -ms-transform: rotate(45deg);
  transform: rotate(45deg);
}

.cesium-popup-header-panel {
  /* display: flex; */
  /* justify-content: space-between; */
  align-items: center;
  font-size: 14px;
  padding: 5px 15px;
  background: rgba(23, 50, 108, 0.8);

  border-bottom: 1px solid #4674d6;
}

.cesium-poput-header-title {
  font-size: 16px;
  font-family: Microsoft YaHei;
  font-weight: 400;
  color: #ffffff;
}

.cesium-popup-content-panel {
  padding: 18px;
}

.cesium-popup-close-btn {
  float: right;
  position: relative;
  right: 10px;
}

.cesium-popup-close-btn,
.cesium-popup-close-btn:focus {
  cursor: pointer;
}

cesium-popup-close-btn>svg:hover {
  color: #00fcf9 !important;
}

.cesium-popup-close-btn>svg {
  user-select: auto;
  color: #4674d6;
  cursor: pointer;
  width: 15px;
  /* height: 15px; */
}

跟随气泡框

气泡框跟随原理也比较简单,即利用渲染器获取 entity 的实时位置即可。

/**
 * @todo 获取中心点
 * @param entity
 * @param viewer
 * @return {Cartesian3}
 */
static getCenter(entity, viewer){

    let positions;
    // 获取当前时间,优先获取 viewer 时间
    const timeTemp = viewer? viewer.clock.currentTime: JulianDate.now();
    if (entity.polygon) {
        // 先获取范围,在获取中心点
        const temp = entity.polygon.hierarchy.getValue(timeTemp).positions;
        positions = temp && BoundingSphere.fromPoints(temp).center;
    } else if (entity.polyline) {
        // 先获取范围,在获取中心点
        const temp = entity.polyline.positions.getValue(timeTemp);
        positions = temp && BoundingSphere.fromPoints(temp).center;
    } else if (entity.point) {
        // 获取点的位置
        positions = entity.position._value;
    } else {
        // 如果是移动对象(一般为模型),则获取实时位置
        positions = entity._position && entity._position.getValue(timeTemp);
    }
    // 中心点
    return positions;
}

// 开启渲染后处理
this._renderListener = this._viewer.scene.postRender.addEventListener(this.render, this);

// 渲染方法
render() {
	// 获取实时位置坐标
	const geometry = CesiumUtil.getCenterByEntity(this.entity, this._viewer);
	// 获取屏幕坐标
	const cartesian2 = new Cartesian2();
	const position = this._viewer.scene.cartesianToCanvasCoordinates(geometry, cartesian2); // 笛卡尔坐标到画布坐标
	
	// 改变气泡框容器位置
	if (this._panelContainer) {
	    this._panelContainer.style.left =
	        position.x - this._panelContainer.offsetWidth / 2 + this.offset[0] + 'px';
	    this._panelContainer.style.top =
	        position.y - this._panelContainer.offsetHeight - 10 + this.offset[1] + 'px';
	}
}

Cesium 实战 - 气泡框跟随飞行-LMLPHP

在线示例

示例中展示了,气泡框跟随飞机飞行。

三维展示气泡框跟随模型效果


参考博客:

Cesium自定义Popup框

06-14 08:12