27 KiB
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:
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:
The current approach is simple:
- The React page only provides a full-screen
iframe - The actual Earth application runs at:
Earth frontend is essentially a standalone static application under public/earth.
Current File Layers
1. Page Entry and Structure
Responsibilities:
- Base HUD DOM
- Layer panel
- Media panel
- Toolbar
- Settings dialog
- Legacy element ID compatibility
2. Main Runtime
Responsibilities:
- Globe initialization
- Three.js scene assembly
- Data loading and refresh
- Layer module integration
- Earth-level state synchronization
3. Earth Control Layer
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.
Earth settings are now grouped by data-settings-tab and data-settings-tab-panel. Desktop and mobile share the same category semantics: Runtime, Display, Panels, Motion, Shortcuts, and System. When adding a setting, first choose its category, then add the DOM, persistence field, and restore logic; do not keep growing one long undifferentiated panel.
Shortcut configuration is a device-local preference owned by controls.js: read, capture, enable/disable, and reset all stay in the Earth frontend. It should not be written to backend user settings and should not affect other browsers. New shortcuts must provide a default key, display label, disabled/enabled state, and reset path instead of being hard-coded only in a keydown handler.
4. UI and Status Messages
Responsibilities:
- Loading panel
- Status message
- Tooltip / error / cleanup logic
5. Motion Capture Control Adapter
Responsibilities:
- Act as the Motion Provider manager for both
browser_cameraandmotion_agent. - Use browser
getUserMediaplus 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, andheartbeatmessages. - 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, andconfirmto the action entry points exposed bymain.js. - Parse
skeletondebug events and dispatchearth: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 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. Detectors are evaluated in this order, first match wins:
getZoomTrend(trend zoom) — derived from the per-frame change inMath.abs(rightWrist.x - leftWrist.x). Both wrists must cross the noise floor (ZOOM_TREND_MIN_WRIST_DELTA = 0.010) and stay withinZOOM_TREND_HEIGHT_TOLERANCEof 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.getZoomHoldPose(sustained zoom) — after motion stops, keeps emittingzoom_inwhile the wrists stay at chest level or above with span >ZOOM_HOLD_SPREAD_FACTOR × shoulderWidth(default 1.30), andzoom_outwhile elbows sit visibly outward and span <ZOOM_HOLD_CLOSE_FACTOR × shoulderWidth(default 0.85).- layer / focus — left-wrist raise + vertical motion fires
layer_prev/next; head tilt firesfocus_prev/next. getRightArmPattern(single-arm rotate) — only considered when both!isZoomCandidatePose(...)andisLeftArmAtRest(...)hold.isLeftArmAtRestrequires 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.getUserMediareturns 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, andconfirmgo throughapplyPoseLatch, 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; downstreamGESTURE_POLICIES.zoom_in/out.cooldownMs = 120rate-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 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
Responsibilities:
- Globe sphere, cloud layer, atmosphere
- Real terrain mesh
- Terrain tile fetch, decode, displacement, and shading
- Whole-globe land/ocean and border base overlays
The Earth surface is a stack of near-concentric shells, not a single mesh. The base sphere and HD texture overlay in earth.js, plus the land/ocean base in country-boundaries.js, need explicit radius separation. At far zoom, GPU depth precision drops; neighboring shells that are too close can z-fight and show black flicker blocks or snow. The current stable spacing is landAltitudeOffset = 0.32 and textureOverlayAltitudeOffset = 0.48. When adding or adjusting whole-globe surface overlays, update Earth Render Layer Order and verify at 50% zoom.
7. Layer Modules
- satellites.js
- cables.js
- vessels.js
- bgp.js
- bgp-cruise-adapter.js
- compute-centers.js renders supercomputer and GPU-cluster markers. The backend renders compute centers only from source-provided coordinates or
compute_center_locationsdimension-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
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.
brand.js manages Earth HUD brand resources. Static assets provide the default brand; runtime overrides come from /api/v1/earth/brand, and uploaded images are served from /earth-brand-assets/.... The frontend must treat logo/title images and text fallback separately: if an image fails, show the text title; if text fields are empty, rely on backend defaults so the HUD brand area never renders blank. The console Earth Content page owns saving and resetting brand configuration; the Earth frontend only consumes it.
The Admin Next Earth Content page must preserve runtime semantics:
Brand: brand preview should use the same dark starfield background, size, spacing, logo/title rendering, and text fallback as the Earth HUD top-left brand block, not a generic form preview.Boundary Precision: build boundary, refresh status, and restore defaults belong inside this section, not in the global page toolbar.TV: the list distinguishes built-in, collected, and custom sources. Card state represents enabled, disabled, draft, or error. Built-in sources cannot be deleted; collected and custom sources can. A new live source only enters draft state after the plus button is clicked; save persists it into the list, while cancel destroys the draft.Basemap,Layer Resources,3D Models, andNews Anchor Strategy: if backend capability is not available yet, the console should show an explicit pending state instead of mixing those items into TV or brand configuration.
TV preview should reuse the Earth runtime live-card structure and state labels as closely as possible so built-in markers, live loading state, stream source, region, and language match what users see on Earth.
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 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
9. Cruise Mode
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
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 indicatorenabled: layer is activehidden: layer is hiddenerror: 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 and surface interaction (terrain opacity, day/night mode, satellite display style, satellite idle breathing, real satellite altitude, track display, hover tooltip mode, etc.) are read during initialization and applied immediately.
The surface hover tooltip preference is persisted by controls.js as shared.surfaceHoverInfoMode, while main.js composes the actual tooltip in the globe-surface hover branch. Country shows country details only when a country polygon is hit and stays silent over ocean; Position shows latitude, longitude, and sampled terrain elevation and clears country-boundary hover; Full shows country + position on land and position over ocean.
The real satellite altitude preference is persisted by controls.js, while the rendering state lives in satellites.js. When enabled, the real radius from SGP4 is compressed logarithmically into the current Earth visual radius range. When disabled, satellite dots, trails, and predicted orbits all return to the legacy same-sphere display. Toggling this setting must refresh satellite positions and clear trail buffers so a trail never mixes both height models. maxRealAltitudeOffset = 25 is a visual cap tuned for the current camera and earthRadius = 100: GEO / MEO remain clearly higher than LEO, but the highest orbits stay within about 25% beyond the globe radius so selection targets, red trails, and the globe do not feel disconnected.
SGP4 propagation returns an inertial-frame position, so it must not be drawn directly as Earth-fixed longitude / latitude. satellites.js uses gstime to convert ECI/TEME positions to ECF, then maps that result into the same Three.js axes as latLonToVector3(). Satellite dots and short trails use Earth-fixed coordinates for each sample time, representing the object's current position relative to the globe surface. The locked predicted orbit uses the gstime from the lock moment for the whole future orbit, projecting the inertial orbit plane onto the current globe pose; that keeps the line closed and keeps the visual orbit inclination aligned with the details card. Fallback predicted orbits must also use a real RAAN + inclination orbital-plane formula, not treat inclination as a constant latitude.
Boundary precision is stored separately by country-boundaries.js under planet.earth.boundaries.highPrecisionEnabled. When high precision is off, Earth keeps using the bundled low-precision countries-admin0.min.geojson fallback even if high-precision manifest/PMTiles files exist locally. When high precision is on but the artifact is missing, the Earth toolbar settings call /api/v1/earth/boundaries/build and poll progress. After success, reloadCountryBoundaries() hot-swaps the boundary layer without refreshing the page. Boundary hover is independent from interactable hover: a country polygon remains highlighted whenever the surface coordinate is inside it, while the tooltip can still prioritize a satellite, vessel, BGP marker, or other interactable.
Current Terrain Pipeline
terrain.jscreates a sphere geometry with enough segments- On load, fetches Terrarium-format elevation tiles from the backend
- Decodes R/G/B into elevation values
- Displaces vertex positions radially based on elevation
- Applies a vertex alpha that fades terrain edges at coastlines
- 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:
- Who is responsible for height
- Who is responsible for scrolling
- 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
- callout-connector.js
- The business adapter pattern from bgp-cruise-adapter.js
Recommended Change Approach
For future Earth changes:
- First identify what you're changing:
- Three.js rendering layer
- HUD structure layer
- Layer state layer
- Panel content layer
- If involving layer buttons, connect to the unified state machine
- If involving visibility toggle, check whether tooltip / legend / info-card / lock all close together
- 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: