Cesium使用czml来实现动态路线

需求场景

 比如项目中的一些场景漫游,在某个地区显示指定路线,某个物体像车辆一样的移动;有些项目涉及到巡逻路线,多个人员的巡逻路线在地图上的显示。这些需求情况都是需要用到动态路线的,在我之前的日常问题文章里面提到了这种,但还是单独拿出来比较好。

参考

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文件第一行都会声明<!DOCTYPE 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 <Major>.<Minor> 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;跨度很大的两个点,不是单单靠设置高度就能解决的,有时候要有取舍。

猜你喜欢

转载自blog.csdn.net/GhostPaints/article/details/125818789