npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cesium-multi-target-framework

v0.3.14

Published

高性能 Cesium 多目标渲染框架:对象池 + 八叉树 + 分帧调度 + 运动预测 + 三级 LOD 渲染

Readme

cesium-multi-target-framework

高性能 Cesium 多目标渲染框架,面向海量船舶、无人机、车辆、站点等目标的实时展示。暴露高层类 MultiTargetFramework,提供目标数据接入、三级 LOD 渲染、对象池、视口裁剪、运动预测、目标选择/定位、轨迹、站点层、名称牌、右键菜单和 web-event-bus 跨模块调用能力。

当前依赖锁定 Cesium 1.107.2,代码兼容 Cesium 1.107.21.128.0 的常见版本敏感 API。详细架构、调参和排障记录见 项目说明书,完整导出说明见 对外接口说明

如何使用

最小接入流程:

  1. 安装包和 Cesium。
  2. 把业务图标、模型放到宿主项目静态资源目录,例如 /icons/model
  3. 创建 Cesium.Viewer
  4. targetTypes 声明每类目标的 type、图标、颜色、模型、运动策略。
  5. 调用 setTargetData 写入全量目标,后续用 upsertTargetData / removeTargetData 做增量。
  6. 按需使用 scene.on(...)tracksSiteLayerweb-event-bus 接入交互。
import {
  Viewer,
  MultiTargetFramework,
  TARGET_DOMAINS
} from 'cesium-multi-target-framework'
import 'cesium/Build/Cesium/Widgets/widgets.css'

const viewer = new Viewer('cesiumContainer', {
  animation: false,
  timeline: false
})

const framework = new MultiTargetFramework(viewer, {
  visualIcons: [
    {
      key: 'ais',
      iconPath: '/icons/target-ais-mask.png',
      pointHeadingOffset: 180
    }
  ],
  targetTypes: [
    {
      type: 'ship-other',
      name: '其他船舶',
      targetDomain: TARGET_DOMAINS.SURFACE,
      iconPath: '/icons/target-ship-mask.png',
      defaultColor: '#25e65c',
      lowModel: { url: '/model/其他船舶.gltf', scale: 100, headingOffset: -90 },
      highModel: { url: '/model/其他船舶.gltf', scale: 100, headingOffset: -90 },
      highModelPoolSize: 30,
      physicalSize: { length: 100, width: 20, height: 20 },
      smoothMove: true,
      predictMove: false
    },
    {
      type: 'air-uav',
      name: '无人机',
      targetDomain: TARGET_DOMAINS.AIR,
      iconPath: '/icons/mtf/uav-radar.png',
      defaultColor: '#00FF00',
      lowModel: { url: '/model/mtf/uav-low.gltf', scale: 0.3, headingOffset: -90 },
      highModel: { url: '/model/mtf/uav-high.gltf', scale: 0.3, headingOffset: -90 },
      smoothMove: true,
      predictMove: true,
      deriveMotionFromPosition: true,
      predictRounds: 1,
      predictRoundSeconds: 10
    }
  ],
  scene: {
    data: { commitIntervalMs: 0, leading: true, worker: true },
    merge: { enabled: true, maxVisible: 10000, cameraMergeBaseHeight: 500, byType: true },
    pick: { enabled: true, hover: true },
    bearingLine: {
      enabled: true,
      durationMinutes: 1,
      lineType: 'dashed',
      width: 2,
      color: '#35d7ff',
      colorsByType: {
        'air-uav': '#39ff88',
        'air-bird': '#ffcf33',
        'air-civilian-aircraft': '#4aa3ff'
      },
      opacity: 0.85,
      maxCameraHeight: 12000
    }
  }
})

await framework.ready

framework.setTargetData([
  {
    id: 'ship-001',
    type: 'ship',
    lon: 121.5,
    lat: 31.2,
    height: 0,
    speedH: 8,
    speedV: 0,
    heading: 45,
    updateTime: Date.now(),
    length: 120,
    width: 28,
    physicalHeight: 18
  }
])

// visualKey 只覆盖图标,不改变目标 type、模型和 LOD 逻辑。
framework.setTargetVisualKey('ship-001', 'ais')
framework.setTargetVisualKey('ship-001', null)

framework.upsertTargetData([
  {
    id: 'uav-001',
    type: 'air-uav',
    lon: 121.58,
    lat: 31.25,
    height: 1200,
    speedH: 45,
    speedV: 0,
    heading: 80
  }
])

framework.scene.on('pick', (target) => {
  console.log('pick target', target)
})

目标数据必须满足:

| 字段 | 说明 | | --- | --- | | id | 全局唯一目标 id | | type | 必须命中 targetTypes[].type | | lon / lat / height | WGS84 经纬度和高度 | | speedH / speedV | 水平/垂直速度,单位 m/s | | heading | 航向角,正北为 0,顺时针 | | bowHeading | 可选艏向角;模型、图标和跟随方向优先使用该值,缺失时回退到 heading | | updateTime | 可选,点位获取时间,Unix 毫秒时间戳;对应类型开启 deriveMotionFromPosition 时用于前端本地计算航向和速度 | | length / width / physicalHeight | 可选,目标真实长、宽、高,单位米;不传时使用 targetTypes[].physicalSize 或域级默认尺寸 | | visualKey | 可选,命中 visualIcons[].key 时只覆盖目标图标;不改变 type、高低模和业务分类 |

常用调用:

| 目标 | API | | --- | --- | | 全量替换 | framework.setTargetData(targets) | | 增量新增/更新 | framework.upsertTargetData(targets) | | 删除目标 | framework.removeTargetData(ids) | | 定位目标 | await framework.locateTarget(id, options) | | 设置颜色 | framework.setTargetColor(id, color) | | 开关发光 | framework.setTargetGlow(id, enabled, color?) | | 覆盖图标 | framework.setTargetVisualKey(id, visualKey);传 null 恢复默认图标 | | 显示轨迹 | framework.tracks.setTracks(records)framework.tracks.showTracks(ids) | | 画业务线 | framework.drawLine(options)framework.updateLine(id, options)framework.removeLine(id) | | 画航道 | framework.createChannel(options)framework.updateChannel(id, options)framework.removeChannel(id) | | 画图片 | framework.drawImage(options)framework.updateImage(id, options)framework.removeImage(id) | | 航向指示器 | framework.showHeadingIndicator(targetId, options)framework.hideHeadingIndicator(targetId)framework.clearHeadingIndicators() | | 画图形 | framework.drawShape(options)framework.startDrawingOverlay(options)framework.editShape(id)framework.clearShapes() | | 通用模型 | framework.modelManager.addModel(options)framework.modelManager.updateModel(id, options)framework.modelManager.removeModel(id) | | 站点层 | new SiteLayer(viewer, options) |

手动画线示例:

线宽规则:

  • lineType: 'dashed' 使用 WebGL/Cesium PolylineCollection,适合虚线、轻量叠加线。浏览器 WebGL 线段宽度通常只支持 1px,很多环境会把更大的线宽降级为 1px。
  • lineType: 'solid'width 不填或 width <= 1 时,也使用 WebGL 线,线宽是屏幕像素。
  • lineType: 'solid'width > 1 时,width 表示实际米数,框架使用地理几何宽线绘制。相机拉远时屏幕上会自然变细,适合河道、管控区边界等需要真实宽度的业务线。
  • 若需要标准“宽实线 + 中心虚线”的航道样式,优先使用 createChannel,见下方航道示例。
const lineId = framework.drawLine({
  // 可主动指定 id;不传时框架生成一个 id 并作为返回值
  id: 'river-main-line',
  points: [
    { lon: 121.5, lat: 31.2, height: 0 },
    { lon: 121.58, lat: 31.26, height: 0 }
  ],
  // solid + width > 1 表示实际宽度:120 米
  width: 120,
  color: '#ffdd55',
  opacity: 0.9,
  lineType: 'solid'
})

framework.updateLine(lineId, {
  points: [
    { lon: 121.5, lat: 31.2, height: 0 },
    { lon: 121.62, lat: 31.3, height: 0 }
  ],
  // dashed 仍是 WebGL 虚线,宽线能力受浏览器限制
  width: 1,
  lineType: 'dashed'
})

// 按 id 删除一条线
framework.removeLine(lineId)
// 删除所有业务线
framework.clearLines()

手动画图片示例:

