Cesium官方教程3 -- 空间数据可视化

原文地址:https://cesiumjs.org/tutorials/Visualizing-Spatial-Data/
这篇教程教你如何使用Cesium的Entity API去绘制空间数据,如点,图标,文字标注,折线,模型,图形和立体图形。虽然这章不需要什么前提,但是如果你对Cesium一无所知,最好从第一个教程开始

Entity API是什么?

Cesium丰富的空间数据可视化API分为两部分:Primitive API 面向三维图形开发者,更底层一些。Entity API 是数据驱动更高级一些。

Primitive API的主要目的是为了完成(可视化)任务的最少的抽象需求。他要求我们以一个图形开发者的方式去思考,并且使用了一些图形学术语。它是为了最高效最灵活的实现可视化效果,忽略了API的一致性。比如绘制三维模型和创建Billboard不同,和多边形绘制更是彻底不同。每种可视化都有自己鲜明的特色。此外,他们每种都有自己的独特的性能提升方式,也需要遵守不同的优化原则。虽然它很强大又很灵活,但是大多数项目需要比Primitive API更高层次的抽象。

Entity AP的主要目的是定义一组高级对象,它们把可视化和信息存储到统一的数据结果中,这个对象叫Entity。 它让我们更加关注我们的数据展示而不是底层的可视化机制。它提供了很方便的创建复杂的,与静态数据相匹配的随时间变化的可视化效果。Entity API实际内部在使用Primitive API ,它的实现细节,我们无需关心。经过各种数据的测试,Entity API提供灵活的,高层次的可视化,同时暴露一些一致性的、容易去学习和使用的接口。

第一个 Entity

学习Entity API基本使用的最好方式就是去读代码。简单其间,我们使用Sandcastle去创建 Hello World 示例。如果你已经创建了本地的cesium项目,那么使用你自己的项目。
假设,我们需要从经纬度列表中创建美国怀俄明州(选择怀俄明州Wyoming,是因为它的边界足够简单)的多边形。把下面的代码粘贴拷贝到Sandcastle中去:

var viewer = new Cesium.Viewer('cesiumContainer');

var wyoming = viewer.entities.add({
  name : 'Wyoming',
  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,
                              -111.047063,42.000709,
                              -111.047063,44.476286,
                              -111.05254,45.002073]),
    height : 0,
    material : Cesium.Color.RED.withAlpha(0.5),
    outline : true,
    outlineColor : Cesium.Color.BLACK
  }
});

viewer.zoomTo(wyoming);

单机Run 按钮(或者按下F8)就看到如下图所示效果:


怀俄明州

第一个 entity.怀俄明州从来没有让人如此兴奋.
我们尽力使Cesium的代码容易理解,上面的代码不用解释也应该明白什么意思。首先创建Cesium程序的基础对象 Viewer widget, 然后使用viewer.entities.add添加 Entity。传给 add 方法的参数一个包含了初始化配置的js 对象. 返回值就是 entity 对象. 最后调用 viewer.zoomTo 定位到到这个entity。

Entity 的配置项里有大量的参数,但是现在我们只是设置了 polygon 的填充面为半透明红色,边界线时黑色的。最后把这个entity命名为“Wyoming”。

面和体

学了基础的添加多边形知识,多亏Entity API的一致性非常好,我们结合Sandcastle 的示例,就很容易就创建各种图形。下面是所有支持的面和体的图形列表:

Box

六面体盒子entity.box

Ellipse

圆和椭圆entity.ellipse

Corridor

Corridor entity.corridor

Cylinder

圆柱和圆锥 entity.cylinder

Polygon

多边形 entity.polygon

Polyline

折线 entity.polyline

Volume

Polyline Volumes entity.polylineVolume

Rectangle

矩形 entity.rectangle

Ellipsoid

球和椭球 entity.ellipsoid

Wall

entity.wall

材质和边线

无论他们的几何体有什么不同,所有形状和体都有一系列相同的属性来控制它们的外观。fill 为boolean类型,控制表面是否填充。 outline 属性控制是否有外边界。
fill=truematerial属性决定了用什么材质填充表面。下个例子,我们创建一个半透明椭圆。默认fill=trueoutline=false,所以我们只需要设置material属性。

var 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);
var ellipse = entity.ellipse;  
半透明椭圆

图片材质

直接设置一个图片的url就可以了。

