思路解析
我们很容易发现,标题中所提到的四项:平滑插值、平滑移动、实时轨迹、历史轨迹, 本质上可以理解为某个值随时间发生变化。
- 平滑插值:在0ms时候值为0,在1ms时值为1,在10ms时值为10......利用微分思想,很容易发现只需要随着时间改变值,并且每次改变间隔短到人类难以感知,那么就能认为这是平滑的值。
- 平滑移动:和平滑插值相同,只是把
值
的概念缩小到坐标
。 - 实时轨迹:随着时间推进,进行平滑插值。在上面平滑插值的概念中,我们的思想是
值随着时间连续变化即为平滑
,而在实时轨迹中,只是把时间确定为实时的。 - 历史轨迹:和实时轨迹类似,也是在平滑插值的基础上确定时间,只是这次把时间确定在一个区间内。
也就是说,只需要有这样一个黑盒,就可以实现上面所有问题:
一个将时间映射到值,并平滑过渡的黑盒。
模型参考:
时间 | 值 | 注释 |
---|---|---|
0 | a0 | 时间没有单位,这里的时间是抽象概念 |
0 + 0.5 | a0 + (a1 - a0) * 0.5 | 插入 a0 和 a1 后,黑盒内部生成了过渡的线性映射 a0.5 |
1 (单位1) | a1 | |
... | ... | |
n-1 (n为整数) | an-1 | |
n-1 + dn (0<dn<1) | an-1 + (an - an-1) * dn | 这一步很关键,意味着黑盒内部根据相邻映射,自动生成过渡的线性映射 |
n | an |
根据上面这个表格所展示的逻辑,你已经能完美实现一个这样的方法。幸运的是,Cesium已经为我们提供了现成的方法:SampledProperty
SampledProperty
由于 SampledProperty
是 Property
的一种,意味着你能在 Entity
的大部分属性中使用他。
官方示例
过一下官方示例:
//Create a linearly interpolated Cartesian2
const property = new Cesium.SampledProperty(Cesium.Cartesian2);
//Populate it with data
property.addSample(Cesium.JulianDate.fromIso8601('2012-08-01T00:00:00.00Z'), new Cesium.Cartesian2(0, 0));
property.addSample(Cesium.JulianDate.fromIso8601('2012-08-02T00:00:00.00Z'), new Cesium.Cartesian2(4, 7));
//Retrieve an interpolated value
const result = property.getValue(Cesium.JulianDate.fromIso8601('2012-08-01T12:00:00.00Z'));
上文举了个时间映射到Cartesian2
的例子,接下来我把他翻译为人话:
第二行 new Cesium.SampledProperty
在示例中,进行实例化时传入了Cesium.Cartesian2
,这里传入了一个构造函数。我们很容易联想到在Vue中,我们也是这么做的:
defineProps({
foo: Number,
bar: String,
})
我们如果想将时间映射到数字,只需要 :
const property = new Cesium.SampledProperty(Number)
第五、六行 property.addSample
其传入的第一个参数为 JulianDate 用于代表时间, 第二个参数为实例化时构造函数所构造的值。也就是我所说的时间映射到值。我们如果想要将时间映射到数字,只需要 :
property.addSample(Cesium.JulianDate.fromDate(new Date('1995-12-17T03:24:00')), 0)
property.addSample(Cesium.JulianDate.fromDate(new Date('2024-12-17T03:24:00')), 10000)
第九行 property.getValue
这个函数传入一个 JulianDate
,返回这个时间对应的值。
实践 - 平滑转动
利用上面内容,我们可以为 Entity
添加一个平滑的 orientation。首先找到其构造函数,在这里可以看到 Quaternion - Cesium Documentation。
假设我们有这样一份数据,表示某时间戳的航向角度和位置:
[
{ time: 1000000000, heading: 0, lon:100, lat: 20 },
{ time: 2000000000, heading: 90, lon:100, lat: 20 },
{ time: 3000000000, heading: 180, lon:100, lat: 20 },
]
可以像这样实现一个实体平滑转动的效果:
const orientation = new Cesium.SampledProperty(Cesium.Quaternion)
// DATA是上面的数据
DATA.forEach(({ time, heading, lon, lat }) => {
orientation.addSample(
Cesium.JulianDate.fromDate(new Date(time)),
Cesium.Transforms.headingPitchRollQuaternion(
Cesium.Cartesian3.fromDegrees(lon, lat),
new Cesium.HeadingPitchRoll(
Cesium.Math.toRadians(heading),
Cesium.Math.toRadians(0),
Cesium.Math.toRadians(0),
),
),
)
})
viewer.entities.add({
// 其实你可以直接把SampledProperty赋值给entity
orientation,
// 也可以利用CallbackProperty
// orientation: new Cesium.CallbackProperty(time => orientation.getValue(time), false)
})
Cesium.SampledPositionProperty
文档 SampledPositionProperty - Cesium Documentation
Cesium公开了一个专门用于位置的SampledProperty
,实际上这个用法和上面相同,只是仅用于Cartesian3作为值的映射。这里就写一个示例,没有必要细说了。
const property = new Cesium.SampledPositionProperty();
property.addSample(
Cesium.JulianDate.fromIso8601('2012-08-01T00:00:00.00Z'),
new Cesium.Cartesian3(0, 0, 0)
);
property.addSample(
Cesium.JulianDate.fromIso8601('2012-08-02T00:00:00.00Z'),
new Cesium.Cartesian3(10000, 20000, 30000)
);
// 其实你可以直接把他赋给entity
viewer.entities.add({
position: property
})
const result = property.getValue(Cesium.JulianDate.fromIso8601('2012-08-01T12:00:00.00Z'));
实战
这里使用到了 cesium-use 这个vue库的 useTimeline - CesiumUse 方法,用于方便控制时间。
假设我们会从WebSocket中获取这样的数据,表示某时间戳的航向角度和位置:
{
id: '这是id!',
time: 1000000000,
heading: 0,
lon:100,
lat: 20
},
这段代码制作了一个实时定位的平滑移动,细节解释请查看注释:
import * as Cesium from 'cesium'
import { useTimeline, toCartesian3, useViewer } from 'cesium-use'
import { useWebSocket } from '@vueuse/core'
const {
play,
// ...
} = useTimeline()
play()
const map = new Map<string, Cesium.Entity>()
const { data: _data } = useWebSocket('ws://呀哈哈,你发现我了!')
watchEffect(() => {
const { id, time, heading, lon, lat } = JSON.parse(_data.value)
if(!id)
return
const e = map.get(id) ?? map.set(id, createEntity()).get(id)
const t = Cesium.JulianDate.fromDate(new Date(time))
const coordinate = toCartesian3(lon, lat))
e.position.addSample(t, coordinate)
e.orientation.addSample(
t,
Cesium.Transforms.headingPitchRollQuaternion(
coordinate,
new Cesium.HeadingPitchRoll(Cesium.Math.toRadians(heading), 0, 0)
)
)
})
const viewer = useViewer()
function createEntity() {
const position = new Cesium.SampledPositionProperty()
const orientation = new Cesium.SampledProperty(Cesium.Quaternion)
return viewer.entities.add({
position,
orientation
})
}
就是这么简单。也可以直接看 useTimeline - CesiumUse 的demo,可以看到demo源码中也使用了类似的实现。
对于历史轨迹的情况,一般来说你可以隐藏实时定位的entity。
import { useViewer, syncEntityCollection } from 'cesium-use'
const viewer = useViewer()
const collection = syncEntityCollection(viewer.entities)
// ...
function createEntity() {
const position = new Cesium.SampledPositionProperty()
const orientation = new Cesium.SampledProperty(Cesium.Quaternion)
return collection.add({
position,
orientation
})
}
// 在这里!
function toggleShow() {
collection.show = !collection.show
}
如果需要两者同时存在,只需要创建一个独立于viewer.clock的时间即可:
import * as Cesium from 'cesium'
import { useTimeline, useEventHandler, useViewer } from 'cesium-use'
const viewer = useViewer()
const clock = new Cesium.Clock()
// Cesium有一个坑点在于,对于新建的Clock,你必须手动调用tick()方法,否则时间是无法流动的
// 这里在preUpdate时调用
const onPreUpdate = useEventHandler(viewer.scene.preUpdate)
onPreUpdate(() => clock.tick())
// 然后正常使用
const { play } = useTimeline(clock)
// ...其他一样