const imageId = framework.drawImage({
  // 可主动指定 id;不传时框架生成一个 id 并作为返回值
  id: 'ship-follow-image',
  imagePath: '/icons/mtf/aircraft.png',
  coordinate: { lon: 121.5, lat: 31.2 },
  // 图片渲染高度,单位米;不传默认 0
  height: 120,
  // 可在最终高度上额外抬高;跟随目标且不传 height 时适合用来避免贴地
  heightOffset: 1,
  followTarget: true,
  targetId: 'ship_001',
  rotateWithTarget: true, // 是否随目标航向旋转;true 时图片会跟着 targetId 的 heading 一起转
  // 默认图片上方为 0 度;这里额外顺时针旋转 15 度
  initialRotation: 15,
  // 未配置米制宽高时,按原图像素换算成默认米制尺寸
  scale: 0.5,
  // 需要透视大小时打开米制尺寸;镜头拉远会变小、拉近会变大
  sizeInMeters: true,
  sizeMeters: 1200
})

framework.drawImage({
  id: 'screen-ratio-image',
  imagePath: '/icons/mtf/ring-scale.svg',
  coordinate: { lon: 121.55, lat: 31.24, height: 0 },
  // 开启透视平面下的屏幕定比;图片宽度约等于屏幕短边的 8%
  scaleByScreen: true,
  maxScreenPercent: 8
})

framework.updateImage(imageId, {
  followTarget: false,
  coordinate: { lon: 121.6, lat: 31.3, height: 0 },
  rotateWithTarget: false // 关闭随目标航向旋转,图片保持 initialRotation 固定朝向
})

// 按 id 删除一张图
framework.removeImage(imageId)
// 删除所有图片覆盖物
framework.clearImages()

内置航向指示器示例:

// 打开目标航向指示器:外圈默认占屏幕短边 70%,内圈自动为外圈的 82%
framework.showHeadingIndicator('ship_001')

// 主动配置外圈定比大小;内圈仍按外圈 * 0.82
framework.showHeadingIndicator('ship_001', {
  maxScreenPercent: 80,
  heightOffset: 1
})

// 按目标关闭
framework.hideHeadingIndicator('ship_001')

// 关闭全部航向指示器
framework.clearHeadingIndicators()

航向指示器说明:

  • 框架内置外圈和内圈两张图,随包分发,不需要业务提供图片路径。
  • 外圈固定正北方向,不随目标旋转。
  • 内圈跟随目标,并随目标航向旋转。
  • maxScreenPercent 表示外圈宽度占屏幕短边百分比;默认 70
  • innerRatio 表示内圈相对外圈的比例;默认 0.82
  • 航向指示器默认跟随目标当前高度,并在目标高度基础上抬高 1m;可通过 heightOffset 调整抬高量。只有显式传入 height 时才使用固定绝对高度。
  • 指示器复用图片覆盖物系统,因此仍是带透视的地图平面图,不是 billboard。

航道示例:

// 创建航道:自动绘制宽实线 + 中心虚线,返回含 id 的航道对象
const channel = framework.createChannel({
  id: 'route-main',
  show: true,
  points: [
    { lon: 110.41, lat: 19.97, height: 0 },
    { lon: 110.38, lat: 20.08 }
  ],
  widthMeters: 50,
  color: '#1677ff',
  dashColor: '#eafcff'
})

// 按 id 更新:可改显示开关、路径点、宽度、颜色等
framework.updateChannel(channel.id, {
  show: false
})

framework.updateChannel(channel.id, {
  show: true,
  points: [
    { lon: 110.41, lat: 19.97 },
    { lon: 110.39, lat: 20.02 },
    { lon: 110.38, lat: 20.08 }
  ],
  widthMeters: 80
})

// 按 id 删除 / 查询 / 清空
framework.removeChannel(channel.id)
framework.getChannel('route-main')
framework.clearChannels()

航道说明:

  • 框架按一条航道自动维护两条业务线:地理宽实线 + 中心 WebGL 虚线,样式与 demo 海口河道一致。
  • show: true 且路径点不少于 2 个时绘制;否则隐藏,但不删除航道记录。
  • pointsheight 未传时默认 0
  • widthMeters 表示航道实际宽度,单位米;默认 50
  • color 为宽实线颜色,默认 #1677ffdashColor 为中心虚线颜色,默认 #eafcff
  • wideOpacity / dashOpacity 分别控制宽实线与虚线透明度,默认 0.7 / 0.95
  • 不传 id 时框架自动生成 mtf-channel-N;返回值 ChannelRecord 含当前完整配置。
  • clearLines() 会同时清空航道;若只想删航道,请用 clearChannels()removeChannel(id)

手动画图形示例:

framework.drawShape({
  id: 'control-circle',
  type: 'circle',
  center: { lon: 121.5, lat: 31.2, height: 0 },
  radiusMeters: 3000,
  fillColor: '#35d7ff',
  fillOpacity: 0.2,
  outlineColor: '#ffffff',
  outlineWidth: 2
})

framework.startDrawingOverlay({
  type: 'polygon',
  fillColor: '#ffdf5a',
  fillOpacity: 0.24,
  outlineColor: '#ffffff',
  outlineWidth: 2,
  // 自定义形状:点击多个路径点;鼠标靠近起点 14px 内会吸附,再点击闭合;
  // 也可以在非起点位置快速双击,自动按当前路径闭合
  snapPixels: 14,
  // 默认 true:绘制完成后立刻进入编辑态
  enterEditAfterComplete: true,
  onStart: (result) => {
    // 点击绘制按钮后即生成 id;完成前它是绘制草稿 id
    console.log('shape start', result.id)
  },
  onChange: (result) => {
    // 新增点、拖动点、在线上插入点等路径变化都会触发
    console.log('shape change', result.changeReason, result.wkt, result.pathPoints)
  },
  onComplete: (result) => {
    console.log('shape done', result.id, result.type, result.wkt, result.pathPoints)
  }
})

// 矩形/圆:第一次点击中心点,移动鼠标预览外轮廓,第二次点击完成
framework.startDrawingOverlay({ type: 'rectangle' })
framework.startDrawingOverlay({ type: 'circle' })
framework.startDrawingOverlay({
  type: 'line',
  outlineColor: '#ffffff',
  outlineWidth: 1,
  lineType: 'solid',
  onComplete: (result) => {
    // result.type === 'line'
    // result.wkt returns LINESTRING (...)
    console.log('line done', result.id, result.wkt, result.pathPoints)
  }
})

// 按 id 进入编辑状态。矩形可拖中心点和边点;圆可拖中心点和半径点。
// 编辑时鼠标在路径点上是移动态;鼠标在路径线上是加点态,点击可插入新路径点。
// 矩形/圆一旦在线上插入新路径点,就会自动转换为 polygon;圆转换时按 36 段生成路径点。
framework.editShape('control-circle')
framework.cancelEditingOverlay()

framework.clearShapes()

startDrawingOverlay 生命周期回调:

| 回调 | 时机 | | --- | --- | | onStart(result) | 调用 startDrawingOverlay 后立即触发,此时已经有 id,但还是绘制草稿 | | onChange(result) | 路径发生实际变化时触发,例如新增点、插入点、拖动点 | | onComplete(result) | 绘制完成后触发,id 从这时开始成为正式图形 id | | onCancel(result) | 绘制取消时触发 | | onEditStart(result) | 绘制完成自动进入编辑态时触发 | | onEditEnd(result) | 退出编辑态时触发 |

result 字段说明:

| 字段 | 含义 | | --- | --- | | id | 图形 id,可传给 editShape(id)updateShape(id, options)removeShape(id) | | type | 图形类型:rectanglecirclepolygon | | status | 当前状态:drawingeditingcompletecancelled | | changeReason | 本次变化原因,例如 startadd-pointinsert-pointmove-pointcompletecancel | | wkt | 当前最终图形的 WKT。三类图形都以 POLYGON 返回;圆会按绘制采样点近似成面 | | pathPoints | 全部外轮廓路径点数组,不包含额外闭合重复点 | | points | pathPoints 的兼容别名,同样表示全部外轮廓路径点 | | controlPoints | 绘制/编辑用控制点。矩形是中心点和边点,圆是中心点和半径点,自定义图形是用户点击的路径点 | | center | 矩形/圆的中心点;自定义图形通常没有 | | radiusMeters | 圆半径,单位米;非圆通常没有 |