ellipse.material = '//cesiumjs.org/tutorials/images/cats.jpg';

图片材质

上面两个示例李, 当设置颜色或者url之后Cesium会自动创建 ColorMaterialProperty 或者ImageMaterialProperty对象。 对于更复杂的材质, 需要手动创建 MaterialProperty对象。 当前, Entity 面和体支持 颜色(colors),纹理图片( images),棋盘 (checkerboard), 条纹(stripe), 网格(grid)等材质.

网格材质

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)
});

网格材质

边线

fill属性不太一样,outline没有对应的材质配置,而是用两个独立的属性outlineColoroutlineWidth
注意outlineWidth属性仅仅在非windows系统上有效,比如Android, iOS, Linux, 和OS X。Windows系统上边线宽度永远为1。主要是因为三大主流浏览器引擎在windows平台上实现webgl上的技术限制。

ellipse.fill = false;
ellipse.outline = true;
ellipse.outlineColor = Cesium.Color.YELLOW;
ellipse.outlineWidth = 2.0;

边线

折线

折线是个特例,他没有填充或者边线属性。除了颜色它有专门的材质属性。由于这种特殊材质,折线宽度和折线的边线宽度,在所有系统都有效。

var entity = viewer.entities.add({
    polyline : {
        positions : Cesium.Cartesian3.fromDegreesArray([-77, 35,
                                                        -77.1, 35]),
    width : 5,
    material : Cesium.Color.RED
}});
viewer.zoomTo(viewer.entities);
var polyline = entity.polyline // For upcoming examples
折线

折线边线

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
});

折线辉光

高度和垂直挤压(Extrusions)

所有的面形状都是平铺在地球上,当前 圆(circles)、椭圆(ellipses)、多边形(polygons)、矩形(rectangles)可以有一个高程属性 或者 垂直挤压变成体。这两种情况种,这些面或者体仍然会贴合地球曲率。
上面我们列出的所有图形,都是只需要在图形对象(graphics )上设置一个高度属性即可。这里顺便说明下,除非在函数上明确说明,否则Cesium总是使用米、弧度、秒做为标准单位。如 Cartesian3.fromDegrees.
下面这行代码把多边形放到了 250,000米高空。

wyoming.polygon.height = 250000;
250,000 米高空的怀俄明

把图形挤压为体,也非常简单。仅仅需要设置 extrudedHeight 属性。将会创建一个在heightextrudedHeight之间的体块。如果 height 没有定义, 体块从 0高程开始。下面代码创建一个从200,000米到 250,000米的体 。也就是说这个体的高度是50000米。

wyoming.polygon.height = 200000;
wyoming.polygon.extrudedHeight = 250000;
垂直挤压

对多边形变成体也非常容易

Viewer中的Entity 元素(feature)

在开始其他可视化效果学习之前,让我们先看看 Viewer 中提供的和Entity相关的函数。

选中和描述

除非明确禁用,否则点击Entity将在它的位置会显示 SelectionIndicator 控件,并且在 InfoBox 控件里显示它的描述信息。回想我们最开始的示例,我们仅仅为 wyoming entity设置了name属性,它显示在 InfoBox标题栏, 也可以通过 Entity.description 设置一段HTML当作infobox的内容。 把下面的代码追加到上面的示例里:

wyoming.description = '\
<img\
  width="50%"\
  style="float:left; margin: 0 1em 1em 0;"\
  src="//cesiumjs.org/tutorials/Visualizing-Spatial-Data/images/Flag_of_Wyoming.svg"/>\
<p>\
  Wyoming is a state in the mountain region of the Western \
  United States.\
</p>\
<p>\
  Wyoming is the 10th most extensive, but the least populous \
  and the second least densely populated of the 50 United \
  States. The western two thirds of the state is covered mostly \
  with the mountain ranges and rangelands in the foothills of \
  the eastern Rocky Mountains, while the eastern third of the \
  state is high elevation prairie known as the High Plains. \
  Cheyenne is the capital and the most populous city in Wyoming, \
  with a population estimate of 62,448 in 2013.\
</p>\
<p>\
  Source: \
  <a style="color: WHITE"\
    target="_blank"\
    href="http://en.wikipedia.org/wiki/Wyoming">Wikpedia</a>\
</p>';

设置Entity描述信息

