diff --git a/CHANGES.md b/CHANGES.md index dd28a47df7..c11414b20e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ - Upgrade webpack to version 5.105.4. - Upgrade eslint to v10 and migrate to eslint flat config. - Extract Terria config parameters from `Terria` class in a separate `TerriaConfig` data store. Config parameters can be update using new `updateConfig` method on `Terria` class, while previous `updateParameters` method is now removed. ([7819](https://github.com/TerriaJS/terriajs/pull/7819)) +- Extract Terria config parameters from `Terria` class in a separate `TerriaConfig` data store. Config parameters can be update using new `applyConfig` method on `Terria` class, while previous `updateParameters` method is now removed. ([7819](https://github.com/TerriaJS/terriajs/pull/7819)) #### 8.12.2 - 2026-03-27 diff --git a/lib/Models/Cesium.ts b/lib/Models/Cesium.ts index 4ae75780c1..598f96550b 100644 --- a/lib/Models/Cesium.ts +++ b/lib/Models/Cesium.ts @@ -89,6 +89,7 @@ import TileErrorHandlerMixin from "../ModelMixins/TileErrorHandlerMixin"; import SplitterTraits from "../Traits/TraitsClasses/SplitterTraits"; import TerriaViewer from "../ViewModels/TerriaViewer"; import CameraView from "./CameraView"; +import CesiumOptions from "./CesiumOptions"; import hasTraits from "./Definition/hasTraits"; import TerriaFeature from "./Feature/Feature"; import GlobeOrMap from "./GlobeOrMap"; @@ -184,6 +185,11 @@ export default class Cesium extends GlobeOrMap { */ readonly terriaPrimitives = new PrimitiveCollection(); + /** + * Helper for setting and persisting cesium options + */ + readonly options: CesiumOptions; + constructor(terriaViewer: TerriaViewer, container: string | HTMLElement) { super(); @@ -203,7 +209,7 @@ export default class Cesium extends GlobeOrMap { AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO \ 9TXL0Y4OHwAAAABJRU5ErkJggg=="; - const options = { + const widgetOptions = { clock: this.terria.timelineClock, baseLayer: ImageryLayer.fromProviderAsync( SingleTileImageryProvider.fromUrl(img), @@ -211,8 +217,7 @@ export default class Cesium extends GlobeOrMap { ), scene3DOnly: true, shadows: true, - useBrowserRecommendedResolution: !this.terria.useNativeResolution, - targetFrameRate: 30 + useBrowserRecommendedResolution: !this.terria.useNativeResolution }; // Workaround for Firefox bug with WebGL and printing: @@ -228,9 +233,19 @@ export default class Cesium extends GlobeOrMap { try { this.cesiumWidget = new CesiumWidget( container, - Object.assign({}, options, firefoxBugOptions) + Object.assign({}, widgetOptions, firefoxBugOptions) ); this.scene = this.cesiumWidget.scene; + + // Initialize CesiumOptions after all default app options have been set + this.options = new CesiumOptions(this, { + // Disable HDR lighting for better performance and to avoid changing imagery colors. + highDynamicRange: false, + ...this.terria.configParameters.cesiumOptions + }); + // Load locally saved preferences + this.options.loadUserPreferences(); + this.scene.primitives.add(this.terriaPrimitives); } catch (error) { throw TerriaError.from(error, { @@ -256,9 +271,6 @@ export default class Cesium extends GlobeOrMap { this._updateTilesLoadingCount(currentLoadQueueLength) ); - // Disable HDR lighting for better performance and to avoid changing imagery colors. - (this.scene as any).highDynamicRange = false; - this.scene.imageryLayers.removeAll(); this.updateCredits(container); diff --git a/lib/Models/CesiumOptions.ts b/lib/Models/CesiumOptions.ts new file mode 100644 index 0000000000..678dbb18d2 --- /dev/null +++ b/lib/Models/CesiumOptions.ts @@ -0,0 +1,206 @@ +import * as z from "zod"; +import Cesium from "./Cesium"; + +const CESIUM_OPTIONS_KEY = "cesiumOptions"; + +export interface CesiumOptionsSchema { + msaaSamples: number; + resolutionScale: number; + highDynamicRange: boolean; + fxaa: boolean; + enableCollisionDetection: boolean; + targetFrameRate?: number | "any"; + bloom: boolean; + bloomBrightness: number; + bloomContrast: number; +} + +export const CesiumOptionsSchema = z.object({ + msaaSamples: z.number(), + resolutionScale: z.number(), + highDynamicRange: z.boolean(), + fxaa: z.boolean(), + enableCollisionDetection: z.boolean(), + targetFrameRate: z.union([z.number(), z.string("any")]).optional(), + bloom: z.boolean(), + bloomBrightness: z.number(), + bloomContrast: z.number() +}); + +/** + * Manage Cesium options + */ +export default class CesiumOptions implements CesiumOptionsSchema { + private readonly cesium: Cesium; + readonly defaultOptions: CesiumOptionsSchema; + + constructor(cesium: Cesium, initialOptions: Partial) { + this.cesium = cesium; + this.updateFromJson(initialOptions); + + // Save snapshot of initial state so that we can diff and reset changes + this.defaultOptions = this.toJson(); + } + + /** + * Reset all options to terria/cesium defaults + */ + resetAll() { + this.updateFromJson(this.defaultOptions); + } + + /** + * Reset specified option to terria/cesium default + */ + resetOption(name: K) { + (this as CesiumOptionsSchema)[name] = this.defaultOptions[name]; + } + + /** + * Return snapshot of all options as JSON object + */ + toJson(): CesiumOptionsSchema { + return Object.fromEntries( + Object.keys(CesiumOptionsSchema.shape).map((name) => [ + name, + (this as any)[name] + ]) + ) as any; + } + + /** + * Update options from JSON + */ + updateFromJson(json: any) { + const parsed = CesiumOptionsSchema.partial().parse(json); + (Object.keys(parsed) as Array).forEach( + (name) => { + (this as any)[name] = parsed[name]; + } + ); + } + + /** + * Return locally stored options + */ + getUserPreferences() { + const str = this.cesium.terria.getLocalProperty(CESIUM_OPTIONS_KEY); + try { + const localChanges = typeof str === "string" ? JSON.parse(str) : {}; + return CesiumOptionsSchema.partial().parse(localChanges); + } catch (error) { + console.error("Ignoring invalid cesium options from local storage"); + console.error(error); + return {}; + } + } + + /** + * Load user preferences + */ + loadUserPreferences(): any { + this.updateFromJson(this.getUserPreferences()); + } + + /** + * Save user preferences + */ + saveUserPreferences() { + const currentOptions = this.toJson(); + // Save only those options that differ from default options + const onlyChangedOptions = Object.fromEntries( + Object.entries(currentOptions).filter( + ([name, value]) => (this.defaultOptions as any)[name] !== value + ) + ); + // TODO: remove item if empty instead of setting to empty object {} + this.cesium.terria.setLocalProperty( + CESIUM_OPTIONS_KEY, + JSON.stringify(onlyChangedOptions) + ); + } + + /** + * Returns true if user has any stored preferences + */ + get hasSavedPreferences() { + return Object.keys(this.getUserPreferences()).length > 0; + } + + get msaaSamples() { + return this.cesium.scene.msaaSamples; + } + + set msaaSamples(value: number) { + this.cesium.scene.msaaSamples = value; + } + + get resolutionScale(): number { + return this.cesium.cesiumWidget.resolutionScale; + } + + set resolutionScale(value: number) { + this.cesium.cesiumWidget.resolutionScale = value; + } + + get highDynamicRange(): boolean { + return this.cesium.scene.highDynamicRange; + } + + set highDynamicRange(value: boolean) { + this.cesium.scene.highDynamicRange = value; + } + + get fxaa(): boolean { + return this.cesium.scene.postProcessStages.fxaa.enabled; + } + + set fxaa(value: boolean) { + this.cesium.scene.postProcessStages.fxaa.enabled = value; + } + + get enableCollisionDetection(): boolean { + return this.cesium.scene.screenSpaceCameraController + .enableCollisionDetection; + } + + set enableCollisionDetection(value: boolean) { + this.cesium.scene.screenSpaceCameraController.enableCollisionDetection = + value; + } + + get targetFrameRate(): number | "any" { + return this.cesium.cesiumWidget.targetFrameRate ?? "any"; + } + + set targetFrameRate(value: number | "any") { + //@ts-expect-error 2322 - Cesium typings does not allow setting + //targetFrameRate=undefined but it is still valid as per doc + this.cesium.cesiumWidget.targetFrameRate = + value === "any" ? undefined : value; + } + + get bloom(): boolean { + return this.cesium.scene.postProcessStages.bloom.enabled; + } + + set bloom(value: boolean) { + this.cesium.scene.postProcessStages.bloom.enabled = value; + } + + get bloomBrightness(): number { + return this.cesium.scene.postProcessStages.bloom.uniforms.brightness; + } + + set bloomBrightness(value: number) { + this.cesium.scene.postProcessStages.bloom.uniforms.brightness = value; + } + + get bloomContrast(): number { + return this.cesium.scene.postProcessStages.bloom.uniforms.contrast; + } + + set bloomContrast(value: number) { + this.cesium.scene.postProcessStages.bloom.uniforms.contrast = value; + } +} diff --git a/lib/Models/SelectableDimensions/SelectableDimensions.ts b/lib/Models/SelectableDimensions/SelectableDimensions.ts index 17c532c328..ede8037996 100644 --- a/lib/Models/SelectableDimensions/SelectableDimensions.ts +++ b/lib/Models/SelectableDimensions/SelectableDimensions.ts @@ -11,6 +11,8 @@ interface Dimension { readonly id?: string; /** Human readable name */ readonly name?: string; + /** A description text */ + readonly description?: string; } export interface EnumDimensionOption { @@ -41,6 +43,15 @@ export interface NumericalDimension extends Dimension { readonly allowUndefined?: boolean; } +export interface NumericalRangeDimension extends Dimension { + readonly value?: number; + readonly min: number; + readonly max: number; + readonly step: number; + readonly marks?: Record; + readonly allowUndefined?: boolean; +} + export interface TextDimension extends Dimension { readonly value?: string; readonly allowUndefined?: boolean; @@ -56,6 +67,7 @@ export interface ButtonDimension extends Dimension { readonly icon?: | IconGlyph // Any Icon glyph | "spinner"; // Animated spinner icon + readonly variant?: "primary" | "secondary" | "warning" | "default"; } export type SelectableDimensionType = @@ -63,6 +75,7 @@ export type SelectableDimensionType = | "select" | "select-multi" | "numeric" + | "numeric-range" | "text" | "checkbox" | "checkbox-group" @@ -141,6 +154,11 @@ export interface SelectableDimensionNumeric type: "numeric"; } +export interface SelectableDimensionNumericRange + extends SelectableDimensionBase, NumericalRangeDimension { + type: "numeric-range"; +} + export interface SelectableDimensionText extends SelectableDimensionBase, TextDimension { type: "text"; @@ -183,6 +201,7 @@ export type SelectableDimension = | SelectableDimensionCheckboxGroup | SelectableDimensionGroup | SelectableDimensionNumeric + | SelectableDimensionNumericRange | SelectableDimensionText | SelectableDimensionButton | SelectableDimensionColor; @@ -209,6 +228,10 @@ export const isNumeric = ( dim: SelectableDimension ): dim is SelectableDimensionNumeric => dim.type === "numeric"; +export const isNumericRange = ( + dim: SelectableDimension +): dim is SelectableDimensionNumericRange => dim.type === "numeric-range"; + export const isText = ( dim: SelectableDimension ): dim is SelectableDimensionText => dim.type === "text"; diff --git a/lib/Models/Terria.ts b/lib/Models/Terria.ts index 42aaa4c30d..8fb3c8bbf3 100644 --- a/lib/Models/Terria.ts +++ b/lib/Models/Terria.ts @@ -109,6 +109,7 @@ import { ConfigParameters, TerriaConfig } from "./TerriaConfig"; import TimelineStack from "./TimelineStack"; import { isViewerMode, setViewerMode } from "./ViewerMode"; import Workbench from "./Workbench"; +import AppWorkflow from "./Workflows/AppWorkflows/AppWorkflow"; import SelectableDimensionWorkflow from "./Workflows/SelectableDimensionWorkflow"; interface StartOptions { @@ -267,6 +268,10 @@ export default class Terria { @observable selectableDimensionWorkflow?: SelectableDimensionWorkflow; + /** Gets or sets the non-blocking app workflow, if defined then the workflow will be displayed on a panel on the right side of the app */ + @observable + appWorkflow?: AppWorkflow; + /** * Flag for zooming to workbench items after all init sources have been loaded. * diff --git a/lib/Models/TerriaConfig.ts b/lib/Models/TerriaConfig.ts index f9ba2814c1..8769ad948f 100644 --- a/lib/Models/TerriaConfig.ts +++ b/lib/Models/TerriaConfig.ts @@ -10,6 +10,7 @@ import { ErrorServiceOptions } from "./ErrorServiceProviders/ErrorService"; import { LanguageConfiguration } from "./Internationalization"; import { RelatedMap } from "./RelatedMaps"; import { StoryVideoSettings } from "./StoryVideoSettings"; +import { CesiumOptionsSchema } from "./CesiumOptions"; type OnlyProps = { [K in keyof T as T[K] extends (...args: never) => unknown ? never : K]: T[K]; @@ -362,6 +363,11 @@ export class TerriaConfig { @observable searchProviders: ModelPropertiesFromTraits[] = []; + /** + * Options for the Cesium 3D globe + */ + cesiumOptions: Partial = {}; + constructor() { makeObservable(this); } diff --git a/lib/Models/Workflows/AppWorkflows/AppWorkflow.ts b/lib/Models/Workflows/AppWorkflows/AppWorkflow.ts new file mode 100644 index 0000000000..0b252e8205 --- /dev/null +++ b/lib/Models/Workflows/AppWorkflows/AppWorkflow.ts @@ -0,0 +1,57 @@ +import { runInAction } from "mobx"; +import ViewState from "../../../ReactViewModels/ViewState"; +import { PanelMenuProps } from "../../../ReactViews/Workflow/PanelMenu"; +import { IconProps } from "../../../Styled/Icon"; +import { SelectableDimension } from "../../SelectableDimensions/SelectableDimensions"; +import Terria from "../../Terria"; + +export default interface AppWorkflow { + /** Human readable name - used as title */ + name: string; + icon?: IconProps["glyph"]; + + onClose?: () => void; + /** Footer button */ + footer?: SelectableDimension[]; + menu?: PanelMenuProps; + + /** + * Main inputs to render + */ + inputs?: SelectableDimension[]; +} + +/** + * Runs an app workflow + * + * @param viewStateOrTerria - The {@link ViewState} or {@link Terria} instance + * @param workflow - A {@link SelectableDimensionWorkflow} instance + */ +export function runAppWorkflow( + viewStateOrTerria: ViewState | Terria, + workflow: AppWorkflow +) { + runInAction(() => { + const terria = + viewStateOrTerria instanceof Terria + ? viewStateOrTerria + : viewStateOrTerria.terria; + terria.appWorkflow = workflow; + }); +} + +export function closeAppWorkflow( + viewStateOrTerria: ViewState | Terria, + workflow: AppWorkflow +) { + runInAction(() => { + const terria = + viewStateOrTerria instanceof Terria + ? viewStateOrTerria + : viewStateOrTerria.terria; + + if (terria.appWorkflow === workflow) { + terria.appWorkflow = undefined; + } + }); +} diff --git a/lib/Models/Workflows/AppWorkflows/CesiumSettingsWorkflow.ts b/lib/Models/Workflows/AppWorkflows/CesiumSettingsWorkflow.ts new file mode 100644 index 0000000000..ef34c5ce23 --- /dev/null +++ b/lib/Models/Workflows/AppWorkflows/CesiumSettingsWorkflow.ts @@ -0,0 +1,461 @@ +import { + IReactionDisposer, + action, + computed, + makeObservable, + observable, + reaction +} from "mobx"; +import ContextLimits from "terriajs-cesium/Source/Renderer/ContextLimits"; +import filterOutUndefined from "../../../Core/filterOutUndefined"; +import Icon from "../../../Styled/Icon"; +import Cesium from "../../Cesium"; +import { CesiumOptionsSchema } from "../../CesiumOptions"; +import { + SelectableDimensionButton, + SelectableDimensionCheckbox, + SelectableDimensionCheckboxGroup, + SelectableDimensionGroup, + SelectableDimensionNumericRange +} from "../../SelectableDimensions/SelectableDimensions"; +import Terria from "../../Terria"; +import AppWorkflow, { closeAppWorkflow } from "./AppWorkflow"; + +const fpsDisplayCss = ` +.cesium-performanceDisplay-defaultContainer { + position: absolute; + top: 10px; + left: 50%; + border-radius: 5px; + border: 1px solid #444; + font: bold 20px sans-serif; + padding: 7px; + background: rgba(40, 40, 40, 0.7); +} + +.cesium-performanceDisplay-ms { color: #de3; } +.cesium-performanceDisplay-fps { color: white; } +.cesium-performanceDisplay-throttled {color: #a42; } +`; + +export default class CesiumSettingsWorkflow implements AppWorkflow { + private readonly cesium: Cesium; + + // Copy of initial options to track and reset changes + @observable + private initialOptions: CesiumOptionsSchema; + + // Reactive copy of cesium options and view changes + @observable + private viewModel: CesiumOptionsSchema; + + // True if custom options exist in localStorage + @observable + private hasSavedChanges: boolean; + + private cesiumReactionDisposer: IReactionDisposer; + + private fpsDisplayStyle: HTMLStyleElement; + + constructor(terria: Terria, cesium: Cesium) { + this.cesium = cesium; + this.initialOptions = cesium.options.toJson(); + this.viewModel = cesium.options.toJson(); + this.hasSavedChanges = this.cesium.options.hasSavedPreferences; + + this.fpsDisplayStyle = document.createElement("style"); + this.fpsDisplayStyle.textContent = fpsDisplayCss; + if (window) { + window.document.body.appendChild(this.fpsDisplayStyle); + } + + this.cesium.scene.debugShowFramesPerSecond = true; + + // Self-close workflow if cesium instance has changed so that we don't keep + // reference to the instance + this.cesiumReactionDisposer = reaction( + () => terria.cesium !== cesium, + (instanceChanged) => { + if (instanceChanged) { + closeAppWorkflow(terria, this); + } + } + ); + + makeObservable(this); + } + + get name() { + return "3D Graphics Settings"; + } + + get icon() { + return Icon.GLYPHS.settings; + } + + onClose() { + if (!this.cesium.cesiumWidget.isDestroyed()) { + this.discardUnsavedChanges(); + } + this.cesium.scene.debugShowFramesPerSecond = false; + this.fpsDisplayStyle.remove(); + this.cesiumReactionDisposer(); + } + + /** + * Returns current value for the option as per the view + */ + private getOption(name: K) { + return this.viewModel[name]; + } + + /** + * Set a new value for the option + */ + @action + private setOption( + name: K, + value: CesiumOptionsSchema[K] | undefined + ) { + (this.cesium.options as CesiumOptionsSchema)[name] = + value ?? this.initialOptions[name]; + this.viewModel[name] = this.cesium.options[name]; + this.cesium.notifyRepaintRequired(); + } + + /** + * Returns true if there are view local changes that will be discarded when the workflow is closed + */ + @computed + private get hasUnsavedChanges() { + const diff = Object.entries(this.viewModel).filter( + ([name, value]) => (this.initialOptions as any)[name] !== value + ); + return diff.length > 0; + } + + /** + * Write options and update state + */ + private writeOptions(options: CesiumOptionsSchema) { + this.cesium.options.updateFromJson(options); + this.cesium.options.saveUserPreferences(); + this.initialOptions = this.cesium.options.toJson(); + this.viewModel = this.cesium.options.toJson(); + this.hasSavedChanges = this.cesium.options.hasSavedPreferences; + this.cesium.notifyRepaintRequired(); + } + + /** + * Discard view local changes + */ + @action + private discardUnsavedChanges() { + this.writeOptions(this.initialOptions); + } + + /** + * Save view local changes + */ + @action + private saveChanges() { + this.writeOptions(this.viewModel); + } + + /** + * Discard all customizations user has made + */ + @action + private resetToAppDefaults() { + this.writeOptions(this.cesium.options.defaultOptions); + } + + @computed + get inputs() { + return filterOutUndefined([this.qualityAndPerformance, this.effects]); + } + + @computed + get footer() { + return [this.saveChangesButton, this.resetToAppDefaultsButton]; + } + + @computed + get saveChangesButton(): SelectableDimensionButton { + return { + id: "save-changes", + type: "button", + value: `Save changes`, + disable: !this.hasUnsavedChanges, + variant: "primary", + setDimensionValue: () => { + this.saveChanges(); + } + }; + } + + @computed + get resetToAppDefaultsButton(): SelectableDimensionButton { + return { + id: "reset-to-app-defaults", + type: "button", + value: `Reset to app defaults`, + disable: !this.hasSavedChanges, + setDimensionValue: () => { + this.resetToAppDefaults(); + } + }; + } + + @computed + get qualityAndPerformance(): SelectableDimensionGroup { + return { + id: "quality-and-performance", + name: "Quality and Performance", + type: "group", + selectableDimensions: filterOutUndefined([ + this.targetFrameRate, + this.resolutionScale, + this.baseMaximumScreenSpaceError, + this.fxaa, + this.msaaSamples + ]) + }; + } + + @computed + get effects(): SelectableDimensionGroup { + return { + id: "effects", + name: "Effects", + type: "group", + selectableDimensions: filterOutUndefined([ + this.highDynamicRange, + this.bloom + ]) + }; + } + + @computed + get targetFrameRate(): SelectableDimensionNumericRange { + // Here we map value above 60 to "any" + const self = this; + const name = `Target frame rate (${this.getOption("targetFrameRate")})`; + const targetFrameRate = self.getOption("targetFrameRate"); + const value = targetFrameRate === "any" ? 70 : targetFrameRate; + return { + id: "target-frame-rate", + type: "numeric-range", + name, + value, + min: 0, + max: 70, + step: 1, + marks: Object.fromEntries([ + [value, value?.toString()], + ...[0, 10, 20, 30, 40, 50, 60, 70].map((fps) => [ + fps, + fps > 60 ? "Any" : fps.toString() + ]) + ]), + setDimensionValue: (_, value) => { + this.setOption( + "targetFrameRate", + value === undefined ? "any" : value > 60 ? "any" : value + ); + } + }; + } + + @computed + get resolutionScale(): SelectableDimensionNumericRange { + const value = this.getOption("resolutionScale"); + const name = `Resolution (${this.getOption("resolutionScale")}x)`; + const native = window.devicePixelRatio; + const max = Math.max(2, native); + return { + id: "resolution-scale", + type: "numeric-range", + name, + value, + description: "Lower resolution looks grainier but improves performance", + min: 0.1, + max, + step: 0.1, + marks: { + [value]: `${value}x`, + 0.1: "0.1x", + 1: "1x", + 2: "2x", + [native]: `${native}x` + }, + setDimensionValue: (_, value) => { + this.setOption("resolutionScale", value); + } + }; + } + + @computed + get baseMaximumScreenSpaceError(): SelectableDimensionNumericRange { + const value = this.cesium.terria.baseMaximumScreenSpaceError; + const name = `Base Maximum SSE`; + return { + id: "bmsse", + type: "numeric-range", + name, + value, + description: + "The base maximum screen space error determines the level-of-detail that is rendered. Higher values tolerate more error, decreasing visual quality but improving performance.", + min: 1, + max: 3, + step: 0.1, + marks: { + [value]: value.toString(), + 1: "Quality", + 2: "Balanced", + 3: "Performance" + }, + setDimensionValue: (_, bmsse) => { + if (typeof bmsse === "number" && bmsse >= 1 && bmsse <= 3) { + this.cesium.terria.baseMaximumScreenSpaceError = bmsse; + // FIXME: save only when user asks to save + this.cesium.terria.setLocalProperty( + "baseMaximumScreenSpaceError", + bmsse.toString() + ); + this.cesium.notifyRepaintRequired(); + } + } + }; + } + + @computed + get highDynamicRange(): SelectableDimensionCheckbox | undefined { + if (!this.cesium.scene.highDynamicRangeSupported) { + return; + } + + const label = "Enable HDR rendering"; + return { + id: "high-dynamic-range", + name: "High dynamic range (HDR)", + type: "checkbox", + selectedId: this.getOption("highDynamicRange") ? "true" : "false", + options: [ + { id: "true", name: label }, + { id: "false", name: label } + ], + setDimensionValue: (_, value) => { + this.setOption("highDynamicRange", value === "true" ? true : false); + } + }; + } + + @computed + get fxaa(): SelectableDimensionCheckbox { + const label = "Enabled"; + return { + id: "fxaa", + name: "FXAA - fast smooth edges", + type: "checkbox", + selectedId: this.getOption("fxaa") ? "true" : "false", + options: [ + { id: "true", name: label }, + { id: "false", name: label } + ], + setDimensionValue: (_, value) => { + this.setOption("fxaa", value === "true" ? true : false); + } + }; + } + + @computed + private get msaaSamples(): SelectableDimensionNumericRange | undefined { + const maximumSamples = ContextLimits.maximumSamples; + if (maximumSamples === 0 || !this.cesium.scene.msaaSupported) { + // Multi-sampling not supported + return; + } + + const value = this.getOption("msaaSamples"); + return { + id: "msaa-samples", + name: `MSAA Samples (${value})`, + description: + "Higher values improves edge smoothness but can greatly reduce performance", + type: "numeric-range", + value, + min: 1, + max: maximumSamples, + step: 1, + marks: Object.fromEntries( + [...new Array(maximumSamples)].map((_, i) => [ + i + 1, + (i + 1).toString() + ]) + ), + setDimensionValue: (_, value) => { + this.setOption("msaaSamples", value); + } + }; + } + + @computed + get bloom(): SelectableDimensionCheckboxGroup { + const label = "Enable glow"; + return { + id: "bloom", + name: "Glow", + type: "checkbox-group", + selectedId: this.getOption("bloom") ? "true" : "false", + options: [ + { id: "true", name: label }, + { id: "false", name: label } + ], + setDimensionValue: (_, value) => { + const enabled = value === "true"; + this.setOption("bloom", enabled); + if (!enabled) { + this.setOption("bloomBrightness", undefined); + this.setOption("bloomContrast", undefined); + } + }, + selectableDimensions: [ + { + id: "brightness", + name: "Brightness", + type: "numeric-range", + min: -1, + max: 1, + step: 0.1, + value: this.getOption("bloomBrightness"), + marks: { + "-1": "-1", + 0: "0", + 1: "1" + }, + setDimensionValue: (_, value) => { + this.setOption("bloomBrightness", value); + } + }, + { + id: "contrast", + name: "Contrast", + type: "numeric-range", + min: -255, + max: 255, + step: 1, + value: this.getOption("bloomContrast"), + marks: { + "-255": "-255", + 0: "0", + 255: "255" + }, + setDimensionValue: (_, value) => { + this.setOption("bloomContrast", value); + } + } + ] + }; + } +} diff --git a/lib/Models/Workflows/AppWorkflows/StoryBuilderWorkflow.ts b/lib/Models/Workflows/AppWorkflows/StoryBuilderWorkflow.ts new file mode 100644 index 0000000000..7d29e62e08 --- /dev/null +++ b/lib/Models/Workflows/AppWorkflows/StoryBuilderWorkflow.ts @@ -0,0 +1,8 @@ +import AppWorkflow from "./AppWorkflow"; + +/** + * Dummy workflow to handle StoryBuilder rendering + */ +export const StoryBuilderWorkflow: AppWorkflow = Object.freeze({ + name: "storyBuilderWorkflow" +}); diff --git a/lib/ReactViewModels/ViewState.ts b/lib/ReactViewModels/ViewState.ts index b6bc502009..e91a7d4a24 100644 --- a/lib/ReactViewModels/ViewState.ts +++ b/lib/ReactViewModels/ViewState.ts @@ -1,45 +1,46 @@ import { + IReactionDisposer, action, computed, - IReactionDisposer, + makeObservable, observable, reaction, - runInAction, - makeObservable + runInAction } from "mobx"; -import { ReactNode, MouseEvent, ComponentType, Ref } from "react"; +import { ComponentType, MouseEvent, ReactNode, Ref } from "react"; import defined from "terriajs-cesium/Source/Core/defined"; -import addedByUser from "../Core/addedByUser"; import { Category, HelpAction, StoryAction } from "../Core/Analytics/analyticEvents"; import Result from "../Core/Result"; +import addedByUser from "../Core/addedByUser"; import triggerResize from "../Core/triggerResize"; import PickedFeatures from "../Map/PickedFeatures/PickedFeatures"; import CatalogMemberMixin, { getName } from "../ModelMixins/CatalogMemberMixin"; import GroupMixin from "../ModelMixins/GroupMixin"; import MappableMixin from "../ModelMixins/MappableMixin"; import ReferenceMixin from "../ModelMixins/ReferenceMixin"; +import CatalogSearchProviderMixin from "../ModelMixins/SearchProviders/CatalogSearchProviderMixin"; +import CzmlCatalogItem from "../Models/Catalog/CatalogItems/CzmlCatalogItem"; import CommonStrata from "../Models/Definition/CommonStrata"; import { BaseModel } from "../Models/Definition/Model"; -import getAncestors from "../Models/getAncestors"; +import { getMarkerCatalogItem } from "../Models/LocationMarkerUtils"; import { SelectableDimension } from "../Models/SelectableDimensions/SelectableDimensions"; import Terria from "../Models/Terria"; import { ViewingControl } from "../Models/ViewingControls"; +import { StoryBuilderWorkflow } from "../Models/Workflows/AppWorkflows/StoryBuilderWorkflow"; +import getAncestors from "../Models/getAncestors"; import { SATELLITE_HELP_PROMPT_KEY } from "../ReactViews/HelpScreens/SatelliteHelpPrompt"; import { animationDuration } from "../ReactViews/StandardUserInterface/StandardUserInterface"; import { FeatureInfoPanelButtonGenerator } from "../ViewModels/FeatureInfoPanel"; +import SearchState from "./SearchState"; import { - defaultTourPoints, RelativePosition, - TourPoint + TourPoint, + defaultTourPoints } from "./defaultTourPoints"; -import SearchState from "./SearchState"; -import CatalogSearchProviderMixin from "../ModelMixins/SearchProviders/CatalogSearchProviderMixin"; -import { getMarkerCatalogItem } from "../Models/LocationMarkerUtils"; -import CzmlCatalogItem from "../Models/Catalog/CatalogItems/CzmlCatalogItem"; export const DATA_CATALOG_NAME = "data-catalog"; export const USER_DATA_NAME = "my-data"; @@ -87,7 +88,6 @@ export default class ViewState { @observable topElement: string = "FeatureInfo"; // Map for storing react portal containers created by component. @observable portals: Map = new Map(); - @observable storyBuilderShown: boolean = false; // Flesh out later @observable showHelpMenu: boolean = false; @@ -579,6 +579,23 @@ export default class ViewState { }, animationDuration); } + /** + * Returns true if story builder UI has been activated + */ + @computed + get storyBuilderShown() { + return this.terria.appWorkflow?.name === StoryBuilderWorkflow.name; + } + + /** + * Show/hide story builder UI + */ + set storyBuilderShown(show: boolean) { + runInAction(() => { + this.terria.appWorkflow = show ? StoryBuilderWorkflow : undefined; + }); + } + @action toggleStoryBuilder(): void { this.storyBuilderShown = !this.storyBuilderShown; diff --git a/lib/ReactViews/Map/MenuBar/MenuBar.jsx b/lib/ReactViews/Map/MenuBar/MenuBar.jsx index 014cfce6df..4a62950fb8 100644 --- a/lib/ReactViews/Map/MenuBar/MenuBar.jsx +++ b/lib/ReactViews/Map/MenuBar/MenuBar.jsx @@ -8,7 +8,7 @@ import { useViewState } from "../../Context"; import LangPanel from "../Panels/LangPanel/LangPanel"; import SettingPanel from "../Panels/SettingPanel"; import SharePanel from "../Panels/SharePanel/SharePanel"; -import ToolsPanel from "../Panels/ToolsPanel/ToolsPanel"; +import DevToolsPanel from "../Panels/DevToolsPanel/DevToolsPanel"; import StoryButton from "./StoryButton/StoryButton"; import HelpButton from "./HelpButton/HelpButton"; @@ -34,7 +34,7 @@ const MenuBar = observer((props) => { }; const storyEnabled = terria.configParameters.storyEnabled; - const enableTools = terria.userProperties.get("tools") === "1"; + const enableDevTools = terria.userProperties.get("tools") === "1"; return ( { >
    - {enableTools && ( + {enableDevTools && (
  • - +
  • )} {!viewState.useSmallScreenInterface && diff --git a/lib/ReactViews/Map/Panels/ToolsPanel/CountDatasets.tsx b/lib/ReactViews/Map/Panels/DevToolsPanel/CountDatasets.tsx similarity index 100% rename from lib/ReactViews/Map/Panels/ToolsPanel/CountDatasets.tsx rename to lib/ReactViews/Map/Panels/DevToolsPanel/CountDatasets.tsx diff --git a/lib/ReactViews/Map/Panels/ToolsPanel/ToolsPanel.jsx b/lib/ReactViews/Map/Panels/DevToolsPanel/DevToolsPanel.jsx similarity index 60% rename from lib/ReactViews/Map/Panels/ToolsPanel/ToolsPanel.jsx rename to lib/ReactViews/Map/Panels/DevToolsPanel/DevToolsPanel.jsx index ff69aeb74e..2c425c84a3 100644 --- a/lib/ReactViews/Map/Panels/ToolsPanel/ToolsPanel.jsx +++ b/lib/ReactViews/Map/Panels/DevToolsPanel/DevToolsPanel.jsx @@ -1,14 +1,14 @@ -"use strict"; - import { observer } from "mobx-react"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import MenuPanel from "../../../StandardUserInterface/customizable/MenuPanel"; import { useViewState } from "../../../Context"; import CountDatasets from "./CountDatasets"; +import EditCesiumSettings from "./EditCesiumSettings"; import Styles from "./tools-panel.scss"; +import styled from "styled-components"; -const ToolsPanel = observer(() => { +const DevToolsPanel = observer(() => { const [isOpen, setIsOpen] = useState(false); const [resultsMessage, setResultsMessage] = useState(""); const dropdownTheme = { @@ -29,19 +29,23 @@ const ToolsPanel = observer(() => { isOpen={isOpen} smallScreen={viewState.useSmallScreenInterface} > - {isOpen && ( -
    -
    - -
    + + {isOpen && setIsOpen(false)} />} + {isOpen && } +
    + {/* eslint-disable-next-line react/no-danger */} +
    - )} -
    - {/* eslint-disable-next-line react/no-danger */} -
    -
    + ); }); -export default ToolsPanel; +const MenuItems = styled.div` + display: flex; + flex-direction: column; + padding: 20px; + gap: 20px; +`; + +export default DevToolsPanel; diff --git a/lib/ReactViews/Map/Panels/DevToolsPanel/EditCesiumSettings.tsx b/lib/ReactViews/Map/Panels/DevToolsPanel/EditCesiumSettings.tsx new file mode 100644 index 0000000000..d08e2a86e6 --- /dev/null +++ b/lib/ReactViews/Map/Panels/DevToolsPanel/EditCesiumSettings.tsx @@ -0,0 +1,35 @@ +import { observer } from "mobx-react"; +import { FC } from "react"; +import { runAppWorkflow } from "../../../../Models/Workflows/AppWorkflows/AppWorkflow"; +import CesiumSettingsWorkflow from "../../../../Models/Workflows/AppWorkflows/CesiumSettingsWorkflow"; +import Button from "../../../../Styled/Button"; +import { useViewState } from "../../../Context"; + +interface PropsType { + closePanel: () => void; +} + +const EditCesiumSettings: FC = observer(({ closePanel }) => { + const terria = useViewState().terria; + const cesium = terria.cesium; + + return ( + + ); +}); + +export default EditCesiumSettings; diff --git a/lib/ReactViews/Map/Panels/ToolsPanel/tools-panel.scss b/lib/ReactViews/Map/Panels/DevToolsPanel/tools-panel.scss similarity index 100% rename from lib/ReactViews/Map/Panels/ToolsPanel/tools-panel.scss rename to lib/ReactViews/Map/Panels/DevToolsPanel/tools-panel.scss diff --git a/lib/ReactViews/Map/Panels/ToolsPanel/tools-panel.scss.d.ts b/lib/ReactViews/Map/Panels/DevToolsPanel/tools-panel.scss.d.ts similarity index 100% rename from lib/ReactViews/Map/Panels/ToolsPanel/tools-panel.scss.d.ts rename to lib/ReactViews/Map/Panels/DevToolsPanel/tools-panel.scss.d.ts diff --git a/lib/ReactViews/SelectableDimensions/Button.tsx b/lib/ReactViews/SelectableDimensions/Button.tsx index 568ceeff53..3a1ebd50a9 100644 --- a/lib/ReactViews/SelectableDimensions/Button.tsx +++ b/lib/ReactViews/SelectableDimensions/Button.tsx @@ -14,11 +14,13 @@ export const SelectableDimensionButton: FC<{ }> = ({ dim }) => { const iconGlyph = dim.icon; const iconProps = { light: true, styledWidth: "16px", styledHeight: "16px" }; + const variant = dim.variant ?? "default"; return (