311 lines
22 KiB
Markdown
311 lines
22 KiB
Markdown
# Earth Frontend Context
|
||
|
||
This document describes the current real structure of the Earth display frontend. The focus is on helping future changes to the HUD, layers, media panel, real terrain, and BGP visualization avoid repeating past structural and state-sync pitfalls.
|
||
|
||
Related references:
|
||
|
||
- [Project Rules](/home/ray/dev/linkong/planet/rules.md)
|
||
- [Frontend Layout Guidelines](/home/ray/dev/linkong/planet/docs/technical/en/frontend-layout-guidelines.md)
|
||
|
||
## Current Goal
|
||
|
||
The Earth frontend is not an ordinary admin page — it is an independent large-screen display frontend. Current product goals:
|
||
|
||
- Maintain the spatial depth and readability of the globe view
|
||
- Keep HUD, layers, media panel, BGP, satellites, cables, and similar elements in a unified interaction model
|
||
- Clearly represent states like loading, enabled, hidden, and locked
|
||
|
||
## Current Entry Point
|
||
|
||
React route entry:
|
||
|
||
- [Earth.tsx](/home/ray/dev/linkong/planet/frontend/src/pages/Earth/Earth.tsx)
|
||
|
||
The current approach is simple:
|
||
|
||
- The React page only provides a full-screen `iframe`
|
||
- The actual Earth application runs at:
|
||
- [index.html](/home/ray/dev/linkong/planet/frontend/public/earth/index.html)
|
||
|
||
Earth frontend is essentially a standalone static application under `public/earth`.
|
||
|
||
## Current File Layers
|
||
|
||
### 1. Page Entry and Structure
|
||
|
||
- [index.html](/home/ray/dev/linkong/planet/frontend/public/earth/index.html)
|
||
|
||
Responsibilities:
|
||
|
||
- Base HUD DOM
|
||
- Layer panel
|
||
- Media panel
|
||
- Toolbar
|
||
- Settings dialog
|
||
- Legacy element ID compatibility
|
||
|
||
### 2. Main Runtime
|
||
|
||
- [main.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/main.js)
|
||
|
||
Responsibilities:
|
||
|
||
- Globe initialization
|
||
- Three.js scene assembly
|
||
- Data loading and refresh
|
||
- Layer module integration
|
||
- Earth-level state synchronization
|
||
|
||
### 3. Earth Control Layer
|
||
|
||
- [controls.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/controls.js)
|
||
|
||
Responsibilities:
|
||
|
||
- Toolbar interaction
|
||
- Layer panel interaction
|
||
- Rotation / zoom / layout
|
||
- HUD panel drag
|
||
- Layer toggle state machine
|
||
- Earth settings read, persist, and reset
|
||
|
||
This is currently the most critical UI control entry point for the Earth frontend.
|
||
|
||
### 4. UI and Status Messages
|
||
|
||
- [ui.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/ui.js)
|
||
|
||
Responsibilities:
|
||
|
||
- Loading panel
|
||
- Status message
|
||
- Tooltip / error / cleanup logic
|
||
|
||
### 5. Motion Capture Control Adapter
|
||
|
||
- [motion-control.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/motion-control.js)
|
||
|
||
Responsibilities:
|
||
|
||
- Act as the Motion Provider manager for both `browser_camera` and `motion_agent`.
|
||
- Use browser `getUserMedia` plus local MediaPipe recognition by default; advanced setups can connect to the local Motion Capture Agent WebSocket.
|
||
- Handle browser camera permission/secure-context errors, plus Agent disconnects, reconnects, `status`, and `heartbeat` messages.
|
||
- Filter low-confidence and overly repeated gesture events.
|
||
- Map `rotate_left`, `rotate_right`, `rotate_up`, `rotate_down`, `zoom_in`, `zoom_out`, `focus_prev`, `focus_next`, `layer_prev`, `layer_next`, and `confirm` to the action entry points exposed by `main.js`.
|
||
- Parse `skeleton` debug events and dispatch `earth:motion-debug-frame`.
|
||
|
||
Gesture recognition may run locally in the browser or inside the local Agent, but neither path sends realtime camera frames to the SaaS cloud. `main.js` exposes rotation, zoom, target focus, layer switching, and confirm entry points, plus a `window.__planetEarth.motion` debug entry. The adapter starts only when `?motion=1` is present, browser local storage contains `planet-earth-motion-control-enabled=true`, or Earth settings enable Motion Debug Mode.
|
||
|
||
[motion-debug-panel.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/motion-debug-panel.js) owns the debug panel. It listens for `earth:motion-debug-frame` and draws normalized skeleton joints and bones on a canvas. The Browser Camera provider also emits `earth:motion-debug-video-source` with the local `<video>` element so the panel can show a local preview behind the skeleton; `shared.motionDebugSkeletonOnly` switches the panel back to skeleton-only rendering. `Stop Matching Gestures` dispatches `earth:motion-recognition-pause`, which suppresses gesture execution while video and skeleton drawing continue. Unmatched skeletons are red; matched gestures turn green and display the gesture name. Settings are persisted under `shared.motionDebugEnabled`, `shared.motionProvider`, and `shared.motionDebugSkeletonOnly` in `planet.earth.settings.v2`, and both the switch and provider selector reserve `data-gatekeeper-permission="earth.motion_debug"`.
|
||
|
||
The Browser Camera provider's gesture pipeline lives in `recognizeGesture()` inside [motion-browser-provider.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/motion-browser-provider.js). Detectors are evaluated in this order, first match wins:
|
||
|
||
1. **`getZoomTrend` (trend zoom)** — derived from the per-frame change in `Math.abs(rightWrist.x - leftWrist.x)`. Both wrists must cross the noise floor (`ZOOM_TREND_MIN_WRIST_DELTA = 0.010`) and stay within `ZOOM_TREND_HEIGHT_TOLERANCE` of each other vertically. Growing span → `zoom_in`, shrinking span → `zoom_out`. Trend has highest priority so mid-motion frames cannot be hijacked by the layer/focus/rotate detectors.
|
||
2. **`getZoomHoldPose` (sustained zoom)** — after motion stops, keeps emitting `zoom_in` while the wrists stay at chest level or above with span > `ZOOM_HOLD_SPREAD_FACTOR × shoulderWidth` (default 1.30), and `zoom_out` while elbows sit visibly outward and span < `ZOOM_HOLD_CLOSE_FACTOR × shoulderWidth` (default 0.85).
|
||
3. **layer / focus** — left-wrist raise + vertical motion fires `layer_prev/next`; head tilt fires `focus_prev/next`.
|
||
4. **`getRightArmPattern` (single-arm rotate)** — only considered when both `!isZoomCandidatePose(...)` and `isLeftArmAtRest(...)` hold. `isLeftArmAtRest` requires the left wrist to hang clearly below the shoulder line (≥ 0.13) and both left elbow and left wrist to stay near the body — any ambiguous left-arm posture (mid-spread, raised, held at chest) blocks single-arm rotate.
|
||
|
||
Two non-obvious decisions worth preserving:
|
||
|
||
- **Mirror-safe**: all zoom checks use `Math.abs(rightWrist.x - leftWrist.x)` and never rely on per-side x direction. `getUserMedia` returns the raw camera feed without horizontal flip, so a subject's anatomical left arm appears on the image right. A direction-based detector (e.g. "left wrist moves left, right wrist moves right") inverts on non-mirrored feeds — span-based detection is invariant.
|
||
- **Continuous vs. discrete**: `rotate`, `layer`, `focus`, and `confirm` go through `applyPoseLatch`, which emits each gesture once until the pose returns to neutral (one wave = one rotation step). Zoom intentionally bypasses the latch and re-matches every frame; downstream `GESTURE_POLICIES.zoom_in/out.cooldownMs = 120` rate-limits to ~8 emits/sec, so holding a spread pose keeps zooming in until the user changes their pose. Do not reuse the latch for zoom — that semantic difference is the point.
|
||
|
||
[presentation-controller.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/presentation-controller.js) is the new Presentation layer. In the first stage only Motion uses it: `motion-cruise-adapter.js` uses a persistent presentation that reuses the cruise fixed-card placement and connector, but mouse movement does not auto-hide the card. The connector recalculates source and target anchors every frame so dragged cards, globe rotation, and moving targets stay connected. BGP/News still use the existing `CruiseSequencer` auto-advance path to preserve the old cruise experience.
|
||
|
||
### 6. Globe and Terrain
|
||
|
||
- [earth.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/earth.js)
|
||
- [terrain.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/terrain.js)
|
||
|
||
Responsibilities:
|
||
|
||
- Globe sphere, cloud layer, atmosphere
|
||
- Real terrain mesh
|
||
- Terrain tile fetch, decode, displacement, and shading
|
||
|
||
### 7. Layer Modules
|
||
|
||
- [satellites.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/satellites.js)
|
||
- [cables.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/cables.js)
|
||
- [vessels.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/vessels.js)
|
||
- [bgp.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/bgp.js)
|
||
- [bgp-cruise-adapter.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/bgp-cruise-adapter.js)
|
||
- [compute-centers.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/compute-centers.js) renders supercomputer and GPU-cluster markers. The backend renders compute centers only from source-provided coordinates or `compute_center_locations` dimension-table coordinates during startup; manual candidate collection can query ROR and Nominatim/OpenStreetMap, and the layer keeps the `?` badge for unconfirmed positions while the details card shows precision, confidence, source notes, and verification date.
|
||
- [country-boundaries.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/country-boundaries.js)
|
||
|
||
Each module is responsible for its own:
|
||
|
||
- Data fetching
|
||
- Three.js mesh creation and update
|
||
- State tracking (loaded, visible, hover, locked)
|
||
- Self-cleanup (dispose on scene destroy)
|
||
|
||
`tv.js` owns the live / aggregation-news tabs inside `media-panel`. Toolbar open and tab-switch actions write back through `earth:tv-visibility-change` and `earth:tv-tab-change`: panel visibility remains viewport-scoped at `views.<scope>.panelVisibility.media-panel`, while the active tab is stored at `shared.mediaPanelActiveTab`. Refreshing the page therefore restores the user's last live/news state. Temporary hides from `closeTransientMobileOverlays()` carry `persist:false` and do not overwrite the preference.
|
||
|
||
The compute-center layer row has a notification badge for GeoJSON `unresolved` records. The badge means "no trustworthy coordinates, cannot render on the globe"; it is different from the `?` marker drawn on already positioned but unconfirmed compute centers. Clicking the badge opens a fixed info card beside the layer panel. Row-level `采集` fetches candidates only. Header-level `一键采用` processes the queue top-to-bottom, saves the highest-confidence valid candidate, removes successful rows, renumbers the list, and dispatches `earth:compute-center-unresolved-count-change` so the badge updates immediately. When the batch ends, `earth:compute-center-location-saved` refreshes the real layer.
|
||
|
||
Location candidate state in the details card is cached in [info-card.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/info-card.js) by `entityType:entityId`. If the user closes the details card or unresolved queue and reopens the same compute center / BGP collector, previously collected candidates and status text are restored. Header-level `一键采用` prefers cached candidates, avoiding repeated online geocoding or LLM factcheck calls. After a location is saved, that entity's candidate list is cleared to a "refreshing layer" status so stale candidates do not keep misleading the user.
|
||
|
||
The `预览 / 保存` buttons on each candidate row use a single delegated `click` handler per candidate root (the `[data-collect-cache-key]` block in the details card, or `[data-unresolved-item]` in the unresolved queue), guarded by a `data-candidate-actions-bound` flag so it cannot be double-bound. Direct `pointerup` / `click` listeners on individual buttons and overlapping delegated handlers were removed. Candidate objects are no longer JSON-stringified into an HTML attribute and parsed back; buttons only carry `data-candidate-index`, and the handler resolves the candidate object from a module-level `Map` keyed by cache-key. This removes the entire class of failures caused by HTML entity escaping of `&` / `<` / `"` in candidate fields. Clicking `预览` dispatches `earth:preview-location-candidate`; `main.js`'s `previewLocationCandidate()` calls `showComputeCenterLocationPreview()`, which attaches a hollow breathing-ring sprite pair at the candidate coordinates (visually mirroring the BGP event ring) and focuses the camera on the candidate. Previewing another candidate replaces the ring; saving clears it and `spawnSavedComputeCenterLocation()` immediately spawns the formal compute-center interactable. Note that `main.js` has no module-level `earth` variable — every location-save / preview handler must call `const earth = getEarth();` first, otherwise the event handler throws a `ReferenceError` that the surrounding `.catch` swallows, producing the failure mode where the button "does nothing".
|
||
|
||
The `earth:compute-center-location-saved` reconciliation pipeline is deliberately silent on background-refresh failures. `spawnComputeCenterAfterLocationSave()` already presents the success toast and locked state; `refreshComputeCentersAfterLocationSave()` only reloads backend data when the scene is ready and no longer emits its own `已保存` toast. `handleComputeCenterLocationSaved()` runs refresh in the background after a successful spawn; only when spawn returns `null` (scene not ready) or throws does refresh take over the success toast. A refresh error is only `console.warn`'d — it must never surface as a `保存失败` message, because the save itself succeeded and the refresh is a follow-up sync.
|
||
|
||
### AIS Vessel Layer
|
||
|
||
The vessel layer now uses `/api/v1/vessels/snapshot` for the initial viewport snapshot and the `/ws` `vessels` channel for realtime deltas. Snapshot requests must include `bbox`, `zoom`, and a bounded `limit`; the backend defaults to `limit=1000` and caps it at `5000`. WebSocket subscriptions must include the same viewport fields so the server can filter updates per connection.
|
||
|
||
The legacy `/api/v1/visualization/geo/vessels` route has been removed. Frontend code should fetch a snapshot for the current viewport when the layer opens, then subscribe to `vessels` deltas. After map pan or zoom, reload the snapshot and send a fresh vessels subscription. The backend only falls back to legacy `vessel_position` / `vessel_static` rows when the current raw window is empty; frontend code can detect that state through `diagnostics.legacy_fallback_used`.
|
||
|
||
The new layer API family is `/api/v1/layers/*`, which separates map rendering payloads from aggregate panel statistics. Layer requests must include `bbox`, `zoom`, and a bounded `limit`; responses include `visible_count`, `returned_count`, and `diagnostics`, where `degraded`, `truncated`, and `limit_clamped` are the frontend signals for fallback UI. Right-side aggregate panels should not sum the layer response. They should read `/api/v1/data-products` or `/api/v1/data-products/{product_id}/status`, because those statistics stay global and do not change with the viewport.
|
||
|
||
Vessel color and vessel type text must use the same normalized classification. `vessels.js` derives `type` from both `vessel_type_name` and the AIS numeric `vessel_type` code; that `type` drives marker color. It also derives `vessel_type_display`, which `main.js` uses for the info card, hover summary, and search result subtitle. Do not make the info card read only the raw `vessel_type_name`, because AISStream can provide a numeric type while the raw name is still `Other`.
|
||
|
||
AISStream `PositionReport` messages commonly carry live position and `MetaData.ShipName`, while vessel type usually comes from lower-frequency `ShipStaticData.Type`. The backend normalizes `MetaData.ShipName` into the vessel name and maps numeric type codes into Cargo / Tanker / Passenger / Fishing / Military where available. Missing type detail should wait for a static AIS message or the planned vessel profile enrichment; the frontend should not invent a more specific type.
|
||
|
||
### 8. HUD Panels and Search
|
||
|
||
- [hud-panels.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/hud-panels.js)
|
||
- [info-card.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/info-card.js)
|
||
- [search.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/search.js)
|
||
- [legend.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/legend.js)
|
||
|
||
### 9. Cruise Mode
|
||
|
||
- [cruise-sequencer.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/cruise-sequencer.js)
|
||
- [callout-connector.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/callout-connector.js)
|
||
|
||
The cruise sequencer handles generic logic: current target, queue order, camera focus, and dwell / hide / switch. Business modules supply target queues and content — they should not contain camera control logic.
|
||
|
||
### 10. Constants
|
||
|
||
- [constants.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/constants.js)
|
||
|
||
All material, layer, satellite, BGP, cable, terrain, celestial, and other style parameters are maintained here. Do not scatter magic numbers in module files.
|
||
|
||
## Current Style Layers
|
||
|
||
CSS files in `frontend/public/earth/css/` each correspond to a specific component scope. Do not write global Earth styles into `base.css` unless they genuinely apply to everything.
|
||
|
||
## Current Layer Toggle State Semantics
|
||
|
||
### `data-status-target`
|
||
|
||
Layer toggle buttons use `data-status-target` attributes to link button state to layer state. The state machine in `controls.js` handles:
|
||
|
||
- `loading`: showing the loading indicator
|
||
- `enabled`: layer is active
|
||
- `hidden`: layer is hidden
|
||
- `error`: layer failed to load
|
||
|
||
This is the canonical way to synchronize button visual state with actual layer state. Do not maintain separate boolean flags for button display.
|
||
|
||
Terrain should not block startup when it is not the restored visible layer. After deferred layer visibility settings are applied, `controls.js` schedules `scheduleTerrainPrefetch()` only when HD texture is enabled, terrain is not ready, and no prefetch is already running. The prefetch uses `setTimeout` plus `requestIdleCallback` so cloud, HD texture, and startup layer work keep first-screen priority.
|
||
|
||
## Current Settings Persistence
|
||
|
||
Earth settings are stored in `localStorage`. The key is typically a namespaced string defined in `constants.js`. `controls.js` handles read, write, and reset.
|
||
|
||
Settings that affect visual layers (terrain opacity, day/night mode, satellite display style, etc.) are read during initialization and applied immediately.
|
||
|
||
## Current Terrain Pipeline
|
||
|
||
1. `terrain.js` creates a sphere geometry with enough segments
|
||
2. On load, fetches Terrarium-format elevation tiles from the backend
|
||
3. Decodes R/G/B into elevation values
|
||
4. Displaces vertex positions radially based on elevation
|
||
5. Applies a vertex alpha that fades terrain edges at coastlines
|
||
6. Terrain writes to the scene as a mesh above the HD texture layer
|
||
|
||
When HD texture is off, terrain is temporarily hidden and its state is remembered. When HD texture comes back on, terrain restores its prior visibility.
|
||
|
||
Terrain tile fetching is batched. `terrain.js` deduplicates required Terrarium tile keys and sends chunks sized by `TERRAIN_CONFIG.batchRequestSize` to `/api/v1/visualization/terrain/terrarium/batch`. The backend proxies S3 Terrarium tiles with an in-memory LRU cache, per-batch deduplication, and bounded concurrency. The single tile endpoint remains for fallback paths and browser cache semantics.
|
||
|
||
## Current High-Frequency Risk Points
|
||
|
||
### 1. Visual State and Business State Out of Sync
|
||
|
||
The most common class of Earth bugs:
|
||
|
||
- Button shows "loaded," but layer has no objects rendered
|
||
- Button shows "hidden," but objects are still visible
|
||
- Loading ended, but button still looks like it hasn't
|
||
|
||
All future changes must prioritize checking state sync.
|
||
|
||
### 2. HUD Layout: Check Structure First, Not CSS Patches
|
||
|
||
Earth HUD has repeatedly experienced:
|
||
|
||
- Panel compressed to a sliver
|
||
- Markdown content clipped
|
||
- Tabs/iframe content consumed by `overflow: hidden`
|
||
|
||
Inspection order:
|
||
|
||
1. Who is responsible for height
|
||
2. Who is responsible for scrolling
|
||
3. Which layer is doing the clipping
|
||
|
||
Do not immediately add `overflow: hidden` or extra wrapper layers.
|
||
|
||
### 3. Transitional Paths Must Be Closed Off
|
||
|
||
Earth has gone through multiple rounds of HUD, toolbar, and media panel refactoring, making it easy to accumulate:
|
||
|
||
- Old helpers
|
||
- Old classes
|
||
- Old fallback logic
|
||
- Deprecated variants
|
||
|
||
After each major feature is complete, do a cleanup pass.
|
||
|
||
### 4. Cruise Mode and Business Events Must Not Be Deeply Coupled
|
||
|
||
The correct boundary:
|
||
|
||
- The generic cruise layer only knows:
|
||
- Current target
|
||
- Queue order
|
||
- Camera focus
|
||
- Dwell / hide / switch
|
||
- Business modules only supply:
|
||
- Target queues
|
||
- Focus coordinates
|
||
- Card content
|
||
- Highlight / layer side effects
|
||
|
||
If future cable, satellite, or news cruise is added, do not copy a new set of `main.js` state variables. Instead reuse:
|
||
|
||
- [cruise-sequencer.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/cruise-sequencer.js)
|
||
- [callout-connector.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/callout-connector.js)
|
||
- The business adapter pattern from [bgp-cruise-adapter.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/bgp-cruise-adapter.js)
|
||
|
||
## Recommended Change Approach
|
||
|
||
For future Earth changes:
|
||
|
||
1. First identify what you're changing:
|
||
- Three.js rendering layer
|
||
- HUD structure layer
|
||
- Layer state layer
|
||
- Panel content layer
|
||
2. If involving layer buttons, connect to the unified state machine
|
||
3. If involving visibility toggle, check whether tooltip / legend / info-card / lock all close together
|
||
4. If involving panel layout, check structure before touching CSS
|
||
|
||
## Current Boundary with the Console Frontend
|
||
|
||
The Earth frontend and the console frontend are not the same UI system:
|
||
|
||
- Console frontend: React + Ant Design workbench
|
||
- Earth frontend: native HUD + Three.js display under `public/earth`
|
||
|
||
Therefore:
|
||
|
||
- Earth should not directly reuse Ant Table / AppLayout semantics
|
||
- The console should not copy Earth HUD animations and glass-layer design language
|
||
|
||
For console structure, see:
|
||
|
||
- [Admin Frontend Context](/home/ray/dev/linkong/planet/docs/technical/en/frontend-admin-frontend-context.md)
|