Compare commits

...

2 Commits

Author SHA1 Message Date
linkong
f22079d33a release: bump version to 0.46.3 2026-04-30 14:46:19 +08:00
linkong
9f737fdb89 release: bump version to 0.46.2 2026-04-30 14:30:12 +08:00
24 changed files with 642 additions and 159 deletions

View File

@@ -1 +1 @@
0.46.1 0.46.3

View File

@@ -8,6 +8,30 @@ This project follows the repository versioning rule:
- `improvement` -> `+0.0.1`bugfix + 小功能混合) - `improvement` -> `+0.0.1`bugfix + 小功能混合)
- `bugfix` -> `+0.0.1` - `bugfix` -> `+0.0.1`
## [0.46.3] — 2026-04-30
Released: 2026-04-30
### 🐛 Fixes
- 优化 Starlink footprint 显示后的地球拖拽性能,避免旋转地球时每帧重建 footprint 大网格,同时保持现有视觉效果不变。
- 恢复点击线缆后的呼吸透明度动画,让 locked / hover 线缆重新使用既有 pulse 配置。
---
## [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 ## [0.46.1] — 2026-04-30
Released: 2026-04-30 Released: 2026-04-30

View File

@@ -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-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-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) - [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) - [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-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) - [frontend-ai-playground-development-plan.md](/home/ray/dev/linkong/planet/docs/plans/frontend-ai-playground-development-plan.md)

View File

@@ -272,7 +272,8 @@ hover / locked 使用少量 overlay
### Phase 3迁移算力中心并评估登陆点 ### Phase 3迁移算力中心并评估登陆点
- 算力中心保留现有业务 icon但接入统一 hover / locked / glow。已完成 - 算力中心保留现有业务 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而不是依赖渲染对象类型。 - 检查图例、搜索和 info-card 是否只依赖业务 payload而不是依赖渲染对象类型。
### Phase 4形成 Earth 图标层规范 ### Phase 4形成 Earth 图标层规范

View 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)

View File

