Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f22079d33a | ||
|
|
9f737fdb89 | ||
|
|
7418ce2fc1 |
@@ -63,6 +63,8 @@ ls docs/technical/zh/ # 查看现有文档
|
|||||||
- 采集器、数据源、凭证、设置页、连接检查、scheduler、后端 API 变化:更新相关后端文档,优先检查 `docs/technical/zh/backend-collectors.md` 和 datasource/settings 专题文档。
|
- 采集器、数据源、凭证、设置页、连接检查、scheduler、后端 API 变化:更新相关后端文档,优先检查 `docs/technical/zh/backend-collectors.md` 和 datasource/settings 专题文档。
|
||||||
- 如果某个旧 plan 的假设已经被当前实现推翻,在对应 `docs/plans/*.md` 增加现状修正或更新该段,不要让计划文档继续给出相反方向。
|
- 如果某个旧 plan 的假设已经被当前实现推翻,在对应 `docs/plans/*.md` 增加现状修正或更新该段,不要让计划文档继续给出相反方向。
|
||||||
- 新增 technical 文档后,如果需要被发现,更新 `docs/technical/zh/README.md`。
|
- 新增 technical 文档后,如果需要被发现,更新 `docs/technical/zh/README.md`。
|
||||||
|
- 如果 technical 文档需要在公开 Docs 页面显示,或从 technical README 链接进入,必须同步更新 `frontend/src/pages/Docs/docs-content.ts` 的 `DOCS_METADATA`。前端使用这份白名单,`docs/technical/{zh,en}/` 中存在 `.md` 文件并不会自动生成路由。
|
||||||
|
- 公开 technical 文档必须按同名文件维护中英文双语版本:`docs/technical/zh/<name>.md` 与 `docs/technical/en/<name>.md`。如果某篇文档刻意只保留单语,完成说明中必须明确写出原因。
|
||||||
- 对本次变更提取旧词做 stale search,例如旧 tab 名、旧路由职责、旧认证假设、改名前 UI 文案:
|
- 对本次变更提取旧词做 stale search,例如旧 tab 名、旧路由职责、旧认证假设、改名前 UI 文案:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -90,6 +92,7 @@ rg -n "旧文案|旧路由职责|旧认证假设" docs/technical docs/plans
|
|||||||
- 中文写作,技术术语保留英文原文
|
- 中文写作,技术术语保留英文原文
|
||||||
- `docs/technical/zh/` 中的文档不得用英文原文占位;如果存在 `docs/technical/en/` 对应文件,禁止逐字复制成中文文件
|
- `docs/technical/zh/` 中的文档不得用英文原文占位;如果存在 `docs/technical/en/` 对应文件,禁止逐字复制成中文文件
|
||||||
- 中文文档内部链接应指向 `docs/technical/zh/...`,除非明确引用英文专属文档
|
- 中文文档内部链接应指向 `docs/technical/zh/...`,除非明确引用英文专属文档
|
||||||
|
- 公开文档的 Markdown 链接显示文字应使用可读标题,不要直接暴露 `manual.md`、`earth-frontend-context.md` 这类裸文件名
|
||||||
|
|
||||||
**文档结构模板**:
|
**文档结构模板**:
|
||||||
|
|
||||||
@@ -141,6 +144,62 @@ PY
|
|||||||
rg -n "/home/ray/dev/linkong/planet/docs/technical/(?!zh|en)" docs/technical/zh --pcre2
|
rg -n "/home/ray/dev/linkong/planet/docs/technical/(?!zh|en)" docs/technical/zh --pcre2
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- 检查公开文档链接已进入 Docs 前端白名单。凡是 `docs/technical/{zh,en}/README.md` 中链接到的 technical `.md`,都必须存在于 `DOCS_METADATA`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python - <<'PY'
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
metadata = Path("frontend/src/pages/Docs/docs-content.ts").read_text()
|
||||||
|
known = set(re.findall(r"'([^']+\.md)':\s*\{", metadata))
|
||||||
|
known.add("README.md")
|
||||||
|
|
||||||
|
missing = []
|
||||||
|
for readme in [Path("docs/technical/zh/README.md"), Path("docs/technical/en/README.md")]:
|
||||||
|
if not readme.exists():
|
||||||
|
continue
|
||||||
|
for href in re.findall(r"\]\(([^)]+\.md)\)", readme.read_text()):
|
||||||
|
path = Path(href)
|
||||||
|
if "docs/technical/" not in href:
|
||||||
|
continue
|
||||||
|
filename = path.name
|
||||||
|
if filename not in known:
|
||||||
|
missing.append(f"{readme}: {filename}")
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
raise SystemExit("docs README links missing DOCS_METADATA: " + ", ".join(missing))
|
||||||
|
print("docs README links are whitelisted")
|
||||||
|
PY
|
||||||
|
```
|
||||||
|
|
||||||
|
- 检查公开文档双语同名文件齐备。除 `README.md` 外,所有白名单文档都应同时存在 zh/en 文件,除非本次说明中明确豁免:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python - <<'PY'
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
metadata = Path("frontend/src/pages/Docs/docs-content.ts").read_text()
|
||||||
|
filenames = sorted(set(re.findall(r"'([^']+\.md)':\s*\{", metadata)) - {"README.md"})
|
||||||
|
missing = []
|
||||||
|
for filename in filenames:
|
||||||
|
for lang in ("zh", "en"):
|
||||||
|
path = Path("docs/technical") / lang / filename
|
||||||
|
if not path.exists():
|
||||||
|
missing.append(str(path))
|
||||||
|
if missing:
|
||||||
|
raise SystemExit("missing bilingual docs: " + ", ".join(missing))
|
||||||
|
print("public docs have zh/en file pairs")
|
||||||
|
PY
|
||||||
|
```
|
||||||
|
|
||||||
|
- 检查公开文档里没有用裸 `.md` 文件名当链接标题。这个命令在 polished public docs 中应无输出:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "\[[^]]+\.md\]\(" docs/technical/zh docs/technical/en
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 对文档中提到的关键路径做快速验证
|
# 对文档中提到的关键路径做快速验证
|
||||||
ls <mentioned_paths>
|
ls <mentioned_paths>
|
||||||
@@ -167,4 +226,7 @@ rg -n "\]\(([^)]+)\)" docs/technical/zh/<doc>.md
|
|||||||
- 不要在文档中引用 PR 号、issue 号、或当前对话——这些会随时间失效
|
- 不要在文档中引用 PR 号、issue 号、或当前对话——这些会随时间失效
|
||||||
- 代码片段保持简洁,只保留说明问题的关键部分,省略无关样板代码
|
- 代码片段保持简洁,只保留说明问题的关键部分,省略无关样板代码
|
||||||
- 如果某个变更已有文档记录,优先在原文档中追加,而不是新建
|
- 如果某个变更已有文档记录,优先在原文档中追加,而不是新建
|
||||||
|
- 公开 technical 文档没有注册 `DOCS_METADATA` 时,Docs 页面不会显示;不要只创建 `.md` 文件就结束。
|
||||||
|
- 公开 technical 文档默认需要 zh/en 同名文件,不要只补一个语言版本。
|
||||||
|
- 链接可见文字使用文档标题或语义标题,不要使用裸文件名。
|
||||||
- 文档是给未来的开发者看的,假设读者熟悉项目但不了解这次改动的背景
|
- 文档是给未来的开发者看的,假设读者熟悉项目但不了解这次改动的背景
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ rg -n "class |def |function |export |router|@router|interface |type " <path>
|
|||||||
- Collector, datasource, credential, settings, connectivity, scheduler, or API changes must update the relevant backend docs, especially `docs/technical/zh/backend-collectors.md` and any datasource/settings-specific doc.
|
- Collector, datasource, credential, settings, connectivity, scheduler, or API changes must update the relevant backend docs, especially `docs/technical/zh/backend-collectors.md` and any datasource/settings-specific doc.
|
||||||
- When a change turns an old plan assumption into current behavior, update the relevant `docs/plans/*.md` with a status note instead of leaving contradictory instructions.
|
- When a change turns an old plan assumption into current behavior, update the relevant `docs/plans/*.md` with a status note instead of leaving contradictory instructions.
|
||||||
- If adding a new technical document, add it to `docs/technical/zh/README.md` when it should be discoverable from the technical docs index.
|
- If adding a new technical document, add it to `docs/technical/zh/README.md` when it should be discoverable from the technical docs index.
|
||||||
|
- If a technical document should be visible in the public Docs page or linked from a technical README, register it in `frontend/src/pages/Docs/docs-content.ts` under `DOCS_METADATA`. The frontend uses this whitelist; files under `docs/technical/{zh,en}/` are not automatically routable.
|
||||||
|
- For every public technical doc, keep the bilingual file pair in sync by filename: `docs/technical/zh/<name>.md` and `docs/technical/en/<name>.md`. If the content is intentionally Chinese-only or English-only, state that intentionally in the final note.
|
||||||
- Search docs for stale terms introduced by the change, for example old tab names, old route responsibilities, obsolete auth assumptions, or renamed UI labels.
|
- Search docs for stale terms introduced by the change, for example old tab names, old route responsibilities, obsolete auth assumptions, or renamed UI labels.
|
||||||
|
|
||||||
4. Write the doc in Chinese:
|
4. Write the doc in Chinese:
|
||||||
@@ -100,6 +102,62 @@ rg -n "/home/ray/dev/linkong/planet/docs/technical/(?!zh|en)" docs/technical/zh
|
|||||||
|
|
||||||
This command should return no matches.
|
This command should return no matches.
|
||||||
|
|
||||||
|
Check that public docs are whitelisted in the frontend Docs registry. Any `.md` linked from `docs/technical/{zh,en}/README.md` and located under `docs/technical/{zh,en}/` must have a matching `DOCS_METADATA` key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python - <<'PY'
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
metadata = Path("frontend/src/pages/Docs/docs-content.ts").read_text()
|
||||||
|
known = set(re.findall(r"'([^']+\.md)':\s*\{", metadata))
|
||||||
|
known.add("README.md")
|
||||||
|
|
||||||
|
missing = []
|
||||||
|
for readme in [Path("docs/technical/zh/README.md"), Path("docs/technical/en/README.md")]:
|
||||||
|
if not readme.exists():
|
||||||
|
continue
|
||||||
|
for href in re.findall(r"\]\(([^)]+\.md)\)", readme.read_text()):
|
||||||
|
path = Path(href)
|
||||||
|
if "docs/technical/" not in href:
|
||||||
|
continue
|
||||||
|
filename = path.name
|
||||||
|
if filename not in known:
|
||||||
|
missing.append(f"{readme}: {filename}")
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
raise SystemExit("docs README links missing DOCS_METADATA: " + ", ".join(missing))
|
||||||
|
print("docs README links are whitelisted")
|
||||||
|
PY
|
||||||
|
```
|
||||||
|
|
||||||
|
Check bilingual parity for public docs. Every whitelisted document except `README.md` should exist in both language directories unless intentionally documented otherwise:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python - <<'PY'
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
metadata = Path("frontend/src/pages/Docs/docs-content.ts").read_text()
|
||||||
|
filenames = sorted(set(re.findall(r"'([^']+\.md)':\s*\{", metadata)) - {"README.md"})
|
||||||
|
missing = []
|
||||||
|
for filename in filenames:
|
||||||
|
for lang in ("zh", "en"):
|
||||||
|
path = Path("docs/technical") / lang / filename
|
||||||
|
if not path.exists():
|
||||||
|
missing.append(str(path))
|
||||||
|
if missing:
|
||||||
|
raise SystemExit("missing bilingual docs: " + ", ".join(missing))
|
||||||
|
print("public docs have zh/en file pairs")
|
||||||
|
PY
|
||||||
|
```
|
||||||
|
|
||||||
|
Check that Markdown links do not expose raw filenames as user-facing titles. This should return no matches for polished public docs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "\[[^]]+\.md\]\(" docs/technical/zh docs/technical/en
|
||||||
|
```
|
||||||
|
|
||||||
If checking many links, prefer deterministic extraction:
|
If checking many links, prefer deterministic extraction:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -118,6 +176,9 @@ rg -n "old label|old route purpose|obsolete provider assumption" docs/technical
|
|||||||
- Do not leave a Chinese doc with only an English title and English first-screen content.
|
- Do not leave a Chinese doc with only an English title and English first-screen content.
|
||||||
- When an English counterpart exists in `docs/technical/en/`, never duplicate it byte-for-byte into `docs/technical/zh/`.
|
- When an English counterpart exists in `docs/technical/en/`, never duplicate it byte-for-byte into `docs/technical/zh/`.
|
||||||
- Internal links inside `docs/technical/zh/` should point to `docs/technical/zh/...` for Chinese docs, unless intentionally linking to an English-only file.
|
- Internal links inside `docs/technical/zh/` should point to `docs/technical/zh/...` for Chinese docs, unless intentionally linking to an English-only file.
|
||||||
|
- Public technical documents must be registered in `frontend/src/pages/Docs/docs-content.ts` before considering them available in the Docs UI.
|
||||||
|
- Public technical documents should have both zh and en files with the same filename, unless intentionally exempted.
|
||||||
|
- Markdown link text in public docs should be a readable title, not a raw filename such as `manual.md`.
|
||||||
- Do not reference PR numbers, issue numbers, or the current conversation.
|
- Do not reference PR numbers, issue numbers, or the current conversation.
|
||||||
- Do not write changelog-style lists like "changed A, changed B, changed C" without the constraints and tradeoffs behind those changes.
|
- Do not write changelog-style lists like "changed A, changed B, changed C" without the constraints and tradeoffs behind those changes.
|
||||||
- Keep code snippets concise and relevant.
|
- Keep code snippets concise and relevant.
|
||||||
@@ -133,4 +194,7 @@ Updated:
|
|||||||
Verified:
|
Verified:
|
||||||
- no identical en/zh docs
|
- no identical en/zh docs
|
||||||
- no language-less docs/technical links in zh docs
|
- no language-less docs/technical links in zh docs
|
||||||
|
- public docs are registered in DOCS_METADATA
|
||||||
|
- public docs have zh/en file pairs
|
||||||
|
- no raw `.md` filenames as public link titles
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -8,6 +8,44 @@ 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
|
||||||
|
|
||||||
|
Released: 2026-04-30
|
||||||
|
|
||||||
|
### 🐛 Fixes
|
||||||
|
- 修复新增 technical docs 文件存在但未进入 Docs 前端白名单时,侧栏不显示且 Markdown 链接无法解析到 `/docs/<slug>` 的问题。
|
||||||
|
- 补齐数据源/采集器连接验证与 Earth Interactable 使用说明的英文文档,保证公开 Docs 切换 EN 时同名页面可访问。
|
||||||
|
- 清理中英文 technical docs 中裸 `.md` 文件名链接标题,改为面向读者的语义标题。
|
||||||
|
|
||||||
|
### 📝 Documentation
|
||||||
|
- 将 Docs 前端白名单、公开文档双语配对、裸文件名链接标题三项检查写入 Claude 与 Codex 的 docs 技能流程。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.46.0] — 2026-04-30
|
## [0.46.0] — 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 接上游实时源
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,16 @@ What belongs here:
|
|||||||
- Earth layer style property index
|
- Earth layer style property index
|
||||||
- Backend runtime control
|
- Backend runtime control
|
||||||
- Collector status
|
- Collector status
|
||||||
|
- Collector settings and connectivity validation
|
||||||
|
- Earth Interactable integration
|
||||||
- Collection format conventions
|
- Collection format conventions
|
||||||
|
|
||||||
## Entry Points
|
## Entry Points
|
||||||
|
|
||||||
- [quickstart.md](/home/ray/dev/linkong/planet/docs/technical/en/quickstart.md): The shortest path to getting Planet running from scratch
|
- [Quickstart](/home/ray/dev/linkong/planet/docs/technical/en/quickstart.md): The shortest path to getting Planet running from scratch
|
||||||
- [manual.md](/home/ray/dev/linkong/planet/docs/technical/en/manual.md): Complete usage guide for the console, `planet.sh`, Earth, and Docs
|
- [Planet Manual](/home/ray/dev/linkong/planet/docs/technical/en/manual.md): Complete usage guide for the console, `planet.sh`, Earth, and Docs
|
||||||
|
- [Collector Settings and Connectivity Validation](/home/ray/dev/linkong/planet/docs/technical/en/datasource-collector-settings-connectivity.md): Data source catalog, collector settings, connectivity validation, and BarentsWatch credentials
|
||||||
|
- [Earth Interactable Usage](/home/ray/dev/linkong/planet/docs/technical/en/earth-interactable-usage.md): API, lifecycle, and integration examples for Earth surface icon Interactable
|
||||||
|
|
||||||
What does not belong here:
|
What does not belong here:
|
||||||
|
|
||||||
@@ -32,4 +36,4 @@ What does not belong here:
|
|||||||
|
|
||||||
Those belong in:
|
Those belong in:
|
||||||
|
|
||||||
- [docs/plans/README.md](/home/ray/dev/linkong/planet/docs/plans/README.md)
|
- [Plans Index](/home/ray/dev/linkong/planet/docs/plans/README.md)
|
||||||
|
|||||||
325
docs/technical/en/datasource-collector-settings-connectivity.md
Normal file
325
docs/technical/en/datasource-collector-settings-connectivity.md
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
# Collector Settings and Connectivity Validation
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
The console now separates the "data source catalog" from "collector configuration":
|
||||||
|
|
||||||
|
- `/datasources`
|
||||||
|
- Lists all data sources, including built-in and custom sources.
|
||||||
|
- Clicking a name only opens an information drawer.
|
||||||
|
- Focuses on status, manual collection, and running collection tasks.
|
||||||
|
- `/settings?tab=collector_credentials`
|
||||||
|
- Displays as "Collector Settings".
|
||||||
|
- Owns endpoint, headers, base parameters, and credentials.
|
||||||
|
- Every collector exposes a connection button for health checks.
|
||||||
|
|
||||||
|
This reduces first-use confusion: API endpoints, headers, credentials, and custom source configuration all belong to collector settings instead of being scattered across the data source list and system settings.
|
||||||
|
|
||||||
|
## User-Facing Rules
|
||||||
|
|
||||||
|
Connection state is not a frontend styling state. The backend derives it from the current configuration checksum and previously validated records.
|
||||||
|
|
||||||
|
A built-in collector is considered "connected" when either condition is true:
|
||||||
|
|
||||||
|
- The current configuration has successfully collected data.
|
||||||
|
- The user clicked the connection button for the current configuration and backend validation succeeded.
|
||||||
|
|
||||||
|
If endpoint, headers, base configuration, or credential fingerprint changes after the last successful validation, the state returns to "needs reconnection".
|
||||||
|
|
||||||
|
## Frontend Entry Points
|
||||||
|
|
||||||
|
### Data Source Catalog
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
- [DataSources.tsx](/home/ray/dev/linkong/planet/frontend/src/pages/DataSources/DataSources.tsx)
|
||||||
|
- [index.css](/home/ray/dev/linkong/planet/frontend/src/index.css)
|
||||||
|
|
||||||
|
Current behavior:
|
||||||
|
|
||||||
|
- Built-in and custom data sources are merged into a `UnifiedDataSource` list.
|
||||||
|
- The table only keeps view, collect, and status actions.
|
||||||
|
- Clicking the name opens a read-only drawer.
|
||||||
|
- The drawer shows:
|
||||||
|
- Whether the source is built in
|
||||||
|
- Whether it is enabled
|
||||||
|
- Module, priority, and frequency
|
||||||
|
- Endpoint
|
||||||
|
- Headers
|
||||||
|
- Base configuration
|
||||||
|
- Whether credentials are required
|
||||||
|
- When tasks are running, the top progress area shows a clickable `Collecting N` pill.
|
||||||
|
- Clicking `Collecting N` opens a task list modal with per-task progress.
|
||||||
|
|
||||||
|
`data-source-bulk-toolbar__running-pill` is the styling entry point for the "Collecting" pill. It is aligned with other status tags, while hover treatment, arrow affordance, and blue outline indicate interactivity.
|
||||||
|
|
||||||
|
### Collector Settings
|
||||||
|
|
||||||
|
File:
|
||||||
|
|
||||||
|
- [Settings.tsx](/home/ray/dev/linkong/planet/frontend/src/pages/Settings/Settings.tsx)
|
||||||
|
|
||||||
|
Current behavior:
|
||||||
|
|
||||||
|
- The `collector_credentials` tab is displayed as "Collector Settings".
|
||||||
|
- A select lists all built-in collectors.
|
||||||
|
- The only button beside the select is a plug icon for health checks.
|
||||||
|
- Status tags below the select show:
|
||||||
|
- `Credentials required` / `No credentials required`
|
||||||
|
- Module
|
||||||
|
- `Enabled` / `Disabled`
|
||||||
|
- `Unchecked` / `Available` / `Unavailable`
|
||||||
|
- Whether the endpoint is overridden
|
||||||
|
- Collectors that require credentials place the credential card above base configuration.
|
||||||
|
- Collectors without credentials only show base configuration.
|
||||||
|
|
||||||
|
The connection button uses an inline Tabler-style plug icon with `plug-connected` semantics, avoiding the older refresh icon for a connection action.
|
||||||
|
|
||||||
|
## Backend APIs
|
||||||
|
|
||||||
|
### Data Source Configuration List
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/datasources/configs/all
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns a merged view of YAML default data sources and database overrides. This route must be declared before `/configs/{config_id}`; otherwise FastAPI treats `all` as a path parameter and returns 422.
|
||||||
|
|
||||||
|
Returned fields include:
|
||||||
|
|
||||||
|
- `name`
|
||||||
|
- `default_url`
|
||||||
|
- `endpoint`
|
||||||
|
- `is_overridden`
|
||||||
|
- `is_active`
|
||||||
|
- `source_type`
|
||||||
|
- `auth_type`
|
||||||
|
- `headers`
|
||||||
|
- `config`
|
||||||
|
- `config_id`
|
||||||
|
- `description`
|
||||||
|
|
||||||
|
Before returning `config`, internal connectivity validation fields are removed so the frontend does not display validation metadata as user configuration.
|
||||||
|
|
||||||
|
### Built-In Collector Connection Status
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/datasources/configs/builtin/connection-status
|
||||||
|
```
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
|
||||||
|
- Accept a candidate configuration.
|
||||||
|
- Compute its checksum.
|
||||||
|
- Determine whether the current configuration is already connected.
|
||||||
|
|
||||||
|
The current frontend mostly performs an immediate check through the connection button and does not strongly depend on this endpoint. It remains the backend basis for future save-button disabling and restoring initial page state.
|
||||||
|
|
||||||
|
### Built-In Collector Connectivity Validation
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/datasources/configs/builtin/connect
|
||||||
|
```
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
|
||||||
|
- Free collectors request the endpoint directly.
|
||||||
|
- Credentialed collectors go through their credential provider.
|
||||||
|
- Successful validation writes a system-level connection record.
|
||||||
|
|
||||||
|
Successful responses include:
|
||||||
|
|
||||||
|
- `success`
|
||||||
|
- `connected`
|
||||||
|
- `checksum`
|
||||||
|
- `stage`
|
||||||
|
- `message`
|
||||||
|
- `response_time_ms`
|
||||||
|
- `credential_provider`
|
||||||
|
- `credential_source`
|
||||||
|
|
||||||
|
### BarentsWatch AIS Connectivity Validation
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/settings/integrations/barentswatch/connect
|
||||||
|
GET /api/v1/settings/integrations/barentswatch/connectivity
|
||||||
|
```
|
||||||
|
|
||||||
|
BarentsWatch uses separate endpoints because draft credentials must be validated before saving:
|
||||||
|
|
||||||
|
- Use draft `client_id` / `client_secret` to fetch a token.
|
||||||
|
- Use that token to request the AIS endpoint.
|
||||||
|
- After success, write a built-in collector connection record using the draft credential fingerprint.
|
||||||
|
|
||||||
|
## Connectivity Validation Service
|
||||||
|
|
||||||
|
File:
|
||||||
|
|
||||||
|
- [datasource_connectivity.py](/home/ray/dev/linkong/planet/backend/app/services/datasource_connectivity.py)
|
||||||
|
|
||||||
|
Core responsibilities:
|
||||||
|
|
||||||
|
- Compute built-in collector configuration checksums.
|
||||||
|
- Read credentials from environment variables and `~/.zshrc`.
|
||||||
|
- Determine whether the current configuration is already connected.
|
||||||
|
- Run endpoint health checks.
|
||||||
|
- Save successful connection records.
|
||||||
|
|
||||||
|
### Checksum Inputs
|
||||||
|
|
||||||
|
The checksum includes:
|
||||||
|
|
||||||
|
- Collector name
|
||||||
|
- Endpoint
|
||||||
|
- Auth type
|
||||||
|
- Headers
|
||||||
|
- Config after removing internal validation fields
|
||||||
|
- Credential provider
|
||||||
|
- Credential fingerprint
|
||||||
|
|
||||||
|
The credential fingerprint is a hash of credential content. Plaintext credentials are not written into connection records.
|
||||||
|
|
||||||
|
### Connection Records
|
||||||
|
|
||||||
|
Successful connection records are written to `SystemSetting`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
category = datasource_connectivity_validations
|
||||||
|
```
|
||||||
|
|
||||||
|
The payload uses collector source as the key:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"barentswatch_vessels": {
|
||||||
|
"checksum": "...",
|
||||||
|
"status": "success",
|
||||||
|
"validated_at": "2026-04-29T00:00:00+00:00",
|
||||||
|
"status_code": 200,
|
||||||
|
"credential_source": "datasource_config",
|
||||||
|
"connected_by": "connection_button"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`connected_by` currently has two sources:
|
||||||
|
|
||||||
|
- `connection_button`: the user manually clicked the connection button.
|
||||||
|
- `collection`: a collection task completed successfully, so the system recorded the current effective configuration as connected.
|
||||||
|
|
||||||
|
### Successful Collection Means Connected
|
||||||
|
|
||||||
|
After a successful collection, the scheduler writes a connection record:
|
||||||
|
|
||||||
|
- [scheduler.py](/home/ray/dev/linkong/planet/backend/app/services/scheduler.py)
|
||||||
|
|
||||||
|
This prevents collectors that already have data from asking the user to validate again. Reconnection is only required when the configuration checksum changes.
|
||||||
|
|
||||||
|
## BarentsWatch AIS Credential Chain
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
- [barentswatch.py](/home/ray/dev/linkong/planet/backend/app/services/barentswatch.py)
|
||||||
|
- [vessel_ais.py](/home/ray/dev/linkong/planet/backend/app/services/collectors/vessel_ais.py)
|
||||||
|
|
||||||
|
Resolution priority:
|
||||||
|
|
||||||
|
1. `DataSourceConfig.auth_config`
|
||||||
|
2. `DataSourceConfig.config`
|
||||||
|
3. Environment variables
|
||||||
|
4. `~/.zshrc`
|
||||||
|
|
||||||
|
Supported environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export BARENTSWATCH_CLIENT_ID="..."
|
||||||
|
export BARENTSWATCH_CLIENT_SECRET="..."
|
||||||
|
```
|
||||||
|
|
||||||
|
Historical misspellings are also supported:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export BARRENTSWATCH_CLIENT_ID="..."
|
||||||
|
export BARRENTSWATCH_CLIENT_SECRET="..."
|
||||||
|
```
|
||||||
|
|
||||||
|
Token request rules:
|
||||||
|
|
||||||
|
- Token URL: `https://id.barentswatch.no/connect/token`
|
||||||
|
- `Content-Type`: `application/x-www-form-urlencoded`
|
||||||
|
- Body:
|
||||||
|
- `grant_type=client_credentials`
|
||||||
|
- `client_id`
|
||||||
|
- `client_secret`
|
||||||
|
- `scope=ais`
|
||||||
|
|
||||||
|
AIS request rules:
|
||||||
|
|
||||||
|
- Default endpoint: `https://live.ais.barentswatch.no/v1/latest/combined`
|
||||||
|
- Header: `Authorization: Bearer <access_token>`
|
||||||
|
|
||||||
|
`VesselAISCollector` no longer reads environment variables directly. It goes through `resolve_barentswatch_config()` and `fetch_barentswatch_access_token()` so settings, connectivity validation, and collection do not fork into three credential flows.
|
||||||
|
|
||||||
|
## Credential Guide
|
||||||
|
|
||||||
|
File:
|
||||||
|
|
||||||
|
- [credential_guides.py](/home/ray/dev/linkong/planet/backend/app/services/credential_guides.py)
|
||||||
|
|
||||||
|
APIs:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/settings/credential-guides/{provider}
|
||||||
|
POST /api/v1/settings/credential-guides/{provider}/generate
|
||||||
|
POST /api/v1/settings/credential-guides/{provider}/reset
|
||||||
|
```
|
||||||
|
|
||||||
|
Currently supported:
|
||||||
|
|
||||||
|
- `barentswatch`
|
||||||
|
|
||||||
|
The default guide includes the official BarentsWatch tutorial:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://developer.barentswatch.no/docs/tutorial
|
||||||
|
```
|
||||||
|
|
||||||
|
If the user clicks that the tutorial is not useful, the backend sends the default prompt to AI Provider, generates a new Chinese tutorial, and saves it to `SystemSetting`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
category = collector_credential_guides
|
||||||
|
```
|
||||||
|
|
||||||
|
Reset deletes the custom tutorial and restores the default guide.
|
||||||
|
|
||||||
|
## Save Rules
|
||||||
|
|
||||||
|
When built-in collector configuration is saved, the internal `connectivity_validation` field is removed so validation state does not mix with user configuration.
|
||||||
|
|
||||||
|
BarentsWatch `client_secret` has special handling:
|
||||||
|
|
||||||
|
- The input shows a masked preview.
|
||||||
|
- If the submitted value still matches the masked preview, the backend keeps the old secret.
|
||||||
|
- If a new value is submitted, the secret is replaced.
|
||||||
|
- The previous separate "clear current secret" checkbox is no longer provided.
|
||||||
|
|
||||||
|
## Test Coverage
|
||||||
|
|
||||||
|
Related tests:
|
||||||
|
|
||||||
|
- [test_vessels.py](/home/ray/dev/linkong/planet/backend/tests/test_vessels.py)
|
||||||
|
|
||||||
|
Added coverage:
|
||||||
|
|
||||||
|
- BarentsWatch credentials can be parsed from `~/.zshrc`.
|
||||||
|
- When environment variables are empty, `resolve_barentswatch_config()` can fall back to `~/.zshrc`.
|
||||||
|
- Vessel data conversion and GeoJSON output remain compatible.
|
||||||
|
|
||||||
|
## Current Provider Coverage
|
||||||
|
|
||||||
|
Credential providers currently supported:
|
||||||
|
|
||||||
|
- `barentswatch`
|
||||||
|
- `spacetrack`
|
||||||
|
|
||||||
|
Other collectors with `requires_credentials=true` return that their credential chain has not been wired yet, and the frontend shows `Unavailable`.
|
||||||
@@ -187,7 +187,7 @@ Current reality:
|
|||||||
- that is expected, because incidents are aggregated and de-noised
|
- that is expected, because incidents are aggregated and de-noised
|
||||||
- but incident-first rendering makes the Earth view look too quiet unless there is another always-available activity layer
|
- but incident-first rendering makes the Earth view look too quiet unless there is another always-available activity layer
|
||||||
|
|
||||||
Implementation detail for the recommended `activity layer` is expanded in [bgp-region-aggregation-plan.md](/home/ray/dev/linkong/planet/docs/plans/earth-bgp-region-aggregation-plan.md).
|
Implementation detail for the recommended `activity layer` is expanded in the [BGP Region Aggregation Plan](/home/ray/dev/linkong/planet/docs/plans/earth-bgp-region-aggregation-plan.md).
|
||||||
|
|
||||||
So the immediate next milestone is:
|
So the immediate next milestone is:
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ This document describes the current real structure of the Earth display frontend
|
|||||||
|
|
||||||
Related references:
|
Related references:
|
||||||
|
|
||||||
- [rules.md](/home/ray/dev/linkong/planet/rules.md)
|
- [Project Rules](/home/ray/dev/linkong/planet/rules.md)
|
||||||
- [frontend-layout-guidelines.md](/home/ray/dev/linkong/planet/docs/technical/en/frontend-layout-guidelines.md)
|
- [Frontend Layout Guidelines](/home/ray/dev/linkong/planet/docs/technical/en/frontend-layout-guidelines.md)
|
||||||
|
|
||||||
## Current Goal
|
## Current Goal
|
||||||
|
|
||||||
@@ -249,4 +249,4 @@ Therefore:
|
|||||||
|
|
||||||
For console structure, see:
|
For console structure, see:
|
||||||
|
|
||||||
- [frontend-admin-frontend-context.md](/home/ray/dev/linkong/planet/docs/technical/en/frontend-admin-frontend-context.md)
|
- [Admin Frontend Context](/home/ray/dev/linkong/planet/docs/technical/en/frontend-admin-frontend-context.md)
|
||||||
|
|||||||
270
docs/technical/en/earth-interactable-usage.md
Normal file
270
docs/technical/en/earth-interactable-usage.md
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
# Earth Interactable Usage
|
||||||
|
|
||||||
|
`Interactable` is the shared rendering entry point for icon-like interactive elements on the Earth surface. It extracts the pattern proven by the vessel layer into reusable behavior: normal state uses batched `THREE.Points`, hover and locked states use small overlays, picking uses screen-space hit testing, icon assets are normalized into canvas textures, and the shared layer handles glow, state, size, ground rendering, and same-coordinate avoidance.
|
||||||
|
|
||||||
|
Currently integrated layers:
|
||||||
|
|
||||||
|
| Layer | Business File | Icon Source | Extra Animation |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| AIS vessels | `frontend/public/earth/js/vessels.js` | canvas draw, moving triangle / anchored dot | Vessel tracks are still maintained by the business layer |
|
||||||
|
| Compute centers | `frontend/public/earth/js/compute-centers.js` | `assets/icons/compute-*.svg` | Estimated-location `?` badge is added through `icon.afterDraw()` |
|
||||||
|
| BGP events | `frontend/public/earth/js/bgp.js` | canvas draw, symbol by event type | Expanding rings are still maintained by the BGP business layer |
|
||||||
|
| BGP observers | `frontend/public/earth/js/bgp.js` | `assets/icons/bgp-broadcast-pin.svg` | Halo, activity core, coverage wedge, and radar sweep remain in the BGP business layer |
|
||||||
|
|
||||||
|
Landing sites were previously attempted on Interactable, but pin-style SVGs were fragmented by `THREE.Points` depth testing near the Earth edge. They now use a dedicated `THREE.Sprite` path with a yellow flat-sphere texture generated by canvas. The old SVG assets remain in `assets/icons/`, but landing sites no longer depend on SVG at runtime.
|
||||||
|
|
||||||
|
## Why Interactable Exists
|
||||||
|
|
||||||
|
Before this layer, each surface icon layer could easily reimplement its own version of:
|
||||||
|
|
||||||
|
- icon texture generation
|
||||||
|
- hover / locked state
|
||||||
|
- glow styling
|
||||||
|
- picking radius
|
||||||
|
- zoom-dependent size strategy
|
||||||
|
- overlap avoidance for identical coordinates
|
||||||
|
|
||||||
|
When this logic is scattered across business files, visual behavior drifts and later tuning becomes layer-by-layer repair. The boundary of `Interactable` is: the shared layer owns how icons remain stable on Earth and how they are selected; the business layer owns where data comes from, what the icon means, what detail cards show, and whether extra animation exists.
|
||||||
|
|
||||||
|
## Entry Point
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { createInteractableLayer } from "./interactable.js";
|
||||||
|
```
|
||||||
|
|
||||||
|
Core call shape:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const layer = createInteractableLayer({
|
||||||
|
id: "example",
|
||||||
|
objectType: "example_object",
|
||||||
|
renderOrder: 4.4,
|
||||||
|
altitudeOffset: 0.2,
|
||||||
|
pointSize: 34,
|
||||||
|
icon: {
|
||||||
|
draw(context, options) {
|
||||||
|
// draw canvas icon
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getPosition: (item) => ({
|
||||||
|
latitude: item.latitude,
|
||||||
|
longitude: item.longitude,
|
||||||
|
}),
|
||||||
|
getKind: (item) => item.kind || "default",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Business modules usually expose only a thin wrapper:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export function getExampleMarkers() {
|
||||||
|
return layer.getMarkers();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExamplePointerIntersections(options) {
|
||||||
|
return layer.getPointerIntersections(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setExampleMarkerState(marker, state = "normal") {
|
||||||
|
layer.setMarkerState(marker, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateExampleVisualState(lockedObjectType, lockedObject, camera) {
|
||||||
|
layer.updateVisualState(lockedObjectType, lockedObject, camera);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id` | required | Unique layer id used for group name, avoidance registration, and debug. |
|
||||||
|
| `objectType` | `id` | Business type written to `marker.userData.type`; the main interaction layer uses it to identify locked objects. |
|
||||||
|
| `renderOrder` | `4` | Base render order for normal points and hover / locked overlays. |
|
||||||
|
| `altitudeOffset` | `0.2` | Business altitude, used as `CONFIG.earthRadius + altitudeOffset` for the original surface position. |
|
||||||
|
| `pointSize` | `32` | Base screen pixel size used by both normal points and overlays. |
|
||||||
|
| `sizeMode` | `"fixed"` | Fixed screen size by default; non-`"fixed"` modes scale by camera distance. |
|
||||||
|
| `sizeScale` | `{ referenceFov: 75, min: 0.12, max: 3 }` | Scaling bounds when `sizeMode !== "fixed"`. |
|
||||||
|
| `atlasCellSize` | `128` | Canvas texture cell size for icons. |
|
||||||
|
| `colors` | `{}` | Supports `normal`, flattened kind keys, and `byKind`. |
|
||||||
|
| `opacity` | `{ normal: 0.88, dimmed: 0.26, hover: 0.98, locked: 1 }` | Opacity per state. |
|
||||||
|
| `stateScale` | `{ hover: 1, locked: 1, dimmed: 1 }` | Size multiplier per state. |
|
||||||
|
| `pulse` | `{}` | Optional locked-state breathing scale, with `enabled`, `speed`, and `amplitude`. |
|
||||||
|
| `avoidance` | `{ enabled: true, precision: 4, radius: 1.1, step: 0.35 }` | Same-coordinate avoidance across Interactable layers. |
|
||||||
|
| `icon` | required | Icon source, supporting canvas draw, SVG / image asset, state asset, anchor, and post-processing. |
|
||||||
|
| `getPosition(item)` | required | Returns `{ latitude, longitude }` or `THREE.Vector3`. |
|
||||||
|
| `getKind(item)` | `item.type || "default"` | Returns a business kind for color and texture buckets. |
|
||||||
|
| `getRotationBin(marker)` | `0` | Returns a rotation bucket, such as 32 heading buckets for vessels. |
|
||||||
|
| `getBucketKey(marker)` | `String(getRotationBin(marker))` | Returns a texture / geometry bucket key. |
|
||||||
|
| `getPointSizeMultiplier(marker)` | `1` | Per-marker size multiplier. BGP events use severity; observers use activity. |
|
||||||
|
| `getUserData(item)` | `item` | Business fields written onto the marker. |
|
||||||
|
|
||||||
|
## Icon Configuration
|
||||||
|
|
||||||
|
`icon.anchor` is optional and defaults to `{ x: 0.5, y: 0.5 }`, meaning the texture center aligns with the marker coordinate. It is only suitable for small visual anchor offsets. If the icon body is large and must remain fully visible at the Earth edge, such as the old landing-site pin, it should not be forced through `THREE.Points + depthTest`; the body will be clipped by Earth depth.
|
||||||
|
|
||||||
|
### Canvas Icons
|
||||||
|
|
||||||
|
Canvas icons fit vessels and BGP events where symbols need to be drawn dynamically by state or rotation:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const vesselIconLayer = createInteractableLayer({
|
||||||
|
id: "vessels",
|
||||||
|
objectType: "vessel",
|
||||||
|
pointSize: 34,
|
||||||
|
icon: {
|
||||||
|
draw(context, { marker, rotationBin = 0, glow = false, color = "#ffffff" }) {
|
||||||
|
if (!marker.userData.anchored) {
|
||||||
|
context.rotate((rotationBin / 32) * Math.PI * 2);
|
||||||
|
}
|
||||||
|
context.fillStyle = color;
|
||||||
|
context.shadowColor = color;
|
||||||
|
context.shadowBlur = glow ? 14 : 0;
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(0, -37);
|
||||||
|
context.lineTo(28, 32);
|
||||||
|
context.lineTo(0, 17);
|
||||||
|
context.lineTo(-28, 32);
|
||||||
|
context.closePath();
|
||||||
|
context.fill();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getRotationBin: getCourseBin,
|
||||||
|
getBucketKey: (marker) => `${marker.userData.anchored ? "anchored" : "moving"}:${getCourseBin(marker)}`,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
When `icon.coordinates !== "canvas"`, `Interactable` translates the context to the atlas center first. Vessel-style icons that already draw around center coordinates do not need to declare `coordinates`.
|
||||||
|
|
||||||
|
### SVG / Image Asset Icons
|
||||||
|
|
||||||
|
Asset icons fit facilities such as compute centers and BGP observers:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const computeCenterIconLayer = createInteractableLayer({
|
||||||
|
id: "computeCenters",
|
||||||
|
objectType: "compute_center",
|
||||||
|
pointSize: 36,
|
||||||
|
atlasCellSize: 128,
|
||||||
|
icon: {
|
||||||
|
coordinates: "canvas",
|
||||||
|
colorable: false,
|
||||||
|
fitSize: 60,
|
||||||
|
glowBlur: 16,
|
||||||
|
getSource({ marker, item }) {
|
||||||
|
const siteType = marker?.userData?.site_type || item?.site_type || "gpu_cluster";
|
||||||
|
return COMPUTE_CENTER_ICON_SOURCES[siteType];
|
||||||
|
},
|
||||||
|
afterDraw(context, { marker, item }) {
|
||||||
|
if (marker?.userData?.is_estimated ?? item?.is_estimated) {
|
||||||
|
drawComputeCenterEstimatedBadge(context, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Asset conventions:
|
||||||
|
|
||||||
|
- SVG / image files live in `frontend/public/earth/assets/icons/` and are referenced as `/earth/assets/icons/name.svg`.
|
||||||
|
- Original SVGs should keep a standard `viewBox` and paths; avoid hard-coding transform only for display size.
|
||||||
|
- Display size is controlled by `icon.fitSize`; it can be a number, `{ width, height }`, or a function.
|
||||||
|
- If `icon.colorable !== false` and state colors are provided, the shared layer first draws the asset to a temporary canvas and then tints it with `source-in`.
|
||||||
|
- Multicolor images or SVGs that should not be tinted must set `colorable: false`.
|
||||||
|
|
||||||
|
## Lifecycle
|
||||||
|
|
||||||
|
Typical load flow:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export async function loadExampleLayer(_scene, earth) {
|
||||||
|
clearExampleData(earth);
|
||||||
|
|
||||||
|
const markerData = await fetchExampleData();
|
||||||
|
await layer.preloadAssets(markerData);
|
||||||
|
layer.setData(markerData);
|
||||||
|
layer.attach(earth);
|
||||||
|
layer.setVisible(showExampleLayer);
|
||||||
|
|
||||||
|
return { totalCount: layer.getCount() };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Method responsibilities:
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| `preloadAssets(items)` | Collects asset sources that may be used by normal / hover / locked states and preloads them with browser `Image`. Canvas-drawn icons can skip this. |
|
||||||
|
| `setData(items)` | Clears old points, creates markers, registers avoidance, and rebuilds `THREE.Points` by bucket. |
|
||||||
|
| `attach(parent)` | Mounts the layer group onto the Earth root. |
|
||||||
|
| `setVisible(next)` | Controls visibility for the group, points, and overlays. |
|
||||||
|
| `setMarkerState(marker, state)` | Sets `normal` / `hover` and other states, then invalidates visual state. |
|
||||||
|
| `updateVisualState(focusType, focusObject, camera)` | Updates normal opacity / size and refreshes hover / locked overlays. |
|
||||||
|
| `getPointerIntersections(options)` | Runs screen-space picking and returns hits sorted by pixel distance. |
|
||||||
|
| `clearData(parent)` | Unregisters avoidance, disposes geometry / material, clears markers, and removes the group from the parent. |
|
||||||
|
|
||||||
|
## Picking Integration
|
||||||
|
|
||||||
|
`Interactable` does not depend on the default Three.js raycast for `Points`. The main interaction layer passes Earth, camera, pointer, and hit radius:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const intersects = getVesselPointerIntersections({
|
||||||
|
earth,
|
||||||
|
camera,
|
||||||
|
pointer,
|
||||||
|
radiusPx: 22,
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The shared layer:
|
||||||
|
|
||||||
|
1. Converts the camera position into Earth-local coordinates.
|
||||||
|
2. Skips markers on the back side.
|
||||||
|
3. Projects marker world position into screen coordinates.
|
||||||
|
4. Uses `radiusPx` for pixel-distance hits.
|
||||||
|
5. Returns the nearest candidate objects.
|
||||||
|
|
||||||
|
Earth dragging, inertia, and hover throttling still belong to `main.js` because they depend on global input state.
|
||||||
|
|
||||||
|
## Same-Coordinate Avoidance
|
||||||
|
|
||||||
|
Avoidance is enabled by default and applies to all layers created through `createInteractableLayer()`. The shared layer builds an `icon_avoidance_key` from latitude / longitude or `THREE.Vector3`, then arranges markers with the same key into a small circle along the surface tangent plane.
|
||||||
|
|
||||||
|
Key points:
|
||||||
|
|
||||||
|
- `icon_base_position` keeps the original business position.
|
||||||
|
- Avoidance only changes rendering and picking position. It does not change business latitude / longitude.
|
||||||
|
- When a single marker returns to its original position, it uses the business surface position computed from `altitudeOffset`.
|
||||||
|
- When multiple markers share coordinates, the first ring uses `avoidance.radius`; later rings add `avoidance.step`.
|
||||||
|
|
||||||
|
If a business layer must stay exactly on the original point, disable avoidance explicitly:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
createInteractableLayer({
|
||||||
|
id: "strict-layer",
|
||||||
|
avoidance: { enabled: false },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Business Animation Boundary
|
||||||
|
|
||||||
|
`Interactable` currently owns only the icon body and common hover / locked overlays. Complex animations remain in business modules, but should follow the Interactable marker position:
|
||||||
|
|
||||||
|
- BGP event expanding rings are independent ring sprites created by `bgp.js`, updated every frame with `position.copy(marker.position)`.
|
||||||
|
- BGP observer halo, status core, coverage halo, and coverage wedge are managed by `bgp.js`; the icon body is managed by Interactable.
|
||||||
|
- Vessel tracks remain in `vessels.js` because they depend on track data loaded after a click.
|
||||||
|
|
||||||
|
This boundary avoids pushing every animation type into the shared interface too early. If multiple layers reuse the same animation type later, it can move into an Interactable `animations` extension.
|
||||||
|
|
||||||
|
## New Layer Checklist
|
||||||
|
|
||||||
|
1. Prepare marker data in the business file and keep required business fields.
|
||||||
|
2. Choose an icon type: canvas draw, SVG / image asset, or dynamic `getSource()`.
|
||||||
|
3. Configure `pointSize`, `icon.fitSize`, `colors`, `opacity`, and `stateScale`.
|
||||||
|
4. Provide `getPointSizeMultiplier()` if business-specific size variation is needed.
|
||||||
|
5. Provide `getRotationBin()` and a stable `getBucketKey()` if rotation exists.
|
||||||
|
6. During load, call `preloadAssets()` before `setData()`, `attach()`, and `setVisible()`.
|
||||||
|
7. Wire `getPointerIntersections()` in `main.js` and reuse the existing hover / locked state update flow.
|
||||||
|
8. Record altitude, `renderOrder`, `pointSize`, and animation ordering in the layer style index and render order documents.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Earth Layer Style Property Index
|
# Earth Layer Style Property Index
|
||||||
|
|
||||||
This document records the material, color, opacity, line width, radius offset, and `renderOrder` style properties of all Earth frontend layers. For layer ordering relationships, see [earth-render-layer-order.md](/home/ray/dev/linkong/planet/docs/technical/en/earth-render-layer-order.md).
|
This document records the material, color, opacity, line width, radius offset, and `renderOrder` style properties of all Earth frontend layers. For layer ordering relationships, see [Earth Render Layer Order](/home/ray/dev/linkong/planet/docs/technical/en/earth-render-layer-order.md).
|
||||||
|
|
||||||
## Naming Conventions
|
## Naming Conventions
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ This document records the current product boundary, data rationale, and implemen
|
|||||||
|
|
||||||
Related context:
|
Related context:
|
||||||
|
|
||||||
- [earth-frontend-context.md](/home/ray/dev/linkong/planet/docs/technical/en/earth-frontend-context.md)
|
- [Earth Frontend Context](/home/ray/dev/linkong/planet/docs/technical/en/earth-frontend-context.md)
|
||||||
- [backend-collectors.md](/home/ray/dev/linkong/planet/docs/technical/en/backend-collectors.md)
|
- [Backend Collectors](/home/ray/dev/linkong/planet/docs/technical/en/backend-collectors.md)
|
||||||
- [backend/app/services/collectors/celestrak.py](/home/ray/dev/linkong/planet/backend/app/services/collectors/celestrak.py)
|
- [backend/app/services/collectors/celestrak.py](/home/ray/dev/linkong/planet/backend/app/services/collectors/celestrak.py)
|
||||||
- [frontend/public/earth/js/satellites.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/satellites.js)
|
- [frontend/public/earth/js/satellites.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/satellites.js)
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ This document describes the current real structure of the console frontend. The
|
|||||||
|
|
||||||
Related references:
|
Related references:
|
||||||
|
|
||||||
- [rules.md](/home/ray/dev/linkong/planet/rules.md)
|
- [Project Rules](/home/ray/dev/linkong/planet/rules.md)
|
||||||
- [frontend-layout-guidelines.md](/home/ray/dev/linkong/planet/docs/technical/en/frontend-layout-guidelines.md)
|
- [Frontend Layout Guidelines](/home/ray/dev/linkong/planet/docs/technical/en/frontend-layout-guidelines.md)
|
||||||
|
|
||||||
## Current Goal
|
## Current Goal
|
||||||
|
|
||||||
@@ -263,7 +263,7 @@ These principles have been repeatedly validated in the project:
|
|||||||
|
|
||||||
For detailed experience, see:
|
For detailed experience, see:
|
||||||
|
|
||||||
- [frontend-layout-guidelines.md](/home/ray/dev/linkong/planet/docs/technical/en/frontend-layout-guidelines.md)
|
- [Frontend Layout Guidelines](/home/ray/dev/linkong/planet/docs/technical/en/frontend-layout-guidelines.md)
|
||||||
|
|
||||||
## Recommended Change Approach
|
## Recommended Change Approach
|
||||||
|
|
||||||
@@ -290,4 +290,4 @@ Therefore:
|
|||||||
|
|
||||||
For Earth-related structure, see:
|
For Earth-related structure, see:
|
||||||
|
|
||||||
- [earth-frontend-context.md](/home/ray/dev/linkong/planet/docs/technical/en/earth-frontend-context.md)
|
- [Earth Frontend Context](/home/ray/dev/linkong/planet/docs/technical/en/earth-frontend-context.md)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ This manual is for daily use, demos, development integration, and local operatio
|
|||||||
- Console: admin backend (login required)
|
- Console: admin backend (login required)
|
||||||
- Docs: public developer documentation and manual
|
- Docs: public developer documentation and manual
|
||||||
|
|
||||||
For the shortest path to getting started, see [quickstart.md](/home/ray/dev/linkong/planet/docs/technical/en/quickstart.md).
|
For the shortest path to getting started, see [Quickstart](/home/ray/dev/linkong/planet/docs/technical/en/quickstart.md).
|
||||||
|
|
||||||
## Entry Overview
|
## Entry Overview
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -585,9 +585,9 @@ When something goes wrong, follow this sequence:
|
|||||||
|
|
||||||
## Related Docs
|
## Related Docs
|
||||||
|
|
||||||
- [quickstart.md](/home/ray/dev/linkong/planet/docs/technical/en/quickstart.md)
|
- [Quickstart](/home/ray/dev/linkong/planet/docs/technical/en/quickstart.md)
|
||||||
- [frontend-admin-frontend-context.md](/home/ray/dev/linkong/planet/docs/technical/en/frontend-admin-frontend-context.md)
|
- [Admin Frontend Context](/home/ray/dev/linkong/planet/docs/technical/en/frontend-admin-frontend-context.md)
|
||||||
- [earth-frontend-context.md](/home/ray/dev/linkong/planet/docs/technical/en/earth-frontend-context.md)
|
- [Earth Frontend Context](/home/ray/dev/linkong/planet/docs/technical/en/earth-frontend-context.md)
|
||||||
- [earth-layer-style-reference.md](/home/ray/dev/linkong/planet/docs/technical/en/earth-layer-style-reference.md)
|
- [Earth Layer Style Reference](/home/ray/dev/linkong/planet/docs/technical/en/earth-layer-style-reference.md)
|
||||||
- [backend-system-service-control.md](/home/ray/dev/linkong/planet/docs/technical/en/backend-system-service-control.md)
|
- [System Service Control](/home/ray/dev/linkong/planet/docs/technical/en/backend-system-service-control.md)
|
||||||
- [backend-collectors.md](/home/ray/dev/linkong/planet/docs/technical/en/backend-collectors.md)
|
- [Backend Collectors](/home/ray/dev/linkong/planet/docs/technical/en/backend-collectors.md)
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ This shuts down the frontend, backend, AI Provider, PostgreSQL, and Redis.
|
|||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
- Full usage guide: [manual.md](/home/ray/dev/linkong/planet/docs/technical/en/manual.md)
|
- Full usage guide: [Planet Manual](/home/ray/dev/linkong/planet/docs/technical/en/manual.md)
|
||||||
- Console structure: [frontend-admin-frontend-context.md](/home/ray/dev/linkong/planet/docs/technical/en/frontend-admin-frontend-context.md)
|
- Console structure: [Admin Frontend Context](/home/ray/dev/linkong/planet/docs/technical/en/frontend-admin-frontend-context.md)
|
||||||
- Earth structure: [earth-frontend-context.md](/home/ray/dev/linkong/planet/docs/technical/en/earth-frontend-context.md)
|
- Earth structure: [Earth Frontend Context](/home/ray/dev/linkong/planet/docs/technical/en/earth-frontend-context.md)
|
||||||
- Backend collectors: [backend-collectors.md](/home/ray/dev/linkong/planet/docs/technical/en/backend-collectors.md)
|
- Backend collectors: [Backend Collectors](/home/ray/dev/linkong/planet/docs/technical/en/backend-collectors.md)
|
||||||
|
|||||||
@@ -21,10 +21,10 @@
|
|||||||
|
|
||||||
## 使用入口
|
## 使用入口
|
||||||
|
|
||||||
- [quickstart.md](/home/ray/dev/linkong/planet/docs/technical/zh/quickstart.md):从零启动 Planet 的最短路径
|
- [快速开始](/home/ray/dev/linkong/planet/docs/technical/zh/quickstart.md):从零启动 Planet 的最短路径
|
||||||
- [manual.md](/home/ray/dev/linkong/planet/docs/technical/zh/manual.md):控制台、`planet.sh`、Earth 和 Docs 的完整使用手册
|
- [Planet 使用手册](/home/ray/dev/linkong/planet/docs/technical/zh/manual.md):控制台、`planet.sh`、Earth 和 Docs 的完整使用手册
|
||||||
- [datasource-collector-settings-connectivity.md](/home/ray/dev/linkong/planet/docs/technical/zh/datasource-collector-settings-connectivity.md):数据源目录、采集器设置、连接验证、BarentsWatch 凭证链路
|
- [数据源、采集器设置与连接验证](/home/ray/dev/linkong/planet/docs/technical/zh/datasource-collector-settings-connectivity.md):数据源目录、采集器设置、连接验证、BarentsWatch 凭证链路
|
||||||
- [earth-interactable-usage.md](/home/ray/dev/linkong/planet/docs/technical/zh/earth-interactable-usage.md):Earth 地表可交互图标 `Interactable` 的接口、生命周期和接入示例
|
- [Earth 可交互图标接入](/home/ray/dev/linkong/planet/docs/technical/zh/earth-interactable-usage.md):Earth 地表可交互图标 `Interactable` 的接口、生命周期和接入示例
|
||||||
|
|
||||||
不适合放入这里的内容:
|
不适合放入这里的内容:
|
||||||
|
|
||||||
@@ -34,4 +34,4 @@
|
|||||||
|
|
||||||
这些应放入:
|
这些应放入:
|
||||||
|
|
||||||
- [docs/plans/README.md](/home/ray/dev/linkong/planet/docs/plans/README.md)
|
- [计划文档索引](/home/ray/dev/linkong/planet/docs/plans/README.md)
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ if datasource.last_status == "success":
|
|||||||
相关实现见:
|
相关实现见:
|
||||||
|
|
||||||
- [datasource_connectivity.py](/home/ray/dev/linkong/planet/backend/app/services/datasource_connectivity.py)
|
- [datasource_connectivity.py](/home/ray/dev/linkong/planet/backend/app/services/datasource_connectivity.py)
|
||||||
- [datasource-collector-settings-connectivity.md](/home/ray/dev/linkong/planet/docs/technical/zh/datasource-collector-settings-connectivity.md)
|
- [数据源、采集器设置与连接验证](/home/ray/dev/linkong/planet/docs/technical/zh/datasource-collector-settings-connectivity.md)
|
||||||
|
|
||||||
## 八、相关代码文件
|
## 八、相关代码文件
|
||||||
|
|
||||||
@@ -314,7 +314,7 @@ POST /api/v1/settings/credential-guides/{provider}/reset
|
|||||||
|
|
||||||
更多细节见:
|
更多细节见:
|
||||||
|
|
||||||
- [datasource-collector-settings-connectivity.md](/home/ray/dev/linkong/planet/docs/technical/zh/datasource-collector-settings-connectivity.md)
|
- [数据源、采集器设置与连接验证](/home/ray/dev/linkong/planet/docs/technical/zh/datasource-collector-settings-connectivity.md)
|
||||||
|
|
||||||
## 十一、数据使用场景
|
## 十一、数据使用场景
|
||||||
|
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ Earth info-card 策略:
|
|||||||
- 这是预期行为,因为 incident 是聚合和去噪后的结果
|
- 这是预期行为,因为 incident 是聚合和去噪后的结果
|
||||||
- 但 incident-first 渲染会让 Earth 显得过于安静,除非有另一层始终可用的 activity layer
|
- 但 incident-first 渲染会让 Earth 显得过于安静,除非有另一层始终可用的 activity layer
|
||||||
|
|
||||||
推荐 `activity layer` 的实现细节在 [bgp-region-aggregation-plan.md](/home/ray/dev/linkong/planet/docs/plans/earth-bgp-region-aggregation-plan.md) 中展开。
|
推荐 `activity layer` 的实现细节在 [BGP 区域聚合计划](/home/ray/dev/linkong/planet/docs/plans/earth-bgp-region-aggregation-plan.md) 中展开。
|
||||||
|
|
||||||
因此最近的里程碑是:
|
因此最近的里程碑是:
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
相关规则建议一起参考:
|
相关规则建议一起参考:
|
||||||
|
|
||||||
- [rules.md](/home/ray/dev/linkong/planet/rules.md)
|
- [项目规则](/home/ray/dev/linkong/planet/rules.md)
|
||||||
- [frontend-layout-guidelines.md](/home/ray/dev/linkong/planet/docs/technical/zh/frontend-layout-guidelines.md)
|
- [前端布局指南](/home/ray/dev/linkong/planet/docs/technical/zh/frontend-layout-guidelines.md)
|
||||||
|
|
||||||
## 当前目标
|
## 当前目标
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ React 路由入口:
|
|||||||
|
|
||||||
新闻巡航摘要计划见:
|
新闻巡航摘要计划见:
|
||||||
|
|
||||||
- [earth-news-cruise-summary-plan.md](/home/ray/dev/linkong/planet/docs/plans/earth-news-cruise-summary-plan.md)
|
- [Earth 新闻巡航摘要计划](/home/ray/dev/linkong/planet/docs/plans/earth-news-cruise-summary-plan.md)
|
||||||
|
|
||||||
## 当前样式分层
|
## 当前样式分层
|
||||||
|
|
||||||
@@ -311,7 +311,7 @@ asset 图标大小由 `Interactable` 的 `icon.fitSize` 控制。SVG / 图片文
|
|||||||
|
|
||||||
接口细节、生命周期和接入示例见:
|
接口细节、生命周期和接入示例见:
|
||||||
|
|
||||||
- [earth-interactable-usage.md](/home/ray/dev/linkong/planet/docs/technical/zh/earth-interactable-usage.md)
|
- [Earth 可交互图标接入](/home/ray/dev/linkong/planet/docs/technical/zh/earth-interactable-usage.md)
|
||||||
|
|
||||||
### 视角控制反馈
|
### 视角控制反馈
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
本文记录当前 Earth 前端各图层的材质、颜色、透明度、线宽、半径偏移和
|
本文记录当前 Earth 前端各图层的材质、颜色、透明度、线宽、半径偏移和
|
||||||
`renderOrder` 等样式属性。层级关系请配合
|
`renderOrder` 等样式属性。层级关系请配合
|
||||||
[earth-render-layer-order.md](/home/ray/dev/linkong/planet/docs/technical/zh/earth-render-layer-order.md)
|
[Earth 渲染图层顺序](/home/ray/dev/linkong/planet/docs/technical/zh/earth-render-layer-order.md)
|
||||||
查看。
|
查看。
|
||||||
|
|
||||||
## 命名约定
|
## 命名约定
|
||||||
@@ -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 底图保留。 |
|
||||||
|
|
||||||
## 交互规则
|
## 交互规则
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
相关上下文:
|
相关上下文:
|
||||||
|
|
||||||
- [earth-frontend-context.md](/home/ray/dev/linkong/planet/docs/technical/zh/earth-frontend-context.md)
|
- [Earth 前端结构](/home/ray/dev/linkong/planet/docs/technical/zh/earth-frontend-context.md)
|
||||||
- [backend-collectors.md](/home/ray/dev/linkong/planet/docs/technical/zh/backend-collectors.md)
|
- [数据采集系统](/home/ray/dev/linkong/planet/docs/technical/zh/backend-collectors.md)
|
||||||
- [backend/app/services/collectors/celestrak.py](/home/ray/dev/linkong/planet/backend/app/services/collectors/celestrak.py)
|
- [backend/app/services/collectors/celestrak.py](/home/ray/dev/linkong/planet/backend/app/services/collectors/celestrak.py)
|
||||||
- [frontend/public/earth/js/satellites.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/satellites.js)
|
- [frontend/public/earth/js/satellites.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/satellites.js)
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
相关规则建议一起参考:
|
相关规则建议一起参考:
|
||||||
|
|
||||||
- [rules.md](/home/ray/dev/linkong/planet/rules.md)
|
- [项目规则](/home/ray/dev/linkong/planet/rules.md)
|
||||||
- [frontend-layout-guidelines.md](/home/ray/dev/linkong/planet/docs/technical/zh/frontend-layout-guidelines.md)
|
- [前端布局指南](/home/ray/dev/linkong/planet/docs/technical/zh/frontend-layout-guidelines.md)
|
||||||
|
|
||||||
## 当前目标
|
## 当前目标
|
||||||
|
|
||||||
@@ -292,7 +292,7 @@
|
|||||||
|
|
||||||
相关后端设计见:
|
相关后端设计见:
|
||||||
|
|
||||||
- [datasource-collector-settings-connectivity.md](/home/ray/dev/linkong/planet/docs/technical/zh/datasource-collector-settings-connectivity.md)
|
- [数据源、采集器设置与连接验证](/home/ray/dev/linkong/planet/docs/technical/zh/datasource-collector-settings-connectivity.md)
|
||||||
|
|
||||||
### 3. 复杂工作区页面
|
### 3. 复杂工作区页面
|
||||||
|
|
||||||
@@ -320,4 +320,4 @@
|
|||||||
|
|
||||||
详细经验见:
|
详细经验见:
|
||||||
|
|
||||||
- [frontend-layout-guidelines.md](/home/ray/dev/linkong/planet/docs/technical/zh/frontend-layout-guidelines.md)
|
- [前端布局指南](/home/ray/dev/linkong/planet/docs/technical/zh/frontend-layout-guidelines.md)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
- 控制台:登录后的管理后台
|
- 控制台:登录后的管理后台
|
||||||
- Docs:公开开发文档与使用手册
|
- Docs:公开开发文档与使用手册
|
||||||
|
|
||||||
快速启动路径见 [quickstart.md](/home/ray/dev/linkong/planet/docs/technical/zh/quickstart.md)。
|
快速启动路径见 [快速开始](/home/ray/dev/linkong/planet/docs/technical/zh/quickstart.md)。
|
||||||
|
|
||||||
## 入口总览
|
## 入口总览
|
||||||
|
|
||||||
@@ -224,7 +224,7 @@ Earth 用于在一个地球视图中观察:
|
|||||||
- 卫星和轨迹
|
- 卫星和轨迹
|
||||||
- 海缆与登陆点
|
- 海缆与登陆点
|
||||||
- 算力中心
|
- 算力中心
|
||||||
- 国界、经纬线、高清材质、云图、地形
|
- 国界线、经纬线、高清材质、云图、地形
|
||||||
- 新闻直播和态势新闻
|
- 新闻直播和态势新闻
|
||||||
- 搜索和聚焦对象详情
|
- 搜索和聚焦对象详情
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@ Earth 用于在一个地球视图中观察:
|
|||||||
常见图层包括:
|
常见图层包括:
|
||||||
|
|
||||||
- 经纬线
|
- 经纬线
|
||||||
- 国界
|
- 国界线
|
||||||
- 高清材质
|
- 高清材质
|
||||||
- 大气云图
|
- 大气云图
|
||||||
- 海缆
|
- 海缆
|
||||||
@@ -260,7 +260,7 @@ Earth 用于在一个地球视图中观察:
|
|||||||
|
|
||||||
- 海缆
|
- 海缆
|
||||||
- 卫星
|
- 卫星
|
||||||
- 国界
|
- 国界线
|
||||||
- 算力中心
|
- 算力中心
|
||||||
- BGP
|
- BGP
|
||||||
- AIS 船只
|
- AIS 船只
|
||||||
@@ -629,10 +629,10 @@ source ~/.zshrc && bun run build
|
|||||||
|
|
||||||
## 相关文档
|
## 相关文档
|
||||||
|
|
||||||
- [quickstart.md](/home/ray/dev/linkong/planet/docs/technical/zh/quickstart.md)
|
- [快速开始](/home/ray/dev/linkong/planet/docs/technical/zh/quickstart.md)
|
||||||
- [frontend-admin-frontend-context.md](/home/ray/dev/linkong/planet/docs/technical/zh/frontend-admin-frontend-context.md)
|
- [控制台前端结构](/home/ray/dev/linkong/planet/docs/technical/zh/frontend-admin-frontend-context.md)
|
||||||
- [earth-frontend-context.md](/home/ray/dev/linkong/planet/docs/technical/zh/earth-frontend-context.md)
|
- [Earth 前端结构](/home/ray/dev/linkong/planet/docs/technical/zh/earth-frontend-context.md)
|
||||||
- [earth-layer-style-reference.md](/home/ray/dev/linkong/planet/docs/technical/zh/earth-layer-style-reference.md)
|
- [Earth 图层样式属性索引](/home/ray/dev/linkong/planet/docs/technical/zh/earth-layer-style-reference.md)
|
||||||
- [backend-system-service-control.md](/home/ray/dev/linkong/planet/docs/technical/zh/backend-system-service-control.md)
|
- [系统服务控制](/home/ray/dev/linkong/planet/docs/technical/zh/backend-system-service-control.md)
|
||||||
- [backend-collectors.md](/home/ray/dev/linkong/planet/docs/technical/zh/backend-collectors.md)
|
- [数据采集系统](/home/ray/dev/linkong/planet/docs/technical/zh/backend-collectors.md)
|
||||||
- [datasource-collector-settings-connectivity.md](/home/ray/dev/linkong/planet/docs/technical/zh/datasource-collector-settings-connectivity.md)
|
- [数据源、采集器设置与连接验证](/home/ray/dev/linkong/planet/docs/technical/zh/datasource-collector-settings-connectivity.md)
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ ss -ltnp | grep -E ':3000|:8000'
|
|||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
- 完整操作说明见 [manual.md](/home/ray/dev/linkong/planet/docs/technical/zh/manual.md)
|
- 完整操作说明见 [Planet 使用手册](/home/ray/dev/linkong/planet/docs/technical/zh/manual.md)
|
||||||
- 控制台结构见 [frontend-admin-frontend-context.md](/home/ray/dev/linkong/planet/docs/technical/zh/frontend-admin-frontend-context.md)
|
- 控制台结构见 [控制台前端结构](/home/ray/dev/linkong/planet/docs/technical/zh/frontend-admin-frontend-context.md)
|
||||||
- Earth 结构见 [earth-frontend-context.md](/home/ray/dev/linkong/planet/docs/technical/zh/earth-frontend-context.md)
|
- Earth 结构见 [Earth 前端结构](/home/ray/dev/linkong/planet/docs/technical/zh/earth-frontend-context.md)
|
||||||
- 后端采集器见 [backend-collectors.md](/home/ray/dev/linkong/planet/docs/technical/zh/backend-collectors.md)
|
- 后端采集器见 [数据采集系统](/home/ray/dev/linkong/planet/docs/technical/zh/backend-collectors.md)
|
||||||
|
|||||||
@@ -16,12 +16,15 @@
|
|||||||
## Current Version
|
## Current Version
|
||||||
|
|
||||||
- `main` 当前主线历史推导到:`0.16.5`
|
- `main` 当前主线历史推导到:`0.16.5`
|
||||||
- `dev` 当前开发分支历史推导到:`0.46.0`
|
- `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.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 |
|
||||||
| `0.44.2` | bugfix | `dev` | `pending` | 补充 Earth 船只批量渲染、屏幕拾取、图层顺序、样式参考和性能计划状态文档 |
|
| `0.44.2` | bugfix | `dev` | `pending` | 补充 Earth 船只批量渲染、屏幕拾取、图层顺序、样式参考和性能计划状态文档 |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "planet-frontend",
|
"name": "planet-frontend",
|
||||||
"version": "0.46.0",
|
"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);
|
||||||
|
|||||||
@@ -95,6 +95,10 @@ const DOCS_METADATA: Record<string, DocsMetadataEntry> = {
|
|||||||
zh: { title: '新闻直播采集格式', group: 'Earth', order: 15 },
|
zh: { title: '新闻直播采集格式', group: 'Earth', order: 15 },
|
||||||
en: { title: 'News Live Streams Collector Format', group: 'Earth', order: 15 },
|
en: { title: 'News Live Streams Collector Format', group: 'Earth', order: 15 },
|
||||||
},
|
},
|
||||||
|
'earth-interactable-usage.md': {
|
||||||
|
zh: { title: 'Earth 可交互图标接入', group: 'Earth', order: 16 },
|
||||||
|
en: { title: 'Earth Interactable Usage', group: 'Earth', order: 16 },
|
||||||
|
},
|
||||||
'frontend-admin-frontend-context.md': {
|
'frontend-admin-frontend-context.md': {
|
||||||
zh: { title: '控制台前端结构', group: 'Frontend', order: 20 },
|
zh: { title: '控制台前端结构', group: 'Frontend', order: 20 },
|
||||||
en: { title: 'Admin Frontend Context', group: 'Frontend', order: 20 },
|
en: { title: 'Admin Frontend Context', group: 'Frontend', order: 20 },
|
||||||
@@ -111,6 +115,10 @@ const DOCS_METADATA: Record<string, DocsMetadataEntry> = {
|
|||||||
zh: { title: '系统服务控制', group: 'Backend', order: 31 },
|
zh: { title: '系统服务控制', group: 'Backend', order: 31 },
|
||||||
en: { title: 'System Service Control', group: 'Backend', order: 31 },
|
en: { title: 'System Service Control', group: 'Backend', order: 31 },
|
||||||
},
|
},
|
||||||
|
'datasource-collector-settings-connectivity.md': {
|
||||||
|
zh: { title: '数据源、采集器设置与连接验证', group: 'Backend', order: 32 },
|
||||||
|
en: { title: 'Datasource Collector Settings and Connectivity', group: 'Backend', order: 32 },
|
||||||
|
},
|
||||||
'agents-aiprovider.md': {
|
'agents-aiprovider.md': {
|
||||||
zh: { title: 'AI Provider 指南', group: 'Agents', order: 40 },
|
zh: { title: 'AI Provider 指南', group: 'Agents', order: 40 },
|
||||||
en: { title: 'AI Provider Guide', group: 'Agents', order: 40 },
|
en: { title: 'AI Provider Guide', group: 'Agents', order: 40 },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "planet"
|
name = "planet"
|
||||||
version = "0.46.0"
|
version = "0.46.3"
|
||||||
description = "智能星球计划 - 态势感知系统"
|
description = "智能星球计划 - 态势感知系统"
|
||||||
requires-python = ">=3.14"
|
requires-python = ">=3.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
Reference in New Issue
Block a user