图片参数说明:

  • imagePath:图片路径,支持项目 public 下路径或可访问 URL。
  • coordinate/position:固定坐标,lon/lat/height
  • height:图片渲染高度,单位米,默认 0。固定坐标时可替代 coordinate.height;跟随目标时不传则使用目标当前高度,显式传入后会覆盖目标高度。
  • heightOffset:在最终渲染高度上额外增加的米数,默认 0。例如跟随目标且不传 height 时,heightOffset: 1 表示目标高度 + 1m
  • followTarget:是否跟随目标;为 true 时优先使用 targetId 当前渲染位置。
  • targetId:跟随的目标 id。
  • rotateWithTarget:是否随目标航向旋转。
  • initialRotation:初始旋转角度,单位度,默认图片上方为 0 度。
  • scale:未主动指定米制尺寸时的默认倍率,默认 1;框架会按图片原始像素换算成米制平面尺寸。
  • sizeInMeters/perspective:是否强调使用米制透视尺寸;当前默认图片覆盖物本身就是地图平面贴图,宽高按真实米数绘制,镜头拉远会变小、拉近会变大。
  • sizeMeters:米制透视尺寸下的最大边长,保持原图宽高比。
  • widthMeters/heightMeters:米制透视尺寸下的指定宽高;只传一个时按原图宽高比补另一个。
  • scaleByScreen:是否在透视平面基础上按屏幕比例控制图片宽度;默认 false。开启后仍然是地图平面贴图,不会切成 billboard。相机缩放导致目标尺寸变化时,会先等待 100ms 稳定,再用 0.5s 线性过渡到新的尺寸;过渡期间如果相机继续移动,会暂停当前过渡并等待下一次稳定后重新执行,避免滚轮缩放时即时变大/变小造成抖动。
  • maxScreenPercent:开启 scaleByScreen 后,图片宽度占屏幕短边的百分比,例如 8 表示约 8%。
  • maxScreenRatio:和 maxScreenPercent 等价的小数写法,例如 0.08

渲染模式说明:

  • 默认模式不是 billboard,而是和点模式地面符号同类的 WebGL 平面贴图:图片躺在当前位置的地表切平面上,使用图片 alpha 裁剪透明区域,支持固定坐标、跟随目标、跟随目标航向旋转。
  • scaleByScreen: true 仍然使用这套 WebGL 平面贴图,只是按相机距离把屏幕比例换算成米制宽度,并在相机缩放稳定后平滑过渡;因此图案有地图透视和遮挡关系,同时宽度会大致保持屏幕定比。

主 demo 在海口附近内置了四种图片覆盖物测试:

  • 固定地面圆环:固定坐标,不跟随目标,使用 sizeInMeters + sizeMeters 体现透视大小。
  • 跟随目标圆环:跟随 demo_ship_0,不随航向旋转,使用米制透视尺寸。
  • 跟随转向圆环:跟随 demo_ship_0,并随目标 heading 旋转,使用米制透视尺寸。
  • 屏幕定比圆环:在“测试”面板点击“屏幕定比图”按钮绘制/清除,两层图片跟随目标当前高度并抬高 1m;外圈罗盘固定正北,内圈跟随摆动 mock 船并随船航向旋转,外圈/内圈均带 30 度刻度。

主 demo 还在海口附近内置了一条示例航道 haikou-river-channel,通过 createChannel 绘制宽实线 + 中心虚线。

Examples

包内包含可运行的 examples 目录,并会随 npm 包发布。它覆盖基础目标、低空五类型、增量更新/一轮预测、轨迹站点事件总线等场景。

源码仓库运行:

npm install
npm run build
cd examples
npm install
npm run dev

已安装 npm 包后运行:

cd node_modules/cesium-multi-target-framework/examples
npm install
npm run dev

如果把示例复制到业务项目,把 examples/src/main.js 里的 ../../dist/cesium-multi-target-framework.js 改成包名 cesium-multi-target-framework,并复制 examples/public 下的静态资源。

AI 快速索引

如果你是 AI 或第一次接手本仓库,优先读这些文件:

| 入口 | 用途 | | --- | --- | | src/core/MultiTargetFramework.ts | 对外高层 API、参数校验、事件总线绑定入口 | | src/core/MultiTargetScene.ts | 场景渲染主流程、LOD、拾取、名称牌、菜单 | | src/config/types.ts | 全部配置类型、默认值、目标类型归一化 | | src/worker/renderWorker.ts | Worker 数据提交、视口裁剪、LOD 选择、预测 | | src/render/ModelPoolRenderer.ts | 高低模、对象池、模型资源解析 | | demo/main.ts | 主 demo:目标类型、mock 数据接入、交互按钮 | | demo/mockBackend.ts | demo mock 接口适配,含低空目标枚举映射 | | demo/mtfTargetTypes.ts | demo 低空目标资源路径和后端枚举到 type 的映射 |

常见任务定位:

| 要改什么 | 优先改哪里 | | --- | --- | | 新增目标类型 | targetTypes 配置;demo 先改 demo/main.tsdemo/mtfTargetTypes.ts | | 新增模型/图标 | 放到 demo/public/modeldemo/public/icons,再在 targetTypes 中引用 | | 调整高低模切换 | scene.lod.pointAbovescene.lod.animatedBelowframe.maxLowModelRenderItems | | 调整运动预测 | 类型级 predictMovederiveMotionFromPositionpredictSpeedScalepredictRoundspredictRoundSeconds | | 调整裁剪/合并 | scene.cullscene.horizonCullscene.merge | | 查性能瓶颈 | 先看 demo 面板日志,再看 perf 事件和 renderWorker.ts |

Demo 低空目标类型

主 demo 已集成从 low-uav/public/icons/mtflow-uav/public/model/mtf 摘取的低空目标资源:

| 后端枚举 | demo type | 名称 | 图标 | 模型 | | --- | --- | --- | --- | --- | | 0 | air-unknown | 未识别飞行物 | /icons/mtf/unknown-aerial.png | /model/mtf/未知飞行器.gltf | | 1 | air-uav | 雷达无人机 | /icons/mtf/uav-radar.png | /model/mtf/uav-low.gltf/model/mtf/uav-high.gltf | | 2 | air-bird | 鸟 | /icons/mtf/bird.png | /model/mtf/飞鸟.gltf | | 6 | air-fpv | 高机动无人机 | /icons/mtf/uav-radar.png | /model/mtf/uav-low.gltf/model/mtf/uav-high.gltf | | 7 | air-civilian-aircraft | 民航飞机 | /icons/mtf/aircraft.png | /model/mtf/民航飞机.gltf |

资源文件位于 demo/public,Vite demo 运行时会按根路径访问。新增低空类型时,先把后端枚举写入 demo/mtfTargetTypes.ts,再在 demo/main.tstargetTypes 中补模型、图标、颜色和预测策略。

框架精华

框架的核心思路是把海量目标渲染拆成“数据进入、后台计算、裁剪筛选、复用渲染、动画衔接、事件调度”几条稳定流水线:大规模计算尽量放到 Worker 和分帧流程中,主线程只消费已经裁好的渲染包,并通过可配置策略在精度、性能和业务表达之间做平衡。

多维度的数据裁剪

同时使用视口 bounds、平视距离/扇形裁剪、LOD、合并抽稀、类型/ID 显隐和名称牌距离限制,尽量让后续渲染只处理真正需要显示的目标。

多线程无阻塞 Worker 计算

目标数据提交、视口筛选、合并抽稀、预测位置计算都在 Web Worker 中完成,避免大批量数据处理直接阻塞 Cesium 主线程。

对象池复用

目标 billboard、发光效果、名称牌、模型和站点渲染对象都走池化复用,减少频繁创建/销毁 Cesium primitive 带来的卡顿和内存抖动。

大计算分帧处理

全量数据提交、全局缓存、层级构建和视口更新按帧调度与节流,避免一次性把扫描、构建和渲染更新压到同一帧。

动画补间操作

目标平滑移动、预测运动、高低模切换、显隐淡入淡出和名称牌缩放都通过补间衔接,让数据跳变不会直接表现成画面跳变。

数据驱动

业务只需要持续写入目标、轨迹、站点和样式数据,框架根据当前相机、LOD、预测和裁剪配置自动生成最终渲染状态。

多扩展配置

目标类型、模型资源、LOD、合并、裁剪、预测、名称牌、站点层、拾取和右键菜单都可配置,便于不同业务场景按性能和显示需求调参。

事件总线调度

内置 web-event-bus action 和事件桥接,跨组件可以直接调用目标、轨迹、站点和菜单能力,降低业务模块之间的耦合。

安装

npm install cesium-multi-target-framework

如果宿主项目单独管理 Cesium,建议保持 Cesium 版本与本包一致:

npm install [email protected]

快速开始

import {
  Viewer,
  MultiTargetFramework,
  getFrameworkEventBus
} from 'cesium-multi-target-framework'

const viewer = new Viewer('cesiumContainer', {
  animation: false,
  timeline: false
})