很多项目都是从服务端返回描述信息,而不是上面这种硬编码,不过这种方法是可行的。
默认,在InfoBox 里所有的HTML是沙盒模式。这个防止外部的数据注入恶意的代码。如果你需要在描述信息里运行js脚本或者浏览器插件,可以通过viewer.infoBox.frame属性来访问这个iframe。更多关于iframe的沙盒模式,请参考这篇文章

相机控制

就像第一个例子中,我们使用 zoomTo 命令去显示一个特定的entity。双击Entity或者点击 InfoBox左上角按钮,也能达到同样效果. 还有一个 flyTo 方法,它不是立即定位过去,而是执行一个相机动画渐变过去。这些方法除了应用在单独一个entity上,也可以作用在 EntityCollection对象上或者一个普通的js entity数组,。
默认,这些方法会自动计算一个视图,确保所有所有传到方法里的entity都可见,相机朝向正北,以45°倾斜俯视。可以提供一个自定义的heading, pitch, and range.来修改这个朝向。下面代码执行后相机会从东方向下倾斜30°角去看怀俄明的多边形。因为我们没有设定range参数,那么这个参数还是按照默认计算的结果。

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

自定义视角

zoomToflyTo 都是异步函数, 也就是说当函数return的时候,并不能保证执行完毕了。一般flyto会在很多个动画帧里都运算。这些函数都返回一个 Promises ,我们可以把飞行或者缩放完成后需要制定的代码放到 then函数里。我们把以下代码片段里换成 zoomTo ,并且在飞行完毕后会同时选中这个entity。

viewer.flyTo(wyoming).then(function(result){
    if (result) {
        viewer.selectedEntity = wyoming;
    }
});

这里回调函数里的result参数,true表示飞行正常完成,false 飞行被打断 或者 用户开启了另一个飞行定位函数,再或者目标对象无法被可视化也就没办法去定位了。
有时候,尤其是展示一个随时间变化的数据,我们希望相机能跟随这个entity。这个通过设置 viewer.trackedEntity就很容易实现。跟随一个entity要求position属性必须存在。还是通过我们的Wyoming 多边形entity来测试这个模式,我们给它增加个position属性,代码如下:

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

viewer.trackedEntity 设置为undefined 或者点击 InfoBox的左上的取消按钮都可以停止跟随模式。 调用zoomTo 或者 flyTo 也会取消跟随模式,并且 把 viewer.trackedEntity 设置为 undefined
大部分情况下,在 Viewer 中定义的和entity相关的相机函数足够使用了。但是如果你想在项目更多的自定义相机视图方式,请查看 相机教程

管理Entity集合

EntityCollection类是一个Entity数组集合,用来它管理和控制一组entity非常方便。我们已经见过它的一个实例 viewer.entities 属性。EntityCollection 提供了基本的数组方法 add, remove, 和 removeAll;同时还有下面我们要讨论的一些特有方法或者属性。
很多项目的数据实际都是存在服务端的,只有客户端需要的时候才会加载。有时候需要更改一个我们已经创建的entity。所有entity对象都有一个独一无二的 id 属性,这种情况情况下就非常有用。前面的示例里,我们并没有指定这个id,Cesium会自动生成一个 GUID 类似182bdba4-2b3e-47ae-bf0b-83f6fde285fd 填充到id属性里。服务端的数据一般都有自己主键id属性,所以可以在enity创建的时候指定这个id。

viewer.entities.add({
    id : 'uniqueId'
});

随后,可以通过 getById来获取Entity对象。如果没有找到对应的id,那么该方法返回 undefined

var entity = viewer.entities.getById('uniqueId');

另一个常见的应用,是如果id不存在就新建,如果id存在就更新。 getOrCreateEntity 总会返回以传入的参数为id的对象实例, 如果id不存在,那么会新建一个,并且增加到entity集合里,然后返回。

var entity = viewer.entities.getOrCreateEntity('uniqueId');

最后,简单的通过 add就可以新建一个Entity实例。这种情况下,add函数会检测如果传入了一个已经存在的id,那么会报异常。

var entity = new Entity({
    id : 'uniqueId'
});
viewer.entities.add(entity);

EntityCollection 最强大的功能其实是collectionChanged Event,我们用它来接收集合里entity被添加、删除甚至更新的通知。当项目里的用户界面或者某个功能需要监控集合里的对象改变的时候,这个功能非常有用。

