Cesium实现类似大屏的部分区域展示柱状图效果
本文介绍了基于Cesium实现区域裁剪和3D柱状图交互功能的技术方案。主要内容包括:(1)使用GeoJSON数据对四川区域进行裁剪优化,通过简化算法提高性能;(2)创建包含柱体、标签和数值的复合数据源;(3)实现hover高亮效果,通过ScreenSpaceEventHandler监听鼠标移动事件;(4)支持点击交互功能,包括选中高亮、其他元素置灰、视角飞向目标及信息展示。文章提供了核心代码片段,
·
实现效果
- 区域裁剪
- hover高亮
- 点击选中(高亮当前,置灰其他,飞向当前,展示html信息)
- 信息跟随视角
- 进行hook封装
非常简单的demo,抛砖引玉
区域裁剪
- 选择裁剪的区域
- 设置裁剪区域
/**
* @description 添加四川区域
* @param {Cesium.Viewer} viewer
*/
function addSCRegion(viewer: Cesium.Viewer) {
viewer.scene.skyAtmosphere!.show = false; // 大气
const coors = SC.features[0].geometry.coordinates[0];
const geojson = turf.polygon(coors);
const simplified = turf.simplify(geojson, {
tolerance: 0.05, // 简化容差,值越小,越精确
highQuality: true, // 是否花费更多时间使用其他算法创建更高质量的简化
});
// 根据简化后的几何体创建裁剪区域
const areas = new Cesium.ClippingPolygon({
positions: Cesium.Cartesian3.fromDegreesArray(
simplified.geometry.coordinates[0].flat()
),
});
// 设置裁剪区域
viewer.scene.globe.clippingPolygons = new Cesium.ClippingPolygonCollection({
polygons: [areas],
inverse: true,
});
// 设置30度向下视角 (pitch = -30度)
const pitch = (-40 * Math.PI) / 180; // 转换为弧度
viewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(103.06, 22.45, 600000),
orientation: {
heading: 0,
pitch: pitch,
roll: 0,
},
});
}
注意:直接应用裁剪的区域会十分卡,因为要计算的点非常多,所以需要优化
添加数据源和实体
- 使用的是数据源,把box、label、value当成一个整体,操作视角时更方便
/**
* @description 初始化bar
*/
const initBar = (item: any) => {
const dataSource = new Cesium.CustomDataSource("bar");
const boxPosition = Cesium.Cartesian3.fromDegrees(
item.position[0],
item.position[1],
0
);
let material = Cesium.Color.BLUE.withAlpha(0.8);
if (item.value >= 200) {
material = Cesium.Color.RED.withAlpha(0.8);
} else if (item.value >= 100) {
material = Cesium.Color.YELLOW.withAlpha(0.8);
} else if (item.value >= 60) {
material = Cesium.Color.GREEN.withAlpha(0.8);
} else {
material = Cesium.Color.BLUE.withAlpha(0.8);
}
const box = new Cesium.Entity({
id: "box" + item.name,
name: "box",
position: boxPosition,
box: {
dimensions: new Cesium.Cartesian3(10000, 10000, item.value * 1000),
material: material,
heightReference: Cesium.HeightReference.RELATIVE_TO_GROUND,
},
properties: item,
});
const labelPosition = Cesium.Cartesian3.fromDegrees(
item.position[0],
item.position[1],
item.value * 1000 + 5000
);
const label = new Cesium.Entity({
id: "label" + item.name,
name: "label",
position: labelPosition,
label: {
text: item.name,
font: "20px sans-serif",
fillColor: Cesium.Color.WHITE,
outlineWidth: 2,
outlineColor: Cesium.Color.BLACK,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
// heightReference: Cesium.HeightReference.RELATIVE_TO_GROUND,
},
properties: item,
});
const valuePosition = Cesium.Cartesian3.fromDegrees(
item.position[0],
item.position[1],
item.value * 1000 + 5000
);
const value = new Cesium.Entity({
id: "value" + item.name,
name: "value",
position: valuePosition,
label: {
text: String(item.value),
font: "16px sans-serif",
fillColor: Cesium.Color.SKYBLUE,
outlineWidth: 2,
outlineColor: Cesium.Color.WHITE,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
pixelOffset: new Cesium.Cartesian2(0, -20),
// heightReference: Cesium.HeightReference.RELATIVE_TO_GROUND,
},
properties: item,
});
dataSource.entities.add(box);
dataSource.entities.add(label);
dataSource.entities.add(value);
return dataSource;
};
hover高亮
目前只设置了label的颜色,可以看自己的需求
/**
* @description 初始化hover事件,hover到要素上时,要素高亮
*/
const initHoverEvent = () => {
const handler = new Cesium.ScreenSpaceEventHandler(viewer.value!.canvas);
let entity = null as null | Cesium.Entity;
handler.setInputAction(
(move: Cesium.ScreenSpaceEventHandler.MotionEvent) => {
const pick = viewer.value?.scene.pick(move.endPosition);
if (pick && pick.id) {
// hover到另一个entity上,移除上一个entity的label
if (entity && entity !== pick.id) {
const entityCollection = entity.entityCollection;
const item = entity.properties as any;
const label = entityCollection.getById("label" + item.name);
label!.label!.fillColor = new Cesium.ConstantProperty(
Cesium.Color.WHITE
);
}
// hover到新的entity上,给新的entity添加label
entity = pick.id as Cesium.Entity;
(viewer.value as any)._container.style.cursor = "pointer";
const entityCollection = entity.entityCollection;
const item = entity.properties as any;
const label = entityCollection.getById("label" + item.name);
label!.label!.fillColor = new Cesium.ConstantProperty(
Cesium.Color.PINK
);
viewer.value?.scene.requestRender();
} else {
// 移除上一个entity的label
(viewer.value as any)._container.style.cursor = "default";
if (!pick && entity) {
const entityCollection = entity.entityCollection;
const item = entity.properties as any;
const label = entityCollection.getById("label" + item.name);
label!.label!.fillColor = new Cesium.ConstantProperty(
Cesium.Color.WHITE
);
}
viewer.value?.scene.requestRender();
}
},
Cesium.ScreenSpaceEventType.MOUSE_MOVE
);
};
click选中
- 高亮当前选中
- 置灰其他
- 视角移动到当前选中
- 展示信息
/**
* @description 初始化点击事件,点击聚焦实体并展示信息
*/
const initClickEvent = () => {
let selectDataSource = null as null | Cesium.DataSource; // 选中数据源
viewer.value?.screenSpaceEventHandler.setInputAction(
(click: Cesium.ScreenSpaceEventHandler.PositionedEvent) => {
const pick = viewer.value?.scene.pick(click.position);
if (pick && pick.id) {
const entity = pick.id as Cesium.Entity;
const entityCollection = entity.entityCollection;
const currentDataSource =
entityCollection.owner as Cesium.CustomDataSource;
// 同一数据源点击不重复处理
if (selectDataSource === currentDataSource) {
return;
}
tooltips.value.show = false;
// 飞向选中实体
viewer.value
?.flyTo(entityCollection, {
duration: 0.5,
})
.then(() => {
// 动画结束后展示信息
currentDataSource?.entities.values.forEach((entity) => {
if (entity.name === "box") {
activeEntity.value = entity;
updatePosition(entity);
tooltips.value.info = entity.properties?.getValue();
tooltips.value.show = true;
}
});
});
selectDataSource = currentDataSource;
// 处理选中和未选中的透明度
dataSourceList?.forEach((dataSource) => {
dataSource?.entities.values.forEach((entity) => {
if (entity.name === "box") {
if (dataSource === currentDataSource) {
// 高亮选中
const material = entity.box!
.material as Cesium.ColorMaterialProperty;
const color = material.color as Cesium.ConstantProperty;
color.setValue(Cesium.Color.fromAlpha(color.getValue(), 0.8));
} else {
// 其他置灰
const material = entity.box!
.material as Cesium.ColorMaterialProperty;
const color = material.color as Cesium.ConstantProperty;
color.setValue(Cesium.Color.fromAlpha(color.getValue(), 0.1));
}
}
});
});
} else {
// 点击空白区域恢复所有实体透明度
if (selectDataSource) {
activeEntity.value = null;
tooltips.value.show = false;
dataSourceList?.forEach((dataSource) => {
dataSource?.entities.values.forEach((entity) => {
if (entity.name === "box") {
const material = entity.box!
.material as Cesium.ColorMaterialProperty;
const color = material.color as Cesium.ConstantProperty;
color.setValue(Cesium.Color.fromAlpha(color.getValue(), 0.8));
}
});
});
selectDataSource = null;
}
}
// 统一在最后请求渲染,避免多次触发
viewer.value?.scene.requestRender();
// 解决点击空白区域后,会有几率box为空的问题
setTimeout(() => {
viewer.value?.scene.requestRender();
}, 100);
},
Cesium.ScreenSpaceEventType.LEFT_CLICK
);
};
画布移动更新信息位置
主要就是监听屏幕,进行坐标转化
- box的笛卡尔坐标转经纬度(因为box的position高度是0,直接使用将会在柱状图底下并与box有重叠,我想要在柱状图右上方)
- 将经纬度右移0.1,避免和box重叠
- 经纬度+高度*0.9倍,展示在box的右上方,重新获取笛卡尔坐标
- 将笛卡尔坐标转屏幕坐标
const initRenderListener = () => {
// 监听场景渲染事件,实时更新坐标
viewer.value?.scene.postRender.addEventListener(addRenderEventListener);
};
/**
* @description 添加屏幕事件监听
*/
function addRenderEventListener() {
if (activeEntity.value && tooltips.value.show) {
updatePosition(activeEntity.value);
}
}
/**
* @description 更新tooltips位置
* @param {Cesium.Entity} entity
*/
function updatePosition(entity: Cesium.Entity) {
// 获取box的笛卡尔坐标
const position = entity.position?.getValue();
// 将笛卡尔坐标转换为经纬度坐标
let p1 = getDegreesFromCartesian3(position!);
p1[0] = p1[0] + 0.1; // 右偏移0.1度
// 经纬度坐标转换为笛卡尔坐标,加了高度
let p2 = getCartesian3FromDegrees(
p1!,
entity.properties?.value.getValue() * 1000 * 0.9
); // 高度在0.9倍位置
// 将笛卡尔坐标转换为屏幕坐标
const p = getScreenPositionFromCartesian3(p2!, viewer.value!);
tooltips.value.x = p!.x;
tooltips.value.y = p!.y;
}
在线预览
更多推荐
所有评论(0)