const framework = new MultiTargetFramework(viewer, {
  targetTypes: [
    {
      type: 'ship',
      name: '船舶',
      iconPath: '/icons/target-ship-mask.png',
      defaultColor: '#25e65c',
      smoothMove: true,
      predictMove: false
    },
    {
      type: 'uav',
      name: '无人机',
      iconPath: '/icons/target-uav-mask.png',
      defaultColor: '#22d2f2',
      smoothMove: true,
      predictMove: true,
      deriveMotionFromPosition: true,
      predictMinCameraHeight: 25000,
      predictSpeedScale: 0.6,
      predictFitSeconds: 2,
      predictRounds: 1,
      predictRoundSeconds: 10
    }
  ],
  scene: {
    data: { commitIntervalMs: 2000, leading: true, worker: true },
    merge: { enabled: true, maxVisible: 10000, cameraMergeBaseHeight: 500, byType: true },
    pick: { enabled: true, hover: true },
    label: {
      maxCount: 50,
      maxCameraHeight: 5000,
      fields: [
        { key: 'id', label: 'ID' },
        { key: 'homePort', source: 'custom', label: '所属渔港', fallback: '-' }
      ]
    }
  }
})

framework.setTargetData([
  {
    id: 'ship-001',
    type: 'ship',
    lon: 121.5,
    lat: 31.2,
    height: 0,
    speedH: 8,
    speedV: 0,
    heading: 45,
    renderColor: '#f81282',
    openAnimate: true,
    animateType: '发光',
    animateColor: '#f81282',
    customInfo: {
      name: '海巡 001',
      homePort: '舟山沈家门渔港'
    }
  }
])

framework.scene.on('hover', (target) => {
  console.log('hover target', target)
})

await framework.ready
await framework.locateTarget('ship-001')

// 创建实例后会自动暴露到 web-event-bus
const bus = getFrameworkEventBus()
bus.on('mtf.scene.pick', console.log)
await bus.invoke('mtf.framework.setTargetColor', {
  id: 'ship-001',
  color: '#00ffff'
})

构造参数

const framework = new MultiTargetFramework(viewer, options)

| 参数 | 类型 | 说明 | | --- | --- | --- | | viewer | Cesium.Viewer | 必填,已创建的 Cesium Viewer 实例 | | options.targetTypes | MultiTargetTypeConfig[] | 必填,业务目标类型配置 | | options.scene | Omit<SceneConfig, "nodeTypes"> | 可选,渲染、合并、预测、拾取、名称牌等场景配置 |

MultiTargetFramework 构造后会同步返回实例,并异步初始化内部 Web Worker。需要作为其它组件底座时,等待 ready

const framework = new MultiTargetFramework(viewer, options)
await framework.ready

也可以使用异步工厂:

const framework = await MultiTargetFramework.create(viewer, options)
// 或
const framework2 = await createMultiTargetFramework(viewer, options)

targetTypes

| 字段 | 说明 | | --- | --- | | type | 目标类型,必须与数据中的 type 对应 | | name | 类型显示名 | | targetDomain | surface / air / underwater,不传时按类型名推断 | | iconPath | 点模式图标;兼容旧版,默认作为可被 renderColor 染色的基底层 | | baseIconPath | 可选,显式指定基底层图标;不传时使用 iconPath | | decorIconPath | 可选,装饰层图标,绘制在基底层之上并保持原始颜色;不传时不进入装饰层批次 | | defaultColor | 类型默认颜色 | | highModel / lowModel | 高模/低模模型资源配置 | | highModelPoolSize | 该类型高模预热数量和可用高模上限;默认 20,超出后降级到低模 | | highModelUrl / lowModelUrl | 旧字段兼容,等同模型 URL | | physicalSize | 类型默认真实尺寸,单位米,形如 { length, width, height };目标未传 length / width / physicalHeight 时使用该默认值 | | pointPixelSize | 点/符号像素大小 | | pointHeadingOffset | 点模式朝向修正,弧度 | | scale | 模型缩放 | | modelHeadingOffset / modelPitchOffset / modelRollOffset | 模型朝向修正,弧度 | | smoothMove | 是否启用平滑移动 | | smoothMoveMinCameraHeight | 相机低于/等于该高度时启用平滑 | | smoothMoveDurationMs | 单次位置变化的平滑时长 | | predict / predictMove | 是否启用运动预测,predict 为兼容别名 | | deriveMotionFromPosition | 是否忽略该类型目标的后端航向/速度,改用同一目标相邻两次权威点位和 updateTime 在 worker 内本地计算 heading / speedH / speedV;默认 false,只在初始化配置时生效 | | predictMinCameraHeight | 相机低于/等于该高度时启用预测 | | predictSpeedScale | 预测速度倍率;默认使用 scene 配置,0.6 表示按 60% 速度外推 | | predictSeconds | 预测窗口秒数 | | predictFitSeconds | 新数据到达后回到最新预测轨迹的误差抹平秒数,默认使用 scene 配置 | | predictRounds | 每次权威数据到达后最多执行几轮预测;不传为无限 | | predictRoundSeconds | 单轮预测持续时间;不传时使用 predictSeconds |

scene

| 配置 | 默认 | 说明 | | --- | --- | --- | | lod.pointAbove | 150000 | 高于此相机高度走点模式 | | lod.animatedBelow | 10000 | 低于此相机高度允许高模动画 | | pool.size | 30000 | 通用目标池容量 | | animatedPool.size | 200 | 所有类型同时渲染高模的全局上限;超出后降级到低模 | | animatedPool.surfaceTypeSize | 20 | 未配置 highModelPoolSize 的地表/水面类型默认预热数量 | | animatedPool.airTypeSize | 20 | 未配置 highModelPoolSize 的空中类型默认预热数量 | | animatedPool.prewarmPerFrame | 2 | 每帧最多启动的高模预热任务数 | | updateFps | 30 | 主更新频率 | | predictionFpsByDomain | 跟随 updateFps | 按目标域设置 Worker 预测终点频率,例如 { surface: 2, air: 10, underwater: 2 } | | predictSeconds | 10 | 默认预测窗口秒数 | | predictFitSeconds | 2 | 新权威数据到达后回到最新预测轨迹的误差抹平秒数 | | predictSpeedScale | 1 | 默认预测速度倍率;类型级 predictSpeedScale 可覆盖 | | predictRounds | Infinity | 每次权威数据到达后最多执行几轮预测;1 表示只预测一轮 | | predictRoundSeconds | predictSeconds | 单轮预测持续时间;一轮结束且新数据未到时停止继续外推 | | fadeMs | 350 | 显隐淡入淡出时间 | | masterHideHeight | 150000 | 高于此高度隐藏模型内容 | | merge.enabled | false | 是否启用合并 | | merge.mode | global | 合并模式 | | merge.maxVisible | pool.size | 最大渲染目标数 | | merge.byType | false | 是否按类型分桶合并 | | pick.enabled | true | 是否启用点击拾取 | | pick.pixelThreshold | 20 | 拾取像素阈值 | | pick.hover | true | 是否启用 hover | | bearingLine.enabled | false | 是否在低模/高模单目标前方显示航向指示线 | | bearingLine.durationMinutes | 1 | 航向线长度,按 speedH * durationMinutes * 60 计算,可配置 | | bearingLine.lineType | dashed | 航向线类型:dashed 虚线、solid 实线 | | bearingLine.width | 2 | 航向线宽度,会按当前 WebGL 能力做安全裁剪 | | bearingLine.color | #35d7ff | 航向线默认颜色 | | bearingLine.colorsByType | 未配置 | 按目标 type 覆盖航向线颜色,例如 { "air-uav": "#39ff88" };未命中时使用 bearingLine.color | | bearingLine.opacity | 0.85 | 航向线透明度 | | bearingLine.arrow | true | 是否在线尾显示箭头 | | bearingLine.arrowLengthMeters | 70 | 箭头头部固定长度,单位米,不随航向线长度变化 | | bearingLine.arrowWidthMeters | 70 | 箭头头部实际张口宽度,单位米;默认 70 米 | | bearingLine.arrowWidthMultiplier | 2 | 兼容旧配置;未配置 arrowWidthMeters 时按主线线宽倍率计算 | | bearingLine.minCameraHeight | 0 | 相机低于该高度时不显示;默认不限制 | | bearingLine.maxCameraHeight | Infinity | 相机高于该高度时不显示;默认不限制 | | bearingLine.targetTypes | 不限 | 只给指定目标类型显示航向线 | | cull.viewportCull | true | 是否启用视口裁剪 | | cull.viewportBufferRatio | 0.5 | 视口缓冲比例 | | cull.flatPitchViewportBufferRatio | 0.12 | 平视/近地平线视角下使用的较小视口缓冲比例 | | cull.flatPitchThreshold | -25 | 相机 pitch 高于/等于该角度时使用平视缓冲比例,默认来自 DEFAULT_FLAT_PITCH_THRESHOLD_DEGREES | | cull.viewportBufferMinMeters | 500 | 视口裁剪缓冲的绝对下限 | | horizonCull.enabled | true | 平视/近地平线视角下启用距离和方向裁剪 | | horizonCull.forwardAngle | 160 | 前方扇形裁剪角度,近处目标保底保留 | | horizonCull.nearKeepDistance | 3000 | 近处保底半径,单位米 | | label.enabled | true | 是否显示名称牌 | | label.maxCount | 50 | 名称牌最多显示数量 | | label.maxCameraHeight | 5000 | 名称牌最大显示相机高度 | | frame.maxLowModelRenderItems | 3000 | 低模后端数量上限 | | frame.maxModelRenderItems | animatedPool.size | 旧配置兼容;未配置 animatedPool.size 时作为全局高模上限 | | data.commitIntervalMs | 2000 | 数据提交周期,0 表示立即提交 | | data.leading | true | 首批数据是否立即提交 | | data.worker | true | 数据处理是否使用 worker | | data.staleTimeoutMs | 600000 | 默认目标离线剔除时间;地面/水下默认 10 分钟 | | data.staleTimeoutByDomainMs.air | 60000 | 空中目标离线剔除时间,默认 1 分钟 | | data.staleTimeoutByDomainMs.surface | 600000 | 地面目标离线剔除时间,默认 10 分钟;设为 0 可关闭该类超时剔除 | | groundSymbols | true | 点档单目标是否使用地表实例化有向符号 | | groundSymbolPixelSize | 30 | 地表符号像素大小 | | physicalSizeIconFallback | false | low/high LOD 下当前类型没有对应模型时,是否用 iconPath 图标按真实尺寸兜底渲染;开启后只按 length 计算整体缩放倍数,宽高同倍率,避免图标被拉伸变形 | | defaultPhysicalSizeByDomain | 内置默认 | 按 surface / air / underwater 配置默认真实尺寸;内置默认分别为 20x5x43x3x120x5x4 米 |