@@ -12,7 +12,7 @@
| 船只规模 | BarentsWatch 阶段全部显示;全球数据接入后按需加船型过滤(默认 Cargo + Tanker + Passenger | | 船只规模 | BarentsWatch 阶段全部显示;全球数据接入后按需加船型过滤(默认 Cargo + Tanker + Passenger |
| 更新频率 | 准实时:前端 5 分钟轮询,后端 Collector 每分钟拉取写库 | | 更新频率 | 准实时:前端 5 分钟轮询,后端 Collector 每分钟拉取写库 |
| 历史轨迹 | 保留(`vessel_position` 表保留 24h后期按需扩展 | | 历史轨迹 | 保留(`vessel_position` 表保留 24h后期按需扩展 |
| 推送方式 | HTTP 轮询(不用 WebSocket换实时数据源后再评估升级 | | 推送方式 | 前端展示仍可先用 HTTP 拉取聚合结果AISStream 等实时源应单独实现 WebSocket 采集器 |
--- ---
@@ -36,13 +36,19 @@
- 字段mmsi, lat, lon, sog, cog, heading, nav_status, name, vessel_type, flag - 字段mmsi, lat, lon, sog, cog, heading, nav_status, name, vessel_type, flag
- 刷新频率:数据约 3060s 更新一次,可随意轮询 - 刷新频率:数据约 3060s 更新一次,可随意轮询
### TODO付费数据源接入 ### TODO多源 AIS 与实时流接入
- [ ] 接入 AISStream WebSocket 采集器,作为 BarentsWatch 覆盖不足的实时补充
- [ ] 将 BarentsWatch、AISStream、自定义 `vessel_ais` 映射源统一写入原始观测层
- [ ] 通过聚合接口做去重、字段合并、冲突记录和默认来源选择
- [ ] 开放字段级聚合策略配置,让用户决定不同字段优先信任哪个来源
- [ ] 评估 AISHub 订阅(全球覆盖,约 $30/月),接入全球实时流 - [ ] 评估 AISHub 订阅(全球覆盖,约 $30/月),接入全球实时流
- [ ] 评估 MarineTraffic API tier对比 AISHub 数据质量与成本 - [ ] 评估 MarineTraffic API tier对比 AISHub 数据质量与成本
- [ ] 实现多数据源适配器,通过 `datasource_config` 切换 - [ ] 实现多数据源适配器,通过 `datasource_config` 切换
- [ ] 真实高频 AIS 稳定接入后,评估将 `vessel_position` 迁移为 TimescaleDB hypertable保留 Postgres 原生分区作为备选) - [ ] 真实高频 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 更新机制 #### 1.4 更新机制
**HTTP 轮询**(不使用 WebSocket **前端聚合结果拉取 + 后端实时采集**
- 前端 `setInterval(fetchVessels, 5 * 60 * 1000)` 定期拉取最新快照 - 前端 `setInterval(fetchVessels, 5 * 60 * 1000)` 定期拉取最新快照
- 后端 Collector 每 60s 从 BarentsWatch 拉取并写库,`vessel_latest` 物化视图随时可查 - 后端 BarentsWatch collector 继续以 HTTP polling 方式采集
- WebSocket 留给告警/事件驱动场景BGP、系统通知不混入周期性位置刷新 - AISStream 等实时源以独立 WebSocket collector 写入原始观测层
- 换用 AISHub / MarineTraffic 实时流后,届时再评估是否升级为 WebSocket delta push - 展示接口从聚合服务读取当前船只视图,而不是由单个 collector 决定最终展示值
- 前端是否升级为 WebSocket delta push 是独立优化,不影响后端采集器可以使用 WebSocket 接上游实时源
--- ---

View File

@@ -74,6 +74,8 @@ This document records the material, color, opacity, line width, radius offset, a
## Land/Ocean Base and Country Borders ## 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 | | Name | Variable | Current Value | Location / Notes |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| Country border data path | `COUNTRY_BOUNDARY_CONFIG.dataPath` | `"/earth/data/countries-admin0.min.geojson"` | GeoJSON input | | 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 color | `COUNTRY_BOUNDARY_CONFIG.lineColor` | `0x7fc7ff` | Normal border line |
| Border line opacity | `COUNTRY_BOUNDARY_CONFIG.lineOpacity` | `0.58` | Normal border line opacity | | 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 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 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 color | `COUNTRY_BOUNDARY_CONFIG.hoverLineColor` | `0xff3b1f` | Neon red-orange |
| Border hover opacity | `COUNTRY_BOUNDARY_CONFIG.hoverLineOpacity` | `1.0` | Hover line opacity | | 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 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 opacity | `COUNTRY_BOUNDARY_CONFIG.hoverGlowOpacity` | `0.38` | Glow line opacity |
| Border hover glow line width | `COUNTRY_BOUNDARY_CONFIG.hoverGlowLineWidth` | `3` | Glow `LineBasicMaterial.linewidth` | | 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 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 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 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 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 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 color | `CABLE_CONFIG.landingPoint.color` | `0xffaa00` | `SpriteMaterial.color` |
| Landing point opacity | `CABLE_CONFIG.landingPoint.opacity` | `1.0` | `SpriteMaterial.opacity` | | 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 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 | | 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 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 | | 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 | | Satellite trail line width | `SATELLITE_CONFIG.trailLineWidth` | `3` | Ribbon shader uniform |
| Selected ring size | `SATELLITE_CONFIG.ringSize` | `0.07` | Hover / locked ring sprite | | Selected ring size | `SATELLITE_CONFIG.ringSize` | `0.07` | Hover / locked ring sprite |
| Satellite overlay renderOrder | `SATELLITE_CONFIG.overlayRenderOrder` | `12` | Locked ring / halo / orbit | | 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 ## Compute Centers

View File

@@ -6,8 +6,8 @@ Note: the layer control panel order and the registration / startup load order ar
| Order type | Current sequence | Notes | | 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. | | 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 → 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. | | 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 ## 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.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.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. | | 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. | | 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. | | 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. | | 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. | | 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. | | Terrain on | Displayed above HD texture, but below country border hover, footprints, satellites, and other emphasis layers. |
| Cloud layer | Only controls cloud mesh visibility. | | 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 ## Interaction Rules

View File

@@ -203,7 +203,7 @@ Earth is used to observe in a single globe view:
- Submarine cables and landing points - Submarine cables and landing points
- Compute centers - Compute centers
- AIS vessels - 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 - Live news streams and situational news
- Search and focused object details - Search and focused object details
@@ -214,7 +214,7 @@ The right-side layer panel toggles visualization layers on or off.
Common layers include: Common layers include:
- Grid lines - Grid lines
- Country borders - Border lines
- HD texture - HD texture
- Atmospheric cloud layer - Atmospheric cloud layer
- Submarine cables - Submarine cables
@@ -239,7 +239,7 @@ Current legend modes include:
- Cables - Cables
- Satellites - Satellites
- Country borders - Border lines
- Compute centers - Compute centers
- BGP - BGP
- AIS vessels - AIS vessels

View File

@@ -80,6 +80,8 @@
## 海陆基座与国界 ## 海陆基座与国界
海陆基座是 Earth 的底图资产随启动预加载图层面板里的“国界线”只控制普通国界线、hover 线和可交互 hover。
| 正式名称 | 变量名 | 当前值 | 使用位置 / 说明 | | 正式名称 | 变量名 | 当前值 | 使用位置 / 说明 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| 国界数据路径 | `COUNTRY_BOUNDARY_CONFIG.dataPath` | `"/earth/data/countries-admin0.min.geojson"` | GeoJSON 输入 | | 国界数据路径 | `COUNTRY_BOUNDARY_CONFIG.dataPath` | `"/earth/data/countries-admin0.min.geojson"` | GeoJSON 输入 |
@@ -149,7 +151,7 @@
| 登陆点半径偏移 | `CABLE_CONFIG.landingPoint.altitudeOffset` | `0.2` | 与海缆线同层贴地,避免凌空 | | 登陆点半径偏移 | `CABLE_CONFIG.landingPoint.altitudeOffset` | `0.2` | 与海缆线同层贴地,避免凌空 |
| 登陆点 sprite 高度 | local `LANDING_POINT_SPRITE_HEIGHT` | `3` | `THREE.Sprite` 基准高度 | | 登陆点 sprite 高度 | local `LANDING_POINT_SPRITE_HEIGHT` | `3` | `THREE.Sprite` 基准高度 |
| 登陆点缩放参考 FOV | local `LANDING_POINT_SIZE_REFERENCE_FOV` | `75` | 与当前 Earth 相机 FOV 一致 | | 登陆点缩放参考 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` | | 登陆点缩放上限 | local `LANDING_POINT_SIZE_SCALE_MAX` | `3` | 远距离时的最大倍率;当前最小缩放约只能到 `2.50` |
| 登陆点 atlas 尺寸 | local `LANDING_POINT_ATLAS_CELL_SIZE` | `128` | canvas 扁平立体球纹理尺寸 | | 登陆点 atlas 尺寸 | local `LANDING_POINT_ATLAS_CELL_SIZE` | `128` | canvas 扁平立体球纹理尺寸 |
| 登陆点颜色 | `CABLE_CONFIG.landingPoint.color` | `0xffaa00` | `SpriteMaterial.color` | | 登陆点颜色 | `CABLE_CONFIG.landingPoint.color` | `0xffaa00` | `SpriteMaterial.color` |
@@ -180,7 +182,7 @@
| 卫星覆盖层 renderOrder | `SATELLITE_CONFIG.overlayRenderOrder` | `12` | locked ring / halo / orbit | | 卫星覆盖层 renderOrder | `SATELLITE_CONFIG.overlayRenderOrder` | `12` | locked ring / halo / orbit |
| 自发光选中点颜色 | inline default | `"#ffd25a"` | `showSelfGlowStyle()` | | 自发光选中点颜色 | inline default | `"#ffd25a"` | `showSelfGlowStyle()` |
| 自发光选中点透明度 | inline | `0.96` | locked dot material | | 自发光选中点透明度 | 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 排序盖过卫星点 | | footprint group renderOrder | inline | `0` | 避免 Group 排序盖过卫星点 |
## AIS 船只 ## AIS 船只
@@ -188,7 +190,7 @@
| 正式名称 | 变量名 | 当前值 | 使用位置 / 说明 | | 正式名称 | 变量名 | 当前值 | 使用位置 / 说明 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| 船只半径偏移 | `VESSEL_CONFIG.altitudeOffset` | `0.2` | 普通 marker 位置,贴近真实地形基础层 | | 船只半径偏移 | `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 | local `VESSEL_RENDER_ORDER` | `4.4` | 普通 marker 和交互 overlay |
| 船只轨迹 renderOrder | `VESSEL_RENDER_ORDER - 0.1` | `4.3` | 低于船只 marker | | 船只轨迹 renderOrder | `VESSEL_RENDER_ORDER - 0.1` | `4.3` | 低于船只 marker |
| 船只点像素尺寸 | local `VESSEL_POINT_SIZE` | `34` | 普通 marker 与 hover / locked overlay 共享尺寸 | | 船只点像素尺寸 | local `VESSEL_POINT_SIZE` | `34` | 普通 marker 与 hover / locked overlay 共享尺寸 |

View File

@@ -7,8 +7,8 @@
| 顺序类型 | 当前顺序 | 说明 | | 顺序类型 | 当前顺序 | 说明 |
| --- | --- | --- | | --- | --- | --- |
| 控制面板顺序 | 海缆 → 轨迹 → 卫星 → 算力中心 → 船只 → BGP → 地形 → 高清材质 → 大气云图 → 国界 → 经纬线 | 由 `displayOrder` 控制,按操作关注度排列。 | | 控制面板顺序 | 海缆 → 轨迹 → 卫星 → 算力中心 → 船只 → BGP → 地形 → 高清材质 → 大气云图 → 国界线 → 经纬线 | 由 `displayOrder` 控制,按操作关注度排列。 |
| 注册 / 启动加载顺序 | 经纬线 → 国界 → 高清材质 → 大气云图 → 海缆 → 算力中心 → 船只 → BGP → 卫星 | 由注册顺序和 `startupPriority` 控制,按地表到天空排列;船只和卫星默认关闭,只有可见时参与启动加载;轨迹和地形是依赖/可选显示层,不参与常规启动数据加载。 | | 注册 / 启动加载顺序 | 经纬线 → 国界线 / 海陆基座 → 高清材质 → 大气云图 → 海缆 → 算力中心 → 船只 → 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.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.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 线;中国和中国(台湾)共享高亮组。 | | 2.3 | 国界 hover 实线 | `country-boundaries.js` | `hoverAltitudeOffset = 0.14` | `depthTest: false`,禁用 raycast | 霓虹红橘 hover 线;中国和中国(台湾)共享高亮组。 |
| 3 | 卫星 footprint 填充 | `satellites.js` | `GROUND_FOOTPRINT_RENDER_ORDER` | depth-testedGroup renderOrder 保持 0 | Footprint 在国界线之上,但在算力中心和卫星之下。 | | 3 | 卫星 footprint 填充 / Iridium coverage ring | `satellites.js`, `iridium-footprint-adapter.js` | `GROUND_FOOTPRINT_RENDER_ORDER` | depth-testedIridium 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 业务逻辑驱动。 | | 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.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`。 | | 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 | 恢复高清材质,并恢复记住的地形 / 昼夜状态。 |
| 地形 on | 显示在高清材质之上,但低于国界 hover、footprint、卫星等强调层。 | | 地形 on | 显示在高清材质之上,但低于国界 hover、footprint、卫星等强调层。 |
| 大气云图 | 只控制云图 mesh 显隐。 | | 大气云图 | 只控制云图 mesh 显隐。 |
| 国界 | 控制国界线和 hover 线显隐;海陆基座填充独立存在,作为 Earth 基座地图使用。 | | 国界线 off | 只隐藏可交互国界线和 hover,高亮状态会清除;海陆基座填充作为 Earth 底图保留。 |
## 交互规则 ## 交互规则

View File

@@ -224,7 +224,7 @@ Earth 用于在一个地球视图中观察:
- 卫星和轨迹 - 卫星和轨迹
- 海缆与登陆点 - 海缆与登陆点
- 算力中心 - 算力中心
- 国界、经纬线、高清材质、云图、地形 - 国界线、经纬线、高清材质、云图、地形
- 新闻直播和态势新闻 - 新闻直播和态势新闻
- 搜索和聚焦对象详情 - 搜索和聚焦对象详情
@@ -235,7 +235,7 @@ Earth 用于在一个地球视图中观察:
常见图层包括: 常见图层包括:
- 经纬线 - 经纬线
- 国界 - 国界线
- 高清材质 - 高清材质
- 大气云图 - 大气云图
- 海缆 - 海缆
@@ -260,7 +260,7 @@ Earth 用于在一个地球视图中观察:
- 海缆 - 海缆
- 卫星 - 卫星
- 国界 - 国界线
- 算力中心 - 算力中心
- BGP - BGP
- AIS 船只 - AIS 船只

View File

@@ -16,12 +16,14 @@
## Current Version ## Current Version
- `main` 当前主线历史推导到:`0.16.5` - `main` 当前主线历史推导到:`0.16.5`
- `dev` 当前开发分支历史推导到:`0.46.1` - `dev` 当前开发分支历史推导到:`0.46.3`
## Timeline ## Timeline
| Version | Type | Branch | Commit | Summary | | Version | Type | Branch | Commit | Summary |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| `0.46.3` | bugfix | `dev` | `pending` | 优化 Starlink footprint 拖拽性能,避免旋转地球时重复重建覆盖网格,并恢复线缆点击呼吸动画 |
| `0.46.2` | bugfix | `dev` | `pending` | 修复 Earth 启动加载顺序、图层 localStorage 恢复、国界线底图语义、媒体面板、船只轨迹和 Iridium footprint 显示问题,并补充 AIS 聚合计划 |
| `0.46.1` | bugfix | `dev` | `pending` | 修复新增 Docs 技术文档未进前端白名单导致页面不可访问的问题,补齐英文文档并固化白名单/双语/裸文件标题检查 | | `0.46.1` | bugfix | `dev` | `pending` | 修复新增 Docs 技术文档未进前端白名单导致页面不可访问的问题,补齐英文文档并固化白名单/双语/裸文件标题检查 |
| `0.46.0` | feature | `dev` | `pending` | Earth 新增通用 Interactable 图标层统一船只、算力中心、BGP 事件/观测站交互图标,并优化登陆点与 toolbar 初始渲染 | | `0.46.0` | feature | `dev` | `pending` | Earth 新增通用 Interactable 图标层统一船只、算力中心、BGP 事件/观测站交互图标,并优化登陆点与 toolbar 初始渲染 |
| `0.45.0` | feature | `dev` | `pending` | 新增采集任务 fetching 阶段量化进度,收敛 AI Provider 运行期环境注入和 Docker build context | | `0.45.0` | feature | `dev` | `pending` | 新增采集任务 fetching 阶段量化进度,收敛 AI Provider 运行期环境注入和 Docker build context |

View File

@@ -1,6 +1,6 @@
{ {
"name": "planet-frontend", "name": "planet-frontend",
"version": "0.46.1", "version": "0.46.3",
"private": true, "private": true,
"packageManager": "bun@1", "packageManager": "bun@1",
"dependencies": { "dependencies": {

View File

@@ -28,7 +28,7 @@ const _lpCameraToPoint = new THREE.Vector3();
const LANDING_POINT_SPRITE_HEIGHT = 3; const LANDING_POINT_SPRITE_HEIGHT = 3;
const LANDING_POINT_SPRITE_ASPECT = 1; const LANDING_POINT_SPRITE_ASPECT = 1;
const LANDING_POINT_SIZE_REFERENCE_FOV = 75; 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_SIZE_SCALE_MAX = 3;
const LANDING_POINT_ATLAS_CELL_SIZE = 128; const LANDING_POINT_ATLAS_CELL_SIZE = 128;
let landingPointTexture = null; let landingPointTexture = null;

View File

@@ -232,7 +232,7 @@ export const VESSEL_CONFIG = {
max: 2.4, max: 2.4,
}, },
track: { track: {
altitudeOffset: 0.22, altitudeOffset: 0.2,
color: 0x7dd3fc, color: 0x7dd3fc,
opacity: 0.82, opacity: 0.82,
}, },

View File

@@ -19,6 +19,7 @@ import {
import { import {
toggleTerrain, toggleTerrain,
setDayNightEnabled, setDayNightEnabled,
toggleClouds,
toggleGridLines, toggleGridLines,
getShowGridLines, getShowGridLines,
} from "./earth.js"; } from "./earth.js";
@@ -53,7 +54,7 @@ import {
} from "./satellites.js"; } from "./satellites.js";
import { getShowCables } from "./cables.js"; import { getShowCables } from "./cables.js";
import { toggleBGP, getShowBGP, getBGPCount } from "./bgp.js"; import { toggleBGP, getShowBGP, getBGPCount } from "./bgp.js";
import { getShowCountryBoundaries } from "./country-boundaries.js"; import { getShowCountryBoundaries, toggleCountryBoundaries } from "./country-boundaries.js";
import { import {
toggleComputeCenters, toggleComputeCenters,
getShowComputeCenters, getShowComputeCenters,
@@ -136,6 +137,7 @@ let focusViewAnimationToken = 0;
let earthSettingsDefaults = null; let earthSettingsDefaults = null;
let lastZoomStatusUpdateTime = 0; let lastZoomStatusUpdateTime = 0;
let earthSettingsState = null; let earthSettingsState = null;
let deferredLayerVisibilitySettings = null;
let layerRegistry = new Map(); let layerRegistry = new Map();
let layerPanelInitialized = false; let layerPanelInitialized = false;
let layoutMode = "desktop"; let layoutMode = "desktop";
@@ -269,7 +271,7 @@ function closeTransientMobileOverlays({ except = null } = {}) {
setMobileDrawerOpen("layer-toggles", false); setMobileDrawerOpen("layer-toggles", false);
} }
if (except !== "media" && isTVPanelVisible()) { if (except !== "media" && except !== "search" && isTVPanelVisible()) {
setTVPanelVisible(false); setTVPanelVisible(false);
} }
} }
@@ -670,6 +672,14 @@ function shouldIncludeLayerInStartupLoad(definition) {
return false; 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") { if (definition.startupMode === "preload") {
return true; return true;
} }
@@ -677,6 +687,14 @@ function shouldIncludeLayerInStartupLoad(definition) {
return Boolean(definition?.getVisible?.()); 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) { function clampEarthZoomLevel(nextZoom) {
const parsedZoom = Number.parseFloat(nextZoom); const parsedZoom = Number.parseFloat(nextZoom);
if (!Number.isFinite(parsedZoom)) { if (!Number.isFinite(parsedZoom)) {
@@ -1127,7 +1145,7 @@ function setDefaultEarthZoom(nextZoom, { persist = true, applyToCurrentView = tr
return defaultEarthZoom; return defaultEarthZoom;
} }
async function applyEarthSettings(settings) { async function applyEarthSettings(settings, { applyLayers = true } = {}) {
if (!settings) return; if (!settings) return;
earthSettingsState = cloneEarthSettings(settings); earthSettingsState = cloneEarthSettings(settings);
@@ -1161,12 +1179,31 @@ async function applyEarthSettings(settings) {
applyToCurrentView: true, applyToCurrentView: true,
}); });
if (!applyLayers) {
const layerVisibility = { ...(settings.shared.layerVisibility || {}) };
applyImmediateLayerVisibilityHints(layerVisibility);
deferredLayerVisibilitySettings = layerVisibility;
return;
}
deferredLayerVisibilitySettings = null;
await applyLayerVisibilitySettings(settings.shared.layerVisibility, { await applyLayerVisibilitySettings(settings.shared.layerVisibility, {
persist: false, persist: false,
silent: true, 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() { function resetEarthSettings() {
const defaults = cloneEarthSettings(captureEarthSettingsDefaults()); const defaults = cloneEarthSettings(captureEarthSettingsDefaults());
earthSettingsState = cloneEarthSettings(defaults); earthSettingsState = cloneEarthSettings(defaults);
@@ -1307,8 +1344,15 @@ async function setCountryBoundariesLayerEnabled(button, enabled, { persist = tru
} }
} }
function setHighResTextureLayerEnabled(button, enabled, { persist = true, silent = false } = {}) { async function setHighResTextureLayerEnabled(button, enabled, { persist = true, silent = false } = {}) {
setHighResTextureEnabled(enabled, { suppressStatus: silent }); if (enabled) {
setLayerButtonState(button, {
active: false,
loading: true,
tooltip: "高清材质加载中...",
});
}
await setHighResTextureEnabled(enabled, { suppressStatus: silent });
setLayerButtonState(button, { setLayerButtonState(button, {
active: enabled, active: enabled,
loading: false, loading: false,
@@ -1319,8 +1363,15 @@ function setHighResTextureLayerEnabled(button, enabled, { persist = true, silent
return enabled; return enabled;
} }
function setAtmosphereCloudsLayerEnabled(button, enabled, { persist = true, silent = false } = {}) { async function setAtmosphereCloudsLayerEnabled(button, enabled, { persist = true, silent = false } = {}) {
setAtmosphereCloudsEnabled(enabled, { suppressStatus: silent }); if (enabled) {
setLayerButtonState(button, {
active: false,
loading: true,
tooltip: "大气云图加载中...",
});
}
await setAtmosphereCloudsEnabled(enabled, { suppressStatus: silent });
setLayerButtonState(button, { setLayerButtonState(button, {
active: enabled, active: enabled,
loading: false, 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() { function getBuiltinLayerDefinitions() {
return [ return [
{ {
@@ -1472,13 +1561,14 @@ function getBuiltinLayerDefinitions() {
id: "countryBoundaries", id: "countryBoundaries",
buttonId: "toggle-country-boundaries", buttonId: "toggle-country-boundaries",
icon: "public", icon: "public",
label: "国界", label: "国界线",
meta: "Country Borders", meta: "Country Borders",
keywords: "国界 国家 borders countries boundary", keywords: "国界 国家 borders countries boundary",
defaultActive: true, defaultActive: true,
displayOrder: 90, displayOrder: 90,
startupPriority: 20, startupPriority: 20,
startupMode: "preload", startupMode: "preload",
startupAlwaysLoad: true,
startupLabel: "海陆基座", startupLabel: "海陆基座",
startupMessage: "正在加载海陆基座...", startupMessage: "正在加载海陆基座...",
getVisible: () => getShowCountryBoundaries(), getVisible: () => getShowCountryBoundaries(),
@@ -2268,7 +2358,7 @@ function setupSettingsControls() {
}); });
captureEarthSettingsDefaults(); captureEarthSettingsDefaults();
settingsApplyPromise = applyEarthSettings(loadEarthSettings()); settingsApplyPromise = applyEarthSettings(loadEarthSettings(), { applyLayers: false });
syncAllHudPanelToggles(); syncAllHudPanelToggles();
syncRotationModeButtons(); syncRotationModeButtons();
syncCruiseModuleControls(); syncCruiseModuleControls();

View File

@@ -25,8 +25,11 @@ let _earthTextureOverlayMaterial = null;
let _earthShaders = []; let _earthShaders = [];
let _dayNightEnabled = true; let _dayNightEnabled = true;
let _loadedTexture = null; let _loadedTexture = null;
let _textureLoadPromise = null;
let _textureVisible = true; let _textureVisible = true;
let _earthRimGlow = null; let _earthRimGlow = null;
let _cloudTexture = null;
let _cloudTextureLoadPromise = null;
const _earthSunDirection = new THREE.Vector3( const _earthSunDirection = new THREE.Vector3(
EARTH_MATERIAL_CONFIG.dayNight.sunDirection.x, EARTH_MATERIAL_CONFIG.dayNight.sunDirection.x,
EARTH_MATERIAL_CONFIG.dayNight.sunDirection.y, EARTH_MATERIAL_CONFIG.dayNight.sunDirection.y,
@@ -301,28 +304,16 @@ export function createClouds(scene, earthObj) {
clouds = new THREE.Mesh(geometry, material); clouds = new THREE.Mesh(geometry, material);
clouds.name = "earth-atmosphere-clouds"; clouds.name = "earth-atmosphere-clouds";
clouds.visible = showClouds; clouds.visible = false;
earthObj.add(clouds); earthObj.add(clouds);
textureLoader.load(
CLOUD_LAYER_CONFIG.textureUrl,
function(texture) {
material.map = texture;
material.needsUpdate = true;
},
undefined,
function(err) {
console.log('云层纹理加载失败');
}
);
return clouds; return clouds;
} }
export function toggleClouds(visible) { export function toggleClouds(visible) {
showClouds = Boolean(visible); showClouds = Boolean(visible);
if (clouds) { if (clouds) {
clouds.visible = showClouds; clouds.visible = showClouds && Boolean(clouds.material?.map);
} }
} }
@@ -330,6 +321,39 @@ export function getShowClouds() {
return showClouds; 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) { export function createTerrain(earthObj) {
const geometry = new THREE.SphereGeometry( const geometry = new THREE.SphereGeometry(
CONFIG.earthRadius + TERRAIN_CONFIG.baseRadiusOffset, CONFIG.earthRadius + TERRAIN_CONFIG.baseRadiusOffset,
@@ -478,6 +502,7 @@ export function getClouds() {
export function clearEarthTexture() { export function clearEarthTexture() {
_loadedTexture = null; _loadedTexture = null;
_textureLoadPromise = null;
if (_earthTextureOverlayMaterial) { if (_earthTextureOverlayMaterial) {
_earthTextureOverlayMaterial.map = null; _earthTextureOverlayMaterial.map = null;
_earthTextureOverlayMaterial.needsUpdate = true; _earthTextureOverlayMaterial.needsUpdate = true;
@@ -522,7 +547,10 @@ export function setDayNightEnabled(enabled) {
} }
export function loadEarthTexture() { 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; } if (!_earthTextureOverlayMaterial) { resolve(); return; }
const urls = EARTH_MATERIAL_CONFIG.textureUrls; const urls = EARTH_MATERIAL_CONFIG.textureUrls;
@@ -549,7 +577,7 @@ export function loadEarthTexture() {
if (_earthRimGlow) { if (_earthRimGlow) {
_earthRimGlow.visible = !_textureVisible; _earthRimGlow.visible = !_textureVisible;
} }
resolve(); resolve(texture);
}, },
null, null,
() => tryLoad(index + 1), () => tryLoad(index + 1),
@@ -557,6 +585,11 @@ export function loadEarthTexture() {
}; };
tryLoad(0); tryLoad(0);
}); });
_textureLoadPromise.finally(() => {
_textureLoadPromise = null;
});
return _textureLoadPromise;
} }
export function setEarthTextureVisible(visible) { export function setEarthTextureVisible(visible) {

View File

@@ -23,6 +23,10 @@ import {
loadVessels, loadVessels,
toggleVessels, toggleVessels,
} from "./vessels.js"; } from "./vessels.js";
import {
loadCloudTexture,
loadEarthTexture,
} from "./earth.js";
import { import {
getCountryBoundaryLegendItems, getCountryBoundaryLegendItems,
loadCountryBoundaries, loadCountryBoundaries,
@@ -83,6 +87,8 @@ export function registerLayerStartupTask(id, taskFactory) {
function registerBuiltinLayerStartupTasks() { function registerBuiltinLayerStartupTasks() {
startupTaskRegistry.clear(); startupTaskRegistry.clear();
registerCountryBoundaryStartupTask(); registerCountryBoundaryStartupTask();
registerEarthTextureStartupTask();
registerCloudStartupTask();
registerCableStartupTask(); registerCableStartupTask();
registerComputeCenterStartupTask(); registerComputeCenterStartupTask();
registerVesselStartupTask(); 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() { function registerCountryBoundaryStartupTask() {
registerLayerStartupTask("countryBoundaries", (context) => async (layer) => { registerLayerStartupTask("countryBoundaries", (context) => async (layer) => {
context.setLoadingMessage( context.setLoadingMessage(

View File

@@ -37,6 +37,7 @@ import {
createGridLines, createGridLines,
getEarth, getEarth,
getEarthSurfacePickTarget, getEarthSurfacePickTarget,
loadCloudTexture,
loadEarthTexture, loadEarthTexture,
clearEarthTexture, clearEarthTexture,
setEarthSunDirection, setEarthSunDirection,
@@ -203,6 +204,7 @@ import {
setDayNightEnabledExternal, setDayNightEnabledExternal,
setTerrainLayerInteractable, setTerrainLayerInteractable,
setDayNightInteractable, setDayNightInteractable,
applyDeferredLayerVisibilitySettings,
} from "./controls.js"; } from "./controls.js";
import { import {
createLayerStartupTaskMap, createLayerStartupTaskMap,
@@ -1470,23 +1472,7 @@ function formatBGPStatusFromSummary(summary) {
return "当前无活跃事件"; return "当前无活跃事件";
} }
function applyEarthStatsSummary(summary) { async function loadEarthStatsSummary({ shouldApply = () => true } = {}) {
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() {
try { try {
const response = await fetch(PATHS.earthSummaryApi); const response = await fetch(PATHS.earthSummaryApi);
if (!response.ok) { if (!response.ok) {
@@ -1505,7 +1491,9 @@ async function loadEarthStatsSummary() {
bgpAnomalyCount: toCount(stats.bgp_anomaly_count), bgpAnomalyCount: toCount(stats.bgp_anomaly_count),
bgpCollectorCount: toCount(stats.bgp_collector_count), bgpCollectorCount: toCount(stats.bgp_collector_count),
}; };
applyEarthStatsSummary(earthStatsSummary); if (shouldApply()) {
updateStatsSummary();
}
} catch (error) { } catch (error) {
console.warn("全球态势聚合统计加载失败:", error); console.warn("全球态势聚合统计加载失败:", error);
} }
@@ -2048,7 +2036,11 @@ function applyCableVisualState() {
switch (state) { switch (state) {
case CABLE_STATE.LOCKED: case CABLE_STATE.LOCKED:
case CABLE_STATE.HOVERED: case CABLE_STATE.HOVERED:
cable.material.opacity = 1; cable.material.opacity = THREE.MathUtils.lerp(
CABLE_CONFIG.lockedOpacityMin,
CABLE_CONFIG.lockedOpacityMax,
pulse,
);
cable.material.color.setRGB(0.92, 0.98, 1.0); cable.material.color.setRGB(0.92, 0.98, 1.0);
break; break;
case CABLE_STATE.NORMAL: case CABLE_STATE.NORMAL:
@@ -2594,24 +2586,14 @@ async function loadData() {
await yieldFrame(18); await yieldFrame(18);
if (loadToken !== currentLoadToken) { isDataLoading = false; return; } if (loadToken !== currentLoadToken) { isDataLoading = false; return; }
setLoadingMessage("正在读取全球态势统计..."); loadEarthStatsSummary({
await loadEarthStatsSummary(); shouldApply: () => loadToken === currentLoadToken && !destroyed,
if (loadToken !== currentLoadToken) { isDataLoading = false; return; } }).catch((error) => {
await yieldFrame(12); console.warn("后台刷新全球态势统计失败:", error);
});
const errors = []; 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({ const startupLoaders = createLayerStartupTaskMap({
scene, scene,
earth, earth,
@@ -2630,6 +2612,7 @@ async function loadData() {
getShowCountryBoundaries, getShowCountryBoundaries,
getShowBGP, getShowBGP,
isEarthTextureVisible: () => getEarthTextureVisible(), isEarthTextureVisible: () => getEarthTextureVisible(),
isCloudsEnabled: () => getShowClouds(),
getInitialSatelliteLoadLimit, getInitialSatelliteLoadLimit,
shouldHydrateFullSatelliteSet, shouldHydrateFullSatelliteSet,
scheduleSatellitePositionWarmup, scheduleSatellitePositionWarmup,
@@ -2704,6 +2687,10 @@ async function loadData() {
hideError(); hideError();
queueStatusMessage("数据已加载", "success"); queueStatusMessage("数据已加载", "success");
} }
applyDeferredLayerVisibilitySettings().catch((error) => {
console.warn("恢复 Earth 图层可见性失败:", error);
});
} }
const POSITION_UPDATE_FORCE_DELTA = 250; const POSITION_UPDATE_FORCE_DELTA = 250;
@@ -2827,8 +2814,17 @@ export async function setCountryBoundariesEnabled(
let _dayNightBeforeTextureOff = null; let _dayNightBeforeTextureOff = null;
let _terrainBeforeTextureOff = null; let _terrainBeforeTextureOff = null;
export function setHighResTextureEnabled(enabled, { suppressStatus = false } = {}) { export async function setHighResTextureEnabled(enabled, { suppressStatus = false } = {}) {
setEarthTextureVisible(enabled); setEarthTextureVisible(enabled);
if (enabled) {
try {
await loadEarthTexture();
setEarthTextureVisible(true);
} catch (error) {
console.warn("高清材质加载失败:", error);
setEarthTextureVisible(false);
}
}
setSurfaceTintEnabled(!enabled); setSurfaceTintEnabled(!enabled);
setLandFillEnabled(true); setLandFillEnabled(true);
setLandFillSuppressed(false); setLandFillSuppressed(false);
@@ -2863,8 +2859,17 @@ export function getHighResTextureEnabled() {
return getEarthTextureVisible(); return getEarthTextureVisible();
} }
export function setAtmosphereCloudsEnabled(enabled, { suppressStatus = false } = {}) { export async function setAtmosphereCloudsEnabled(enabled, { suppressStatus = false } = {}) {
toggleClouds(enabled); toggleClouds(enabled);
if (enabled) {
try {
await loadCloudTexture();
toggleClouds(true);
} catch (error) {
console.warn("大气云图加载失败:", error);
toggleClouds(false);
}
}
if (!suppressStatus) { if (!suppressStatus) {
showStatusMessage(enabled ? "大气云图已显示" : "大气云图已隐藏", "info"); showStatusMessage(enabled ? "大气云图已显示" : "大气云图已隐藏", "info");
} }

View File

@@ -28,6 +28,7 @@ let lockedRingSprite = null;
let lockedDotSprite = null; let lockedDotSprite = null;
let lockedHaloMesh = null; let lockedHaloMesh = null;
let lockedGroundFootprintMesh = null; let lockedGroundFootprintMesh = null;
let lockedGroundFootprintFillMesh = null;
let lockedIridiumFootprintMesh = null; let lockedIridiumFootprintMesh = null;
let predictedOrbitLine = null; let predictedOrbitLine = null;
let relatedSatelliteSprites = []; let relatedSatelliteSprites = [];
@@ -159,6 +160,9 @@ const GROUND_FOOTPRINT_GAP_CENTER_MAX_RATIO = 0.82;
const GROUND_FOOTPRINT_GAP_WIDTH_CENTER_KM = 60; const GROUND_FOOTPRINT_GAP_WIDTH_CENTER_KM = 60;
const GROUND_FOOTPRINT_GAP_WIDTH_EDGE_KM = 120; const GROUND_FOOTPRINT_GAP_WIDTH_EDGE_KM = 120;
const GROUND_FOOTPRINT_GAP_LENGTH_RATIO = 1.08; const GROUND_FOOTPRINT_GAP_LENGTH_RATIO = 1.08;
const GROUND_FOOTPRINT_REBUILD_DISTANCE = 0.001;
const GROUND_FOOTPRINT_REBUILD_DISTANCE_SQ =
GROUND_FOOTPRINT_REBUILD_DISTANCE * GROUND_FOOTPRINT_REBUILD_DISTANCE;
const scratchWorldSatellitePosition = new THREE.Vector3(); const scratchWorldSatellitePosition = new THREE.Vector3();
const scratchToCamera = new THREE.Vector3(); const scratchToCamera = new THREE.Vector3();
@@ -168,7 +172,9 @@ const scratchFootprintLateral = new THREE.Vector3();
const scratchFootprintReference = new THREE.Vector3(); const scratchFootprintReference = new THREE.Vector3();
const scratchFootprintVelocity = new THREE.Vector3(); const scratchFootprintVelocity = new THREE.Vector3();
const scratchFootprintTangent = new THREE.Vector3(); const scratchFootprintTangent = new THREE.Vector3();
const scratchLastGroundFootprintPosition = new THREE.Vector3();
const satelliteSunDirection = new THREE.Vector3(1, 0.2, 0.4).normalize(); const satelliteSunDirection = new THREE.Vector3(1, 0.2, 0.4).normalize();
let hasGroundFootprintGeometry = false;
export let breathingPhase = 0; export let breathingPhase = 0;
@@ -1335,13 +1341,10 @@ export function setSatelliteCamera(camera) {
export function setSatelliteSunDirection(direction) { export function setSatelliteSunDirection(direction) {
if (!direction) return; if (!direction) return;
satelliteSunDirection.copy(direction).normalize(); satelliteSunDirection.copy(direction).normalize();
if (lockedGroundFootprintMesh) { if (lockedGroundFootprintFillMesh?.material?.uniforms?.uSunDirectionWorld) {
const fillMesh = lockedGroundFootprintMesh.getObjectByName("footprint-fill"); lockedGroundFootprintFillMesh.material.uniforms.uSunDirectionWorld.value.copy(
if (fillMesh?.material?.uniforms?.uSunDirectionWorld) { satelliteSunDirection,
fillMesh.material.uniforms.uSunDirectionWorld.value.copy( );
satelliteSunDirection,
);
}
} }
} }
@@ -1586,14 +1589,12 @@ function createGroundFootprintMaterial() {
}, },
vertexShader: ` vertexShader: `
varying vec3 vWorldPosition; varying vec3 vWorldPosition;
varying vec3 vWorldNormal;
varying vec2 vUv; varying vec2 vUv;
void main() { void main() {
vUv = uv; vUv = uv;
vec4 worldPosition = modelMatrix * vec4(position, 1.0); vec4 worldPosition = modelMatrix * vec4(position, 1.0);
vWorldPosition = worldPosition.xyz; vWorldPosition = worldPosition.xyz;
vWorldNormal = normalize(mat3(modelMatrix) * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
} }
`, `,
@@ -1617,7 +1618,6 @@ function createGroundFootprintMaterial() {
uniform vec3 uSunDirectionWorld; uniform vec3 uSunDirectionWorld;
uniform float uDayVisibilityBoost; uniform float uDayVisibilityBoost;
varying vec3 vWorldPosition; varying vec3 vWorldPosition;
varying vec3 vWorldNormal;
varying vec2 vUv; varying vec2 vUv;
float bowtieHalfWidth(float xEast) { float bowtieHalfWidth(float xEast) {
@@ -1723,6 +1723,8 @@ function clearLockedSatelliteStyleVisuals() {
if (lockedGroundFootprintMesh) { if (lockedGroundFootprintMesh) {
disposeObjectTree(lockedGroundFootprintMesh); disposeObjectTree(lockedGroundFootprintMesh);
lockedGroundFootprintMesh = null; lockedGroundFootprintMesh = null;
lockedGroundFootprintFillMesh = null;
hasGroundFootprintGeometry = false;
} }
if (lockedIridiumFootprintMesh) { if (lockedIridiumFootprintMesh) {
disposeIridiumFootprintAdapter(lockedIridiumFootprintMesh, earthObjRef); disposeIridiumFootprintAdapter(lockedIridiumFootprintMesh, earthObjRef);
@@ -1933,28 +1935,6 @@ function buildGroundFootprintGeometry(position) {
); );
} }
function toEastNorth(alongKm, crossKm) {
const offset = alongTrack
.clone()
.multiplyScalar(alongKm)
.addScaledVector(crossTrack, crossKm);
return {
xEast: offset.dot(east),
yNorth: offset.dot(north),
};
}
function fromEastNorth(xEast, yNorth) {
const offset = east
.clone()
.multiplyScalar(xEast)
.addScaledVector(north, yNorth);
return {
alongKm: offset.dot(alongTrack),
crossKm: offset.dot(crossTrack),
};
}
function bowtieHalfWidth(xEast) { function bowtieHalfWidth(xEast) {
const t = THREE.MathUtils.clamp( const t = THREE.MathUtils.clamp(
Math.abs(xEast) / Math.max(exclusionLengthKm, 1), Math.abs(xEast) / Math.max(exclusionLengthKm, 1),
@@ -1969,6 +1949,7 @@ function buildGroundFootprintGeometry(position) {
} }
const vertices = []; const vertices = [];
const uvs = [];
const indices = []; const indices = [];
const indexMap = []; const indexMap = [];
@@ -2000,6 +1981,10 @@ function buildGroundFootprintGeometry(position) {
); );
row.push(vertices.length / 3); row.push(vertices.length / 3);
vertices.push(point.x, point.y, point.z); vertices.push(point.x, point.y, point.z);
uvs.push(
THREE.MathUtils.mapLinear(alongKm, -majorKm, majorKm, 0, 1),
THREE.MathUtils.mapLinear(crossKm, -minorKm, minorKm, 0, 1),
);
} }
indexMap.push(row); indexMap.push(row);
} }
@@ -2021,29 +2006,8 @@ function buildGroundFootprintGeometry(position) {
"position", "position",
new THREE.Float32BufferAttribute(vertices, 3), new THREE.Float32BufferAttribute(vertices, 3),
); );
const uvs = [];
for (let iy = 0; iy <= GROUND_FOOTPRINT_GRID_Y; iy += 1) {
const crossKm = THREE.MathUtils.lerp(
-minorKm,
minorKm,
iy / GROUND_FOOTPRINT_GRID_Y,
);
for (let ix = 0; ix <= GROUND_FOOTPRINT_GRID_X; ix += 1) {
const alongKm = THREE.MathUtils.lerp(
-majorKm,
majorKm,
ix / GROUND_FOOTPRINT_GRID_X,
);
if (!isInsideEllipse(alongKm, crossKm)) continue;
uvs.push(
THREE.MathUtils.mapLinear(alongKm, -majorKm, majorKm, 0, 1),
THREE.MathUtils.mapLinear(crossKm, -minorKm, minorKm, 0, 1),
);
}
}
fillGeometry.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2)); fillGeometry.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2));
fillGeometry.setIndex(indices); fillGeometry.setIndex(indices);
fillGeometry.computeVertexNormals();
const basisToEastNorth = { const basisToEastNorth = {
eastAlongDot: alongTrack.dot(east), eastAlongDot: alongTrack.dot(east),
@@ -2064,10 +2028,18 @@ function buildGroundFootprintGeometry(position) {
function updateGroundFootprintTransform(position) { function updateGroundFootprintTransform(position) {
if (!lockedGroundFootprintMesh || !position || !earthObjRef) return; if (!lockedGroundFootprintMesh || !position || !earthObjRef) return;
if (
hasGroundFootprintGeometry &&
scratchLastGroundFootprintPosition.distanceToSquared(position) <=
GROUND_FOOTPRINT_REBUILD_DISTANCE_SQ
) {
return;
}
const geometrySet = buildGroundFootprintGeometry(position); const geometrySet = buildGroundFootprintGeometry(position);
if (!geometrySet) return; if (!geometrySet) return;
const fillMesh = lockedGroundFootprintMesh.getObjectByName("footprint-fill"); const fillMesh = lockedGroundFootprintFillMesh;
if (fillMesh?.geometry) fillMesh.geometry.dispose(); if (fillMesh?.geometry) fillMesh.geometry.dispose();
@@ -2089,6 +2061,8 @@ function updateGroundFootprintTransform(position) {
fillMesh.material.uniforms.uNorthCrossDot.value = fillMesh.material.uniforms.uNorthCrossDot.value =
geometrySet.basisToEastNorth.northCrossDot; geometrySet.basisToEastNorth.northCrossDot;
} }
scratchLastGroundFootprintPosition.copy(position);
hasGroundFootprintGeometry = true;
} }
} }
@@ -2130,6 +2104,8 @@ function showGroundFootprintStyle(position) {
fill.name = "footprint-fill"; fill.name = "footprint-fill";
fill.renderOrder = GROUND_FOOTPRINT_RENDER_ORDER; fill.renderOrder = GROUND_FOOTPRINT_RENDER_ORDER;
lockedGroundFootprintMesh.add(fill); lockedGroundFootprintMesh.add(fill);
lockedGroundFootprintFillMesh = fill;
hasGroundFootprintGeometry = false;
earthObjRef.add(lockedGroundFootprintMesh); earthObjRef.add(lockedGroundFootprintMesh);
updateGroundFootprintTransform(position); updateGroundFootprintTransform(position);
} }
@@ -2139,7 +2115,7 @@ function showIridiumReservedStyle(position) {
lockedIridiumFootprintMesh = createIridiumFootprintAdapter({ lockedIridiumFootprintMesh = createIridiumFootprintAdapter({
earthObj: earthObjRef, earthObj: earthObjRef,
earthRadiusWorld: CONFIG.earthRadius, earthRadiusWorld: CONFIG.earthRadius,
renderOrder: 0, renderOrder: GROUND_FOOTPRINT_RENDER_ORDER,
}); });
updateIridiumReservedStyle(position); updateIridiumReservedStyle(position);
} }

View File

@@ -11,6 +11,7 @@ const VESSEL_RENDER_ORDER = 4.4;
const VESSEL_POINT_SIZE = 34; const VESSEL_POINT_SIZE = 34;
const VESSEL_ATLAS_CELL_SIZE = 128; const VESSEL_ATLAS_CELL_SIZE = 128;
const VESSEL_COURSE_BINS = 32; const VESSEL_COURSE_BINS = 32;
const VESSEL_TRACK_ENDPOINT_EPSILON = 0.001;
function normalizeVesselType(value, code) { function normalizeVesselType(value, code) {
const type = String(value || "").trim().toLowerCase(); const type = String(value || "").trim().toLowerCase();
@@ -72,6 +73,26 @@ function getCourseBin(marker) {
return Math.round((normalized / 360) * VESSEL_COURSE_BINS) % VESSEL_COURSE_BINS; 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({ const vesselIconLayer = createInteractableLayer({
id: "vessels", id: "vessels",
objectType: "vessel", objectType: "vessel",
@@ -207,14 +228,15 @@ export async function showVesselTrack(marker, earth) {
if (coordinates.length < 2) return null; if (coordinates.length < 2) return null;
const points = coordinates const points = coordinates
.map(([lon, lat]) => .map(([lon, lat]) => buildTrackPoint(lon, lat))
latLonToVector3( .filter(
Number(lat), (point) =>
Number(lon), point &&
CONFIG.earthRadius + VESSEL_CONFIG.track.altitudeOffset, Number.isFinite(point.x) &&
), Number.isFinite(point.y) &&
) Number.isFinite(point.z),
.filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y) && Number.isFinite(point.z)); );
appendCurrentMarkerTrackPoint(points, marker);
if (points.length < 2) return null; if (points.length < 2) return null;
const geometry = new THREE.BufferGeometry().setFromPoints(points); const geometry = new THREE.BufferGeometry().setFromPoints(points);

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "planet" name = "planet"
version = "0.46.1" version = "0.46.3"
description = "智能星球计划 - 态势感知系统" description = "智能星球计划 - 态势感知系统"
requires-python = ">=3.14" requires-python = ">=3.14"
dependencies = [ dependencies = [

2
uv.lock generated
View File

@@ -475,7 +475,7 @@ wheels = [
[[package]] [[package]]
name = "planet" name = "planet"
version = "0.46.1" version = "0.46.3"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "aiofiles" }, { name = "aiofiles" },