为了验证这点,可以试下Sandcastle的实例 Geometry 示例 。把下面的代码拷贝到紧跟viewer 创建的地方。

function onChanged(collection, added, removed, changed){
  var msg = 'Added ids';
  for(var i = 0; i < added.length; i++) {
    msg += '\n' + added[i].id;
  }
  console.log(msg);
}
viewer.entities.collectionChanged.addEventListener(onChanged);

当运行示例的时候,控制台输出了65条消息。每调用一次 viewer.entities.add就会有一条消息 ( removedchanged在这里没有提示,因为我们这个项目里只有add)。为了更新可视化效果,Cesium内部实际也订阅了这个事件。当一次性更新的数量过多的时候,先一个个更新,最后统一发消息效率更高。因为Cesium只处理了一遍变化消息,所以这个对性能有提升。 在修改之前,我们先调用 viewer.entities.suspendEvents,修改完之后再调用 viewer.entities.resumeEvents.

我们试下这个。在第一次调用 viewer.entities.add 前添加一个suspend调用,在最后调用一下resume 。再次运行下程序,我们现在只收到一条消息,但是里面包含了65条entity添加记录。 这个函数调用有内部计数,所以多重嵌入调用suspend 和resume没有任何问题。可是,如果忘了调用resume,那么在处理完之后会获取不了任何信息。因为resume只有在对应层次的suspend下才会发出消息(也就是suspend和resume必须是匹配的)。

拾取

拾取,也就是返回特定屏幕坐标(通常是鼠标位置)的对象,这也是这部分唯一需要和Primitive API打交道的功能。这部分未来在讲Cesium的Entity拾取技术功能的时候会再次讨论。 现在我们使用一些低层次的方法 scene.pickscene.drillPick 。下面代码是拾取部分的一个基本实现,基本上可以直接在项目里使用 。

/**
 * 返回对应窗口位置最上面一个Entity 如果该位置没有对象那么返回undefined
 * @param {Cartesian2} windowPosition 窗口坐标
 * @returns {Entity} 返回值
 */
function pickEntity(viewer, windowPosition) {
  var picked = viewer.scene.pick(windowPosition);
  if (defined(picked)) {
    var id = Cesium.defaultValue(picked.id, picked.primitive.id);
    if (id instanceof Cesium.Entity) {
      return id;
    }
  }
  return undefined;
};

/**
 *返回对应窗口位置所有Entity的列表 如果该位置没有对象那么返回undefined
 * 返回值按可视化顺序从前到后存储在数组里
 *
 * @param {Cartesian2} windowPosition 窗口位置
 * @returns {Entity[]}  
 */
function drillPickEntities(viewer, windowPosition) {
  var i;
  var entity;
  var picked;
  var pickedPrimitives = viewer.scene.drillPick(windowPosition);
  var length = pickedPrimitives.length;
  var result = [];
  var 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;
};

来解释下。 场景的拾取函数返回的是图元信息而不是entity对象,但是Entity API的结构限定每一个图元会对应到一个entity实体上,通过他们的 id 属性来区分。所以我们只需要检测拾取的对象id是否是一个 Entity. 这些函数是不重要的(trivial),它还没有被当作Cesium的正式部分,我们有一些更加稳定的函数计划(more robust functionality planned) 。

点(Points),公告牌( Billboards), 标注(Labels)

别考虑面和体了,我们来学下在Cesium上如何展示POI点。 创建一个点或者标注非常简单,只需要设置entity 的 position 属性,以及point 或者label 可视化对象。比如,我想在我最喜欢的球队主场放一个点。

var viewer = new Cesium.Viewer('cesiumContainer');

var 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);

点和标注

上面的示例里,我们精确指定了公告牌的宽度和高度,但其实是不需要的,如果没有指定,那么将使用图片的高度和宽度。
标注和公告板有大量的选项,我们就不深入讲解了。具体可以查看Sandcastle里的对应示例: 标注, 公告板

三维模型

Cesium通过 glTF格式支持三维模型,glTF是 WebGL, OpenGL ES, and OpenGL的实时载入模型(the runtime asset format)。Cesium包含了一些可以使用的glTF模型 : 带螺旋桨动画的飞机,带轮子动画的汽车模型,带行走动画的人物模型。在Sandcastle 示例里可以看到他们 三维模型
加载三维模型和前面其他的可视数据区别不大。只需要entity带position属性和一个指向glTF模型资源的uri路径。