数据格式

TargetData / StandardTargetData 使用 WGS84 经纬度,速度单位为米/秒(m/s),角度单位为度。

| 字段 | 必填 | 说明 | | --- | --- | --- | | id | 是 | 唯一 id | | type | 是 | 目标类型,必须命中 targetTypes | | lon / lat / height | 是 | 经度、纬度、高度 | | speedH / speedV | 是 | 水平速度、垂直速度,单位米/秒(m/s);speedV 向上为正 | | heading | 是 | 航向角,正北为 0,顺时针 | | bowHeading | 否 | 艏向角,正北为 0,顺时针;只影响渲染方向,不参与运动预测 | | updateTime | 否 | 点位获取时间,Unix 毫秒时间戳;对应类型开启 deriveMotionFromPosition 时用于前端本地计算航向和速度 | | renderColor | 否 | 单目标渲染颜色,#RRGGBB#RRGGBBAA | | visualKey | 否 | 已注册的图标 key;命中 visualIcons[].key 时只覆盖图标,不改变目标 type、模型和 LOD | | length / width / physicalHeight | 否 | 目标真实长、宽、高,单位米;优先级高于类型级 physicalSize 和域级默认尺寸 | | openAnimate | 否 | 是否开启动画 | | animateType | 否 | 当前支持 glow / 发光 | | animateColor | 否 | 动画颜色 | | nationality | 否 | 国籍/归属 | | customInfo | 否 | 自定义信息,可用于名称牌字段 | | track | 否 | 轨迹点数组 | | secrecy | 否 | 密级,当前透传 | | extra | 否 | 额外业务状态 |

PackedTargetData 支持同名 typed array 字段:bowHeading?: Float32ArrayupdateTime?: Float64Arraylength?: Float32Arraywidth?: Float32ArrayphysicalHeight?: Float32Array,以及字符串数组 visualKeys?: string[]bowHeading 中使用 NaN 表示该目标没有艏向;其他字段与对象数据语义一致,适合大批量 packed 数据。

low/high 模型 LOD 默认使用模型配置里的 scale,由使用者自行控制显示大小。只有模型配置显式设置 useRealLength: true,且目标数据传了 length 时,才会按 目标 length / 类型 physicalSize.length 对模型做同倍率缩放。模型缺失时同一尺寸还会用于图标兜底,普通远距离点图标仍保持固定像素大小,不会增加大规模点渲染成本。

启用 useRealLength: true 后,如果模型配置的 scalephysicalSize.length 基本一致,框架会把它识别为“按 1m 基准模型配置”的实长模型:加载 glTF/GLB 时先计算模型包围盒最长边,若最长边不在 0.8m ~ 1.3m 范围内,会自动乘以 1 / 最长边 做一次归一化,再叠加目标实际 length 缩放。没有开启 useRealLength 或目标没有传 length 时,不会触发这套真实船长和归一化逻辑。

开启真实船长:

{
  physicalSize: { length: 100, width: 20, height: 20 },
  highModel: {
    url: '/model/集装箱货船.gltf',
    scale: 100,
    headingOffset: -90,
    useRealLength: true
  }
}

关闭真实船长:不配置 useRealLength,或显式写 useRealLength: false。此时目标数据里的 length 不会改变模型大小,模型只按配置的 scale 显示。

{
  highModel: {
    url: '/model/集装箱货船.gltf',
    scale: 100,
    useRealLength: false
  }
}

自定义图标覆盖

目标默认按 targetTypes[].iconPath 显示图标;iconPath 会作为可染色基底层,仍兼容旧配置。需要装饰层时可配置 decorIconPath,装饰层保持原始颜色,不受目标 renderColor 影响。业务只需要少量目标显示特殊图标时,不建议新增目标类型,也不建议在目标数据里直接传 URL;推荐先在框架配置中注册 visualIcons,再通过目标数据或 API 写入 visualKey

const framework = new MultiTargetFramework(viewer, {
  visualIcons: [
    {
      key: 'ais',
      iconPath: '/icons/target-ais-mask.png',
      decorIconPath: '/icons/target-ais-decor.png',
      pointHeadingOffset: 180
    },
    {
      key: 'fishing-boat',
      iconPath: '/icons/fishing-boat-mask.png',
      pointHeadingOffset: 180
    }
  ],
  targetTypes: [
    {
      type: 'ship',
      iconPath: '/icons/target-ship-mask.png',
      decorIconPath: '/icons/target-ship-decor.png',
      defaultColor: '#25e65c'
    }
  ]
})

framework.upsertTargetData([
  {
    id: 'ship-001',
    type: 'ship',
    lon: 121.5,
    lat: 31.2,
    height: 0,
    speedH: 8,
    speedV: 0,
    heading: 45,
    visualKey: 'ais'
  }
])

// 支持单个 id 或 id 数组,适合批量测试/业务批量切换。
framework.setTargetVisualKey('ship-001', 'fishing-boat')
framework.setTargetVisualKey(['ship-001', 'ship-002'], 'ais')
framework.setTargetVisualKey('ship-001', null)

性能说明:

  • visualKey 进入 worker 后会解析成稳定的数字 iconIndex,渲染循环只消费数字索引。
  • setTargetVisualKey(id, key) 默认只更新命中的目标 id,并失效相关渲染缓存,不需要重新 upsert 完整目标。
  • visualKey 只影响图标图集索引;目标 type、高低模选择、业务分类、名称牌类型判断仍使用原始 type
  • 高空合并时会按 type + iconIndex 分桶,避免默认船图标和 AIS 图标被合成同一个代表点后丢失视觉差异。

API

MultiTargetFramework

| 方法 | 说明 | | --- | --- | | setData(targets) | setTargetData 的别名,全量替换目标数据 | | upsert(targets) | upsertTargetData 的别名,增量新增或更新 | | remove(ids) | removeTargetData 的别名,按 id 删除 | | addTarget(target) | 校验并实时 upsert 单目标,返回校验后的目标 | | updateTarget(target) | 校验并实时 upsert 单目标,返回校验后的目标 | | setTargetData(targets) | 全量替换目标数据 | | upsertTargetData(targets) | 增量新增或更新目标数据 | | removeTargetData(ids) | 实时删除目标 | | applyTargetIncremental(payload) | 应用 { upserts, removes } 增量包 | | setTargetColor(id, color, options?) | 设置目标颜色 | | setTargetGlow(id, enabled, color?, options?) | 开关发光动画 | | setTargetVisualKey(id, visualKey?, options?) | 设置目标图标覆盖;id 支持字符串或字符串数组,visualKeynull/空值恢复类型默认图标 | | hideTarget(id, options?) | 隐藏目标 | | showTarget(id, options?) | 显示目标 | | setTargetVisibility({ visible, ids?, types? }) | 强制显示/隐藏目标;不传 ids/types 时作用于全局 | | setNameplateVisibility({ visible, ids?, types? }) | 强制显示/隐藏名称牌;不传 ids/types 时作用于全局 | | setBearingLineVisibility(visible) | 打开/关闭目标航向指向线;默认关闭 | | setPointLayerVisibility(visible) | 打开/关闭点图层和地表实例化符号层 | | disappearTarget(id, options?) | hideTarget 别名 | | appearTarget(id, options?) | showTarget 别名 | | setNameplateConfig(config) | 动态更新名称牌配置 | | selectTarget(id, options?) | 选中目标,返回 TargetSnapshot \| null | | selectTargets(ids, options?) | 批量选中目标,返回 TargetSnapshot[] | | unselectTarget(id) | 取消单目标选中 | | clearSelection() | 清空选中和框选 | | boxTarget(id, enabled?, options?) | 设置黄色框选覆盖层 | | unboxTarget(id, options?) | 取消框选 | | locateTarget(id, options?) | 飞行定位目标,返回目标快照 | | registerContextMenuItem(item) | 注册右键菜单项,返回注销函数 | | showContextMenu(ctx) | 展示统一右键菜单 | | closeContextMenu() | 关闭右键菜单 | | destroy() | 销毁菜单 DOM、事件监听、场景和 worker |

