Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f737fdb89 |
@@ -8,6 +8,20 @@ This project follows the repository versioning rule:
|
||||
- `improvement` -> `+0.0.1`(bugfix + 小功能混合)
|
||||
- `bugfix` -> `+0.0.1`
|
||||
|
||||
## [0.46.2] — 2026-04-30
|
||||
|
||||
Released: 2026-04-30
|
||||
|
||||
### 🐛 Fixes
|
||||
- 修复 Earth 启动时高清材质、云图和图层可见性绕过 `startupPriority` 的问题,统一由启动队列按文档顺序加载。
|
||||
- 修复保存为关闭的高清材质/图层仍会先加载再关闭的问题,并保持海陆基座作为国界线图层的常驻底图。
|
||||
- 修复搜索跳转会误关媒体面板、船只轨迹末端不贴合当前船只、Iridium footprint 被地表层遮挡等 Earth 交互问题。
|
||||
|
||||
### 📝 Documentation
|
||||
- 更新 Earth 图层顺序、样式参考、使用手册和 AIS 聚合计划,补齐中英文说明与后续接入策略。
|
||||
|
||||
---
|
||||
|
||||
## [0.46.1] — 2026-04-30
|
||||
|
||||
Released: 2026-04-30
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
- [earth-news-source-configuration-and-collector-plan.md](/home/ray/dev/linkong/planet/docs/plans/earth-news-source-configuration-and-collector-plan.md)
|
||||
- [earth-news-cruise-summary-plan.md](/home/ray/dev/linkong/planet/docs/plans/earth-news-cruise-summary-plan.md)
|
||||
- [earth-vessel-rendering-performance-plan.md](/home/ray/dev/linkong/planet/docs/plans/earth-vessel-rendering-performance-plan.md)
|
||||
- [AIS 多源采集、冲突记录与聚合接口计划](/home/ray/dev/linkong/planet/docs/plans/earth-vessel-ais-aggregation-plan.md)
|
||||
- [earth-interactable-layer-plan.md](/home/ray/dev/linkong/planet/docs/plans/earth-interactable-layer-plan.md)
|
||||
- [frontend-public-docs-site-plan.md](/home/ray/dev/linkong/planet/docs/plans/frontend-public-docs-site-plan.md)
|
||||
- [frontend-ai-playground-development-plan.md](/home/ray/dev/linkong/planet/docs/plans/frontend-ai-playground-development-plan.md)
|
||||
|
||||
@@ -272,7 +272,8 @@ hover / locked 使用少量 overlay:
|
||||
### Phase 3:迁移算力中心并评估登陆点
|
||||
|
||||
- 算力中心保留现有业务 icon,但接入统一 hover / locked / glow。(已完成)
|
||||
- 登陆点曾接入同一套 `Points` 渲染,但 pin 类 SVG 在地球边缘会被深度测试裁切;当前保留专用 `THREE.Sprite`,并使用 canvas 生成黄色扁平球,贴到海缆层级。后续如要重新设计登陆点,需要先确认图标能在边缘视角完整显示。
|
||||
- 登陆点曾接入同一套 `Points` 渲染,但 pin 类 SVG 在地球边缘会被深度测试裁切;当前保留专用 `THREE.Sprite`,并使用 canvas 生成黄色扁平球,贴到海缆层级。
|
||||
- TODO:登陆点暂不迁移到完整 Interactable。后续若要统一交互接口,优先考虑 Sprite-backed adapter,只对齐 `getMarkers()`、`getPointerIntersections()`、`setMarkerState()`、`updateVisualState()` 等外观协议,不强行复用 `THREE.Points`、atlas 和跨图层避让。
|
||||
- 检查图例、搜索和 info-card 是否只依赖业务 payload,而不是依赖渲染对象类型。
|
||||
|
||||
### Phase 4:形成 Earth 图标层规范
|
||||
|
||||
261
docs/plans/earth-vessel-ais-aggregation-plan.md
Normal file
261
docs/plans/earth-vessel-ais-aggregation-plan.md
Normal file
@@ -0,0 +1,261 @@
|
||||
# AIS 多源采集、冲突记录与聚合接口计划
|
||||
|
||||
**状态**:规划中
|
||||
**创建日期**:2026-04-30
|
||||
**核心原则**:采集器只写原始观测;去重、合并、冲突解释放在聚合接口中完成
|
||||
|
||||
## 已确认决策
|
||||
|
||||
| 项目 | 决策 |
|
||||
|-----|------|
|
||||
| AISStream 接入方式 | 单独实现 WebSocket 采集器,不塞进现有 BarentsWatch HTTP collector |
|
||||
| 采集器职责 | 连接上游、标准化字段、写入原始观测,不直接决定最终展示值 |
|
||||
| 去重合并位置 | 放在聚合服务和聚合 API 中,而不是散落在每个 collector 的保存逻辑里 |
|
||||
| 冲突处理 | 先记录冲突事实和当前选择原因,后续再开放用户规则配置 |
|
||||
| 默认可信度 | 同类 AIS 数据源优先按 `delivery_mode` 评估:`realtime_stream` 优于 `batch_stream`,再优于 `polling` 和 `snapshot` |
|
||||
| 过期保护 | 实时流源断流超过 freshness 窗口后,不能仅凭“实时源”身份压过更新的轮询数据 |
|
||||
|
||||
## 背景
|
||||
|
||||
当前 AIS 链路以 BarentsWatch 为主。它是 HTTP polling 模式,覆盖挪威附近海域,适合作为稳定的免费起点,但不适合承担全球实时船只数据的全部职责。后续接入 AISStream 后,会出现同一个 MMSI 被多个来源同时上报的情况:
|
||||
|
||||
- 位置、航速、航向可能在多个来源之间存在秒级差异。
|
||||
- 船名、IMO、呼号、船型、尺寸等静态字段可能不完整,甚至互相冲突。
|
||||
- WebSocket 或其他实时流通常更接近实时,但也可能断流或批量延迟。
|
||||
- 如果每个 collector 自己做去重合并,规则会分散、不可审计,也很难让用户后续配置“某个字段信任哪个来源”。
|
||||
|
||||
因此 v1 不应让采集器直接覆盖最终船只表。更稳的方式是先保留观测事实,再由聚合接口统一给出当前展示视图。
|
||||
|
||||
## 目标架构
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[BarentsWatch HTTP collector] --> D[AIS raw observations]
|
||||
B[AISStream WebSocket collector] --> D
|
||||
C[Custom mapped vessel_ais sources] --> D
|
||||
D --> E[AIS aggregation service]
|
||||
E --> F[Conflict records]
|
||||
E --> G[GeoJSON vessels API]
|
||||
E --> H[Vessel detail API]
|
||||
I[Aggregation strategy config] --> E
|
||||
```
|
||||
|
||||
### 原始观测层
|
||||
|
||||
原始观测层保存每个来源看到的事实。建议模型包含:
|
||||
|
||||
| 字段 | 用途 |
|
||||
|-----|------|
|
||||
| `target_schema` | 例如 `vessel_ais` |
|
||||
| `source` | 例如 `barentswatch_vessels`、`aisstream_vessels` |
|
||||
| `entity_key` | AIS 使用 MMSI |
|
||||
| `delivery_mode` | `realtime_stream`、`batch_stream`、`polling`、`snapshot` |
|
||||
| `transport` | `websocket`、`sse`、`http`、`file` 等 |
|
||||
| `observed_at` | 上游数据时间,优先使用 AIS 消息时间 |
|
||||
| `collected_at` | 本系统接收或采集时间 |
|
||||
| `normalized_payload` | 标准化后的 AIS JSON |
|
||||
| `raw_payload` | 可选,保存原始或裁剪后的上游记录 |
|
||||
|
||||
`delivery_mode` 和 `transport` 不应混为一谈。WebSocket 是传输方式;streaming 是交付模式。聚合可信度主要看 `delivery_mode`,`transport` 只作为辅助信息。
|
||||
|
||||
### 冲突记录层
|
||||
|
||||
聚合服务发现同一个实体、同一个字段存在多个非空不同值时,写入冲突记录。冲突记录不代表错误,只代表“有多个可用候选值”。
|
||||
|
||||
```json
|
||||
{
|
||||
"target_schema": "vessel_ais",
|
||||
"entity_key": "257123000",
|
||||
"field": "name",
|
||||
"candidates": {
|
||||
"barentswatch_vessels": "OSLO TRADER",
|
||||
"aisstream_vessels": "OSLO TRADER II"
|
||||
},
|
||||
"selected_source": "aisstream_vessels",
|
||||
"selected_value": "OSLO TRADER II",
|
||||
"selected_reason": "delivery_mode_priority",
|
||||
"resolved_by": "system",
|
||||
"status": "open"
|
||||
}
|
||||
```
|
||||
|
||||
第一阶段只需要记录冲突和当前选择原因,不需要做人工逐条确认。后续 UI 的目标也不是让用户处理每条冲突,而是把冲突沉淀成字段级规则。
|
||||
|
||||
## 聚合规则
|
||||
|
||||
### 字段分类
|
||||
|
||||
| 类型 | 字段 | 默认策略 |
|
||||
|-----|------|----------|
|
||||
| 动态位置 | `lat`、`lon`、`sog`、`cog`、`heading`、`nav_status` | 优先最新 `observed_at`,同时间再按来源优先级 |
|
||||
| 静态身份 | `name`、`callsign`、`imo`、`flag` | 非空优先,再按字段策略或来源优先级 |
|
||||
| 静态规格 | `vessel_type`、`vessel_type_name`、`length`、`width`、`draught` | 非空优先;冲突时记录候选值 |
|
||||
| 元信息 | `field_sources`、`conflict_count`、`selected_reasons` | 聚合接口生成,便于调试和后续 UI 展示 |
|
||||
|
||||
### 默认优先级
|
||||
|
||||
默认优先级应使用两个维度:
|
||||
|
||||
```yaml
|
||||
delivery_mode_priority:
|
||||
- realtime_stream
|
||||
- batch_stream
|
||||
- polling
|
||||
- snapshot
|
||||
|
||||
transport_priority:
|
||||
- websocket
|
||||
- sse
|
||||
- http
|
||||
- file
|
||||
```
|
||||
|
||||
`delivery_mode_priority` 是主判断。比如 AISStream 如果提供实时推送,应标记为 `realtime_stream + websocket`;BarentsWatch 当前是 `polling + http`。
|
||||
|
||||
### 断流保护
|
||||
|
||||
实时流不能永久凭身份占优。聚合时需要 freshness 窗口:
|
||||
|
||||
```yaml
|
||||
freshness:
|
||||
realtime_stream_seconds: 900
|
||||
polling_seconds: 3600
|
||||
```
|
||||
|
||||
如果 `aisstream_vessels` 最近 15 分钟没有该 MMSI 的新观测,而 BarentsWatch 轮询源有更新位置,则位置类字段应采用 BarentsWatch 的更新观测,并记录选择原因 `newest_observation` 或 `freshness_fallback`。
|
||||
|
||||
## 聚合接口
|
||||
|
||||
现有展示接口应逐步改为消费聚合服务,而不是自己直接拼 `VesselPosition + VesselStatic`。
|
||||
|
||||
```text
|
||||
GET /api/v1/visualization/geo/vessels
|
||||
GET /api/v1/visualization/vessels/{mmsi}
|
||||
GET /api/v1/visualization/vessels/{mmsi}/track
|
||||
GET /api/v1/visualization/vessels/{mmsi}/conflicts
|
||||
```
|
||||
|
||||
GeoJSON properties 建议增加:
|
||||
|
||||
```json
|
||||
{
|
||||
"mmsi": 257123000,
|
||||
"name": "OSLO TRADER",
|
||||
"lat": 59.91,
|
||||
"lon": 10.73,
|
||||
"received_at": "2026-04-30T10:00:00Z",
|
||||
"field_sources": {
|
||||
"name": "aisstream_vessels",
|
||||
"lat": "aisstream_vessels",
|
||||
"lon": "aisstream_vessels",
|
||||
"vessel_type": "barentswatch_vessels"
|
||||
},
|
||||
"selected_reasons": {
|
||||
"name": "delivery_mode_priority",
|
||||
"lat": "newest_observation",
|
||||
"vessel_type": "non_empty_priority"
|
||||
},
|
||||
"conflict_count": 2
|
||||
}
|
||||
```
|
||||
|
||||
## 开放配置计划
|
||||
|
||||
### Phase 1 — 内置默认策略和只读解释
|
||||
|
||||
- 实现后端默认策略。
|
||||
- 聚合接口返回 `field_sources`、`selected_reasons`、`conflict_count`。
|
||||
- 冲突记录可查询,但不允许用户修改。
|
||||
- 保持现有前端船只图层接口形状基本兼容,新增字段只作为调试和后续 UI 输入。
|
||||
|
||||
### Phase 2 — 系统设置中的 JSON/YAML 策略配置
|
||||
|
||||
新增系统设置项,例如:
|
||||
|
||||
```yaml
|
||||
collector_aggregation:
|
||||
vessel_ais:
|
||||
source_priority:
|
||||
- aisstream_vessels
|
||||
- barentswatch_vessels
|
||||
field_rules:
|
||||
name:
|
||||
mode: source_priority
|
||||
vessel_type:
|
||||
mode: source_priority
|
||||
source_priority:
|
||||
- barentswatch_vessels
|
||||
- aisstream_vessels
|
||||
lat:
|
||||
mode: newest
|
||||
lon:
|
||||
mode: newest
|
||||
```
|
||||
|
||||
配置校验要求:
|
||||
|
||||
- 未知 source 只警告,不阻断保存,便于先配置后启用。
|
||||
- 未知 field 必须拒绝,避免拼写错误悄悄失效。
|
||||
- 动态位置字段默认不允许被固定来源永久锁死,除非显式开启高级选项。
|
||||
- 空值不覆盖非空值是全局保护,不建议开放关闭。
|
||||
|
||||
### Phase 3 — 冲突治理 UI
|
||||
|
||||
基于冲突记录提供页面或 drawer:
|
||||
|
||||
- 查看某个 MMSI 的冲突字段。
|
||||
- 查看每个字段的候选来源和值。
|
||||
- 查看当前选择原因。
|
||||
- 将一次人工选择保存成字段规则,而不是只处理单条冲突。
|
||||
- 支持恢复默认策略。
|
||||
|
||||
## AISStream 采集器计划
|
||||
|
||||
AISStream 采集器单独实现,建议命名为 `aisstream_vessels`。它的职责是:
|
||||
|
||||
- 维护 WebSocket 连接、订阅范围和重连。
|
||||
- 将上游 AIS 消息标准化为 `vessel_ais` payload。
|
||||
- 标记 `delivery_mode = realtime_stream`,`transport = websocket`。
|
||||
- 写入原始观测层。
|
||||
- 不直接 upsert 最终展示数据。
|
||||
|
||||
配置应放入采集器设置,而不是硬编码:
|
||||
|
||||
```yaml
|
||||
aisstream_vessels:
|
||||
api_key: "${AISSTREAM_API_KEY}"
|
||||
bounding_boxes:
|
||||
- [[-180, -90], [180, 90]]
|
||||
message_types:
|
||||
- PositionReport
|
||||
- ShipStaticData
|
||||
```
|
||||
|
||||
## 实施顺序
|
||||
|
||||
1. 新增原始观测模型和冲突记录模型。
|
||||
2. 实现 AIS 聚合服务,先从现有 `vessel_position` / `vessel_static` 兼容读取,再逐步切换到原始观测层。
|
||||
3. 将 `/geo/vessels` 和 `/vessels/{mmsi}` 改为走聚合服务。
|
||||
4. 改造 BarentsWatch 保存逻辑,让它写入原始观测,同时保留现有表作为兼容缓存。
|
||||
5. 实现 AISStream WebSocket collector。
|
||||
6. 接入系统设置中的聚合策略配置。
|
||||
7. 做冲突治理 UI。
|
||||
|
||||
## 测试计划
|
||||
|
||||
- 同一来源同一 `mmsi + observed_at + lat + lon` 重复记录只聚合一次。
|
||||
- 多来源同一 MMSI 的位置字段优先选择最新观测。
|
||||
- 实时流和轮询源同时间冲突时,实时流优先。
|
||||
- 实时流过期后,更新的轮询源可以接管动态字段。
|
||||
- 静态字段不会被空值覆盖。
|
||||
- 静态字段冲突会写入冲突记录。
|
||||
- 字段级配置可以覆盖默认来源优先级。
|
||||
- 聚合接口在没有冲突表时仍可返回兼容 GeoJSON。
|
||||
|
||||
## 相关文件
|
||||
|
||||
- [实时船只监控系统计划](/home/ray/dev/linkong/planet/docs/plans/earth-vessel-tracking-plan.md)
|
||||
- [自定义 API 数据源与 LLM 映射系统计划](/home/ray/dev/linkong/planet/docs/plans/datasource-custom-api-mapping-plan.md)
|
||||
- [BarentsWatch AIS collector](/home/ray/dev/linkong/planet/backend/app/services/collectors/vessel_ais.py)
|
||||
- [船只模型](/home/ray/dev/linkong/planet/backend/app/models/vessel.py)
|
||||
- [可视化 API](/home/ray/dev/linkong/planet/backend/app/api/v1/visualization.py)
|
||||
@@ -12,7 +12,7 @@
|
||||
| 船只规模 | BarentsWatch 阶段全部显示;全球数据接入后按需加船型过滤(默认 Cargo + Tanker + Passenger) |
|
||||
| 更新频率 | 准实时:前端 5 分钟轮询,后端 Collector 每分钟拉取写库 |
|
||||
| 历史轨迹 | 保留(`vessel_position` 表保留 24h,后期按需扩展) |
|
||||
| 推送方式 | HTTP 轮询(不用 WebSocket);换实时数据源后再评估升级 |
|
||||
| 推送方式 | 前端展示仍可先用 HTTP 拉取聚合结果;AISStream 等实时源应单独实现 WebSocket 采集器 |
|
||||
|
||||
---
|
||||
|
||||
@@ -36,13 +36,19 @@
|
||||
- 字段:mmsi, lat, lon, sog, cog, heading, nav_status, name, vessel_type, flag
|
||||
- 刷新频率:数据约 30–60s 更新一次,可随意轮询
|
||||
|
||||
### TODO:付费数据源接入
|
||||
### TODO:多源 AIS 与实时流接入
|
||||
|
||||
- [ ] 接入 AISStream WebSocket 采集器,作为 BarentsWatch 覆盖不足的实时补充
|
||||
- [ ] 将 BarentsWatch、AISStream、自定义 `vessel_ais` 映射源统一写入原始观测层
|
||||
- [ ] 通过聚合接口做去重、字段合并、冲突记录和默认来源选择
|
||||
- [ ] 开放字段级聚合策略配置,让用户决定不同字段优先信任哪个来源
|
||||
- [ ] 评估 AISHub 订阅(全球覆盖,约 $30/月),接入全球实时流
|
||||
- [ ] 评估 MarineTraffic API tier,对比 AISHub 数据质量与成本
|
||||
- [ ] 实现多数据源适配器,通过 `datasource_config` 切换
|
||||
- [ ] 真实高频 AIS 稳定接入后,评估将 `vessel_position` 迁移为 TimescaleDB hypertable(保留 Postgres 原生分区作为备选)
|
||||
|
||||
多源 AIS 的详细设计见 [AIS 多源采集、冲突记录与聚合接口计划](/home/ray/dev/linkong/planet/docs/plans/earth-vessel-ais-aggregation-plan.md)。
|
||||
|
||||
---
|
||||
|
||||
## 二、实施计划
|
||||
@@ -151,12 +157,13 @@ GeoJSON Feature 格式:
|
||||
|
||||
#### 1.4 更新机制
|
||||
|
||||
**HTTP 轮询**(不使用 WebSocket):
|
||||
**前端聚合结果拉取 + 后端实时采集**:
|
||||
|
||||
- 前端 `setInterval(fetchVessels, 5 * 60 * 1000)` 定期拉取最新快照
|
||||
- 后端 Collector 每 60s 从 BarentsWatch 拉取并写库,`vessel_latest` 物化视图随时可查
|
||||
- WebSocket 留给告警/事件驱动场景(BGP、系统通知),不混入周期性位置刷新
|
||||
- 换用 AISHub / MarineTraffic 实时流后,届时再评估是否升级为 WebSocket delta push
|
||||
- 后端 BarentsWatch collector 继续以 HTTP polling 方式采集
|
||||
- AISStream 等实时源以独立 WebSocket collector 写入原始观测层
|
||||
- 展示接口从聚合服务读取当前船只视图,而不是由单个 collector 决定最终展示值
|
||||
- 前端是否升级为 WebSocket delta push 是独立优化,不影响后端采集器可以使用 WebSocket 接上游实时源
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -74,6 +74,8 @@ This document records the material, color, opacity, line width, radius offset, a
|
||||
|
||||
## Land/Ocean Base and Country Borders
|
||||
|
||||
The land/ocean base is an Earth base-map asset and preloads at startup; the "Border Lines" layer toggle only controls normal border lines, hover lines, and interactive hover.
|
||||
|
||||
| Name | Variable | Current Value | Location / Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| Country border data path | `COUNTRY_BOUNDARY_CONFIG.dataPath` | `"/earth/data/countries-admin0.min.geojson"` | GeoJSON input |
|
||||
@@ -89,11 +91,11 @@ This document records the material, color, opacity, line width, radius offset, a
|
||||
| Border line color | `COUNTRY_BOUNDARY_CONFIG.lineColor` | `0x7fc7ff` | Normal border line |
|
||||
| Border line opacity | `COUNTRY_BOUNDARY_CONFIG.lineOpacity` | `0.58` | Normal border line opacity |
|
||||
| Border dimmed opacity on hover | `COUNTRY_BOUNDARY_CONFIG.dimmedLineOpacity` | `0.18` | Normal border opacity during hover |
|
||||
| Border line radius offset | `COUNTRY_BOUNDARY_CONFIG.lineAltitudeOffset` | `0.24` | Normal border line radius |
|
||||
| Border line radius offset | `COUNTRY_BOUNDARY_CONFIG.lineAltitudeOffset` | `0.115` | Normal border line radius; slightly above HD texture `0.10` and below terrain base `0.16` to reduce floating |
|
||||
| Border line renderOrder | `COUNTRY_BOUNDARY_CONFIG.lineRenderOrder` | `2.2` | Normal border line level |
|
||||
| Border hover color | `COUNTRY_BOUNDARY_CONFIG.hoverLineColor` | `0xff3b1f` | Neon red-orange |
|
||||
| Border hover opacity | `COUNTRY_BOUNDARY_CONFIG.hoverLineOpacity` | `1.0` | Hover line opacity |
|
||||
| Border hover radius offset | `COUNTRY_BOUNDARY_CONFIG.hoverAltitudeOffset` | `0.32` | Hover line radius |
|
||||
| Border hover radius offset | `COUNTRY_BOUNDARY_CONFIG.hoverAltitudeOffset` | `0.14` | Hover line radius; close to the surface but above normal border lines |
|
||||
| Border hover renderOrder | `COUNTRY_BOUNDARY_CONFIG.hoverLineRenderOrder` | `2.3` | Hover line level |
|
||||
| Border hover glow opacity | `COUNTRY_BOUNDARY_CONFIG.hoverGlowOpacity` | `0.38` | Glow line opacity |
|
||||
| Border hover glow line width | `COUNTRY_BOUNDARY_CONFIG.hoverGlowLineWidth` | `3` | Glow `LineBasicMaterial.linewidth` |
|
||||
@@ -143,13 +145,14 @@ This document records the material, color, opacity, line width, radius offset, a
|
||||
| Landing point radius offset | `CABLE_CONFIG.landingPoint.altitudeOffset` | `0.2` | Same surface height as cable lines, avoiding a floating marker |
|
||||
| Landing point sprite height | local `LANDING_POINT_SPRITE_HEIGHT` | `3` | `THREE.Sprite` base height |
|
||||
| Landing point reference FOV | local `LANDING_POINT_SIZE_REFERENCE_FOV` | `75` | Matches the current Earth camera FOV |
|
||||
| Landing point scale minimum | local `LANDING_POINT_SIZE_SCALE_MIN` | `0.36` | Minimum multiplier at maximum zoom; `3 * 0.36 = 1.08` |
|
||||
| Landing point scale minimum | local `LANDING_POINT_SIZE_SCALE_MIN` | `0.16` | Minimum multiplier after roughly 200% zoom, limiting high-zoom screen footprint; `3 * 0.16 = 0.48` |
|
||||
| Landing point scale maximum | local `LANDING_POINT_SIZE_SCALE_MAX` | `3` | Maximum multiplier at far distance; current minimum zoom reaches roughly `2.50` |
|
||||
| Landing point atlas size | local `LANDING_POINT_ATLAS_CELL_SIZE` | `128` | Canvas flat shaded sphere texture size |
|
||||
| Landing point color | `CABLE_CONFIG.landingPoint.color` | `0xffaa00` | `SpriteMaterial.color` |
|
||||
| Landing point opacity | `CABLE_CONFIG.landingPoint.opacity` | `1.0` | `SpriteMaterial.opacity` |
|
||||
| Landing point renderOrder | `CABLE_CONFIG.landingPoint.renderOrder` | `1` | Same level as cable lines; `depthTest: false` keeps the ball whole, while camera-to-center globe occlusion hides back-side points |
|
||||
| Landing point dim brightness | `landingPointVisual.dimBrightness` | `0.62` | Dim state color multiplier |
|
||||
| Related landing point opacity | `landingPointVisual.related.opacityBase / opacityPulse` | `0.8 / 0.2` | Highlight pulse |
|
||||
| Dimmed landing point color | `landingPointVisual.dimmed.colorRGB` | `{ r: 180, g: 116, b: 28 }` | Dim state color; avoids dark base showing through as a dark hole |
|
||||
| Dimmed landing point opacity | `landingPointVisual.dimmed.opacity` | `0.78` | Dim state opacity; no longer uses low alpha blending with dark base |
|
||||
|
||||
@@ -168,7 +171,21 @@ This document records the material, color, opacity, line width, radius offset, a
|
||||
| Satellite trail line width | `SATELLITE_CONFIG.trailLineWidth` | `3` | Ribbon shader uniform |
|
||||
| Selected ring size | `SATELLITE_CONFIG.ringSize` | `0.07` | Hover / locked ring sprite |
|
||||
| Satellite overlay renderOrder | `SATELLITE_CONFIG.overlayRenderOrder` | `12` | Locked ring / halo / orbit |
|
||||
| Footprint renderOrder | local `GROUND_FOOTPRINT_RENDER_ORDER` | `3` | Footprint fill |
|
||||
| Footprint renderOrder | local `GROUND_FOOTPRINT_RENDER_ORDER` | `3` | Starlink footprint fill and Iridium coverage ring; must stay above land / texture / terrain surface layers |
|
||||
|
||||
## AIS Vessels
|
||||
|
||||
| Name | Variable | Current Value | Location / Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| Vessel radius offset | `VESSEL_CONFIG.altitudeOffset` | `0.2` | Normal marker position, close to the real terrain base layer |
|
||||
| Vessel track radius offset | `VESSEL_CONFIG.track.altitudeOffset` | `0.2` | Selected vessel track line, aligned to the vessel marker radius; the frontend anchors the track endpoint to the current marker position |
|
||||
| Vessel renderOrder | local `VESSEL_RENDER_ORDER` | `4.4` | Normal marker and interactive overlay |
|
||||
| Vessel track renderOrder | `VESSEL_RENDER_ORDER - 0.1` | `4.3` | Below vessel markers |
|
||||
| Vessel point pixel size | local `VESSEL_POINT_SIZE` | `34` | Shared size for normal markers and hover / locked overlays |
|
||||
| Vessel texture canvas size | local `VESSEL_ATLAS_CELL_SIZE` | `128` | Canvas point texture |
|
||||
| Course bucket count | local `VESSEL_COURSE_BINS` | `32` | Moving vessels are bucketed by COG to reduce draw calls while preserving direction |
|
||||
| Vessel hover picking throttle | local `VESSEL_HOVER_PICK_INTERVAL_MS` | `100` | `main.js` hover picking |
|
||||
| Vessel screen hit radius | local `VESSEL_POINTER_RADIUS_PX` | `22` | `main.js` screen-space picking |
|
||||
|
||||
## Compute Centers
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ Note: the layer control panel order and the registration / startup load order ar
|
||||
|
||||
| Order type | Current sequence | Notes |
|
||||
| --- | --- | --- |
|
||||
| Control panel order | Cables → Trails → Satellites → Compute Centers → BGP → Terrain → HD Texture → Cloud Layer → Borders → Grid | Controlled by `displayOrder`, sorted by operational relevance. |
|
||||
| Registration / startup load order | Grid → Borders → HD Texture → Cloud Layer → Cables → Compute Centers → BGP → Satellites | Controlled by registration order and `startupPriority`, sorted surface-to-sky; Trails and Terrain are dependency/optional display layers and do not participate in normal startup data loading. |
|
||||
| Control panel order | Cables → Trails → Satellites → Compute Centers → BGP → Terrain → HD Texture → Cloud Layer → Border Lines → Grid | Controlled by `displayOrder`, sorted by operational relevance. |
|
||||
| Registration / startup load order | Grid → Border Lines / Land-Ocean Base → HD Texture → Cloud Layer → Cables → Compute Centers → BGP → Satellites | Controlled by registration order and `startupPriority`, sorted surface-to-sky; the startup queue reads persisted layer visibility first, skips normal layers explicitly saved as hidden, and HD Texture does not download the texture when disabled; Border Lines are the exception: the land-ocean base always preloads, while the persisted state only controls interactive border lines and hover; Trails and Terrain are dependency/optional display layers and do not participate in normal startup data loading. |
|
||||
|
||||
## Surface Layer Stack
|
||||
|
||||
@@ -26,7 +26,7 @@ Note: the layer control panel order and the registration / startup load order ar
|
||||
| 2.2 | Country borders | `country-boundaries.js` | `lineAltitudeOffset` | Raycast disabled | Only needs to stay above HD texture. |
|
||||
| 2.29 | Country border hover glow | `country-boundaries.js` | Hover radius + glow offset | `depthTest: false`, raycast disabled | Additive glow to reinforce border edge and terrain hover visibility. |
|
||||
| 2.3 | Country border hover line | `country-boundaries.js` | `hoverAltitudeOffset` | `depthTest: false`, raycast disabled | Neon red-orange hover line; China and Taiwan share the same highlight group. |
|
||||
| 3 | Satellite footprint fill | `satellites.js` | `GROUND_FOOTPRINT_RENDER_ORDER` | depth-tested, Group renderOrder stays 0 | Footprint above country borders, below compute centers and satellites. |
|
||||
| 3 | Satellite footprint fill / Iridium coverage ring | `satellites.js`, `iridium-footprint-adapter.js` | `GROUND_FOOTPRINT_RENDER_ORDER` | depth-tested; Iridium adapter fill / ring use the same renderOrder | Footprint above land / texture / terrain and country borders, below compute centers and satellites. |
|
||||
| 3-5 | BGP markers and overlays | `bgp.js` | Each marker's own renderOrder | BGP picking path | Preserves existing BGP visual level. |
|
||||
| 4.5 | Compute centers | `compute-centers.js` | `COMPUTE_CENTER_RENDER_ORDER` | Compute center picking path | Surface facilities, below satellites. |
|
||||
| 5 | Satellite background dot | `satellites.js` | Fixed renderOrder | Screen-space satellite picking | Below satellite dots. |
|
||||
@@ -42,7 +42,7 @@ Note: the layer control panel order and the registration / startup load order ar
|
||||
| HD texture on | Restores HD texture and the remembered terrain / day/night states. |
|
||||
| Terrain on | Displayed above HD texture, but below country border hover, footprints, satellites, and other emphasis layers. |
|
||||
| Cloud layer | Only controls cloud mesh visibility. |
|
||||
| Country borders | Controls border line and hover line visibility; land/ocean base fill exists independently as the Earth base map. |
|
||||
| Border Lines off | Hides only interactive border lines and hover, clearing hover state; the land/ocean base fill remains as the Earth base map. |
|
||||
|
||||
## Interaction Rules
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ Earth is used to observe in a single globe view:
|
||||
- Submarine cables and landing points
|
||||
- Compute centers
|
||||
- AIS vessels
|
||||
- Country borders, grid lines, HD texture, cloud layer, terrain
|
||||
- Border lines, grid lines, HD texture, cloud layer, terrain
|
||||
- Live news streams and situational news
|
||||
- Search and focused object details
|
||||
|
||||
@@ -214,7 +214,7 @@ The right-side layer panel toggles visualization layers on or off.
|
||||
Common layers include:
|
||||
|
||||
- Grid lines
|
||||
- Country borders
|
||||
- Border lines
|
||||
- HD texture
|
||||
- Atmospheric cloud layer
|
||||
- Submarine cables
|
||||
@@ -239,7 +239,7 @@ Current legend modes include:
|
||||
|
||||
- Cables
|
||||
- Satellites
|
||||
- Country borders
|
||||
- Border lines
|
||||
- Compute centers
|
||||
- BGP
|
||||
- AIS vessels
|
||||
|
||||
@@ -80,6 +80,8 @@
|
||||
|
||||
## 海陆基座与国界
|
||||
|
||||
海陆基座是 Earth 的底图资产,随启动预加载;图层面板里的“国界线”只控制普通国界线、hover 线和可交互 hover。
|
||||
|
||||
| 正式名称 | 变量名 | 当前值 | 使用位置 / 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| 国界数据路径 | `COUNTRY_BOUNDARY_CONFIG.dataPath` | `"/earth/data/countries-admin0.min.geojson"` | GeoJSON 输入 |
|
||||
@@ -149,7 +151,7 @@
|
||||
| 登陆点半径偏移 | `CABLE_CONFIG.landingPoint.altitudeOffset` | `0.2` | 与海缆线同层贴地,避免凌空 |
|
||||
| 登陆点 sprite 高度 | local `LANDING_POINT_SPRITE_HEIGHT` | `3` | `THREE.Sprite` 基准高度 |
|
||||
| 登陆点缩放参考 FOV | local `LANDING_POINT_SIZE_REFERENCE_FOV` | `75` | 与当前 Earth 相机 FOV 一致 |
|
||||
| 登陆点缩放下限 | local `LANDING_POINT_SIZE_SCALE_MIN` | `0.36` | 地球放到最大时的最小倍率;`3 * 0.36 = 1.08` |
|
||||
| 登陆点缩放下限 | local `LANDING_POINT_SIZE_SCALE_MIN` | `0.16` | 地球放到 200% 之后的最小倍率,限制高倍 zoom 下的屏幕占比;`3 * 0.16 = 0.48` |
|
||||
| 登陆点缩放上限 | local `LANDING_POINT_SIZE_SCALE_MAX` | `3` | 远距离时的最大倍率;当前最小缩放约只能到 `2.50` |
|
||||
| 登陆点 atlas 尺寸 | local `LANDING_POINT_ATLAS_CELL_SIZE` | `128` | canvas 扁平立体球纹理尺寸 |
|
||||
| 登陆点颜色 | `CABLE_CONFIG.landingPoint.color` | `0xffaa00` | `SpriteMaterial.color` |
|
||||
@@ -180,7 +182,7 @@
|
||||
| 卫星覆盖层 renderOrder | `SATELLITE_CONFIG.overlayRenderOrder` | `12` | locked ring / halo / orbit |
|
||||
| 自发光选中点颜色 | inline default | `"#ffd25a"` | `showSelfGlowStyle()` |
|
||||
| 自发光选中点透明度 | inline | `0.96` | locked dot material |
|
||||
| footprint renderOrder | local `GROUND_FOOTPRINT_RENDER_ORDER` | `3` | footprint fill |
|
||||
| footprint renderOrder | local `GROUND_FOOTPRINT_RENDER_ORDER` | `3` | Starlink footprint fill 和 Iridium coverage ring;必须高于地表 land / texture / terrain 层 |
|
||||
| footprint group renderOrder | inline | `0` | 避免 Group 排序盖过卫星点 |
|
||||
|
||||
## AIS 船只
|
||||
@@ -188,7 +190,7 @@
|
||||
| 正式名称 | 变量名 | 当前值 | 使用位置 / 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| 船只半径偏移 | `VESSEL_CONFIG.altitudeOffset` | `0.2` | 普通 marker 位置,贴近真实地形基础层 |
|
||||
| 船只轨迹半径偏移 | `VESSEL_CONFIG.track.altitudeOffset` | `0.22` | 选中船只轨迹线,略高于船只 marker 以保持可见 |
|
||||
| 船只轨迹半径偏移 | `VESSEL_CONFIG.track.altitudeOffset` | `0.2` | 选中船只轨迹线,与船只 marker 同一半径;前端会把轨迹末端锚到当前 marker 位置 |
|
||||
| 船只 renderOrder | local `VESSEL_RENDER_ORDER` | `4.4` | 普通 marker 和交互 overlay |
|
||||
| 船只轨迹 renderOrder | `VESSEL_RENDER_ORDER - 0.1` | `4.3` | 低于船只 marker |
|
||||
| 船只点像素尺寸 | local `VESSEL_POINT_SIZE` | `34` | 普通 marker 与 hover / locked overlay 共享尺寸 |
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
| 顺序类型 | 当前顺序 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 控制面板顺序 | 海缆 → 轨迹 → 卫星 → 算力中心 → 船只 → BGP → 地形 → 高清材质 → 大气云图 → 国界 → 经纬线 | 由 `displayOrder` 控制,按操作关注度排列。 |
|
||||
| 注册 / 启动加载顺序 | 经纬线 → 国界 → 高清材质 → 大气云图 → 海缆 → 算力中心 → 船只 → BGP → 卫星 | 由注册顺序和 `startupPriority` 控制,按地表到天空排列;船只和卫星默认关闭,只有可见时参与启动加载;轨迹和地形是依赖/可选显示层,不参与常规启动数据加载。 |
|
||||
| 控制面板顺序 | 海缆 → 轨迹 → 卫星 → 算力中心 → 船只 → BGP → 地形 → 高清材质 → 大气云图 → 国界线 → 经纬线 | 由 `displayOrder` 控制,按操作关注度排列。 |
|
||||
| 注册 / 启动加载顺序 | 经纬线 → 国界线 / 海陆基座 → 高清材质 → 大气云图 → 海缆 → 算力中心 → 船只 → BGP → 卫星 | 由注册顺序和 `startupPriority` 控制,按地表到天空排列;启动队列会先读取保存的图层可见状态,明确关闭的普通图层不预加载,高清材质关闭时不下载贴图;国界线图层例外,海陆基座始终预加载,保存状态只控制可交互国界线和 hover;轨迹和地形是依赖/可选显示层,不参与常规启动数据加载。 |
|
||||
|
||||
## 地表图层栈
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
| 2.2 | 国界线 | `country-boundaries.js` | `lineAltitudeOffset = 0.115` | `depthTest: true`,禁用 raycast | 略高于高清材质 `0.10`,低于地形基准 `0.16`,减少悬浮感;地形 `depthWrite: false`,所以地形开启时仍可见。 |
|
||||
| 2.29 | 国界 hover 光晕 | `country-boundaries.js` | hover 半径加 glow 偏移 | `depthTest: false`,禁用 raycast | 用 additive 光晕增强交界边和地形开启时的 hover 可见性。 |
|
||||
| 2.3 | 国界 hover 实线 | `country-boundaries.js` | `hoverAltitudeOffset = 0.14` | `depthTest: false`,禁用 raycast | 霓虹红橘 hover 线;中国和中国(台湾)共享高亮组。 |
|
||||
| 3 | 卫星 footprint 填充 | `satellites.js` | `GROUND_FOOTPRINT_RENDER_ORDER` | depth-tested,Group renderOrder 保持 0 | Footprint 在国界线之上,但在算力中心和卫星之下。 |
|
||||
| 3 | 卫星 footprint 填充 / Iridium coverage ring | `satellites.js`, `iridium-footprint-adapter.js` | `GROUND_FOOTPRINT_RENDER_ORDER` | depth-tested;Iridium adapter 的 fill / ring 也使用同一 renderOrder | Footprint 在 land / texture / terrain 和国界线之上,但在算力中心和卫星之下。 |
|
||||
| 3-4.5 | BGP 观测站、事件扩散圈和事件 marker | `bgp.js`, `interactable.js` | BGP 观测站和事件 marker 均使用 `Interactable` 批量 `THREE.Points`;事件 marker 使用 `BGP_EVENT_RENDER_ORDER = 4.5`;观测站主图标使用 `BGP_COLLECTOR_RENDER_ORDER = 4.4` 和 `BGP_CONFIG.collectorAltitudeOffset = 0.2`;事件 overlay 进入 `bgp-event-overlay-layer`;观测站 halo 和覆盖扇形进入 `bgp-collector-radar-layer` | BGP 事件和观测站都通过 `Interactable` 屏幕空间 picking,并参与同坐标避让 | BGP 观测站主图标与船只同层;BGP 事件与算力中心同层;向外扩散圈、观测站雷达/覆盖动画继续由 BGP 业务逻辑驱动。 |
|
||||
| 4.3 | AIS 船只轨迹线 | `vessels.js` | `VESSEL_RENDER_ORDER - 0.1`;`CONFIG.earthRadius + VESSEL_CONFIG.track.altitudeOffset` | 跟随船只显隐,不单独参与拾取 | 选中船只后显示最近轨迹,低于船只 marker。 |
|
||||
| 4.4 | AIS 船只 marker | `vessels.js`, `interactable.js` | `VESSEL_RENDER_ORDER`;业务高度为 `CONFIG.earthRadius + VESSEL_CONFIG.altitudeOffset`;普通 marker 为分桶 `THREE.Points`,hover / locked 为单点 `THREE.Points` overlay | `depthTest: true`;`main.js` 使用屏幕空间 picking,只取正面 marker;参与 Interactable 同坐标避让 | 航行船只用三角点纹理,停泊/低速用圆点;普通态无 glow,交互态叠加同尺寸 glow;低于算力中心 `4.5`。 |
|
||||
@@ -45,7 +45,7 @@
|
||||
| 高清材质 on | 恢复高清材质,并恢复记住的地形 / 昼夜状态。 |
|
||||
| 地形 on | 显示在高清材质之上,但低于国界 hover、footprint、卫星等强调层。 |
|
||||
| 大气云图 | 只控制云图 mesh 显隐。 |
|
||||
| 国界 | 控制国界线和 hover 线显隐;海陆基座填充独立存在,作为 Earth 基座地图使用。 |
|
||||
| 国界线 off | 只隐藏可交互国界线和 hover,高亮状态会清除;海陆基座填充仍作为 Earth 底图保留。 |
|
||||
|
||||
## 交互规则
|
||||
|
||||
|
||||
@@ -224,7 +224,7 @@ Earth 用于在一个地球视图中观察:
|
||||
- 卫星和轨迹
|
||||
- 海缆与登陆点
|
||||
- 算力中心
|
||||
- 国界、经纬线、高清材质、云图、地形
|
||||
- 国界线、经纬线、高清材质、云图、地形
|
||||
- 新闻直播和态势新闻
|
||||
- 搜索和聚焦对象详情
|
||||
|
||||
@@ -235,7 +235,7 @@ Earth 用于在一个地球视图中观察:
|
||||
常见图层包括:
|
||||
|
||||
- 经纬线
|
||||
- 国界
|
||||
- 国界线
|
||||
- 高清材质
|
||||
- 大气云图
|
||||
- 海缆
|
||||
@@ -260,7 +260,7 @@ Earth 用于在一个地球视图中观察:
|
||||
|
||||
- 海缆
|
||||
- 卫星
|
||||
- 国界
|
||||
- 国界线
|
||||
- 算力中心
|
||||
- BGP
|
||||
- AIS 船只
|
||||
|
||||
@@ -16,12 +16,13 @@
|
||||
## Current Version
|
||||
|
||||
- `main` 当前主线历史推导到:`0.16.5`
|
||||
- `dev` 当前开发分支历史推导到:`0.46.1`
|
||||
- `dev` 当前开发分支历史推导到:`0.46.2`
|
||||
|
||||
## Timeline
|
||||
|
||||
| Version | Type | Branch | Commit | Summary |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `0.46.2` | bugfix | `dev` | `pending` | 修复 Earth 启动加载顺序、图层 localStorage 恢复、国界线底图语义、媒体面板、船只轨迹和 Iridium footprint 显示问题,并补充 AIS 聚合计划 |
|
||||
| `0.46.1` | bugfix | `dev` | `pending` | 修复新增 Docs 技术文档未进前端白名单导致页面不可访问的问题,补齐英文文档并固化白名单/双语/裸文件标题检查 |
|
||||
| `0.46.0` | feature | `dev` | `pending` | Earth 新增通用 Interactable 图标层,统一船只、算力中心、BGP 事件/观测站交互图标,并优化登陆点与 toolbar 初始渲染 |
|
||||
| `0.45.0` | feature | `dev` | `pending` | 新增采集任务 fetching 阶段量化进度,收敛 AI Provider 运行期环境注入和 Docker build context |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "planet-frontend",
|
||||
"version": "0.46.1",
|
||||
"version": "0.46.2",
|
||||
"private": true,
|
||||
"packageManager": "bun@1",
|
||||
"dependencies": {
|
||||
|
||||
@@ -28,7 +28,7 @@ const _lpCameraToPoint = new THREE.Vector3();
|
||||
const LANDING_POINT_SPRITE_HEIGHT = 3;
|
||||
const LANDING_POINT_SPRITE_ASPECT = 1;
|
||||
const LANDING_POINT_SIZE_REFERENCE_FOV = 75;
|
||||
const LANDING_POINT_SIZE_SCALE_MIN = 0.36;
|
||||
const LANDING_POINT_SIZE_SCALE_MIN = 0.16;
|
||||
const LANDING_POINT_SIZE_SCALE_MAX = 3;
|
||||
const LANDING_POINT_ATLAS_CELL_SIZE = 128;
|
||||
let landingPointTexture = null;
|
||||
|
||||
@@ -232,7 +232,7 @@ export const VESSEL_CONFIG = {
|
||||
max: 2.4,
|
||||
},
|
||||
track: {
|
||||
altitudeOffset: 0.22,
|
||||
altitudeOffset: 0.2,
|
||||
color: 0x7dd3fc,
|
||||
opacity: 0.82,
|
||||
},
|
||||
|
||||
108
frontend/public/earth/js/controls.js
vendored
108
frontend/public/earth/js/controls.js
vendored
@@ -19,6 +19,7 @@ import {
|
||||
import {
|
||||
toggleTerrain,
|
||||
setDayNightEnabled,
|
||||
toggleClouds,
|
||||
toggleGridLines,
|
||||
getShowGridLines,
|
||||
} from "./earth.js";
|
||||
@@ -53,7 +54,7 @@ import {
|
||||
} from "./satellites.js";
|
||||
import { getShowCables } from "./cables.js";
|
||||
import { toggleBGP, getShowBGP, getBGPCount } from "./bgp.js";
|
||||
import { getShowCountryBoundaries } from "./country-boundaries.js";
|
||||
import { getShowCountryBoundaries, toggleCountryBoundaries } from "./country-boundaries.js";
|
||||
import {
|
||||
toggleComputeCenters,
|
||||
getShowComputeCenters,
|
||||
@@ -136,6 +137,7 @@ let focusViewAnimationToken = 0;
|
||||
let earthSettingsDefaults = null;
|
||||
let lastZoomStatusUpdateTime = 0;
|
||||
let earthSettingsState = null;
|
||||
let deferredLayerVisibilitySettings = null;
|
||||
let layerRegistry = new Map();
|
||||
let layerPanelInitialized = false;
|
||||
let layoutMode = "desktop";
|
||||
@@ -269,7 +271,7 @@ function closeTransientMobileOverlays({ except = null } = {}) {
|
||||
setMobileDrawerOpen("layer-toggles", false);
|
||||
}
|
||||
|
||||
if (except !== "media" && isTVPanelVisible()) {
|
||||
if (except !== "media" && except !== "search" && isTVPanelVisible()) {
|
||||
setTVPanelVisible(false);
|
||||
}
|
||||
}
|
||||
@@ -670,6 +672,14 @@ function shouldIncludeLayerInStartupLoad(definition) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const persistedVisible = getPersistedLayerVisibilityOverride(definition.id);
|
||||
if (typeof persistedVisible === "boolean") {
|
||||
if (definition.startupMode === "preload" && definition.startupAlwaysLoad) {
|
||||
return true;
|
||||
}
|
||||
return persistedVisible;
|
||||
}
|
||||
|
||||
if (definition.startupMode === "preload") {
|
||||
return true;
|
||||
}
|
||||
@@ -677,6 +687,14 @@ function shouldIncludeLayerInStartupLoad(definition) {
|
||||
return Boolean(definition?.getVisible?.());
|
||||
}
|
||||
|
||||
function getPersistedLayerVisibilityOverride(layerId) {
|
||||
if (!layerId) return null;
|
||||
const layerVisibility =
|
||||
deferredLayerVisibilitySettings || earthSettingsState?.shared?.layerVisibility;
|
||||
const persistedVisible = layerVisibility?.[layerId];
|
||||
return typeof persistedVisible === "boolean" ? persistedVisible : null;
|
||||
}
|
||||
|
||||
function clampEarthZoomLevel(nextZoom) {
|
||||
const parsedZoom = Number.parseFloat(nextZoom);
|
||||
if (!Number.isFinite(parsedZoom)) {
|
||||
@@ -1127,7 +1145,7 @@ function setDefaultEarthZoom(nextZoom, { persist = true, applyToCurrentView = tr
|
||||
return defaultEarthZoom;
|
||||
}
|
||||
|
||||
async function applyEarthSettings(settings) {
|
||||
async function applyEarthSettings(settings, { applyLayers = true } = {}) {
|
||||
if (!settings) return;
|
||||
earthSettingsState = cloneEarthSettings(settings);
|
||||
|
||||
@@ -1161,12 +1179,31 @@ async function applyEarthSettings(settings) {
|
||||
applyToCurrentView: true,
|
||||
});
|
||||
|
||||
if (!applyLayers) {
|
||||
const layerVisibility = { ...(settings.shared.layerVisibility || {}) };
|
||||
applyImmediateLayerVisibilityHints(layerVisibility);
|
||||
deferredLayerVisibilitySettings = layerVisibility;
|
||||
return;
|
||||
}
|
||||
|
||||
deferredLayerVisibilitySettings = null;
|
||||
await applyLayerVisibilitySettings(settings.shared.layerVisibility, {
|
||||
persist: false,
|
||||
silent: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function applyDeferredLayerVisibilitySettings(options = {}) {
|
||||
const layerVisibility = deferredLayerVisibilitySettings;
|
||||
deferredLayerVisibilitySettings = null;
|
||||
if (!layerVisibility) return;
|
||||
await applyLayerVisibilitySettings(layerVisibility, {
|
||||
persist: false,
|
||||
silent: true,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
function resetEarthSettings() {
|
||||
const defaults = cloneEarthSettings(captureEarthSettingsDefaults());
|
||||
earthSettingsState = cloneEarthSettings(defaults);
|
||||
@@ -1307,8 +1344,15 @@ async function setCountryBoundariesLayerEnabled(button, enabled, { persist = tru
|
||||
}
|
||||
}
|
||||
|
||||
function setHighResTextureLayerEnabled(button, enabled, { persist = true, silent = false } = {}) {
|
||||
setHighResTextureEnabled(enabled, { suppressStatus: silent });
|
||||
async function setHighResTextureLayerEnabled(button, enabled, { persist = true, silent = false } = {}) {
|
||||
if (enabled) {
|
||||
setLayerButtonState(button, {
|
||||
active: false,
|
||||
loading: true,
|
||||
tooltip: "高清材质加载中...",
|
||||
});
|
||||
}
|
||||
await setHighResTextureEnabled(enabled, { suppressStatus: silent });
|
||||
setLayerButtonState(button, {
|
||||
active: enabled,
|
||||
loading: false,
|
||||
@@ -1319,8 +1363,15 @@ function setHighResTextureLayerEnabled(button, enabled, { persist = true, silent
|
||||
return enabled;
|
||||
}
|
||||
|
||||
function setAtmosphereCloudsLayerEnabled(button, enabled, { persist = true, silent = false } = {}) {
|
||||
setAtmosphereCloudsEnabled(enabled, { suppressStatus: silent });
|
||||
async function setAtmosphereCloudsLayerEnabled(button, enabled, { persist = true, silent = false } = {}) {
|
||||
if (enabled) {
|
||||
setLayerButtonState(button, {
|
||||
active: false,
|
||||
loading: true,
|
||||
tooltip: "大气云图加载中...",
|
||||
});
|
||||
}
|
||||
await setAtmosphereCloudsEnabled(enabled, { suppressStatus: silent });
|
||||
setLayerButtonState(button, {
|
||||
active: enabled,
|
||||
loading: false,
|
||||
@@ -1449,6 +1500,44 @@ async function applyLayerVisibilitySettings(layerVisibility = {}, options = {})
|
||||
}
|
||||
}
|
||||
|
||||
function applyImmediateLayerVisibilityHints(layerVisibility = {}) {
|
||||
if (typeof layerVisibility.gridLines === "boolean") {
|
||||
setGridLinesLayerEnabled(getLayerButton("gridLines"), layerVisibility.gridLines, {
|
||||
persist: false,
|
||||
silent: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof layerVisibility.countryBoundaries === "boolean") {
|
||||
toggleCountryBoundaries(layerVisibility.countryBoundaries, {
|
||||
showLandFill: true,
|
||||
});
|
||||
setLayerButtonState(getLayerButton("countryBoundaries"), {
|
||||
active: layerVisibility.countryBoundaries,
|
||||
loading: false,
|
||||
tooltip: layerVisibility.countryBoundaries ? "隐藏国界线" : "显示国界线",
|
||||
});
|
||||
}
|
||||
|
||||
if (layerVisibility.earthHighResTexture === false) {
|
||||
void setHighResTextureEnabled(false, { suppressStatus: true });
|
||||
setLayerButtonState(getLayerButton("earthHighResTexture"), {
|
||||
active: false,
|
||||
loading: false,
|
||||
tooltip: "显示高清材质",
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof layerVisibility.atmosphereClouds === "boolean") {
|
||||
toggleClouds(layerVisibility.atmosphereClouds);
|
||||
setLayerButtonState(getLayerButton("atmosphereClouds"), {
|
||||
active: layerVisibility.atmosphereClouds,
|
||||
loading: false,
|
||||
tooltip: layerVisibility.atmosphereClouds ? "隐藏大气云图" : "显示大气云图",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getBuiltinLayerDefinitions() {
|
||||
return [
|
||||
{
|
||||
@@ -1472,13 +1561,14 @@ function getBuiltinLayerDefinitions() {
|
||||
id: "countryBoundaries",
|
||||
buttonId: "toggle-country-boundaries",
|
||||
icon: "public",
|
||||
label: "国界",
|
||||
label: "国界线",
|
||||
meta: "Country Borders",
|
||||
keywords: "国界 国家 borders countries boundary",
|
||||
defaultActive: true,
|
||||
displayOrder: 90,
|
||||
startupPriority: 20,
|
||||
startupMode: "preload",
|
||||
startupAlwaysLoad: true,
|
||||
startupLabel: "海陆基座",
|
||||
startupMessage: "正在加载海陆基座...",
|
||||
getVisible: () => getShowCountryBoundaries(),
|
||||
@@ -2268,7 +2358,7 @@ function setupSettingsControls() {
|
||||
});
|
||||
|
||||
captureEarthSettingsDefaults();
|
||||
settingsApplyPromise = applyEarthSettings(loadEarthSettings());
|
||||
settingsApplyPromise = applyEarthSettings(loadEarthSettings(), { applyLayers: false });
|
||||
syncAllHudPanelToggles();
|
||||
syncRotationModeButtons();
|
||||
syncCruiseModuleControls();
|
||||
|
||||
@@ -25,8 +25,11 @@ let _earthTextureOverlayMaterial = null;
|
||||
let _earthShaders = [];
|
||||
let _dayNightEnabled = true;
|
||||
let _loadedTexture = null;
|
||||
let _textureLoadPromise = null;
|
||||
let _textureVisible = true;
|
||||
let _earthRimGlow = null;
|
||||
let _cloudTexture = null;
|
||||
let _cloudTextureLoadPromise = null;
|
||||
const _earthSunDirection = new THREE.Vector3(
|
||||
EARTH_MATERIAL_CONFIG.dayNight.sunDirection.x,
|
||||
EARTH_MATERIAL_CONFIG.dayNight.sunDirection.y,
|
||||
@@ -301,28 +304,16 @@ export function createClouds(scene, earthObj) {
|
||||
|
||||
clouds = new THREE.Mesh(geometry, material);
|
||||
clouds.name = "earth-atmosphere-clouds";
|
||||
clouds.visible = showClouds;
|
||||
clouds.visible = false;
|
||||
earthObj.add(clouds);
|
||||
|
||||
textureLoader.load(
|
||||
CLOUD_LAYER_CONFIG.textureUrl,
|
||||
function(texture) {
|
||||
material.map = texture;
|
||||
material.needsUpdate = true;
|
||||
},
|
||||
undefined,
|
||||
function(err) {
|
||||
console.log('云层纹理加载失败');
|
||||
}
|
||||
);
|
||||
|
||||
return clouds;
|
||||
}
|
||||
|
||||
export function toggleClouds(visible) {
|
||||
showClouds = Boolean(visible);
|
||||
if (clouds) {
|
||||
clouds.visible = showClouds;
|
||||
clouds.visible = showClouds && Boolean(clouds.material?.map);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,6 +321,39 @@ export function getShowClouds() {
|
||||
return showClouds;
|
||||
}
|
||||
|
||||
export function loadCloudTexture() {
|
||||
if (_cloudTexture) return Promise.resolve(_cloudTexture);
|
||||
if (_cloudTextureLoadPromise) return _cloudTextureLoadPromise;
|
||||
|
||||
_cloudTextureLoadPromise = new Promise((resolve, reject) => {
|
||||
if (!clouds?.material) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
textureLoader.load(
|
||||
CLOUD_LAYER_CONFIG.textureUrl,
|
||||
(texture) => {
|
||||
_cloudTexture = texture;
|
||||
clouds.material.map = texture;
|
||||
clouds.material.needsUpdate = true;
|
||||
clouds.visible = showClouds;
|
||||
resolve(texture);
|
||||
},
|
||||
undefined,
|
||||
(error) => {
|
||||
console.warn("云层纹理加载失败");
|
||||
reject(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
_cloudTextureLoadPromise.finally(() => {
|
||||
_cloudTextureLoadPromise = null;
|
||||
});
|
||||
return _cloudTextureLoadPromise;
|
||||
}
|
||||
|
||||
export function createTerrain(earthObj) {
|
||||
const geometry = new THREE.SphereGeometry(
|
||||
CONFIG.earthRadius + TERRAIN_CONFIG.baseRadiusOffset,
|
||||
@@ -478,6 +502,7 @@ export function getClouds() {
|
||||
|
||||
export function clearEarthTexture() {
|
||||
_loadedTexture = null;
|
||||
_textureLoadPromise = null;
|
||||
if (_earthTextureOverlayMaterial) {
|
||||
_earthTextureOverlayMaterial.map = null;
|
||||
_earthTextureOverlayMaterial.needsUpdate = true;
|
||||
@@ -522,7 +547,10 @@ export function setDayNightEnabled(enabled) {
|
||||
}
|
||||
|
||||
export function loadEarthTexture() {
|
||||
return new Promise((resolve) => {
|
||||
if (_loadedTexture) return Promise.resolve(_loadedTexture);
|
||||
if (_textureLoadPromise) return _textureLoadPromise;
|
||||
|
||||
_textureLoadPromise = new Promise((resolve) => {
|
||||
if (!_earthTextureOverlayMaterial) { resolve(); return; }
|
||||
|
||||
const urls = EARTH_MATERIAL_CONFIG.textureUrls;
|
||||
@@ -549,7 +577,7 @@ export function loadEarthTexture() {
|
||||
if (_earthRimGlow) {
|
||||
_earthRimGlow.visible = !_textureVisible;
|
||||
}
|
||||
resolve();
|
||||
resolve(texture);
|
||||
},
|
||||
null,
|
||||
() => tryLoad(index + 1),
|
||||
@@ -557,6 +585,11 @@ export function loadEarthTexture() {
|
||||
};
|
||||
tryLoad(0);
|
||||
});
|
||||
|
||||
_textureLoadPromise.finally(() => {
|
||||
_textureLoadPromise = null;
|
||||
});
|
||||
return _textureLoadPromise;
|
||||
}
|
||||
|
||||
export function setEarthTextureVisible(visible) {
|
||||
|
||||
@@ -23,6 +23,10 @@ import {
|
||||
loadVessels,
|
||||
toggleVessels,
|
||||
} from "./vessels.js";
|
||||
import {
|
||||
loadCloudTexture,
|
||||
loadEarthTexture,
|
||||
} from "./earth.js";
|
||||
import {
|
||||
getCountryBoundaryLegendItems,
|
||||
loadCountryBoundaries,
|
||||
@@ -83,6 +87,8 @@ export function registerLayerStartupTask(id, taskFactory) {
|
||||
function registerBuiltinLayerStartupTasks() {
|
||||
startupTaskRegistry.clear();
|
||||
registerCountryBoundaryStartupTask();
|
||||
registerEarthTextureStartupTask();
|
||||
registerCloudStartupTask();
|
||||
registerCableStartupTask();
|
||||
registerComputeCenterStartupTask();
|
||||
registerVesselStartupTask();
|
||||
@@ -210,6 +216,42 @@ function registerBGPStartupTask() {
|
||||
});
|
||||
}
|
||||
|
||||
function registerEarthTextureStartupTask() {
|
||||
registerLayerStartupTask("earthHighResTexture", (context) => async (layer) => {
|
||||
if (!context.isEarthTextureVisible()) return;
|
||||
|
||||
context.setLoadingMessage(
|
||||
resolveStartupMessage(layer, "load", "正在加载地球纹理..."),
|
||||
);
|
||||
await context.yieldFrame(12);
|
||||
try {
|
||||
await loadEarthTexture();
|
||||
} catch (error) {
|
||||
console.warn("地球纹理加载失败:", error);
|
||||
}
|
||||
if (context.isCancelled()) return;
|
||||
await context.yieldFrame(16);
|
||||
});
|
||||
}
|
||||
|
||||
function registerCloudStartupTask() {
|
||||
registerLayerStartupTask("atmosphereClouds", (context) => async (layer) => {
|
||||
if (!context.isCloudsEnabled()) return;
|
||||
|
||||
context.setLoadingMessage(
|
||||
resolveStartupMessage(layer, "load", "正在加载大气云图..."),
|
||||
);
|
||||
await context.yieldFrame(12);
|
||||
try {
|
||||
await loadCloudTexture();
|
||||
} catch (error) {
|
||||
context.reportError(layer?.startupLabel || layer?.label || "大气云图", error);
|
||||
}
|
||||
if (context.isCancelled()) return;
|
||||
await context.yieldFrame(16);
|
||||
});
|
||||
}
|
||||
|
||||
function registerCountryBoundaryStartupTask() {
|
||||
registerLayerStartupTask("countryBoundaries", (context) => async (layer) => {
|
||||
context.setLoadingMessage(
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
createGridLines,
|
||||
getEarth,
|
||||
getEarthSurfacePickTarget,
|
||||
loadCloudTexture,
|
||||
loadEarthTexture,
|
||||
clearEarthTexture,
|
||||
setEarthSunDirection,
|
||||
@@ -203,6 +204,7 @@ import {
|
||||
setDayNightEnabledExternal,
|
||||
setTerrainLayerInteractable,
|
||||
setDayNightInteractable,
|
||||
applyDeferredLayerVisibilitySettings,
|
||||
} from "./controls.js";
|
||||
import {
|
||||
createLayerStartupTaskMap,
|
||||
@@ -1470,23 +1472,7 @@ function formatBGPStatusFromSummary(summary) {
|
||||
return "当前无活跃事件";
|
||||
}
|
||||
|
||||
function applyEarthStatsSummary(summary) {
|
||||
if (!summary) return;
|
||||
updateEarthStats({
|
||||
cableCount: `${summary.cableCount}个`,
|
||||
landingPointCount: `${summary.landingPointCount}个`,
|
||||
satelliteCount: `${summary.satelliteCount} 颗`,
|
||||
computeCenterCount: `${summary.computeCenterCount} 个`,
|
||||
vesselCount: `${summary.vesselCount} 艘`,
|
||||
bgpAnomalyCount: `${summary.bgpEventCount} 起`,
|
||||
bgpCollectorCount: `${summary.bgpCollectorCount} 个`,
|
||||
bgpStatusSummary: formatBGPStatusFromSummary(summary),
|
||||
terrainOn: getShowTerrain(),
|
||||
textureQuality: "8K 卫星图",
|
||||
});
|
||||
}
|
||||
|
||||
async function loadEarthStatsSummary() {
|
||||
async function loadEarthStatsSummary({ shouldApply = () => true } = {}) {
|
||||
try {
|
||||
const response = await fetch(PATHS.earthSummaryApi);
|
||||
if (!response.ok) {
|
||||
@@ -1505,7 +1491,9 @@ async function loadEarthStatsSummary() {
|
||||
bgpAnomalyCount: toCount(stats.bgp_anomaly_count),
|
||||
bgpCollectorCount: toCount(stats.bgp_collector_count),
|
||||
};
|
||||
applyEarthStatsSummary(earthStatsSummary);
|
||||
if (shouldApply()) {
|
||||
updateStatsSummary();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("全球态势聚合统计加载失败:", error);
|
||||
}
|
||||
@@ -2594,24 +2582,14 @@ async function loadData() {
|
||||
await yieldFrame(18);
|
||||
if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
|
||||
|
||||
setLoadingMessage("正在读取全球态势统计...");
|
||||
await loadEarthStatsSummary();
|
||||
if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
|
||||
await yieldFrame(12);
|
||||
loadEarthStatsSummary({
|
||||
shouldApply: () => loadToken === currentLoadToken && !destroyed,
|
||||
}).catch((error) => {
|
||||
console.warn("后台刷新全球态势统计失败:", error);
|
||||
});
|
||||
|
||||
const errors = [];
|
||||
|
||||
// Step 1 — Earth texture
|
||||
setLoadingMessage("正在加载地球纹理...");
|
||||
await yieldFrame(12);
|
||||
try {
|
||||
await loadEarthTexture();
|
||||
} catch (err) {
|
||||
// texture failure is non-fatal
|
||||
}
|
||||
if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
|
||||
await yieldFrame(16);
|
||||
|
||||
const startupLoaders = createLayerStartupTaskMap({
|
||||
scene,
|
||||
earth,
|
||||
@@ -2630,6 +2608,7 @@ async function loadData() {
|
||||
getShowCountryBoundaries,
|
||||
getShowBGP,
|
||||
isEarthTextureVisible: () => getEarthTextureVisible(),
|
||||
isCloudsEnabled: () => getShowClouds(),
|
||||
getInitialSatelliteLoadLimit,
|
||||
shouldHydrateFullSatelliteSet,
|
||||
scheduleSatellitePositionWarmup,
|
||||
@@ -2704,6 +2683,10 @@ async function loadData() {
|
||||
hideError();
|
||||
queueStatusMessage("数据已加载", "success");
|
||||
}
|
||||
|
||||
applyDeferredLayerVisibilitySettings().catch((error) => {
|
||||
console.warn("恢复 Earth 图层可见性失败:", error);
|
||||
});
|
||||
}
|
||||
|
||||
const POSITION_UPDATE_FORCE_DELTA = 250;
|
||||
@@ -2827,8 +2810,17 @@ export async function setCountryBoundariesEnabled(
|
||||
let _dayNightBeforeTextureOff = null;
|
||||
let _terrainBeforeTextureOff = null;
|
||||
|
||||
export function setHighResTextureEnabled(enabled, { suppressStatus = false } = {}) {
|
||||
export async function setHighResTextureEnabled(enabled, { suppressStatus = false } = {}) {
|
||||
setEarthTextureVisible(enabled);
|
||||
if (enabled) {
|
||||
try {
|
||||
await loadEarthTexture();
|
||||
setEarthTextureVisible(true);
|
||||
} catch (error) {
|
||||
console.warn("高清材质加载失败:", error);
|
||||
setEarthTextureVisible(false);
|
||||
}
|
||||
}
|
||||
setSurfaceTintEnabled(!enabled);
|
||||
setLandFillEnabled(true);
|
||||
setLandFillSuppressed(false);
|
||||
@@ -2863,8 +2855,17 @@ export function getHighResTextureEnabled() {
|
||||
return getEarthTextureVisible();
|
||||
}
|
||||
|
||||
export function setAtmosphereCloudsEnabled(enabled, { suppressStatus = false } = {}) {
|
||||
export async function setAtmosphereCloudsEnabled(enabled, { suppressStatus = false } = {}) {
|
||||
toggleClouds(enabled);
|
||||
if (enabled) {
|
||||
try {
|
||||
await loadCloudTexture();
|
||||
toggleClouds(true);
|
||||
} catch (error) {
|
||||
console.warn("大气云图加载失败:", error);
|
||||
toggleClouds(false);
|
||||
}
|
||||
}
|
||||
if (!suppressStatus) {
|
||||
showStatusMessage(enabled ? "大气云图已显示" : "大气云图已隐藏", "info");
|
||||
}
|
||||
|
||||
@@ -2139,7 +2139,7 @@ function showIridiumReservedStyle(position) {
|
||||
lockedIridiumFootprintMesh = createIridiumFootprintAdapter({
|
||||
earthObj: earthObjRef,
|
||||
earthRadiusWorld: CONFIG.earthRadius,
|
||||
renderOrder: 0,
|
||||
renderOrder: GROUND_FOOTPRINT_RENDER_ORDER,
|
||||
});
|
||||
updateIridiumReservedStyle(position);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ const VESSEL_RENDER_ORDER = 4.4;
|
||||
const VESSEL_POINT_SIZE = 34;
|
||||
const VESSEL_ATLAS_CELL_SIZE = 128;
|
||||
const VESSEL_COURSE_BINS = 32;
|
||||
const VESSEL_TRACK_ENDPOINT_EPSILON = 0.001;
|
||||
|
||||
function normalizeVesselType(value, code) {
|
||||
const type = String(value || "").trim().toLowerCase();
|
||||
@@ -72,6 +73,26 @@ function getCourseBin(marker) {
|
||||
return Math.round((normalized / 360) * VESSEL_COURSE_BINS) % VESSEL_COURSE_BINS;
|
||||
}
|
||||
|
||||
function buildTrackPoint(lon, lat) {
|
||||
const latitude = Number(lat);
|
||||
const longitude = Number(lon);
|
||||
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null;
|
||||
return latLonToVector3(
|
||||
latitude,
|
||||
longitude,
|
||||
CONFIG.earthRadius + VESSEL_CONFIG.track.altitudeOffset,
|
||||
);
|
||||
}
|
||||
|
||||
function appendCurrentMarkerTrackPoint(points, marker) {
|
||||
if (!(marker?.position instanceof THREE.Vector3)) return;
|
||||
const markerPosition = marker.position.clone();
|
||||
const lastPoint = points[points.length - 1];
|
||||
if (!lastPoint || lastPoint.distanceToSquared(markerPosition) > VESSEL_TRACK_ENDPOINT_EPSILON) {
|
||||
points.push(markerPosition);
|
||||
}
|
||||
}
|
||||
|
||||
const vesselIconLayer = createInteractableLayer({
|
||||
id: "vessels",
|
||||
objectType: "vessel",
|
||||
@@ -207,14 +228,15 @@ export async function showVesselTrack(marker, earth) {
|
||||
if (coordinates.length < 2) return null;
|
||||
|
||||
const points = coordinates
|
||||
.map(([lon, lat]) =>
|
||||
latLonToVector3(
|
||||
Number(lat),
|
||||
Number(lon),
|
||||
CONFIG.earthRadius + VESSEL_CONFIG.track.altitudeOffset,
|
||||
),
|
||||
)
|
||||
.filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y) && Number.isFinite(point.z));
|
||||
.map(([lon, lat]) => buildTrackPoint(lon, lat))
|
||||
.filter(
|
||||
(point) =>
|
||||
point &&
|
||||
Number.isFinite(point.x) &&
|
||||
Number.isFinite(point.y) &&
|
||||
Number.isFinite(point.z),
|
||||
);
|
||||
appendCurrentMarkerTrackPoint(points, marker);
|
||||
if (points.length < 2) return null;
|
||||
|
||||
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "planet"
|
||||
version = "0.46.1"
|
||||
version = "0.46.2"
|
||||
description = "智能星球计划 - 态势感知系统"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
|
||||
Reference in New Issue
Block a user