File size: 5,130 Bytes
9f21d0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// The one module that names a DOM container and asks for a WebGL context.
//
// Split out of CesiumController's constructor, which used to do this itself
// against a hardcoded element id. Nothing else in the app constructs a Viewer,
// so this is the only file that cannot run outside a browser — and everything
// downstream of it takes the viewer as an argument instead of making one.

import { Tonemapper } from "@cesium/engine";
import { Viewer } from "@cesium/widgets";

/**
 * A viewer with this app's widget selection and scene defaults already applied.
 *
 * No `animation` and no `timeline` on any device: the clock deck replaces both
 * (`ClockDeck.vue`), and neither widget fits a phone's width.
 *
 * `minimalUI` is the caller's answer, not this module's, and decides only the
 * fullscreen button: nothing to do in an iframe, no meaning on iOS. Read once
 * (`DeviceDetect.minimalUI`) rather than re-derived per consumer.
 */
export function createViewer(container: string | Element, options: { minimalUI: boolean }): Viewer {
  const { minimalUI } = options;

  const viewer = new Viewer(container, {
    animation: false,
    // No base layer here: the store's layer stack is the only default, and it
    // arrives through sceneSync's immediate watcher a tick later. Naming one here
    // as well meant two defaults that could drift.
    baseLayer: false,
    baseLayerPicker: false,
    fullscreenButton: !minimalUI,
    fullscreenElement: document.body,
    geocoder: false,
    homeButton: false,
    infoBox: false,
    navigationHelpButton: false,
    navigationInstructionsInitiallyVisible: false,
    sceneModePicker: false,
    selectionIndicator: false,
    timeline: false,
    contextOptions: {
      webgl: {
        alpha: true,
      },
    },
  });

  // Cesium's own wording for the lightbox link is "Data attribution", which is 28 px
  // of a credit line the clock deck has to share the bottom row with — and the two
  // breakpoints that decide where that line goes are measured off its width
  // (`useClockDeckChrome`, and the corner rule in main.css). "Attribution" says the
  // same thing about the same lightbox.
  //
  // Written once, because CreditDisplay sets this text in its constructor and never
  // rewrites it: only the credit list beside it is rebuilt as credits come and go.
  const expandLink = viewer.container.querySelector(".cesium-credit-expand-link");
  if (expandLink) {
    expandLink.textContent = "Attribution";
  }

  viewer.clock.shouldAnimate = true;
  viewer.scene.globe.enableLighting = true;
  viewer.scene.highDynamicRange = true;
  viewer.scene.maximumRenderTimeChange = 1 / 30;
  viewer.scene.requestRenderMode = true;
  // Away from Cesium's PBR_NEUTRAL default, because that curve is wrong for a
  // scene this dark.
  //
  // Its black-point term subtracts min(r,g,b) from every channel while that
  // minimum is under linear 0.08 (see czm_pbrNeutralTonemapping). On a near-black
  // pixel the minimum is nearly the whole signal, so the subtraction strips the
  // achromatic part and leaves only the channel imbalance — which czm_inverseGamma
  // then stretches. A neutral sRGB grey of (18,17,16) comes out around (9,7,2).
  //
  // Measured, not assumed. It renders the faint sky as olive mottling at any star
  // map resolution (src/config/starMaps.ts), and it is not confined to the sky:
  // over Manhattan with OSM Buildings, 37% of the frame sits under luminance 40,
  // and there it claims a mean saturation of 0.598 against ACES's 0.260. Shadowed
  // building faces get the same treatment as the sky.
  //
  // ACES costs about 5% luminance and 6% saturation on properly lit surfaces, and
  // agrees with PBR_NEUTRAL to within 6% in the 80-140 luminance band, with a mean
  // absolute channel delta under 11 everywhere — the globe and the satellite models
  // are visually unchanged. Same pass count either way, so no frame-time change.
  viewer.scene.postProcessStages.tonemapper = Tonemapper.ACES;

  // Otherwise render-on-demand and Cesium's "wait for the data sources" rule
  // deadlock each other.
  //
  // A static geometry primitive is built asynchronously and only advances a
  // state per render. While it is unfinished `DataSourceDisplay.update` reports
  // false, and by default the Viewer answers that by clearing `clock.canAnimate`
  // — so simulation time stops. But `maximumRenderTimeChange` above is what asks
  // for the next frame, and it asks based on simulation time having moved. Time
  // cannot move until the primitive is ready, the primitive cannot become ready
  // without frames, and no frame is requested: the clock stops dead the first
  // time a ground-track corridor is switched on and never restarts.
  //
  // Nothing here needs the guarantee it gives up. Suspending animation exists so
  // a CZML clip does not run ahead of geometry still streaming in; every
  // geometry in this app is generated locally from a propagator, and a swath
  // corridor arriving a frame late is not worth stopping time over.
  viewer.allowDataSourcesToSuspendAnimation = false;

  return viewer;
}