比如项目中的一些场景漫游,在某个地区显示指定路线,某个物体像车辆一样的移动;有些项目涉及到巡逻路线,多个人员的巡逻路线在地图上的显示。这些需求情况都是需要用到动态路线的,在我之前的日常问题文章里面提到了这种,但还是单独拿出来比较好。
cesium在github里面的wiki有详细介绍czml的结构,以及各属性可以设置的值。
https://github.com/AnalyticalGraphicsInc/czml-writer/wiki/CZML-Structure
我在处理问题的时候,也在csdn上找到了一个博主介绍czml的使用。这是博文链接
cesium-CZML鸟模型飞行轨迹
当时我正在困扰,如何在czml对象里面设置 orientation的velocityReference值,cesium的wiki只说这个值应该是string类型,但没有用例。然后就找到了上面那篇文章。给了我很大帮助。
个人认为cesium好用的地方在于,它的展现维度除了地理位置外,还有就是时空间的概念。在某一个时间段内,某区域或者entity对象所处的位置展示。
理解了这一点,就会明白czml,它就是用来展示label或者billboard或者model在某一个时间段的位置关系。czml文件本身就是一个数组,数组里面的每一个子项都是一个对象,有一个硬性要求是这个数组的第一个对象是一个申明对象,就像h5的html文件第一行都会声明。
从第二个对象开始,就是将要显示的entity了,在官方wiki里面有解释,我就不重复了。就解释一下概念和一些注意的地方。
首先是id,如果你后续需要拿到这些entity,那么你就需要为每一个对象设置id值,这个id值会变成生成后的entity的id。如果你用的id重复了,那么CzmlDataSource会认为,你是在描述同一个entity对象。如果这些重复id的对象,拥有同样的时间段,那么会以最后那个为准。
这就是为什么,有些情况下,设置了四五个但地图上只显示一个的问题。我在绘制同一个时间段多条航海运输路线的时候遇到的问题。就是因为czml里面的id重复了。
和用代码设置entity不同,czml是属于数组,意味着里面的数据都是key-value的形式。对象名,值可能是字符串,数字,布尔类型或者数组。当你不确定你要使用的属性应该设置什么类型的值时候,最好是去上面的wiki里面看看。
使用js代码设置entity属性,这些值都可以被看作是property。如果使用czml来设置,那么这些都会被看作是等待打包的Packet。
CzmlDataSource里面内置了许多方法来将这些值使用js方法来封装成属性,比如postion来说,它的值为数组,会有不同的方法去判断该怎么解析。因为这个数组可能就是简单的地理位置[x,y,height],也可能包含了时间属性[time,x,y,height]。
下面是源码5110行-5140行的代码
CzmlDataSource._processCzml = function (
czml,
entityCollection,
sourceUri,
updaterFunctions,
dataSource
) {
updaterFunctions = defaultValue(updaterFunctions, CzmlDataSource.updaters);
if (Array.isArray(czml)) {
for (let i = 0, len = czml.length; i < len; ++i) {
processCzmlPacket(
czml[i],
entityCollection,
updaterFunctions,
sourceUri,
dataSource
);
}
} else {
processCzmlPacket(
czml,
entityCollection,
updaterFunctions,
sourceUri,
dataSource
);
}
};
这个updaterFunctions其实就是数据处理方式,在源码中,它实际上是这些。下面代码在源码5006行-5130行
CzmlDataSource.updaters = [
processBillboard, //
processBox, //
processCorridor, //
processCylinder, //
processEllipse, //
processEllipsoid, //
processLabel, //
processModel, //
processName, //
processDescription, //
processPath, //
processPoint, //
processPolygon, //
processPolyline, //
processPolylineVolume, //
processProperties, //
processRectangle, //
processPosition, //
processTileset, //
processViewFrom, //
processWall, //
processOrientation, //
processAvailability,
];
这些涵盖了几乎所有可以设置的值。以process开头,后面就是它对应处理的东西。
那么既然czml被视作一个数组,第一个对象要按特定值设定。源码里面是怎么识别的呢。
function processCzmlPacket(
packet,
entityCollection,
updaterFunctions,
sourceUri,
dataSource
) {
let objectId = packet.id;
if (!defined(objectId)) {
objectId = createGuid();
}
currentId = objectId;
if (!defined(dataSource._version) && objectId !== "document") {
throw new RuntimeError(
"The first CZML packet is required to be the document object."
);
}
if (packet["delete"] === true) {
entityCollection.removeById(objectId);
} else if (objectId === "document") {
processDocument(packet, dataSource);
} else {
const entity = entityCollection.getOrCreateEntity(objectId);
const parentId = packet.parent;
if (defined(parentId)) {
entity.parent = entityCollection.getOrCreateEntity(parentId);
}
for (let i = updaterFunctions.length - 1; i > -1; i--) {
updaterFunctions[i](entity, packet, entityCollection, sourceUri);
}
}
currentId = undefined;
}
上面代码就是处理czml数组的时候,执行的方法。可以看到,初始化一个dataSource的时候,并没有设置版本version,如果你的czml数组第一个值没有document对象,就直接报错了。正确设置了之后,第一次执行会进入processDocument方法里面。它在源码的2634行-2682行。
function processDocument(packet, dataSource) {
const version = packet.version;
if (defined(version)) {
if (typeof version === "string") {
const tokens = version.split(".");
if (tokens.length === 2) {
if (tokens[0] !== "1") {
throw new RuntimeError("Cesium only supports CZML version 1.");
}
dataSource._version = version;
}
}
}
if (!defined(dataSource._version)) {
throw new RuntimeError(
"CZML version information invalid. It is expected to be a property on the document object in the . version format."
);
}
const documentPacket = dataSource._documentPacket;
if (defined(packet.name)) {
documentPacket.name = packet.name;
}
const clockPacket = packet.clock;
if (defined(clockPacket)) {
const clock = documentPacket.clock;
if (!defined(clock)) {
documentPacket.clock = {
interval: clockPacket.interval,
currentTime: clockPacket.currentTime,
range: clockPacket.range,
step: clockPacket.step,
multiplier: clockPacket.multiplier,
};
} else {
clock.interval = defaultValue(clockPacket.interval, clock.interval);
clock.currentTime = defaultValue(
clockPacket.currentTime,
clock.currentTime
);
clock.range = defaultValue(clockPacket.range, clock.range);
clock.step = defaultValue(clockPacket.step, clock.step);
clock.multiplier = defaultValue(clockPacket.multiplier, clock.multiplier);
}
}
}
很简单粗暴,直接检查string类型,并检查长度,version必须是1开头,不管你后面多少,只要字符串按小数点分割的时候,能分隔出两个值就行。很粗暴。后面就是设置时间clock了。
如果是从czml文件中读取,那么直接使用Cesium.CzmlDataSource.load方法就好了。但是有些时候,是根据某些勾选条件,或者时间设置,想要手动来设置一个czml数组来加载。可以考虑按下面的思路来:
1.创建一个数组entityArr来存储手动创建的对象,这个数组里面每一个子项都是对象。
2.创建一个object对象,设置version,clock时间信息。将该对象添加到数组entityArr的头部。
3.使用Cesium.CzmlDataSource.load(entityArr)方法加载,该方法返回一个promise,所以可以在then方法中,拿到生成的dataSource并获取对应的entity来再特殊化处理。
很多时候,这个czml在哪个时间段显示,是要通过获取判断来的,既然这样,那第一个对象就没必要首先生成,直接后面再添加就行。在知道所有entity显示的时间段之后,自然就能设置clock在何时结束。
Cesium.CzmlDataSource.load(....).then(function(datasource) { 处理代码 });
像一些自定义的材质,就需要在加载完成后再手动添加,datasource.entities.values里面有所有czml数组里面定义的entity,当然也可以通过设置好的id获取到特定的。如果没有给id值,会自动生成id,但是你需要它的时候你就不知道它对应的是哪个了。
availability和clock里面那些奇怪的时间字符串,按官网wiki的解释就是一种Iso8601时间格式。cesium使用的是JulianDate,所以也存在转换。
Cesium.JulianDate.toIso8601()方法接收两个参数,第一个参数是一个JulianDate对象,第二个参数是数字,类似于toFixed方法,保留小数点后几位一样。基本都是传0。它会返回一个字符串。
示例代码
// 将当前时间设置为开始时间
const start = Cesium.JulianDate.fromDate(new Date());
//生成一个czml数组的子项对象
const obj = {
id: `test`,
availability: `${Cesium.JulianDate.toIso8601(start, 0)}/${Cesium.JulianDate.toIso8601(Cesium.JulianDate.addSeconds(
start,
6000,
new Cesium.JulianDate()
), 0)}`,
path: {
width: 8,
leadTime: 10,
trailTime: 1000,
resolution: 5,
zIndex: 3,
},
model: {
gltf: '模型文件路径',
minimumPixelSize: 64
},
orientation: {
interpolationAlgorithm: 'LINEAR',
interpolationDegree: 1,
epoch: `${Cesium.JulianDate.toIso8601(start, 0)}`,
velocityReference: "#position"
},
position: {
epoch: `${Cesium.JulianDate.toIso8601(start, 0)}`,
interpolationDegree: 5,
interpolationAlgorithm: 'HERMITE',
cartographicDegrees: 一个数组值
}
};
// 通过某些计算,得知时间将在某个值结束
const end = 通过代码设置的值;
// czml数组首个对象设置
let czml = [
{
id: "document",
name: "duangongfenxi",
version: "1.0",
clock: {
interval: `${Cesium.JulianDate.toIso8601(start, 0)}/${Cesium.JulianDate.toIso8601(end, 0)}`,
currentTime: `${Cesium.JulianDate.toIso8601(start, 0)}`,
multiplier: 10,
range: 'CLAMPED'
}
}
];
不建议使用czml的path来制作那种跨度很打的路径,因为会穿地。它不像polyline那样有贴地属性,而且两个坐标点直接太远,设置插值器也没办法解决穿地问题。
如果使用czml后,发现只有部分地方可以看到路线,可以尝试在控制台设置成平面模式viewer.scene.model = 2;跨度很大的两个点,不是单单靠设置高度就能解决的,有时候要有取舍。