Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f22079d33a | ||
|
|
9f737fdb89 |
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 图标层规范
|
||||||
|
|||||||
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) |
|
| 船只规模 | 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
|
||||||
- 刷新频率:数据约 30–60s 更新一次,可随意轮询
|
- 刷新频率:数据约 30–60s 更新一次,可随意轮询
|
||||||
|
|
||||||
### 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 接上游实时源
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 共享尺寸 |
|
||||||
|
|||||||
@@ -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-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 业务逻辑驱动。 |
|
| 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 底图保留。 |
|
||||||
|
|
||||||
## 交互规则
|
## 交互规则
|
||||||
|
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ Earth 用于在一个地球视图中观察:
|
|||||||
- 卫星和轨迹
|
- 卫星和轨迹
|
||||||
- 海缆与登陆点
|
- 海缆与登陆点
|
||||||
- 算力中心
|
- 算力中心
|
||||||
- 国界、经纬线、高清材质、云图、地形
|
- 国界线、经纬线、高清材质、云图、地形
|
||||||
- 新闻直播和态势新闻
|
- 新闻直播和态势新闻
|
||||||
- 搜索和聚焦对象详情
|
- 搜索和聚焦对象详情
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@ Earth 用于在一个地球视图中观察:
|
|||||||
常见图层包括:
|
常见图层包括:
|
||||||
|
|
||||||
- 经纬线
|
- 经纬线
|
||||||
- 国界
|
- 国界线
|
||||||
- 高清材质
|
- 高清材质
|
||||||
- 大气云图
|
- 大气云图
|
||||||
- 海缆
|
- 海缆
|
||||||
@@ -260,7 +260,7 @@ Earth 用于在一个地球视图中观察:
|
|||||||
|
|
||||||
- 海缆
|
- 海缆
|
||||||
- 卫星
|
- 卫星
|
||||||
- 国界
|
- 国界线
|
||||||
- 算力中心
|
- 算力中心
|
||||||
- BGP
|
- BGP
|
||||||
- AIS 船只
|
- AIS 船只
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
108
frontend/public/earth/js/controls.js
vendored
108
frontend/public/earth/js/controls.js
vendored
@@ -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();
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
Reference in New Issue
Block a user