var viewer = new Cesium.Viewer('cesiumContainer');
var entity = viewer.entities.add({
    position : Cesium.Cartesian3.fromDegrees(-123.0744619, 44.0503706),
    model : {
        uri : '../../../../Apps/SampleData/models/GroundVehicle/GroundVehicle.glb'
    }
});
viewer.trackedEntity = entity;

卡车模型

你可以配置一个 scale 属性,它将等比例缩放模型。也可以配置一个 minimumPixelSize 属性,它保证距离模型很远的时候,模型不会小于设定的大小。
默认,模型向右朝向东方。可以通过 Entity.orientation 的属性设定一个 四元数Quaternion。这个比前面只用位置的示例更麻烦一些,让我们设定一下模型的 heading, pitch, roll。把下面代码拷贝到 Sandcastle,修改一下值 可以查看具体的效果。

var viewer = new Cesium.Viewer('cesiumContainer');
var position = Cesium.Cartesian3.fromDegrees(-123.0744619, 44.0503706);
var heading = Cesium.Math.toRadians(45.0);
var pitch = Cesium.Math.toRadians(15.0);
var roll = Cesium.Math.toRadians(0.0);
var orientation = Cesium.Transforms.headingPitchRollQuaternion(position, new Cesium.HeadingPitchRoll(heading, pitch, roll));

var entity = viewer.entities.add({
    position : position,
    orientation : orientation,
    model : {
        uri : '../../../../Apps/SampleData/models/GroundVehicle/GroundVehicle.glb'
    }
});
viewer.trackedEntity = entity;

因此模型需要转为glTF格式才能在Cesium中使用。我们提供了一个 在线的转换工具 ,你可以上传COLLADA (dae)模型就会下载到glTF格式的。
当前Entity API还不支持模型的高级使用场景,比如模型节点的拾取或者动画控制,不过可以使用Primitive API 实现。我们有一个单独的教程来实现这些功能 三维模型高级教程 。未来我们肯定会增强 Entity API包含这些功能。这个高级教程包含了如何在Cesium下调试模型显示的异常效果,所以一定要去学习它。如果你设计了自己的模型,一定要去看看我们的 建模人员 glTF 贴士.

属性系统

到目前,我们都是设置了entity的图形对象属性,还没有实际读取过属性。事实上,我们可能会对返回的结果感觉惊讶。回想我们第一个多边形示例里,我们把outline属性设置为 true 。直觉告诉我们,如果我们用日志输出(console.log)获取wyoming.polygon.outline的类型,将输出 boolean

console.log(typeof wyoming.polygon.outline);

可是上述代码的输出实际是 object。因为 outline 不是一个简单的布尔类型,而是一个ConstantProperty类的实例。实时上,这个教程整个使用的一种叫隐形属性转换的简略形式来设置属性,它会自动的使用原始值创建一个对应的 ConstantProperty 类实例。如果没有这种简略形式,我们就不得不去写一个更长的初始化示例代码:

var wyoming = new Cesium.Entity();
wyoming.name = 'Wyoming';

var 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);

为什么 属性是这种形式?原因很简单,整个Entity API的属性设计是不仅仅考虑是一个常量值,而需要设置一些随时间变换的值。
所有的属性类实现 Property 接口, Cesium中定义了很多种属性类。本教程的第二部分将重点关注属性系统,使用它去创建一个时间变化的动态可视化效果。 现在,我们唯一需要知道的是:为了读取属性的值,我们需要调用 getValue函数。所以为了获得多边形的outline属性,应该写类似下面的代码,时间参数传当前场景时间即可。

console.log(wyoming.polygon.outline.getValue(viewer.clock.currentTime));

严格来说,如果我们明确知道正在读取一个 ConstantProperty的值,那么可以不需要传递时间参数。但是明确指定时间参数是个惯例。

接下来干什么

我们勉强学习了Cesium加载空间数据可视化的一点皮毛,但是我们已经解锁了一个巨大的可能性。等待这个教程第二部分的同时,或许可以学习下Cesium对 影像图层 或者 地形和水面的支持。也可以看下所有教程列表 看看有没有感兴趣的。

中国最专业的Cesium开发者社区

推荐阅读更多精彩内容