ModelManager

framework.modelManager 管理不跟随多目标 LOD 数据生命周期的业务模型。位置单位为 WGS84 度/米,headingpitchroll 使用度。

const model = await framework.modelManager.addModel({
  id: 'port-drilling-rig',
  url: '/models/钻井.gltf',
  category: 'normal',
  longitude: 110.329,
  latitude: 20.086,
  height: 0,
  heading: 20,
  scale: 500,
  runAnimations: true
})

framework.modelManager.updateModel(model.id, {
  longitude: 110.331,
  heading: 45,
  pitch: 2
})

framework.modelManager.removeModel(model.id)

| 方法 | 说明 | | --- | --- | | addModel(options) | 异步添加并返回 ManagedModel;同 id 已存在时直接返回已有模型 | | getModel(id) | 查询模型快照;普通模型的 model 字段是原生 Cesium.Model | | updateModel(id, options) | 按 id 更新经纬高、航向/俯仰/翻滚、缩放、显隐或动画状态 | | removeModel(id) | 按 id 删除,返回是否找到并删除 | | clearModels(category?) | 清空全部模型,或只清空 normal / instanced 分类 |

  • normal:默认分类。每个 id 对应独立 Cesium.Model,支持 glTF/GLB 完整材质、蒙皮和动画。
  • instanced:同文件名共享一个 GPU 实例批次,适合大量静态模型;不执行 glTF 动画和蒙皮。
  • 资源键取 URL 的文件名并忽略大小写。仍有同名模型存在时,新 id 即使传入不同目录的同名 URL,也复用第一次注册的 URL;业务侧应避免不同模型使用相同文件名。

MultiTargetScene

高层实例的 framework.scene 是底层渲染引擎实例,适合需要 packed 数据、底层查询或事件监听的场景。

| 方法/事件 | 说明 | | --- | --- | | setData(targets) | 全量目标数据 | | applyIncremental(payload) | 增量目标数据 | | applyRealtimeUpsert(targets) | 实时 upsert | | applyRealtimeRemove(ids) | 实时删除 | | applyPacked(payload) | 提交 typed array packed 数据 | | flushNow() | 立即提交 staged 数据 | | pause() / resume() | 暂停/恢复提交和视口刷新 | | getTargetSnapshot(id) | 查询目标逻辑快照 | | getRenderedTargetFrame(id, result?) | 查询当前实际渲染帧位置和航向 | | on("perf", handler) | 性能事件 | | on("dirty", handler) | 普通 diff 事件 | | on("packedDirty", handler) | packed diff 事件 | | on("pick", handler) | 点击目标事件,载荷 PickedNodeLike \| null | | on("hover", handler) | hover 目标事件,载荷 PickedNodeLike \| null | | on("select", handler) | 目标选中事件,载荷 { id, target, targets } | | on("unselect", handler) | 目标取消选中事件,载荷 { id, target?, targets } | | on("selection", handler) | 选中集合事件 | | on("contextmenu", handler) | 右键目标或空地事件 |

TrackManager

可通过 framework.tracksframework.scene.tracks 使用。

| 方法/事件 | 说明 | | --- | --- | | on("hover", handler) | 监听轨迹点 hover,载荷 TrackHoverInfo \| null | | setTracks(tracks) | 新增或更新轨迹 | | getTracks(targetIds?) | 查询轨迹,返回拷贝 | | showTracks(targetIds) | 显示轨迹 | | hideTracks(targetIds) | 隐藏轨迹 | | removeTracks(targetIds) | 删除轨迹 | | clearTracks() | 清空轨迹 | | setPointLabelOptions(options) | 配置轨迹点标签文案/样式/数量上限 | | showPointLabels(targetIds) | 按目标 id 打开轨迹点标签 | | hidePointLabels(targetIds) | 按目标 id 关闭轨迹点标签 | | clearPointLabels() | 一键关闭全部轨迹点标签 |

SiteLayer

站点层独立于目标系统,用于固定站点图标/低模展示。

const siteLayer = new SiteLayer(viewer, {
  modelSwitchHeight: 8000,
  mergeDistanceMeters: 100,
  defaultCoverageRangeMeters: 10000,
  defaultInstallationHeight: 30,
  defaultPitchDeg: 0,
  defaultFieldOfViewDeg: 60,
  siteTypes: [
    {
      type: 'radar',
      category: 'radar',
      iconPath: '/icons/radar.png',
      coverageRangeMeters: 18000,
      installationHeight: 35,
      pitchDeg: 0.15,
      fieldOfViewDeg: 70,
      lowModel: { url: '/model/radar.glb' }
    }
  ]
})

siteLayer.setData([
  {
    id: 'radar-1',
    type: 'radar',
    lon: 121,
    lat: 31,
    height: 0,
    heading: 90,
    coverageRangeMeters: 12000,
    installationHeight: 30,
    pitchDeg: 0.15,
    fieldOfViewDeg: 60
  }
])

站点覆盖范围默认 10km,可在 SiteLayerOptions.defaultCoverageRangeMetersSiteTypeConfig.coverageRangeMeters 或单个 SiteData.coverageRangeMeters 覆盖。installationHeight 默认为 30m,表示设备安装高度;pitchDeg 表示相对水平面的向下俯仰角,fieldOfViewDeg 表示水平视场角宽度。指向扇形使用 heading +/- fieldOfViewDeg / 2 展开,扇形长度优先按 installationHeight / tan(abs(pitchDeg)) 计算,并限制在覆盖半径内;pitchDeg 为空或接近 0 时直接使用覆盖半径。

站点覆盖圆、指向扇形和中心指向线默认不渲染。需要业务主动打开时调用:

siteLayer.setCoverageVisibility({ visible: true })
// 或分别控制
siteLayer.setCoverageVisibility({ showCoverage: true, showDirectionSector: false })
// 或只打开某个站点的覆盖范围/指向扇形
siteLayer.setCoverageVisibility({ ids: ['site_radar_haikou_c'], visible: true, showCoverage: true })
siteLayer.setCoverageVisibility({ ids: ['site_radar_haikou_c'], visible: true, showDirectionSector: true })
// 调整站点 heading、fieldOfViewDeg、pitchDeg 等数据时,覆盖/指向几何默认 150ms 补间
siteLayer.setData(nextSites, { animationMs: 150 })

默认 mergeDistanceMeters100m。视口内站点距离小于该阈值时会合并成一个站点集合:底部显示圆环,不同类型按环绕 slot 展示;同一集合内多个同类型站点只显示一个该类型图标,并在图标右上角显示数量角标。集合右键仍触发 contextmenu,payload 保留 site 代表站点,同时增加 sitesslotSitesclusterIdisCluster。命中集合时,site.lon / site.lat / site.height 是集合 slot 的实际渲染位置,便于业务直接画选中态;原始代表站点可从 site.originalSite 读取。

| 方法/事件 | 说明 | | --- | --- | | setData(sites, options?) | 全量设置站点;options.animationMs 控制覆盖/指向几何补间,默认 150 | | setVisibility({ visible, ids?, types? }) | 强制显示/隐藏站点;不传 ids/types 时作用于全局 | | setCoverageVisibility({ visible?, ids?, types?, showCoverage?, showDirectionSector?, animationMs? }) | 打开/关闭站点覆盖范围、指向扇形和中心指向线;可按站点 id/type 过滤;默认 150ms 补间 | | pickAt(position, thresholdPx?) | 按屏幕坐标拾取最近站点 | | clear() | 清空站点 | | destroy() | 销毁站点层 | | on("contextmenu", handler) | 站点右键事件 | | on("pick", handler) | 站点点击事件 | | on("hover", handler) | 站点 hover 事件 | | on("select", handler) | 站点选中事件 | | on("unselect", handler) | 站点取消选中事件 |

