使用技术:
Vue
+Cesium
产品需求:
需要在地球上指定经纬度展示项目数据,展示内容包含项目进度 、项目类型 、项目名称 、项目负责人。点击不同的信息,展示不同的弹窗。并且地球视角转动,展示地球上展示的数据也要随之转动。
UI设计图:
关于文字:
Cesium
提供的label
只能实现简单的文字加载,支持修改文字大小、边框、背景等
关于Icon:
Cesium
提供的billboard
可以加载指定图片,对于项目类型这种可枚举的字段倒是可以通过if
判断来加载指定类型的图片,但是类似于项目进度这种可选值数量巨大的字段,总不至于要切100张图片吧 🤯
开发思路:
经过度娘的一番搜索,发现大部分思路都是先写自定义的<div>
标签,然后使用绝对定位,给定位到指定经纬度的屏幕坐标上,当地球发生视角变化时,重新计算定位位置。
这种方法当数据量小的时候还好说,需要展示的数据增多,获取最新位置的计算量也会随之增长。😟
🤔 实现思路:
因为项目中有将Dom
元素保存为本地图片的需求和实现方式。所以就想到了,那么可以将自定义的<div>
标签转成image
,然后Cesium
通过billboard
直接把图片加载到地球上么?
经过一番尝试,证明这个思路是可行的。
Cesium加载图片
引入自定义
Vue
组件,初始化所需数据,并挂载到Dom
树上
js
import ProjectProgress from './ProjectProgress.vue'
const ProjectProgressConstructor = Vue.extend(ProjectProgress);
const ProjectProgressDom = new ProjectProgressConstructor({
data: { progress: progress }
}).$mount();
document.body.appendChild(ProjectProgressDom.$el);
将挂载在Dom树上的元素转换为图片,并清除元素
js
const progressImage = await domConvertToImage(ProjectProgressDom.$el); // 进度组件Image
ProjectProgressDom.$el.remove(); // 将使用完成的DOM元素清除
Cesium
加载billboard
js
app.$Viewer.entities.add({
name: "mapPorjectProgressElement",
useful: item,
position,
billboard: {
image: progressImage,
width: 26, // 宽度(以像素为单位)
height: 26, // 高度(以像素为单位)
verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 相对于坐标的垂直位置
horizontalOrigin: Cesium.HorizontalOrigin.CENTER, // 相对于坐标的水平位置
pixelOffset: new Cesium.Cartesian2(0, -26) // 该属性指定标签在屏幕空间中距此标签原点的像素偏移量
}
});
Dom元素转image
使用了
html2canvas
依赖
js
import html2canvas from 'html2canvas'
/**
* 将DOM元素转换为图片
* @param {DOM} dom
* @returns {String} ImageUrl
*/
export async function domConvertToImage(dom) {
let saveUrl
await html2canvas(dom, {
backgroundColor: null // 设置图片背景为透明
})
.then((canvas) => {
saveUrl = canvas.toDataURL('image/png')
})
.catch((err) => {
console.error('组件转换图片失败:', err)
})
return saveUrl
}
实现效果图
🧐 存在问题
当然这种方式也有缺点,因为是将元素转换为image加载的,所有无法实现加载数据的动画效果 和鼠标交互效果。
实际开发,根据需求的情况择优选择。
完整代码
自定组件
进度百分比的圆环使用了
element
的组件,也可以自己实现
html
<template>
<div class="project_progress_main_content">
<el-progress
type="circle"
:percentage="progress"
:stroke-width="3"
color="#FCB718"
define-back-color="rgba(255, 255, 255, 0.2)"
:width="26"
:show-text="false"
/>
<div class="progress_number">
{{ progress }}
</div>
</div>
</template>
<script>
export default {
data() {
return {
progress: 0
}
}
}
</script>
<style lang="scss" scoped>
.project_progress_main_content {
position: fixed;
top: -999px;
left: -999px;
width: 26px;
height: 26px;
.progress_number {
position: absolute;
top: 3px;
left: 3px;
width: 20px;
height: 20px;
background: rgba(0, 0, 0, 0.4);
border-radius: 50%;
font-family: D-DIN;
font-size: 12px;
font-weight: bold;
line-height: 10px;
color: #ffffff;
text-align: center;
line-height: 20px;
}
}
</style>
Cesium加载元素
因为项目的每个属性都需要单独的点击交互,所以把四个信息拆成独立的元素。
如果不需要多个点击事件,也可把所有信息都写到自定义的
Vue
组件中
js
import Vue from "vue";
import app from "@/main";
// 绘制项目打卡icon
export function drawPorjectRecordIcon(projectRecordList) {
projectRecordList.forEach(async (item) => {
const position = Cesium.Cartesian3.fromDegrees(
parseFloat(item.longitude),
parseFloat(item.latitude),
0
); // 项目坐标
/* ---------------------------------- 绘制项目Icon ----------------------------------- */
const image = ""; // 项目类别图标 --自定义补充
app.$Viewer.entities.add({
name: "mapPorjectRecordIconElement",
useful: item,
position,
billboard: {
image,
show: true,
width: 24, // 宽度(以像素为单位)
height: 30, // 高度(以像素为单位)
verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 相对于坐标的垂直位置
horizontalOrigin: Cesium.HorizontalOrigin.CENTER // 相对于坐标的水平位置
}
});
/* ---------------------------------- 绘制进度组件 ----------------------------------- */
const ProjectProgressConstructor = Vue.extend(ProjectProgress);
const ProjectProgressDom = new ProjectProgressConstructor({
data: { progress: item.progress }
}).$mount();
document.body.appendChild(ProjectProgressDom.$el);
const progressImage = await domConvertToImage(ProjectProgressDom.$el); // 进度组件Image
ProjectProgressDom.$el.remove(); // 将使用完成的DOM元素清除
const progressBillboard = {
image: progressImage,
width: 26, // 宽度(以像素为单位)
height: 26, // 高度(以像素为单位)
verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 相对于坐标的垂直位置
horizontalOrigin: Cesium.HorizontalOrigin.CENTER, // 相对于坐标的水平位置
pixelOffset: new Cesium.Cartesian2(0, -26) // 该属性指定标签在屏幕空间中距此标签原点的像素偏移量
};
app.$Viewer.entities.add({
name: "mapPorjectProgressElement",
useful: item,
position,
billboard: progressBillboard
});
/* ---------------------------------- 绘制人员信息 ----------------------------------- */
const userLabel = {
text: item.responsibleName,
font: "14px Source Han Sans CN", // 字体样式
fillColor: Cesium.Color.WHITE, // 字体颜色
backgroundColor: Cesium.Color.BLACK.withAlpha(0.4), // 背景颜色
showBackground: true, // 是否显示背景颜色
outlineWidth: 20, // 文字外边框宽度
verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 相对于坐标的水平位置
horizontalOrigin: Cesium.HorizontalOrigin.LEFT, // 相对于坐标的垂直位置
pixelOffset: new Cesium.Cartesian2(20, -22) // 该属性指定标签在屏幕空间中距此标签原点的像素偏移量
};
app.$Viewer.entities.add({
name: "mapProjectResponsibleElement",
useful: item,
position,
label: userLabel
});
/* ---------------------------------- 绘制项目名称 ----------------------------------- */
app.$Viewer.entities.add({
name: "mapProjectNameElement",
useful: item,
position,
label: {
text: item.projectName,
font: "16px Source Han Sans CN", // 字体样式
style: Cesium.LabelStyle.FILL_AND_OUTLINE, // 文字描边
fillColor: Cesium.Color.WHITE, // 字体颜色
outlineWidth: 20, // 文字外边框宽度
outlineColor: Cesium.Color.BLACK.withAlpha(0.6), // 文字外边框颜色
verticalOrigin: Cesium.VerticalOrigin.TOP, // 相对于坐标的水平位置
horizontalOrigin: Cesium.HorizontalOrigin.LEFT, // 相对于坐标的垂直位置
pixelOffset: new Cesium.Cartesian2(20, -30) // 该属性指定标签在屏幕空间中距此标签原点的像素偏移量
}
});
});
/* ------------------------------ 项目点击事件 ------------------------------ */
const mapPorjectRecordHandler = new Cesium.ScreenSpaceEventHandler(
app.$Viewer.scene.canvas
);
mapPorjectRecordHandler.setInputAction((click) => {
var pick = app.$Viewer.scene.pick(click.position);
if (pick && pick.id && pick.id.name === "mapPorjectRecordIconElement") {
console.log("点击了项目类别图标,项目信息为:", pick.id.useful);
}
if (pick && pick.id && pick.id.name === "mapPorjectProgressElement") {
console.log("点击了绘制进度组件,项目信息为:", pick.id.useful);
}
if (pick && pick.id && pick.id.name === "mapProjectResponsibleElement") {
console.log("点击了项目人员信息,项目信息为:", pick.id.useful);
}
if (pick && pick.id && pick.id.name === "mapProjectNameElement") {
console.log("点击了项目名称,项目信息为:", pick.id.useful);
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
}