本教程将学习使用 CesiumJS 中的 Entity API 来绘制空间数据,例如点(points)、标记(markers)、标签(labels)、线(lines)、平面(shapes)、立体(volumes)和模型(models)。
刚接触 Cesium?
- 查看 在三维城市中展示拟建建筑 / 在三维城市中展示拟建建筑 - 掘金 (juejin.cn) 或 构建航班追踪器 / 构建航班跟踪器 - 掘金 (juejin.cn) 教程,了解 Cesium 应用程序的构建方式及如何与和实体进行交互。
- 如果是首次设置应用程序,请先阅读 CesiumJS 快速入门 / CesiumJS 快速入门 - 掘金 (juejin.cn) 教程。
什么是 Entity API?
CesiumJS 提供了丰富的空间数据 API,这些 API 可以分为两类:
- 面向图形开发者的低级
Primitive API - 用于数据驱动可视化的高级
Entity API。
Primitive API 提供了完成任务所需的最低限度的抽象,旨在为图形开发者提供高度灵活的实现方式,而并非注重 API 一致性。例如,加载一个模型的方法与创建广告牌的方法截然不同,而这两种操作与创建多边形的方法又全然不同。每种可视化方式都有各自特定的功能和独特的性能特点及最佳实践。虽然这种方法功能强大且灵活,但对于大多数应用程序来说,更适合使用高层次的抽象 API。
相比之下,Entity API 提供了一组设计一致的高级对象,将可视化和相关信息聚合到一个统一的数据结构中,我们称之为 Entity。这使得开发者可以专注于数据展示,而不用担心底层的实现机制。Entity API 还提供了便捷的结构,使得利用静态数据构建复杂、动态的可视化变得容易。虽然 Entity API 实际上是基于 Primitive API 构建的,但在使用时我们几乎不需要关心这些实现细节。得益于内部优化和自动处理,Entity API 能够提供灵活且高性能的可视化效果,同时拥有一致、易学且易用的接口。
创建第一个实体
首先,打开 Sandcastle 中的 Hello World 示例。
添加一个多边形,设置表示美国怀俄明州的经纬度坐标。
const viewer = new Cesium.Viewer("cesiumContainer");
const wyoming = viewer.entities.add({
polygon: {
hierarchy: Cesium.Cartesian3.fromDegreesArray([
-109.080842, 45.002073, -105.91517, 45.002073, -104.058488, 44.996596,
-104.053011, 43.002989, -104.053011, 41.003906, -105.728954, 40.998429,
-107.919731, 41.003906, -109.04798, 40.998429, -111.047063, 40.998429,
点击工具栏上的运行按钮(或键盘上的 F8)后会看到如下内容:
]),
height: 0,
material: Cesium.Color.RED.withAlpha(0.5),
outline: true,
outlineColor: Cesium.Color.BLACK,
},
});
viewer.zoomTo(wyoming);
点击工具栏上的运行按钮(或键盘上的 F8)后会生成如下内容:
上述代码首先创建了一个 Viewer实例,然后通过 viewer.entities.add 方法添加一个新的 Entity,在 add 方法中传入的参数是 Entity 的一些初始值。在这个示例中,我们创建了一个polygon,使用透明红色进行填充,并添加了黑色轮廓。add 方法的返回值是实际的 Entity 实例。最后,使用viewer.zoomTo 方法让相机聚焦到该实体,从而让用户能够清晰地看到这个新创建的图形。
平面和立体
以下是我们可以使用 Entity API 创建图形的完整列表:
| 类型 | 示例 | API 文档 | 效果展示 |
|---|---|---|---|
| Boxes (盒子) | Code example | Reference | |
| Circles and ellipses (圆形和椭圆) | Code example | Reference | |
| Corridor (走廊) | Code example | Reference | |
| Cylinder and cones (圆柱和圆锥) | Code example | Reference | |
| Polygons (多边形) | Code example | Reference | |
| Polylines (折线) | Code example | Reference | |
| Polyline volumes (折线体积) | Code example | Reference | |
| Rectangles (矩形) | Code example | Reference | |
| Spheres and ellipsoids (球和椭球) | Code example | Reference | |
| Walls (墙) | Code example | Reference |
材料和外形
所有图形都具有一组通用属性用于控制其外观。fill 属性指定是否填充几何图形,而 outline 属性指定几何图形是否有轮廓。
当 fill 为 true 时,material 属性决定填充的外观。以下代码创建了一个透明的蓝色椭圆:
const entity = viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(-103.0, 40.0),
ellipse: {
semiMinorAxis: 250000.0,
semiMajorAxis: 400000.0,
material: Cesium.Color.BLUE.withAlpha(0.5),
},
});
viewer.zoomTo(viewer.entities);
const ellipse = entity.ellipse; // 供将来示例使用
图片
我们还可以指定一个图片URL来代替颜色。
ellipse.material = "/docs/tutorials/creating-entities/images/cats.jpg";
在上述两种情况下,ColorMaterialProperty 或 ImageMaterialProperty 会在赋值时自动创建。对于更复杂的材质,我们需要自行创建一个 MaterialProperty 实例。
除颜色、图像外,Entity 还支持棋盘格、条纹和网格等材质。
棋盘格
ellipse.material = new Cesium.CheckerboardMaterialProperty({
evenColor: Cesium.Color.WHITE,
oddColor: Cesium.Color.BLACK,
repeat: new Cesium.Cartesian2(4, 4),
});
条纹
ellipse.material = new Cesium.StripeMaterialProperty({
evenColor: Cesium.Color.WHITE,
oddColor: Cesium.Color.BLACK,
repeat: 32,
});
网格
ellipse.material = new Cesium.GridMaterialProperty({
color: Cesium.Color.YELLOW,
cellAlpha: 0.2,
lineCount: new Cesium.Cartesian2(8, 8),
lineThickness: new Cesium.Cartesian2(2.0, 2.0),
});
轮廓
outline 依赖 outlineColor 和 outlineWidth 属性。在非 Windows 系统(如 Android、iOS、Linux 和 OS X)上,outlineWidth 属性是有效的。但对于 Windows 系统,轮廓宽度始终为 1。这是由于 WebGL 在 Windows 上的实现限制。
ellipse.fill = false;
ellipse.outline = true;
ellipse.outlineColor = Cesium.Color.YELLOW;
ellipse.outlineWidth = 2.0;
折线
折线 是一个特例,它们没有 fill 或 outline 属性。它们依赖于特殊材质以实现不同于颜色的效果。正是由于这些特殊材质,折线才可以在所有系统上正确显示不同的宽度和细节。
const entity = viewer.entities.add({
polyline: {
positions: Cesium.Cartesian3.fromDegreesArray([-77, 35, -77.1, 35]),
width: 5,
material: Cesium.Color.RED,
},
});
viewer.zoomTo(viewer.entities);
const polyline = entity.polyline; // 供将来示例使用
折线轮廓
polyline.material = new Cesium.PolylineOutlineMaterialProperty({
color: Cesium.Color.ORANGE,
outlineWidth: 3,
outlineColor: Cesium.Color.BLACK,
});
折线发光
polyline.material = new Cesium.PolylineGlowMaterialProperty({
glowPower: 0.2,
color: Cesium.Color.BLUE,
});
高度和拉伸
包括 corridor(走廊)、ellipse(椭圆)、polygon(多边形)和 rectangles(矩形)在内的平面形状可以被放置在特定的高度上,或者可以沿高度方向拉伸成为立体形状。不论是处于平面状态还是立体状态,这些形状都将遵循 WGS84 椭球的曲率。这意味着它们会适应地球表面的弯曲,保持与地球表面一致的形态。
在相应的图形对象上设置 height 属性(以米为单位),如下代码将多边形提高到距地表 250,000 米。
wyoming.polygon.height = 250000;
要将平面形状拉伸成立体形状,需要设置 extrudedHeight 属性。这会在 height 和 extrudedHeight 之间创建一个立体效果。如果 height 未定义,则立体形状将从地面(0 米)开始。下面的代码示例创建了一个从 200,000 米延伸到 250,000 米的立体形状:
wyoming.polygon.height = 200000;
wyoming.polygon.extrudedHeight = 250000;
我们可以轻松地将平面多边形拉伸成立体形状
在 Viewer 中查看 Entity 要素
以下是 Viewer 提供的用于直接处理 Entity 的功能。
选择和描述
在 CesiumJS 的 Viewer 中点击实体时,Viewer 会在实体的位置显示一个 SelectionIndicator 小部件,这个小部件用来突出显示被选中的实体。同时,会弹出一个 InfoBox,展示与该实体相关的更多信息。我们可以通过设置实体的 name 属性来确定 InfoBox 的标题。此外,还可以将包含 HTML 内容的字符串赋值给 Entity.description 属性,以展示更详细和丰富的信息。
wyoming.name = "Wyoming";
wyoming.description =
'\
<img\
width="50%"\
style="float:left; margin: 0 1em 1em 0;"\
src="/docs/tutorials/creating-entities/Flag_of_Wyoming.svg"/>\
<p>\
怀俄明州是美国西部山区地区的一个州。\
</p>\
<p>\
它是面积第10大州,但人口最少,也是50个州中人口密度第二低的州。州的西部三分之二覆盖着主要的山脉和东部落基山脉的山麓地区,而东部三分之一是高海拔草原,被称为高原。夏延是怀俄明州的首府,也是人口最多的城市,2017年估计人口为63,624。\
</p>\
<p>\
来源: \
<a style="color: WHITE"\
target="_blank"\
href="http://en.wikipedia.org/wiki/Wyoming">维基百科</a>\
</p>';
通过设置
Entity.description 在 InfoBox 中显示 HTML 格式的信息。
所有在 InfoBox 中显示的 HTML 都经过沙箱化处理。这可以防止外部数据源将恶意代码注入到 Cesium 应用程序中。要在 description 中运行 JavaScript 或浏览器插件,可以通过 viewer.infoBox.frame 属性访问沙箱用的 iframe。更多信息请参见 本文 了解控制 iframe 沙箱化的方式。
相机控制
在 CesiumJS 中,你可以使用 viewer.zoomTo 命令直接查看特定实体,快速将视角移到实体的位置。另一种选择是使用 viewer.flyTo 方法,该方法会执行一个相机飞行动画,平滑地将视角移动到指定的实体位置。
这两个方法都可以接受以下类型的参数:
Entity:单个实体对象。EntityCollection:一组实体对象的集合。DataSource:包含多个实体的数据源。- 实体数组:一个实体对象的数组。
这些方法使得你能够更方便地定位和查看特定的实体或实体集合,无论它们是单个对象、多个对象的集合,还是存储在数据源中的对象。
viewer.zoomTo 和 viewer.flyTo 还会根据提供的实体计算一个合适的视角。默认情况下,相机会面朝北,并以45° 角向下看。通过传入 HeadingPitchRange 可以自定义视角。
const heading = Cesium.Math.toRadians(90);
const pitch = Cesium.Math.toRadians(-30);
viewer.zoomTo(wyoming, new Cesium.HeadingPitchRange(heading, pitch));
zoomTo 和 flyTo 都是异步函数,当它们被调用时,不会立即完成所有操作。例如,飞往一个实体需要多个动画帧的过渡。在这些方法返回之前,无法保证飞行或缩放动作已经完成。这两个函数都会返回 Promise,可以在飞行或缩放完成后执行某个函数。例如,以下代码片段会飞往怀俄明州,并在飞行结束后选中它:
const result = await viewer.flyTo(wyoming);
if (result) {
viewer.selectedEntity = wyoming;
}
result 为 true 表示飞行成功完成;如果飞行被取消(例如用户在飞行完成之前启动了另一个飞行或缩放)或目标没有相应的可视化对象(即没有要缩放的对象),result 将为 false。
此外,有时候,特别是在处理动态时间数据时,我们希望相机保持跟随一个特定的实体,而不是盯着地球上的某个点。这可以通过设置 viewer.trackedEntity 属性来实现。被跟踪的实体需要设置 position 属性。例如:
wyoming.position = Cesium.Cartesian3.fromDegrees(-107.724, 42.68);
viewer.trackedEntity = wyoming;
通过将 viewer.trackedEntity 设置为 undefined 或双击远离实体可以停止跟踪。调用 zoomTo 或 flyTo 也会取消当前的跟踪。
管理实体
EntityCollection 是一个用于管理和监控多个实体的集合。它类似于一个关联数组,可以存储、管理和操作成组的实体。viewer.entities 就是一个 EntityCollection 实例。EntityCollection 集合提供了一些方法,如 add(添加实体)、remove (删除实体)和 removeAll(删除所有实体),用于管理其中的实体。
有时我们需要更新之前创建的实体,所以需要获取之前已经创建完成的实体对象。所有实体实例都有一个唯一的 id,可以用来从集合中检索实体。当我们创建一个实体时,可以手动为它指定一个 ID。如果没有指定 ID,系统会自动生成一个唯一的 ID。
// 手动指定ID
const entityWithId = viewer.entities.add({
id: "uniqueId",
});
// 自动生成ID
const entityWithoutId = viewer.entities.add({});
可以使用 getById 方法来检索具有指定 ID 的实体。如果没有找到对应的 ID,则返回 undefined。
const entity = viewer.entities.getById("uniqueId");
另外,如果希望获取一个实体,并且在该实体不存在时创建一个新实体,可以使用 getOrCreateEntity 方法。这个方法会首先检查集合中是否已存在具有指定 ID 的实体。如果存在,它将返回该实体;如果不存在,它将创建一个新的实体并返回。
const entity = viewer.entities.getOrCreateEntity("uniqueId");
需要注意的是,当你使用 add 方法手动创建并添加一个新实体到集合中时,如果集合中已存在具有相同 ID 的实体,该方法会抛出错误。
const entity = new Cesium.Entity({
id: "uniqueId",
});
viewer.entities.add(entity);
EntityCollection 的强大之处在于使用 collectionChanged 事件。当集合中的实体被添加、移除或更新时,这个事件会通知监听器。
使用 Sandcastle 中的 Geometry Showcase 示例。在创建 viewer 的代码行之后粘贴以下代码。
function onChanged(collection, added, removed, changed) {
let message = "Added ids";
for (var i = 0; i < added.length; i++) {
message += "\n" + added[i].id;
}
console.log(message);
}
viewer.entities.collectionChanged.addEventListener(onChanged);
当您运行示例时,应该会在控制台中看到大约65条消息,每条消息对应调用 viewer.entities.add 的每一次调用。
当需要一次性更新大量实体时,最好先暂停事件处理,把所有的更新操作都“排队”起来,等到所有操作都完成后再一次性发送更新事件。这种做法在性能上会更高效,因为你只需要一轮处理所有变化,而不是每次发生变化时都立即处理。
可以通过在更新操作开始时调用 viewer.entities.suspendEvents 方法来暂停事件处理,在所有更新操作完成后调用 viewer.entities.resumeEvents 方法来恢复事件处理。
// 暂停事件处理
viewer.entities.suspendEvents();
try {
// 执行大量实体更新操作
// ...
} finally {
// 恢复事件处理并发送一次性更新事件
viewer.entities.resumeEvents();
}
重新运行代码示例时,现在我们将得到一个包含所有更新的单个事件。这些方法是引用计数的,因此多个暂停和恢复调用可以嵌套。
选择
选择(通过点击来选择一个对象)是我们需要与 Primitive API 进行简要交互的领域之一。使用 scene.pick 和 scene.drillPick 来检索实体。
/**
* 返回在提供的窗口坐标处的最上层实体,如果该位置没有实体则返回 undefined。
*
* @param {Cartesian2} windowPosition 窗口坐标。
* @returns {Entity} 被选择的实体或 undefined。
*/
function pickEntity(viewer, windowPosition) {
const picked = viewer.scene.pick(windowPosition);
if (Cesium.defined(picked)) {
const id = Cesium.defaultValue(picked.id, picked.primitive.id);
if (id instanceof Cesium.Entity) {
return id;
}
}
return undefined;
}
/**
* 返回在提供的窗口坐标处的实体列表。实体按其视觉顺序从前到后排序。
*
* @param {Cartesian2} windowPosition 窗口坐标。
* @returns {Entity[]} 被选择的实体或 undefined。
*/
function drillPickEntities(viewer, windowPosition) {
let picked, entity, i;
const pickedPrimitives = viewer.scene.drillPick(windowPosition);
const length = pickedPrimitives.length;
const result = [];
const hash = {};
for (i = 0; i < length; i++) {
picked = pickedPrimitives[i];
entity = Cesium.defaultValue(picked.id, picked.primitive.id);
if (entity instanceof Cesium.Entity && !Cesium.defined(hash[entity.id])) {
result.push(entity);
hash[entity.id] = true;
}
}
return result;
}
点、广告牌和标签
通过设置 position、point 和 label,可以创建一个图形点和标签。例如,在我们喜欢的棒球队的主场体育场位置上放置一个点。
const viewer = new Cesium.Viewer("cesiumContainer");
const citizensBankPark = viewer.entities.add({
name: "Citizens Bank Park",
position: Cesium.Cartesian3.fromDegrees(-75.166493, 39.9060534),
point: {
pixelSize: 5,
color: Cesium.Color.RED,
outlineColor: Cesium.Color.WHITE,
outlineWidth: 2,
},
label: {
text: "Citizens Bank Park",
font: "14pt monospace",
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
outlineWidth: 2,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
pixelOffset: new Cesium.Cartesian2(0, -9),
},
});
viewer.zoomTo(viewer.entities);
默认情况下,标签是水平和垂直居中的。由于标签和点共享相同的位置,它们会在屏幕上重叠。为了避免这种情况,可以指定标签的原点为 VerticalOrigin.BOTTOM,并设置像素偏移 (0, -9)。
将 point 替换为 billboard(广告牌),这是一种始终面向用户的标记,即无论用户如何旋转视角,广告牌都是朝向用户方向的。
const citizensBankPark = viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(-75.166493, 39.9060534),
billboard: {
image: "/docs/images/tutorials/creating-entities/Philadelphia_Phillies.png",
width: 64,
height: 64,
},
label: {
text: "Citizens Bank Park",
font: "14pt monospace",
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
outlineWidth: 2,
verticalOrigin: Cesium.VerticalOrigin.TOP,
pixelOffset: new Cesium.Cartesian2(0, 32),
},
});
请参阅 Sandcastle 中的 Labels 和 Billboards 示例,了解更多自定义选项。
3D 模型
CesiumJS 支持使用 glTF 格式来展示 3D 模型。glTF 是一种高效的 3D 模型格式,专为在运行时环境中传输和加载3D模型而设计。你可以在 3D 模型 Sandcastle 示例中找到示例模型。
设置 position 和 glTF 模型的 URI 属性来创建模型的 Entity。
const viewer = new Cesium.Viewer("cesiumContainer");
const entity = viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(-123.0744619, 44.0503706),
model: {
uri: "../../../../Apps/SampleData/models/GroundVehicle/GroundVehicle.glb",
},
});
viewer.trackedEntity = entity;
CesiumJS 示例中的卡车模型。
默认情况下,模型是直立且面朝东的。通过为 Entity.orientation 属性指定一个 Quaternion,可以控制模型的方向,这决定了模型的航向、俯仰和翻滚:
const viewer = new Cesium.Viewer("cesiumContainer");
const position = Cesium.Cartesian3.fromDegrees(-123.0744619, 44.0503706);
const heading = Cesium.Math.toRadians(45.0);
const pitch = Cesium.Math.toRadians(15.0);
const roll = Cesium.Math.toRadians(0.0);
const orientation = Cesium.Transforms.headingPitchRollQuaternion(
position,
new Cesium.HeadingPitchRoll(heading, pitch, roll)
);
const entity = viewer.entities.add({
position: position,
orientation: orientation,
model: {
uri: "../../../../Apps/SampleData/models/GroundVehicle/GroundVehicle.glb",
},
});
viewer.trackedEntity = entity;
属性系统
我们为实体定义的所有值都存储为 Property 对象。例如,查看我们怀俄明州轮廓的值:
console.log(typeof wyoming.polygon.outline);
outline 是一个 ConstantProperty 实例。本教程使用了一种叫做隐式属性转换的简写形式,这种方式会自动为我们创建相应的属性对象。没有这种简写,初始示例的代码会更长:
const wyoming = new Cesium.Entity();
wyoming.name = "Wyoming";
const polygon = new Cesium.PolygonGraphics();
polygon.material = new Cesium.ColorMaterialProperty(
Cesium.Color.RED.withAlpha(0.5)
);
polygon.outline = new Cesium.ConstantProperty(true);
polygon.outlineColor = new Cesium.ConstantProperty(Cesium.Color.BLACK);
wyoming.polygon = polygon;
viewer.entities.add(wyoming);
使用 Property 的优势在于,实体 API 不仅可以表示固定不变的常量值,还可以表示会随着时间动态变化的值。。参见 Sandcastle 的 Callback Property 和 Interpolation 示例,了解一些时间动态属性的使用方法。