@zephytiju/prism-globe-viewport
v0.2.0
Published
Platform Prism globe-viewport micro-UI (component id "globe-viewport"): the authoritative GeoVision world-view layout on an ION-FREE CesiumJS/Resium foundation — default OpenStreetMap imagery over the default ellipsoid (no Cesium ion dependency anywhere),
Readme
@zephytiju/prism-globe-viewport
Platform Prism globe-viewport micro-UI (component id "globe-viewport") — the GeoVision 3D globe viewport on an ION-FREE CesiumJS/Resium foundation.
- Component id:
globe-viewport - Package:
@zephytiju/prism-globe-viewport(private; publication HELD — no tags, no releases) - Version: 0.1.0
- Foundation:
cesium1.143.0 +resium1.24.0, React 19, Mantine 8 chrome
Source layout
src/GlobeViewport.tsx— thin orchestrator: prop defaults, locale resolution, the channel→marker derivation and the composition of the hooks/sub-components below. The public component;src/index.tsre-exports it unchanged.src/chrome/— world-view chrome overlays:WorldHeader.tsx(world-head bar: brand + live UTC clock + camera coords),MapTools.tsx(glyph tool stack + the injected MODE tool slot + the SAVE tool),FloatboxStack.tsx(bottom-corner floatbox composition),FusionBoard.tsx(LIVE FUSION legend),ReplayBar.tsx(bottom timeline); shared style tokens + the comfortable ratio bands / chosen chrome sizes insrc/chromeTokens.ts.src/floatboxes/— bottom-corner floatboxes:AuditChainBox.tsx(.audit-win),FocusedEntitiesBox.tsx(.focus-win).src/cesium/— viewer/camera wiring:useCesiumViewer.ts(viewer refs + the asynchronous ready gate),useMarkerColors.ts(token→Cesium colors),useCameraCoords.ts(coordinate line on camera move),useBaseImagery.ts(ion-free imagery/terrain providers),useCameraFocus.ts(initialView + focused-entity flights),MarkersLayer.tsx(entity-results → Cesium entities).src/tools/— the recorded Mode behavior's mode tool:ModeTool.tsx(click cycles / long-press configures),ModePopover.tsx+HeatConfigFields.tsx(the ANCHORED live-apply config popover),ModeToolSlot.tsx(assembly from the view state).src/layers/— the layer rail (LayerRail.tsx: normal toggle + stackable heat layers) andHeatLayer.tsx(the kernel-density canvas ImageryLayer overlay).src/colors/— the palette-adjacent side-coloring config:SideColorConfig.tsx+SideRow.tsx(anchored to the SETTINGS tool — never a layer).src/hooks/— data + state hooks:useGlobeViewState.ts(the mode/layer/side state machine with its saved-view baseline),useGlobeSaveGather.ts(SAVE-time snapshot gathering over the existing clients + channels),useReplayTimeline.ts(ITimelineEvent fetch + scrub/play),useAuditChain.ts(IAuditChain query),useGeoViewSave.ts(the SAVE tool write flow),useUtcClock.ts(live world-head clock).src/model/— pure derivations, re-exported throughsrc/viewModel.ts:floatboxes.ts(corner resolution),markers.ts(channel→marker mapping, fusion rows, token colors),geo.ts(ion-free defaults, camera math),time.ts(UTC labels, replay ticks/scrub, default windows),selection.ts(the selected-entity channel payload type),mode.ts(mode cycle + per-mode configs),heat.ts(selection-driven kernel density),sideColors.ts(the side-color mapping),viewFile.ts(the globe-side view-state shapes),geovisionFile.ts(the authoritative geovision-view format adapter + IFileEntry-write-backed injectable writer),viewDefaults.ts(the saved-view baseline).src/types.ts— public prop/phase types;src/locales/— en + zh-CN bundles.src/demo.tsx+src/demo/— the demo host; ALL demo-only logic (themes, mock executor, seed data, the zoom control) lives here and nowhere else.
Ion-free setup (hard requirement, honored)
There is no Cesium ion dependency anywhere:
- Imagery is always a
UrlTemplateImageryProviderover plain XYZ tiles. The default is ion-free OpenStreetMap raster tiles (https://tile.openstreetmap.org/{z}/{x}/{y}.png, credit© OpenStreetMap contributors); other styles are configuration (baseLayer.urlTemplate, e.g. a CARTO dark or satellite template supplied by the host/composition). - The Resium
<Viewer>is constructed withbaseLayer={false}— explicitly suppressing Cesium's ion-backed default imagery layer — plus an explicitEllipsoidTerrainProvider(the default ellipsoid, no ion terrain). geocoderandbaseLayerPickerare disabled (both would require ion).- No
Ion.defaultAccessToken, no ion asset ids, nocesium.comendpoints — guarded by a static source test (test/globe-viewport.test.tsx, "ships no Cesium ion code path").
Vite asset handling (copied from PrismSpatialMicroUI)
Cesium's static asset tree (Workers/Assets/ThirdParty/Widgets) is served straight from node_modules via publicDir, with CESIUM_BASE_URL defined as "/" (vite.config.ts), and the widget CSS imported once in the demo. The library build (tsc → dist/) is separate from the demo build (demo-dist/).
Note:
package.jsonpins@cesium/engine26.1.0 and@cesium/widgets16.1.0 via npmoverrides.[email protected]'s semver ranges otherwise resolve engine 26.3.x (which removed shader re-exports thatcesium/Source/Cesium.jsstill re-exports — an upstream break) and widgets 16.2 (whose engine range would nest a second engine 26.1.0 copy; two engine copies break Cesium's cross-packageinstanceof/context wiring). The pinned pair keeps a single hoisted engine, matching the working Cesium 1.143 stack.
Rendering — the authoritative world-view layout
The chrome matches the CONFIRMED GeoVision v7 world view (section 04) with the RECORDED MODE BEHAVIOR (Globe Viewport doc §5, 2026-09-22): a full-width 72px world-head top bar, tool stack top-left, LIVE FUSION board top-right, a layer rail below the tool stack, two bottom-corner floatboxes and a full-width replay bar along the bottom — all INTERNAL overlays inside the viewport container (never Prism composition children), all token-themed. The view-toggle pills (3D GLOBAL | HEATMAP | HISTORY) are REMOVED — all mode and layer control resides in the tools stack.
The scene is always Cesium SceneMode.SCENE3D — there is no 2D mode (the sceneMode prop is gone): a "2D" look is purely the camera zoomed into the 3D scene, never a scene-mode switch.
Geometry — comfortable ratio bands on the 1920×1080 frame. The authoritative CSS draws its panels inside a 778px-wide .world; this viewport renders the world view full-frame, so neither reusing those lengths nor rescaling them (a prior 1920/778 ≈ 2.47x made the data panels swallow the map) is correct. Instead every floating-panel size must land inside a COMFORTABLE BAND (CHROME_BANDS + CHROME_LAYOUT in src/chromeTokens.ts — the single tuning point): top row (tools / legend) 12-16px below the 72px header (chosen frame top 86); the layer rail hangs below the tool stack; tool buttons 32-36px (chosen 34); fusion legend 10-14% wide, ≤ 18% tall (chosen 220px); floatboxes 14-23.5% × 15-24.5% each (chosen 444×260, toward the upper band so three comparison cards abreast carry LEGIBLE 9-10px mono text) anchored bottom with 12-16px margins (chosen 14px above the 56px replay bar at bottom:12px, 12px side gutters); total floating chrome ≤ 40% of the frame; the region below the midline ≥ 55% free of floatbox coverage. Panel internals (paddings, mono fonts, dots, row heights) keep the HTML's absolute pixel sizes — the comfortable reference at 1080 height.
Machine layout audit. npm run shot-demo enforces those bands AT CAPTURE: before any screenshot is saved it measures every chrome panel with getBoundingClientRect() against the instance frame, prints a per-panel audit table (sizes, frame %, top/bottom gaps, coverage via rect intersection math) and FAILS non-zero without saving on any violation — the bands are imported from the very dev-server module the components render with, so the audit cannot drift from the tokens. A pure-math test (test/chrome-bands.test.ts) additionally pins every CHROME_LAYOUT token inside its CHROME_BANDS band.
- Markers derive from the shared channel
usePrismStateValue("query-box.entity-results"): items carrying a usable IGeoLocatablelat/lonbecome entities with token-colored points + mono (IBM Plex Mono) labels. Marker colors resolve the host theme's semantic tokens (--mantine-color-{token}-filled) from the viewport container at runtime — Cesium needs concrete colors, so this is the single place theme tokens cross into globe visuals; a fallback map covers first paint. - Focused entity from
usePrismStateValue("search-results.selected-entity"): the matching marker is emphasized (larger point, outline, emphasized label) and the camera centers on it at the configuredfocusZoom. - world-head: brand lines on the left — locale-bundled defaults (
title/subtitlekeys; "GEOVISION" / "GLOBAL FUSION COMMAND WORKSPACE" in en) that the configtitle/subtitleprops override; on the right a live mint mono UTC clock (ticking every second) plus a coordinate line tracking the camera position on move ("34.202° N / 38.314° E"). - map-tools (left:12 / frame top:86): the glyph stack — ⌖ select, ✥ pan, ◫ layers, ⟳ reset-north, ⚙ settings — PLUS the MODE tool and the 💾 SAVE tool. The mode tool (recorded §5.1): click cycles 3D GLOBAL → HEAT → HISTORY (the icon morphs; a corner dot flags a non-saved mode at a glance); long-press / right-click / Shift+M opens the ANCHORED config popover — never a modal, the map stays visible, every change applies LIVE (no Apply button) and a mode switch while open swaps the content in place; a persistent settings dot (hollow at saved-view defaults, filled when customized) is the one-click reopen affordance and dirty signal; the first activation of a mode auto-opens its popover once (teaching moment) and remembers the dismissal; Esc / click-away dismiss; the popover carries "reset to saved view". Keyboard mirrors: M cycles, Shift+M opens. The ⚙ settings tool anchors the side-coloring panel. The SAVE tool is the ONLY persistence affordance: a click opens the ANCHORED view-name prompt (never a modal); confirming gathers the full workspace snapshot (entity-results + selection from the shared channels; the replay window's timeline events; the relation graph + evidence rows for the selected entity fetched at save time through the existing generated clients — every section best-effort, the format allows empty sections) and serializes the FULL view state (camera, baseLayer, layers incl. heat selections, side-color mapping, workspace filters/selection/focus/replay window) through the AUTHORITATIVE bundle format (
@zephytiju/lattice-common-bundle/geovisionView:saveGeovisionViewderives the digest; the file self-validates viareadGeovisionViewbefore the write). The write goes through the generated client —createFileEntryClient(useLatticeTransport()).write({ name, kind: "geovision-view", content })— wrapped by the INJECTABLEviewWriterprop (GeovisionFileWriter; defaultcreateFileEntryViewWriter), which also FAILS the save when the stored digest disagrees with the content. The painted layer state maps onto the format's layer variants (normal → overlay, each ENABLED heat rail layer → a heat layer with radius in meters, an ACTIVE side-color mapping → a side-color layer with one party per condition value); snapshot entity-results items are normalized to the format's declared fields (enrichment dimensions such as nationality/org/tags stay in the source system). On success the tool carries the written summary (name · digest) andonViewSavedreports{ name, fileRef, digest }to the host (the demo's SAVED FILES readout). There is NO restore capability at all (restoration arrives later from the File Hub / the geovision embed opening the file); failures are guarded inline on the tool. - LIVE FUSION legend (right:12 / frame top:86, 220px wide): stream-type rows (colored dot per
tagPalettetoken, uppercase label, per-type count) plus the "N STREAMS FUSED" total, derived from the same entity-results channel. - layer rail (left:12, below the tool stack) (recorded §5.2): the NORMAL layer (basemap + markers) and every HEAT layer toggle independently; heat renders OVER the always-visible basemap, alpha-composited. Heat layers STACK (one per selection): "+" appends another (a fresh authored selection when the catalog is exhausted), ✕ removes, and the first heat row doubles as the quick-toggle the former HEATMAP pill stood for (entering HEAT mode enables the default heat layer when none is active). Heat is selection-driven: a named reusable filter over entities AND events (
{entityKinds, tags, eventKinds}), authored live in the HEAT popover with per-layer{radiusKm, intensity, rampToken}. Computation: CLIENT-SIDE kernel density on a bounded equirectangular grid (512×256 cells, kernel radius ≤ 48 cells —src/model/heat.ts), painted to an offscreen canvas served as a single whole-world level-0 tile through a minimal custom ImageryProvider; the provider is recreated whenever the regeneration key (selection · radius · ramp · filtered point set) changes. - side coloring (NOT a layer) (recorded §5.2): a user-defined color mapping over entities, surfaced BESIDE the palette configuration (anchored to the ⚙ settings tool — never in the layer rail): define an arbitrary number of sides with base semantic tokens; parties match by condition (nationality/organization/tags) with
solid | stripe | accentstyles and an optional secondary token (stripe alternates same-side parties by within-side index; accent draws the secondary as the point outline — the focused-entity emphasis outranks both). When ACTIVE it OVERRIDES the tagPalette mapping and coexists with any layer state. - Floatboxes (bottom corners, 444×260 each, 14px above the replay bar):
"audit"(bottom-left by default) listsinterfaces/IAuditChain/queryrecent entries within the configuredrecentWindowas dot + monoHH:mm:ssZtime + action rows (3 rows, exactly the HTML) under a VERIFIED pill, with DISMISS/TRACK/ALERT affordances rendered as visual-only spans (no handlers, nothing published);"focused"(bottom-right by default) pins "N PINNED" plus COMPARISON CARDS (dot + name + flat attribute rows from entity-results, selection highlighted): three 136px cards abreast in a horizontally scrolling row, mono 10px titles and 8px keys / 9px values so the attribute rows and ↑/↓ diff arrows stay readable at the 1920×1080 frame. Configfloatboxes{bl,br}selects the subset. - replay bar (full-width bottom, 56px): play button, mint mono time readout, track + fill + head (an invisible native range input drives the scrub), window ticks and a LIVE pill — fetching
interfaces/ITimelineEvent/timelineevents for the selected entity and scrubbing them locally (a simple display scrub that sets nothing global).
The component is a pure consumer: it publishes no channels, emits no events, and writes no audit records.
Configuration keys
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| title / subtitle | string | locale-derived (GEOVISION / GLOBAL FUSION COMMAND WORKSPACE in en, GEOVISION / 全球融合指挥工作台 in zh-CN) | world-head brand lines; explicit props override the bundled defaults |
| height | number \| string | 420 | viewport height; width fills the container |
| baseLayer | { urlTemplate: string; credit?: string } | OSM template | ion-free XYZ tile template for satellite/other styles |
| tagPalette | Record<string, string> | {} | entity type (stream tag) → semantic color token (fallback accent) |
| focusZoom | number | 1_500_000 | camera height (m) when centering the focused entity |
| initialView | { lon, lat, height } | — | camera pose flown to on mount AND on change (degrees + meters); zoom-only — the scene always stays SCENE3D |
| floatboxes | { bl?, br?: "audit" \| "focused" } | bl: audit, br: focused | bottom corner → panel; a mode configured twice is dropped from the later corner; {} disables both |
| timelineWindow | { start, end } (ISO-8601) | last 6 h | replay bar fetch window |
| recentWindow | { start, end } (ISO-8601) | last 24 h | audit floatbox query window |
| locale | "en" \| "zh-CN" | "en" | bundled string tables |
| initialMode | "global" \| "heat" \| "history" | "global" | mode tool's initial mode; also the saved-view baseline until the first SAVE |
| heatSelections | readonly HeatSelection[] | 3 example selections | catalog of named entity+event filters the heat layers bind to |
| initialLayers | { normal, heat: HeatLayerConfig[] } | normal on + 1 default heat layer (off) | initial layer-rail state (embeds reproduce saved layer sets via this) |
| sideColorMapping | SideColorMapping | inactive, no sides | initial side-color mapping (overrides tagPalette when active) |
| viewWriter | GeovisionFileWriter | createFileEntryViewWriter(transport) | injectable geovision-view writer the SAVE tool flows through |
| savedBy | string | "globe-viewport" | savedBy recorded in the file |
| onViewSaved | (summary) => void | — | reports { name, fileRef, digest } after a successful write |
i18n
en + zh-CN locale JSONs namespaced under "globe-viewport" (exact key parity, compile-checked), exported via ./locales/* and locales on the index for downstream deep-merge. The locale prop drives every component-authored string, including the world-head title/subtitle DEFAULTS (title/subtitle keys; the "GEOVISION" brand value is identical in both bundles — only the subtitle translates) and the container/scrub aria-labels. An explicit title/subtitle prop always wins and is then composition-localized. Failure copy is locale-resolved too: when a backend rejection carries no Error message, the timeline/audit/serialize fallbacks come from the bundle (errTimeline/errAudit/errSerializeFailed/errCameraUnavailable); backend-provided Error messages pass through untranslated. NOT localized, by contract: backend-derived text — entity names/ids and labels, stream tag names, region names, coordinates, counts, timestamps-as-data and audit event content — plus the language-neutral timestamp templates (UTC "HH:mm:ss Z" digits + ISO Z designator). The demo HOST chrome (control strip, zoom control, captions) follows the same en + zh-CN contract, driven by the demo's global LANGUAGE switch.
Tests and the mocking strategy (honest notes)
Cesium/Resium render through WebGL, which jsdom cannot provide (and evaluating the real ~30MB engine inside a jsdom worker OOMs CI runners). vitest.config.ts therefore aliases cesium and resium onto resolver-level stub modules (test/stubs/cesium.ts, test/stubs/resium.tsx): the stubs surface received props as data attributes and a fake viewer exposes a camera with plain positionCartographic/flyTo doubles, a camera.changed listener registry and a scene whose mode the component assigns. Around those stubs, the tests cover the channel→marker derivation as rendered output, the world chrome (world-head clock/coords, map tools incl. the mode tool + SAVE, layer rail, LIVE FUSION board, replay bar), the mode popover lifecycle (cycling, anchored popover open/reopen/dirty/teaching/dismissal, live-apply), heat config → overlay regeneration, and side-color mapping application, the floatbox config resolution, the IGeoView serialize payload mapping and failure guards (no restore — removed with the feature), the timeline/audit client wiring through a host action executor, i18n/key parity, the ion-free construction options and a static no-ion source guard. The library build (tsc) still typechecks against the real packages — only the test runtime is stubbed. test/viewModel.test.ts unit-tests the pure derivations (markers, fusion rows, floatbox subset, view-state mapping, clock/coordinate/tick labels, scrub math, token resolution) with no React and no stubs. Real-WebGL rendering is covered by the demo screenshot driver.
126 tests total (npm test).
Scripts
npm run dev— vite demo host (two themed instances EACH at the full authoritative 1920×1080 frame, page scrolls sideways; an IN-FLOW control strip carries the EN | 中文 switchers — global plus per-instance — the demo ZOOM control (VIEW GLOBAL / VIEW REGION — camera flights only, the scene always stays SCENE3D), channel monitors; mock executor, channel seeding)npm run typecheck/npm test/npm run buildnpm run shot-demo— headless-Chrome screenshot driver (tall viewport, clips the instance element). Before saving, the MACHINE LAYOUT AUDIT measures every chrome panel against the comfortable ratio bands (per-panel table printed; any violation fails non-zero with no file saved) →/tmp/guanlan-review/demo-globe-mode-3d.png(3D whole globe from space, MACHINE-VERIFIED by PNG pixel sampling: dark space corners + textured imagery center + a lit round horizon with space margins) +/tmp/guanlan-review/demo-globe-mode-2d.png(still SCENE3D, camera zoomed into the region), each verified 1920×1080 and >150 KB, plus/tmp/guanlan-review/demo-globe-focused-v3.png(zoomed pose with the LEGIBLE focused-entities comparison cards — DOM-verified card count, rendered card width ≥130px, three abreast unclipped, arrows, one selected reference card) and/tmp/guanlan-review/demo-globe-focused-v3-closeup.png(~500×350 proof closeup of the focused-entities floatbox region)
CI / publishing
ci.yml (typecheck + test + build on every PR/push to main) and publish.yml (OIDC trusted publishing on v* tags) mirror the query-box workflows. This repository is private and publication is HELD — no tags are pushed and no releases are made.