Web Event Bus 使用方式

包会依赖并复用 web-event-bus。创建 MultiTargetFramework 后会自动把 framework/tracks 的 public API 注册成可跨模块调用的 action,并桥接 scene/tracks 事件;创建 SiteLayer 后会自动注册 site 相关 action,并桥接站点事件。默认 action 名固定为:

mtf.framework.<方法名>
mtf.tracks.<方法名>
mtf.site.<方法名>
import {
  getFrameworkEventBus
} from 'cesium-multi-target-framework'

const bus = getFrameworkEventBus()

bus.on('mtf.scene.hover', (payload) => {
  console.log('hover', payload)
})

await bus.invoke('mtf.framework.locateTarget', {
  id: 'ship-001',
  options: { range: 12000 }
})

await bus.invoke('mtf.tracks.showTracks', {
  targetIds: ['ship-001']
})

await bus.invoke('mtf.tracks.setPointLabelOptions', {
  template: '{time}\\n{speed}m/s',
  maxLabels: 1000,
  maxLabelsPerTarget: 100
})

await bus.invoke('mtf.tracks.showPointLabels', {
  targetIds: ['ship-001']
})

await bus.invoke('mtf.framework.drawImage', {
  imagePath: '/icons/mtf/aircraft.png',
  followTarget: true,
  targetId: 'ship-001',
  rotateWithTarget: true, // 是否随目标航向旋转;true 时图片会跟着 targetId 的 heading 一起转
  initialRotation: 0
})

多实例使用同一组固定 action 名:后创建的同类实例会接管对应 action;旧实例销毁时只会注销自己仍持有的 action,不会误删后来接管的实例。实例 destroy() 会自动解绑本实例注册的 action 和事件桥接。

Web Event Bus 对外能力

事件

| Web Event Bus 事件 | 载荷 | 说明 | | --- | --- | --- | | mtf.scene.perf | SceneEvents["perf"] | 输入、worker、渲染慢帧等性能事件 | | mtf.scene.dirty | DiffResult | 普通 diff | | mtf.scene.packedDirty | PackedDiffResult | packed diff | | mtf.scene.pick | PickedNodeLike \| null | 点击目标 | | mtf.scene.hover | PickedNodeLike \| null | hover 目标 | | mtf.scene.select | TargetSelectEvent | 目标选中 | | mtf.scene.unselect | TargetUnselectEvent | 目标取消选中 | | mtf.scene.selection | PickedNodeLike[] | 选中集合 | | mtf.scene.contextmenu | SceneContextMenuPayload \| null | 右键目标或空地 | | mtf.track.hover | TrackHoverInfo \| null | 轨迹点 hover | | mtf.site.contextmenu | SiteContextMenuPayload | 站点右键 | | mtf.site.pick | PickedSiteData \| null | 站点点击;集合命中时坐标为集合渲染位置 | | mtf.site.hover | PickedSiteData \| null | 站点 hover;集合命中时坐标为集合渲染位置 | | mtf.site.select | SiteSelectEvent | 站点选中 | | mtf.site.unselect | SiteUnselectEvent | 站点取消选中 |

action

| Web Event Bus 对外能力 | 参数 | 说明 | | --- | --- | --- | | mtf.framework.setData | { targets } | setTargetData 别名 | | mtf.framework.upsert | { targets } | upsertTargetData 别名 | | mtf.framework.remove | { ids } | removeTargetData 别名 | | mtf.framework.addTarget | { target } | 新增/更新单目标,返回校验后的目标 | | mtf.framework.updateTarget | { target } | 新增/更新单目标,返回校验后的目标 | | mtf.framework.setTargetData | { targets } | 全量替换目标数据 | | mtf.framework.upsertTargetData | { targets } | 增量新增或更新目标 | | mtf.framework.removeTargetData | { ids } | 删除目标 | | mtf.framework.applyTargetIncremental | { payload } | 应用 { upserts, removes } 增量包 | | mtf.framework.setTargetColor | { id, color, options? } | 设置目标颜色 | | mtf.framework.setTargetGlow | { id, enabled, color?, options? } | 开关目标发光 | | mtf.framework.setTargetVisualKey | { id, visualKey?, options? } | 设置目标图标覆盖;visualKeynull/空值恢复默认图标 | | mtf.framework.hideTarget | { id, options? } | 隐藏目标 | | mtf.framework.showTarget | { id, options? } | 显示目标 | | mtf.framework.setTargetVisibility | { visible, ids?, types? } | 强制显示/隐藏目标 | | mtf.framework.setNameplateVisibility | { visible, ids?, types? } | 强制显示/隐藏名称牌 | | mtf.framework.setBearingLineVisibility | { visible } | 打开/关闭目标航向指向线 | | mtf.framework.setPointLayerVisibility | { visible } | 打开/关闭点图层和地表实例化符号层 | | mtf.framework.disappearTarget | { id, options? } | 隐藏目标别名 | | mtf.framework.appearTarget | { id, options? } | 显示目标别名 | | mtf.framework.setNameplateConfig | { config } | 更新名称牌配置 | | mtf.framework.selectTarget | { id, options? } | 选中目标,返回 TargetSnapshot \| null | | mtf.framework.selectTargets | { ids, options? } | 批量选中目标,返回 TargetSnapshot[] | | mtf.framework.unselectTarget | { id } | 取消选中 | | mtf.framework.clearSelection | 无 | 清空选中 | | mtf.framework.boxTarget | { id, enabled?, options? } | 设置框选覆盖层 | | mtf.framework.unboxTarget | { id, options? } | 取消框选 | | mtf.framework.locateTarget | { id, options? } | 飞行定位目标,返回 TargetSnapshot \| null | | mtf.framework.registerContextMenuItem | { item } | 注册右键菜单项,返回注销函数 | | mtf.framework.showContextMenu | { context } | 展示统一右键菜单 | | mtf.framework.closeContextMenu | 无 | 关闭右键菜单 | | mtf.framework.drawLine | DrawLineOptions | 画业务线,返回线 id | | mtf.framework.updateLine | { id, options } | 更新业务线 | | mtf.framework.removeLine | { id } | 删除业务线 | | mtf.framework.clearLines | 无 | 清空业务线;同时清空航道 | | mtf.framework.drawImage | DrawImageOptions | 画图片覆盖物,返回图片 id | | mtf.framework.updateImage | { id, options } | 更新图片覆盖物 | | mtf.framework.removeImage | { id } | 删除图片覆盖物 | | mtf.framework.clearImages | 无 | 清空图片覆盖物 | | mtf.framework.showHeadingIndicator | { targetId, options? } | 按目标 id 打开内置航向指示器 | | mtf.framework.hideHeadingIndicator | { targetId } | 按目标 id 关闭航向指示器 | | mtf.framework.clearHeadingIndicators | 无 | 关闭全部航向指示器 | | mtf.framework.createChannel | CreateChannelOptions | 创建航道,返回 ChannelRecord | | mtf.framework.updateChannel | { id, options } | 按 id 更新航道,返回更新后的 ChannelRecord \| undefined | | mtf.framework.removeChannel | { id } | 按 id 删除航道,返回是否删除成功 | | mtf.framework.clearChannels | 无 | 清空全部航道 | | mtf.framework.getChannel | { id } | 按 id 查询航道,返回 ChannelRecord \| undefined | | mtf.framework.drawShape | DrawShapeOptions | 画矩形、圆或自定义图形,返回图形 id | | mtf.framework.updateShape | { id, options } | 更新图形覆盖物 | | mtf.framework.removeShape | { id } | 删除图形覆盖物 | | mtf.framework.clearShapes | 无 | 清空图形覆盖物 | | mtf.framework.editShape | { id } | 按 id 进入图形编辑状态 | | mtf.framework.startDrawingOverlay | StartOverlayDrawOptions | 进入交互绘制模式 | | mtf.framework.cancelDrawingOverlay | 无 | 取消当前交互绘制 | | mtf.framework.cancelEditingOverlay | 无 | 退出当前图形编辑状态 | | mtf.framework.destroy | 无 | 销毁当前接管 framework action 的实例 | | mtf.tracks.setTracks | { tracks } | 新增或更新轨迹 | | mtf.tracks.getTracks | { targetIds? } 或无 | 查询轨迹 | | mtf.tracks.showTracks | { targetIds } | 显示轨迹 | | mtf.tracks.hideTracks | { targetIds } | 隐藏轨迹 | | mtf.tracks.removeTracks | { targetIds } | 删除轨迹 | | mtf.tracks.clearTracks | 无 | 清空轨迹 | | mtf.tracks.setPointLabelOptions | TrackPointLabelOptions | 配置轨迹点标签;bus 推荐使用 template | | mtf.tracks.showPointLabels | { targetIds } | 按目标 id 打开轨迹点标签 | | mtf.tracks.hidePointLabels | { targetIds } | 按目标 id 关闭轨迹点标签 | | mtf.tracks.clearPointLabels | 无 | 一键关闭全部轨迹点标签 | | mtf.site.setData | { sites, options? } | 全量设置站点 | | mtf.site.setVisibility | { visible, ids?, types? } | 强制显示/隐藏站点 | | mtf.site.setCoverageVisibility | { visible?, ids?, types?, showCoverage?, showDirectionSector?, animationMs? } | 打开/关闭站点覆盖范围、指向扇形和中心指向线 | | mtf.site.clear | 无 | 清空站点 | | mtf.site.pickAt | { position, thresholdPx? } | 按屏幕坐标拾取站点,返回 PickedSiteData \| null | | mtf.site.destroy | 无 | 销毁当前接管 site action 的站点层 |

TypeScript 类型包括 FrameworkEventBusFrameworkBusActionMapFrameworkBusEventMapFrameworkBusActionNameFrameworkEventName 等。

名称牌配置

名称牌就是目标旁边悬浮的小信息框。它可以显示目标 id、速度、高度、业务名称等字段,也可以按目标类型显示不同内容。

最常用的是 fields:每一项表示名称牌里的一行。

| 字段 | 说明 | | --- | --- | | key | 要显示哪个字段名。默认从目标对象顶层取值,例如 target.idtarget.speedHtarget.height | | label | 显示名。不写时只显示值;写了会显示成 label: value | | source | 取值来源。默认 standard 表示从目标顶层取;custom 表示从 target.customInfo 取 | | fallback | 取不到值时显示什么。不配置时该行为空就不显示 | | minCameraHeight | 相机高度低于该值时不显示这一行 | | maxCameraHeight | 相机高度高于该值时不显示这一行 |

名称牌整体配置常用字段:

| 字段 | 说明 | | --- | --- | | enabled | 是否启用名称牌 | | maxCount | 最多显示多少个名称牌,防止屏幕被铺满 | | maxCameraHeight | 相机高于该高度时整体不显示名称牌 | | maxDistance | 目标距离相机超过该距离时不显示 | | height | 名称牌锚点高度,不配时跟随目标当前渲染高度 | | showFieldLabels | 是否显示 label:,默认 true | | fields | 默认字段列表,适用于所有类型 | | byType | 按目标 type 精确覆盖配置 | | resolve | 高级自定义函数;返回字符串/字符串数组/null/undefined |

基础例子

假设目标数据是:

{
  id: 'uav-001',
  type: 'air-uav',
  lon: 121.5,
  lat: 31.2,
  height: 1200,
  speedH: 35,
  speedV: 0,
  heading: 80,
  customInfo: {
    name: '无人机 A',
    source: '雷达'
  }
}

配置:

framework.setNameplateConfig({
  enabled: true,
  maxCount: 50,
  maxCameraHeight: 10000,
  fields: [
    { key: 'id', label: 'ID' },
    { key: 'speedH', label: '速度', fallback: '0', maxCameraHeight: 6000 },
    { key: 'name', label: '名称', source: 'custom', maxCameraHeight: 3000 }
  ]
})

相机高度小于等于 3000 米时显示:

ID: uav-001
速度: 35
名称: 无人机 A

相机高度在 30006000 米之间时,name 这一行会隐藏:

ID: uav-001
速度: 35

相机高度超过 6000 米时,speedH 这一行也会隐藏:

ID: uav-001

相机高度超过 10000 米时,整个名称牌隐藏。

按类型覆盖

byType 用目标数据里的 type 精确匹配。比如你的无人机类型是 air-uav,这里就必须写 air-uav,不能写 uav

framework.setNameplateConfig({
  fields: [
    { key: 'id', label: 'ID' },
    { key: 'speedH', label: '速度' }
  ],
  byType: {
    'air-uav': {
      fields: [
        { key: 'id', label: 'ID' },
        { key: 'height', label: '高度', maxCameraHeight: 8000 },
        { key: 'source', label: '来源', source: 'custom' }
      ]
    },
    ship: {
      fields: [
        { key: 'id', label: 'ID' },
        { key: 'nationality', label: '国籍', fallback: '-' }
      ]
    }
  }
})

规则是:

  • 没命中 byType 时,使用外层 fields
  • 命中 byType[type] 时,使用该类型自己的 fields
  • byType 的 key 必须等于目标数据里的 type,例如 air-uavair-birdship-other
  • byType[type].maxCameraHeight 可以单独控制该类型名称牌的最大显示高度。

resolve 高级自定义

resolve(ctx) 可以完全接管名称牌内容。它适合做复杂逻辑,比如聚合目标、不同高度显示不同文案、格式化单位。

framework.setNameplateConfig({
  resolve: (ctx) => {
    if (ctx.cameraHeight > 10000) return null
    if (ctx.count > 1) return `聚合目标 x${ctx.count}`
    return [
      `${ctx.type}:${ctx.id}`,
      `高度 ${ctx.data?.height ?? '-'} m`
    ]
  }
})

返回值含义:

| 返回值 | 效果 | | --- | --- | | string | 显示一行 | | string[] | 显示多行 | | null | 不显示名称牌 | | undefined | 不接管,继续使用 fields / byType 默认逻辑 |

ctx 常用字段:

| 字段 | 说明 | | --- | --- | | ctx.id | 目标 id | | ctx.type | 目标类型 | | ctx.data | 原始目标数据,可能是 undefined | | ctx.cameraHeight | 当前相机高度,单位米 | | ctx.distance | 目标到相机距离,可能为空 | | ctx.count | 聚合数量;普通单目标通常是 1 |

优先级:resolve 先执行。只要它返回的不是 undefined,框架就不会再走 fields / byType

完整配置片段

framework.setNameplateConfig({
  enabled: true,
  maxCount: 50,
  height: 120,
  showFieldLabels: true,
  fields: [
    { key: 'id', label: 'ID' },
    { key: 'speedH', label: '速度', fallback: '0', maxCameraHeight: 6000 },
    { key: 'homePort', source: 'custom', label: '所属渔港', fallback: '-', maxCameraHeight: 3000 }
  ],
  byType: {
    ship: {
      fields: [
        { key: 'id', label: 'ID' },
        { key: 'homePort', source: 'custom', label: '所属渔港', fallback: '-' },
        { key: 'nationality', label: '国籍', fallback: '-' }
      ]
    },
    'air-uav': {
      fields: [
        { key: 'id', label: 'ID' },
        { key: 'height', label: '高度', maxCameraHeight: 8000 }
      ]
    }
  },
  resolve: (ctx) => {
    if (ctx.cameraHeight > 10000) return null
    return [`${ctx.type}:${ctx.id}`, `数量 ${ctx.count}`]
  }
})

framework.setNameplateVisibility({ visible: false, types: ['air-uav'] })
framework.setTargetVisibility({ visible: false, ids: ['ship-001'] })
siteLayer.setVisibility({ visible: false, types: ['radar'] })

轨迹格式

framework.tracks.setTracks([
  {
    targetId: 'ship-001',
    visible: true,
    pointList: [
      { lon: 121.5, lat: 31.2, height: 0, time: Date.now(), heading: 45 }
    ]
  }
])

TrackRecord 支持 pointListtrackLinetrackLine 格式为 LINESTRING (...)LINESTRING Z (...)

轨迹点标签默认关闭,可按目标 id 打开。标签使用 billboard + atlas 画布渲染,默认只渲染当前轨迹 LOD 下的点,并受 maxLabels / maxLabelsPerTarget 限制。template 支持 {targetId}{pointIndex}{time}{speed}{heading}{source}{lon}{lat}{height}

framework.tracks.setPointLabelOptions({
  template: '{time}\\n速度:{speed}m/s',
  maxLabels: 1000,
  maxLabelsPerTarget: 100
})
framework.tracks.showPointLabels(['ship-001'])
framework.tracks.hidePointLabels(['ship-001'])
framework.tracks.clearPointLabels()

聚类工具

import {
  createQuadClusterer,
  clusterProgressive,
  lonLatToLocalPlane,
  createClusterWorkerClient
} from 'cesium-multi-target-framework'

const plane = lonLatToLocalPlane(lons, lats, count)
const result = clusterProgressive(plane.xs, plane.ys, count, {
  baseCellMeters: 200,
  maxClusters: 30000
})

const client = createClusterWorkerClient()
const workerResult = await client.run(lons, lats, count)
client.terminate()

开发

npm install
npm run dev
npm run typecheck
npm run build

npm run dev 会启动 demo,默认地址为 http://localhost:5173

更多文档