本文内容主要是因为最近在学习 Cesium 的相关内容,其中有一个比较重要的概念那就是 CZML,所以就抽了点时间借住 官网 当中提供的介绍内容简单的梳理一下,方便自己更好的理解
什么是 CZML
CZML 是一种用来描述动态场景的 JSON 架构的语言,主要用于 Cesium 在浏览器中的展示,它可以用来描述点、线、布告板、模型以及其他的图元,同时定义他们是怎样随时间变化的,Cesium 拥有一套富客户端 API,通过 CZML 采用数据驱动的方式,不用写代码我们就可以使用通用的 Cesium Viewer 构建出丰富的场景
Cesium 与CZML 的关系就如同 Google Earth 和 KML 的关系,CZML 和 KML 都是用来描述场景的数据格式,可以通到很多其他的程序自动生成,但是还是有一些区别于 KML 的
CZML是基于JSON的CZML可以准确的描述值随时间变化的属性CZML通过增量流的方式传送到客户端,在场景显示之前,整个CZML文档需要首先被下载到客户端CZML高度优化,旨在解析时更紧凑也更容易,让人工的读写更容易CZML可扩展,尽管CZML的主要作用在与虚拟地球客户端程序与场景的交流,但它可以很容易的通过扩展来满足其他一些辅助的程序对静态或动态数据的需求CZML是一个开放的格式,另外可以通过 czml-writer 来生成CZML
我们将CZML 标准以及它的相应实现分为四个部分
CZML Structure,CZML文档的整体结构CZML Content,表示内容CZML in Cesium,Cesium中解析和显示CZML的流程CZML Architecture,这部分内容可以参考文档当中的 Architecture
这里我们主要了解一下 CZML Structure 的相关内容,也就是 CZML 文档的整体结构,关于其他相关内容可以参考官方文档当中的 CZML Content 和 CZML Guide 等
CZML Structure
CZML 是 JSON 的一个子集,也就是说一个有效的 CZML 文档同时也是一个有效的 JSON 文档,一个 CZML 文档当中包含了一个 JSON 数组,数组中个每一个对象都是一个 CZML 数据包(packet),而一个 packet 就对应着一个场景中的对象,例如一架飞机
另外需要注意的是,在下面的例子中我们使用注释的方式来帮助理解
CZML,但是在实际开发场景当中是不允许这样操作的
1 | [ |
每个 packet 都有一个 id 属性用来标示我们当前描述的对象,id 在同一个 CZML 以及与它载入同一个范围(scope)内的其他 CZML 文件中必须是唯一的,因为假如没有指定 id,那么客户端将自动生成一个唯一的 id,但是这样的话在随后的包中我们就没有办法引用它了,例如我们想给这个对象添加更多的数据
除了 id 以外,一个包通常还包含零到多个(正常情况下是 1 到多个)定义对象图形特征的属性,正如上面的例子,我们定义了一个 GroundControlStation 对象,它拥有一个固定的 WGS84 坐标 [-75.5, 40.0, 0.0](它表示经度 -75.0,维度 40.0,高度为 0),并在这个位置显示一个蓝色的点
CZML 还有很多标准的属性,包括用来添加点、布告板、模型、线以及其他图形到场景的属性,所有这些属性都可以在 CZML Content 当中找到,在这里我们主要讨论这些数据是怎样组织的,例如我们怎样定义一个属性,使它在两个不同的时间拥有两个不同的值,也就是在 CZML 当中比较特殊的跟时间序列相关的属性,也就是时间间隔 Intervals
时间间隔(Intervals)
通常情况下 CZML 的属性值是一个数组,数组中的每个元素对应每一个不同的时间,属性的值,比如下面这个
1 | { |
这里我们定义了一个 someProperty 属性,它有两个时间间隔,第一个是从中午到下午一点,属性值为 5,第二个是从下午一点到下午两点,属性值为 6,在时间由第一个间隔变化到第二个间隔的时候,属性值会瞬间从 5 变到 6
但是这里需要注意的是,这里的时间为
UTC(协调世界时),而我们国家通常使用的是北京时间(UTC+08:00)
我们使用 number 来表示属性是一个数字类型的属性,值得注意的是有些属性允许通过不同的格式来定义,例如表示位置的属性可以通过笛卡儿坐标的 x、y、z 表示,也可以通过经纬度和高程来表示
Interval 属性是可选的,你可以定义也可以不定义,如果没有定义,则默认为整个时间,通常定义多个无限的时间间隔或者间隔之间有重叠是没什么意义的,如果非要这么做,那么在 CZML 中最后定义的时间间隔将拥有较高的优先级,通常情况下属性值只跨越一个时间间隔,这时候你间隔列表可以省略
1 | { |
而对于一些比较简单的值,例如上面我们使用的数字类型,加入它对于整个时间来说都不变,那么我们还可以定义的更简单一些
1 | { |
这种简略的表示法,适用于所有值是简单的 JSON 数据类型(string、number、boolean)的的属性
复合值(Composite Values)
对于一些复杂的复合值,例如笛卡尔坐标位置或颜色,是通过 JSON 数组的形式来表示,例如坐标位置,所使用的数组有三个元素,分别对应于坐标的 x、y 和 z
1 | { |
合成值必须定义在间隔中,即使它的时间间隔是无限的也是一样,不能采用简略的写法,假如允许值 [1.0, 2.0, 3.0] 直接作为 someComplexProperty 的属性值,那么客户端代码就需要解析 CZML 并且判断数组里面的值到底是时间间隔列表(intervals list)还是说是简单的值,所以为了简单起见,通常不建议使用这样的写法
属性值采样(Sampled Property Values)
到目前为止我们讨论了怎样为横跨整个时间的的属性定义单一的值,以及针对离散的时间间隔定义不同的值,而一些属性则允许我们定义时间戳采样,客户端通过给定的时间差值计算出属性的值(时间的定义是采用 ISO8601 标准字符串)
1 | { |
在这里我们指定在中午的时候属性值为 [1.0, 2.0, 3.0],一分钟后为 [4.0, 5.0, 6.0],再过一分钟后变为 [7.0, 8.0, 9.0],假如客户端当前的时间是 12 点 0 分 30 秒,也就是离开始时间过去了 30 秒,那当前的属性值将通过在 [1.0, 2.0, 3.0] 和 [4.0, 5.0, 6.0] 线性差值得出,为 [2.5, 3.5, 4.5]
简单起见,时间使用距离起始时间(epoch)的秒数来表示,与每个时间都通过 ISO8601 字符串表示相比这样不是很精确,不过对于采样间隔在一天以内,或者偏移值时整数的时候这样的精度还是绰绰有余
1 | { |
最后,使用时间戳采样的属性还有一些附加的可选的子属性,用来控制采样的方式
1 | { |
interpolationAlgorithm 定义了采样使用的算法,它定义了用来插值所使用的多项式的次数,1 表示线性差值,2 表示二次插值法,默认为 1,每个采样值的时间没有必要落在包含它的时间间隔内,但是这些采样值在他们的时间间隔内不会被使用,这样的好处就是对于多次差值有更好的精度,下表总结了可插值属性的一些子属性
| 名称 | Scope 范围 |
JSON 类型 |
说明 |
|---|---|---|---|
epoch |
Packet |
string |
使用 ISO8601 规范来表示日期和时间 |
nextTime |
Packet |
string or number |
在时间间隔内下一个采样的时间,可以通过 ISO8061 方式,也可以通过与 epoch 秒数来定义,它决定了不同 packet 之间的采样是否有停顿 |
previousTime |
Packet |
String or number |
在时间间隔内前一个采样的时间,可以通过 ISO8061 方式,也可以通过与 epoch 秒数来定义,它决定了不同 packet 之间的采样是否有停顿 |
InterpolationAlgorithm |
Interval |
String |
用于插值的算法,有 LAGTANGE,HERMITE和GEODESIC,默认是 LAGRANGE,如果位置不在该采样区间,那么这个属性值会被忽略 |
interpolationDegree |
Interval |
Number |
定义了用来插值所使用的多项式的次数,1 表示线性差值,2 表示二次插值法,默认为 1,如果使用 GEODESIC 插值算法,那么这个属性将被忽略 |
事件源(EventSource and Streaming)
如果将整个 CZML 文件安排在一个大的 JSON 数组中,这使增量加载变得很困难,虽然浏览器允许我们访问没有读取完的流数据,但是解析不完整的数据需要漫长而繁琐的字符串操作
为了高效,CZML 使用浏览器的 EventSource API 来处理流数据,在实际操作中,每一个 CZML 的 packet 包会被作为单独的一个事件传输到客户端
当浏览器接收到一个 packet 后就会发出一个事件,事件中会包含刚刚接收到了数据,这样我们就可以通过增量的方式高效的处理 CZML 数据,目前为止我们都是使用一个 packet 包来描述一个对象,这个 packet 包含了所有这个对象的图形属性,但是我们还可以使用其他的方式,例如一个 CZML 文件或流可以包含多个 packet,每个 packet 都有相同的 id,分别描述同一个对象的不同方面的属性
事实上,在大多数情况下我们使用两个 packet 来描述一个对象,当对象属性跨越多个时间间隔,或者一个时间间隔有很多个时间戳采样时,这样做就很有用了,通过将一个属性定义打包进多个 packet,我们可以使数据更快的传输到 Cesium 中,减少用户等待的时间,当客户端接收到一个 packet,它会遍历 packet 中的每一个属性,对于每个属性,它会遍历属性定义的每个时间间隔,对于每个时间间隔,它会判断这个时间间隔是否已经定义,假如这个间隔已经定义,将更新已经存在的间隔,如果没有定义,那么就根据这个间隔创建一个新的
当更新一个已存在的时间间隔时,假如有子属性,那么子属性将覆盖原有的值,有一个例外,就是当已有的属性和新接收到的属性都包含时间戳采样时,新接收到的采样不会覆盖已有的,而是加到已有的采样列表中,当新的时间间隔与已有的发生重叠时,新的间隔拥有较高优先级,原有的间隔将被截断或者整个移除,这点必须要牢记,在同一个 packet 中的时间间隔的时间必须以增序排列,不同 packet 之间就没有要求,但是对于不连续的采样还是应该考虑合理的插值顺序
但是如果我们有一个需要插值的属性,时间是 0 到 10 秒,间隔为 1.0 秒,第一个 packet 包含 0 到 3 秒,第二个包含 8 到 10 秒,在客户端还没有接收到包含 4 到 7 秒的 packet 时,我们可以渲染时间为 5 的场景吗?
一种方式是我们就是使用已经接收到的两个 packet 来插值,这可能不太好,因为即使我们使用高次插值,在这个两个 packet 的间隙中得出的值可能也是错的,所以我们最好还是先暂停等待中间的那个 packet 赶快到来,但是我们是怎么知道两个包之间有间隙的呢?针对于这种情况, CZML 提供了 previousTime 和 nextTime 子属性,用来处理这种情况
1 | { |
它的作用是告诉客户端 3.0 后下一个时间是 4.0,就像我们上面举的那个例子,3 的后面是 8,根据 nextTime 我们就知道 3 和 8 之间肯定还有一段数据没有接收到,所以在开始插值之前我们就需要先等待数据读取完成
没有必要同时设置 previousTime 和 nextTime,在不同的情况下选择使用其中最方便的一个就可以了,只要定义其中的一个,在进行插值前 Cesium 就会首先对数据进行完整性检查
Availability 属性
除了 id 属性外,CZML 的 packet 还有一个特别的额外属性 availability
1 | { |
它用来标示一个对象的数据在什么时候是可用的,假如一个对象在当前的动画时间内是可用,但是客户端现在还没有获取到相应的数据(可能在下一 packet 里面,但现在还没有获取到),那么 Cesium 就会先暂停,直到获取到数据为止,这个属性的值可以一个字符串表示的一个时间段,也可以是一个字符串数组表示的多个时间段
假如 availability 变化了或者被发现是不正确的,那么随后的 packet 将会更新它的值,例如,一个 SGP4 propagator 可能总是可用的,但是随后他发出了一个异常,所以他的值需要调整,如果 availability 属性没有定义,那么默认是全部时间内都可用的, Availability 的范围被限定到一个特定的 CZML 流中,所以对同一个对象在两个不同的流中可以有不同的 availability,在一个流中,只有定义在最后的那个 availability 起作用,其他的都会被忽略,在某一时刻,如果一个对象是可用的,那么这个对象至少要有一个可用的属性并且在此时间段内需要的属性都要有定义(也就是获取到了数据),不然 Cesium 就会等待数据直到接收到数据为止
扩展 Extending CZML
另外我们还可以给 CZML 增加自定义属性,但是为避免冲突,我们强烈建议给自定义属性加上特有的前缀