CesiumJS 实体(Entity)创建指南

2,157 阅读14分钟

原文地址:Creating Entities – Cesium

本教程将学习使用 CesiumJS 中的 Entity API 来绘制空间数据,例如点(points)、标记(markers)、标签(labels)、线(lines)、平面(shapes)、立体(volumes)和模型(models)。

刚接触 Cesium?

什么是 Entity API?

CesiumJS 提供了丰富的空间数据 API,这些 API 可以分为两类:

  1. 面向图形开发者的低级 Primitive API
  2. 用于数据驱动可视化的高级 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 exampleReference
Circles and ellipses (圆形和椭圆)Code exampleReference
Corridor (走廊)Code exampleReference
Cylinder and cones (圆柱和圆锥)Code exampleReference
Polygons (多边形)Code exampleReference
Polylines (折线)Code exampleReferenceimage.png
Polyline volumes (折线体积)Code exampleReference
Rectangles (矩形)Code exampleReference
Spheres and ellipsoids (球和椭球)Code exampleReference
Walls (墙)Code exampleReference

材料和外形

所有图形都具有一组通用属性用于控制其外观。fill 属性指定是否填充几何图形,而 outline 属性指定几何图形是否有轮廓。

filltrue 时,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";

在上述两种情况下,ColorMaterialPropertyImageMaterialProperty 会在赋值时自动创建。对于更复杂的材质,我们需要自行创建一个 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 依赖 outlineColoroutlineWidth 属性。在非 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;

折线

折线 是一个特例,它们没有 filloutline 属性。它们依赖于特殊材质以实现不同于颜色的效果。正是由于这些特殊材质,折线才可以在所有系统上正确显示不同的宽度和细节。

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 属性。这会在 heightextrudedHeight 之间创建一个立体效果。如果 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.zoomToviewer.flyTo 还会根据提供的实体计算一个合适的视角。默认情况下,相机会面朝北,并以45° 角向下看。通过传入 HeadingPitchRange 可以自定义视角。

const heading = Cesium.Math.toRadians(90);
const pitch = Cesium.Math.toRadians(-30);
viewer.zoomTo(wyoming, new Cesium.HeadingPitchRange(heading, pitch));

zoomToflyTo 都是异步函数,当它们被调用时,不会立即完成所有操作。例如,飞往一个实体需要多个动画帧的过渡。在这些方法返回之前,无法保证飞行或缩放动作已经完成。这两个函数都会返回 Promise,可以在飞行或缩放完成后执行某个函数。例如,以下代码片段会飞往怀俄明州,并在飞行结束后选中它:

const result = await viewer.flyTo(wyoming);
if (result) {
  viewer.selectedEntity = wyoming;
}

resulttrue 表示飞行成功完成;如果飞行被取消(例如用户在飞行完成之前启动了另一个飞行或缩放)或目标没有相应的可视化对象(即没有要缩放的对象),result 将为 false

此外,有时候,特别是在处理动态时间数据时,我们希望相机保持跟随一个特定的实体,而不是盯着地球上的某个点。这可以通过设置 viewer.trackedEntity 属性来实现。被跟踪的实体需要设置 position 属性。例如:

wyoming.position = Cesium.Cartesian3.fromDegrees(-107.724, 42.68);
viewer.trackedEntity = wyoming;

通过将 viewer.trackedEntity 设置为 undefined 或双击远离实体可以停止跟踪。调用 zoomToflyTo 也会取消当前的跟踪。

管理实体

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.pickscene.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;
}

点、广告牌和标签

通过设置 positionpointlabel,可以创建一个图形点和标签。例如,在我们喜欢的棒球队的主场体育场位置上放置一个点。

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 中的 LabelsBillboards 示例,了解更多自定义选项。

3D 模型

CesiumJS 支持使用 glTF 格式来展示 3D 模型。glTF 是一种高效的 3D 模型格式,专为在运行时环境中传输和加载3D模型而设计。你可以在 3D 模型 Sandcastle 示例中找到示例模型。

设置 positionglTF 模型的 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 PropertyInterpolation 示例,了解一些时间动态属性的使用方法。