diff --git a/CHANGES.md b/CHANGES.md index 8d5b7f793e9..ff344de6298 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ - Replace unmaintained jsdom-global with maintained global-jsom. - Upgrade babel packages to latest versions (7.28.0/7.29.0). - Upgrade webpack to version 5.105.4. +- 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/doc/customizing/client-side-config.md b/doc/customizing/client-side-config.md index 69bfb32dab7..7011a954cc7 100644 --- a/doc/customizing/client-side-config.md +++ b/doc/customizing/client-side-config.md @@ -56,7 +56,7 @@ It is also possible to add version 7 init files using the `v7initializationUrls` ## Parameters -**The best reference for now is [`interface ConfigParameters`](https://github.com/TerriaJS/terriajs/blob/main/lib/Models/Terria.ts#L101) (you may have to search for `interface ConfigParameters` on that page to find it if future code changes change line numbers).** +**The best reference is [`interface ConfigParameters`](../../lib/Models/TerriaConfig.ts#L14).** Specifies various options for configuring TerriaJS: diff --git a/lib/Models/Terria.ts b/lib/Models/Terria.ts index 4431cded088..6550f33f67d 100644 --- a/lib/Models/Terria.ts +++ b/lib/Models/Terria.ts @@ -21,6 +21,13 @@ import queryToObject from "terriajs-cesium/Source/Core/queryToObject"; import Entity from "terriajs-cesium/Source/DataSources/Entity"; import SplitDirection from "terriajs-cesium/Source/Scene/SplitDirection"; import URI from "urijs"; +import NoopAnalytics from "../Core/Analytics/NoopAnalytics"; +import { + Category, + DataSourceAction, + LaunchAction +} from "../Core/Analytics/analyticEvents"; +import { Analytics } from "../Core/Analytics/types"; import AsyncLoader from "../Core/AsyncLoader"; import Class from "../Core/Class"; import CorsProxy from "../Core/CorsProxy"; @@ -39,14 +46,6 @@ import TerriaError, { TerriaErrorOverrides, TerriaErrorSeverity } from "../Core/TerriaError"; -import { Complete } from "../Core/TypeModifiers"; -import NoopAnalytics from "../Core/Analytics/NoopAnalytics"; -import { - Category, - DataSourceAction, - LaunchAction -} from "../Core/Analytics/analyticEvents"; -import { Analytics } from "../Core/Analytics/types"; import ensureSuffix from "../Core/ensureSuffix"; import filterOutUndefined from "../Core/filterOutUndefined"; import getDereferencedIfExists from "../Core/getDereferencedIfExists"; @@ -67,13 +66,8 @@ import MappableMixin, { isDataSource } from "../ModelMixins/MappableMixin"; import ReferenceMixin from "../ModelMixins/ReferenceMixin"; import TimeVarying from "../ModelMixins/TimeVarying"; import NotificationState from "../ReactViewModels/NotificationState"; -import { HelpContentItem } from "../ReactViewModels/defaultHelpContent"; -import { Term, defaultTerms } from "../ReactViewModels/defaultTerms"; -import { ICredit } from "../ReactViews/Map/BottomBar/Credits"; import { SHARE_VERSION } from "../ReactViews/Map/Panels/SharePanel/BuildShareLink"; import { shareConvertNotification } from "../ReactViews/Notification/shareConvertNotification"; -import { SearchBarTraits } from "../Traits/SearchProviders/SearchBarTraits"; -import SearchProviderTraits from "../Traits/SearchProviders/SearchProviderTraits"; import MappableTraits from "../Traits/TraitsClasses/MappableTraits"; import MapNavigationModel from "../ViewModels/MapNavigation/MapNavigationModel"; import TerriaViewer from "../ViewModels/TerriaViewer"; @@ -83,20 +77,14 @@ import Catalog from "./Catalog/Catalog"; import CatalogGroup from "./Catalog/CatalogGroup"; import CatalogMemberFactory from "./Catalog/CatalogMemberFactory"; import CatalogProvider from "./Catalog/CatalogProvider"; -import MagdaReference, { - MagdaReferenceHeaders -} from "./Catalog/CatalogReferences/MagdaReference"; +import MagdaReference from "./Catalog/CatalogReferences/MagdaReference"; import SplitItemReference from "./Catalog/CatalogReferences/SplitItemReference"; import CommonStrata from "./Definition/CommonStrata"; import { BaseModel } from "./Definition/Model"; -import ModelPropertiesFromTraits from "./Definition/ModelPropertiesFromTraits"; import hasTraits from "./Definition/hasTraits"; import updateModelFromJson from "./Definition/updateModelFromJson"; import upsertModelFromJson from "./Definition/upsertModelFromJson"; -import { - ErrorServiceOptions, - ErrorServiceProvider -} from "./ErrorServiceProviders/ErrorService"; +import { ErrorServiceProvider } from "./ErrorServiceProviders/ErrorService"; import StubErrorServiceProvider from "./ErrorServiceProviders/StubErrorServiceProvider"; import TerriaFeature from "./Feature/Feature"; import GlobeOrMap from "./GlobeOrMap"; @@ -111,275 +99,19 @@ import InitSource, { isInitFromOptions, isInitFromUrl } from "./InitSource"; -import Internationalization, { - I18nStartOptions, - LanguageConfiguration -} from "./Internationalization"; +import Internationalization, { I18nStartOptions } from "./Internationalization"; import MapInteractionMode from "./MapInteractionMode"; import NoViewer from "./NoViewer"; -import { RelatedMap } from "./RelatedMaps"; import CatalogIndex from "./SearchProviders/CatalogIndex"; import { SearchBarModel } from "./SearchProviders/SearchBarModel"; import ShareDataService from "./ShareDataService"; -import { StoryVideoSettings } from "./StoryVideoSettings"; +import { ConfigParameters, TerriaConfig } from "./TerriaConfig"; import TimelineStack from "./TimelineStack"; import { isViewerMode, setViewerMode } from "./ViewerMode"; import Workbench from "./Workbench"; import SelectableDimensionWorkflow from "./Workflows/SelectableDimensionWorkflow"; -export interface ConfigParameters { - /** - * TerriaJS uses this name whenever it needs to display the name of the application. - */ - appName?: string; - /** - * The email address shown when things go wrong. - */ - supportEmail?: string; - /** - * The maximum number of "feature info" boxes that can be displayed when clicking a point. - */ - defaultMaximumShownFeatureInfos: number; - /** - * URL of the JSON file that contains index of catalog. - */ - catalogIndexUrl?: string; - /** - * **Deprecated** - please use regionMappingDefinitionsUrls array instead. If this is defined, it will override `regionMappingDefinitionsUrls` - */ - regionMappingDefinitionsUrl?: string | undefined; - /** - * URLs of the JSON file that defines region mapping for CSV files. First matching region will be used (in array order) - */ - regionMappingDefinitionsUrls: string[]; - /** - * URL of Proj4 projection lookup service (part of TerriaJS-Server). - */ - proj4ServiceBaseUrl?: string; - /** - * URL of CORS proxy service (part of TerriaJS-Server) - */ - corsProxyBaseUrl?: string; - /** - * @deprecated - */ - proxyableDomainsUrl?: string; - serverConfigUrl?: string; - shareUrl?: string; - /** - * URL of the service used to send feedback. If not specified, the "Give Feedback" button will not appear. - */ - feedbackUrl?: string; - /** - * An array of base paths to use to try to use to resolve init fragments in the URL. For example, if this property is `[ "init/", "http://example.com/init/"]`, then a URL with `#test` will first try to load `init/test.json` and, if that fails, next try to load `http://example.com/init/test.json`. - */ - initFragmentPaths: string[]; - /** - * Whether the story is enabled. If false story function button won't be available. - */ - storyEnabled: boolean; - /** - * Whether to show the saving instructions message in the story builder panel. Defaults to false. - */ - showStorySaveInstructions?: boolean; - /** - * True (the default) to intercept the browser's print feature and use a custom one accessible through the Share panel. - */ - interceptBrowserPrint?: boolean; - /** - * True to create a separate explorer panel tab for each top-level catalog group to list its items in. - */ - tabbedCatalog?: boolean; - /** - * True to use Cesium World Terrain from Cesium ion. False to use terrain from the URL specified with the `"cesiumTerrainUrl"` property. If this property is false and `"cesiumTerrainUrl"` is not specified, the 3D view will use a smooth ellipsoid instead of a terrain surface. Defaults to true. - */ - useCesiumIonTerrain?: boolean; - /** - * The URL to use for Cesium terrain in the 3D Terrain viewer, in quantized mesh format. This property is ignored if "useCesiumIonTerrain" is set to true. - */ - cesiumTerrainUrl?: string; - /** - * The Cesium Ion Asset ID to use for Cesium terrain in the 3D Terrain viewer. `cesiumIonAccessToken` will be used to authenticate. This property is ignored if "useCesiumIonTerrain" is set to true. - */ - cesiumTerrainAssetId?: number; - /** - * The access token to use with Cesium ion. If `"useCesiumIonTerrain"` is true and this property is not specified, the Cesium default Ion key will be used. It is a violation of the Ion terms of use to use the default key in a deployed application. - */ - cesiumIonAccessToken?: string; - /** - * True to use Bing Maps from Cesium ion (Cesium World Imagery). By default, Ion will be used, unless the `bingMapsKey` property is specified, in which case that will be used instead. To disable the Bing Maps layers entirely, set this property to false and set `bingMapsKey` to null. - */ - useCesiumIonBingImagery?: boolean; - /** - * The OAuth2 application ID to use to allow login to Cesium ion on the "Add Data" panel. The referenced application must be configured on - * Cesium ion with a Redirect URI of `[TerriaMap Base URL]/build/TerriaJS/cesium-ion-oauth2.html`. For example, if users access your TerriaJS - * application at `https://example.com/AwesomeMap` then the Redirect URI must be exactly - * `https://example.com/AwesomeMap/build/TerriaJS/cesium-ion-oauth2.html`. - */ - cesiumIonOAuth2ApplicationID?: number; - /** - * Specifies where to store the Cesium ion login token. Valid values are: - * - `page` (default) - The login token is associated with the current page load. Even simply reloading the current page will clear the token. This is the safest option. - * - `sessionStorage` - The login token is associated with a browser session, which means it is shared/accessible from any page hosted on the same domain and running in the same browser tab. - * - `localStorage` - The login token is shared/accessible from any page hosted on the same domain, even when running in different tabs or after exiting and restarted the web browser. - */ - cesiumIonLoginTokenPersistence?: string; - /** - * Whether or not Cesium ion assets added via the "Add Data" panel will be shared with others via share links. If true, users will be asked to select a Cesium ion token when adding assets, - * and this choice must be made carefully to avoid exposing more Cesium ion assets than intended. If false (the default), the user's login token will be used, which is safe because this - * token will not be shared with others. - */ - cesiumIonAllowSharingAddedAssets?: boolean; - /** - * A [Bing Maps API key](https://msdn.microsoft.com/en-us/library/ff428642.aspx) used for requesting Bing Maps base maps and using the Bing Maps geocoder for searching. It is your responsibility to request a key and comply with all terms and conditions. - */ - bingMapsKey?: string; - hideTerriaLogo?: boolean; - /** - * An array of strings of HTML that fill up the top left logo space (see `brandBarSmallElements` or `displayOneBrand` for small screens). - */ - brandBarElements?: string[]; - /** - * An array of strings of HTML that fill up the top left logo space - used for small screens. - */ - brandBarSmallElements?: string[]; - /** - * Index of which `brandBarElements` to show for mobile header. This will be used if `this.brandBarSmallElements` is undefined. - */ - displayOneBrand?: number; - /** - * True to disable the "Centre map at your current location" button. - */ - disableMyLocation?: boolean; - disableSplitter?: boolean; - - disablePedestrianMode?: boolean; - - experimentalFeatures?: boolean; - magdaReferenceHeaders?: MagdaReferenceHeaders; - locationSearchBoundingBox?: number[]; - /** - * A Google API key for [Google Analytics](https://analytics.google.com). If specified, TerriaJS will send various events about how it's used to Google Analytics. - */ - googleAnalyticsKey?: string; - - /** - * Options for Google Analytics - */ - googleAnalyticsOptions?: unknown; - - /** - * Error service provider configuration. - */ - errorService?: ErrorServiceOptions; - - globalDisclaimer?: any; - /** - * True to display welcome message on startup. - */ - showWelcomeMessage?: boolean; - - // TODO: make themeing TS - /** Theme overrides, this is applied in StandardUserInterface and merged in order of highest priority: - * `StandardUserInterface.jsx` `themeOverrides` prop -> `theme` config parameter (this object) -> default `terriaTheme` (see `StandardTheme.jsx`) - */ - theme?: any; - /** - * Video to show in welcome message. - */ - welcomeMessageVideo?: any; - /** - * Video to show in Story Builder. - */ - storyVideo?: StoryVideoSettings; - /** - * True to display in-app guides. - */ - showInAppGuides?: boolean; - /** - * The content to be displayed in the help panel. - */ - helpContent?: HelpContentItem[]; - helpContentTerms?: Term[]; - /** - * - */ - languageConfiguration?: LanguageConfiguration; - /** - * Custom concurrent request limits for domains in Cesium's RequestScheduler. Cesium's default is 6 per domain (the maximum allowed by browsers unless the server supports http2). For servers supporting http2 try 12-24 to have more parallel requests. Setting this too high will undermine Cesium's prioritised request scheduling and important data may load slower. Format is {"domain_without_protocol:port": number}. - */ - customRequestSchedulerLimits?: Record; - - /** - * Whether to load persisted viewer mode from local storage. - */ - persistViewerMode?: boolean; - - /** - * Whether to open the add data explorer panel on load. - */ - openAddData?: boolean; - - /** - * Text showing at the top of feedback form. - */ - feedbackPreamble?: string; - - /** - * Text showing at the bottom of feedback form. - */ - feedbackPostamble?: string; - /** - * Minimum length of feedback comment. - */ - feedbackMinLength?: number; - - /** Maximum zoom level for Leaflet map */ - leafletMaxZoom: number; - - /** If undefined, then Leaflet's default attribution will be used */ - leafletAttributionPrefix?: string; - - /** - * Extra links to show in the credit line at the bottom of the map (currently only the Cesium map). - */ - extraCreditLinks?: ICredit[]; - - /** - * Configurable discalimer that shows up in print view - */ - printDisclaimer?: { url: string; text: string }; - - /** - * Prefix to which `:story-id` is added to fetch JSON for stories when using /story/:story-id routes. Should end in / - */ - storyRouteUrlPrefix?: string; - - /** - * For Console Analytics - */ - enableConsoleAnalytics?: boolean; - - relatedMaps?: RelatedMap[]; - - /** - * Optional plugin configuration - */ - plugins?: Record; - - aboutButtonHrefUrl?: string | null; - - /** - * The search bar allows requesting information from various search services at once. - */ - searchBarConfig?: ModelPropertiesFromTraits; - searchProviders: ModelPropertiesFromTraits[]; - - /** - * Keep catalog open when adding / removing items - */ - keepCatalogOpen: boolean; -} +export type { ConfigParameters } from "./TerriaConfig"; interface StartOptions { configUrl: string; @@ -485,8 +217,15 @@ export default class Terria { ) ); - appName: string = "TerriaJS App"; - supportEmail: string = "info@terria.io"; + @computed + get appName(): string { + return this.configParameters.appName; + } + + @computed + get supportEmail(): string { + return this.configParameters.supportEmail; + } /** * Gets or sets the {@link this.corsProxy} used to determine if a URL needs to be proxied and to proxy it if necessary. @@ -506,92 +245,7 @@ export default class Terria { */ readonly timelineStack = new TimelineStack(this, this.timelineClock); - @observable - readonly configParameters: Complete = { - appName: "TerriaJS App", - supportEmail: "info@terria.io", - defaultMaximumShownFeatureInfos: 100, - catalogIndexUrl: undefined, - regionMappingDefinitionsUrl: undefined, - regionMappingDefinitionsUrls: ["build/TerriaJS/data/regionMapping.json"], - proj4ServiceBaseUrl: "proj4def/", - corsProxyBaseUrl: "proxy/", - proxyableDomainsUrl: "proxyabledomains/", // deprecated, will be determined from serverconfig - serverConfigUrl: "serverconfig/", - shareUrl: "share", - feedbackUrl: undefined, - initFragmentPaths: ["init/"], - storyEnabled: true, - showStorySaveInstructions: false, - interceptBrowserPrint: true, - tabbedCatalog: false, - useCesiumIonTerrain: true, - cesiumTerrainUrl: undefined, - cesiumTerrainAssetId: undefined, - cesiumIonAccessToken: undefined, - useCesiumIonBingImagery: undefined, - cesiumIonOAuth2ApplicationID: undefined, - cesiumIonLoginTokenPersistence: "page", - cesiumIonAllowSharingAddedAssets: false, - bingMapsKey: undefined, - hideTerriaLogo: false, - brandBarElements: undefined, - brandBarSmallElements: undefined, - displayOneBrand: 0, - disableMyLocation: undefined, - disableSplitter: undefined, - disablePedestrianMode: false, - keepCatalogOpen: false, - experimentalFeatures: undefined, - magdaReferenceHeaders: undefined, - locationSearchBoundingBox: undefined, - googleAnalyticsKey: undefined, - errorService: undefined, - globalDisclaimer: undefined, - theme: {}, - showWelcomeMessage: false, - welcomeMessageVideo: { - videoTitle: "Getting started with the map", - videoUrl: "https://www.youtube-nocookie.com/embed/FjSxaviSLhc", - placeholderImage: - "https://img.youtube.com/vi/FjSxaviSLhc/maxresdefault.jpg" - }, - storyVideo: { - videoUrl: "https://www.youtube-nocookie.com/embed/fbiQawV8IYY" - }, - showInAppGuides: false, - helpContent: [], - helpContentTerms: defaultTerms, - languageConfiguration: undefined, - customRequestSchedulerLimits: undefined, - persistViewerMode: true, - openAddData: false, - feedbackPreamble: "translate#feedback.feedbackPreamble", - feedbackPostamble: undefined, - feedbackMinLength: 0, - leafletMaxZoom: 18, - leafletAttributionPrefix: undefined, - extraCreditLinks: [ - // Default credit links (shown at the bottom of the Cesium map) - { - text: "map.extraCreditLinks.dataAttribution", - url: "https://terria.io/attributions" - }, - { - text: "map.extraCreditLinks.termsOfUse", - url: "https://terria.io/demo-terms" - } - ], - printDisclaimer: undefined, - storyRouteUrlPrefix: undefined, - enableConsoleAnalytics: undefined, - googleAnalyticsOptions: undefined, - relatedMaps: [], - aboutButtonHrefUrl: "about.html", - plugins: undefined, - searchBarConfig: undefined, - searchProviders: [] - }; + readonly configParameters = new TerriaConfig(); @observable pickedFeatures: PickedFeatures | undefined; @@ -625,10 +279,6 @@ export default class Terria { */ private focusWorkbenchItemsAfterLoadingInitSources: boolean = false; - private _loadPersistedSettings: { baseMapPromise?: Promise } = { - baseMapPromise: undefined - }; - @computed get baseMapContrastColor() { return ( @@ -1021,7 +671,7 @@ export default class Terria { } runInAction(() => { if (isJsonObject(config) && isJsonObject(config.parameters)) { - this.updateParameters(config.parameters); + this.applyConfig(config.parameters); } this.setupInitializationUrls(baseUri, config); }); @@ -1324,15 +974,8 @@ export default class Terria { } @action - updateParameters(parameters: ConfigParameters | JsonObject): void { - Object.entries(parameters).forEach(([key, value]) => { - if (Object.hasOwnProperty.call(this.configParameters, key)) { - (this.configParameters as any)[key] = value; - } - }); - - this.appName = this.configParameters.appName ?? this.appName; - this.supportEmail = this.configParameters.supportEmail ?? this.supportEmail; + applyConfig(config: Partial): void { + this.configParameters.apply(config); } protected async forceLoadInitSources(): Promise { @@ -2025,7 +1668,7 @@ export default class Terria { const configParams = aspects["terria-config"]?.parameters; if (configParams) { - this.updateParameters(configParams); + this.applyConfig(configParams); } const initObj = aspects["terria-init"]; diff --git a/lib/Models/TerriaConfig.ts b/lib/Models/TerriaConfig.ts new file mode 100644 index 00000000000..69353191337 --- /dev/null +++ b/lib/Models/TerriaConfig.ts @@ -0,0 +1,449 @@ +import { action, makeObservable, observable } from "mobx"; +import { HelpContentItem } from "../ReactViewModels/defaultHelpContent"; +import { Term, defaultTerms } from "../ReactViewModels/defaultTerms"; +import { ICredit } from "../ReactViews/Map/BottomBar/Credits"; +import { SearchBarTraits } from "../Traits/SearchProviders/SearchBarTraits"; +import SearchProviderTraits from "../Traits/SearchProviders/SearchProviderTraits"; +import { MagdaReferenceHeaders } from "./Catalog/CatalogReferences/MagdaReference"; +import ModelPropertiesFromTraits from "./Definition/ModelPropertiesFromTraits"; +import { ErrorServiceOptions } from "./ErrorServiceProviders/ErrorService"; +import { LanguageConfiguration } from "./Internationalization"; +import { RelatedMap } from "./RelatedMaps"; +import { StoryVideoSettings } from "./StoryVideoSettings"; + +export interface ConfigParameters { + /** + * TerriaJS uses this name whenever it needs to display the name of the + * application. + */ + appName?: string; + /** + * The email address shown when things go wrong. + */ + supportEmail?: string; + /** + * The maximum number of "feature info" boxes that can be displayed when + * clicking a point. + */ + defaultMaximumShownFeatureInfos: number; + /** + * URL of the JSON file that contains index of catalog. + */ + catalogIndexUrl?: string; + /** + * **Deprecated** - please use regionMappingDefinitionsUrls array instead. If + * this is defined, it will override `regionMappingDefinitionsUrls` + */ + regionMappingDefinitionsUrl?: string | undefined; + /** + * URLs of the JSON file that defines region mapping for CSV files. First + * matching region will be used (in array order) + */ + regionMappingDefinitionsUrls: string[]; + /** + * URL of Proj4 projection lookup service (part of TerriaJS-Server). + */ + proj4ServiceBaseUrl?: string; + /** + * URL of CORS proxy service (part of TerriaJS-Server) + */ + corsProxyBaseUrl?: string; + /** + * @deprecated + */ + proxyableDomainsUrl?: string; + serverConfigUrl?: string; + shareUrl?: string; + /** + * URL of the service used to send feedback. If not specified, the "Give + * Feedback" button will not appear. + */ + feedbackUrl?: string; + /** + * An array of base paths to use to try to use to resolve init fragments in + * the URL. For example, if this property is `[ "init/", + * "http://example.com/init/"]`, then a URL with `#test` will first try to + * load `init/test.json` and, if that fails, next try to load + * `http://example.com/init/test.json`. + */ + initFragmentPaths: string[]; + /** + * Whether the story is enabled. If false story function button won't be + * available. + */ + storyEnabled: boolean; + /** + * Whether to show the saving instructions message in the story builder panel. + * Defaults to false. + */ + showStorySaveInstructions?: boolean; + /** + * True (the default) to intercept the browser's print feature and use a + * custom one accessible through the Share panel. + */ + interceptBrowserPrint?: boolean; + /** + * True to create a separate explorer panel tab for each top-level catalog + * group to list its items in. + */ + tabbedCatalog?: boolean; + /** + * True to use Cesium World Terrain from Cesium ion. False to use terrain from + * the URL specified with the `"cesiumTerrainUrl"` property. If this property + * is false and `"cesiumTerrainUrl"` is not specified, the 3D view will use a + * smooth ellipsoid instead of a terrain surface. Defaults to true. + */ + useCesiumIonTerrain?: boolean; + /** + * The URL to use for Cesium terrain in the 3D Terrain viewer, in quantized + * mesh format. This property is ignored if "useCesiumIonTerrain" is set to + * true. + */ + cesiumTerrainUrl?: string; + /** + * The Cesium Ion Asset ID to use for Cesium terrain in the 3D Terrain viewer. + * `cesiumIonAccessToken` will be used to authenticate. This property is + * ignored if "useCesiumIonTerrain" is set to true. + */ + cesiumTerrainAssetId?: number; + /** + * The access token to use with Cesium ion. If `"useCesiumIonTerrain"` is true + * and this property is not specified, the Cesium default Ion key will be + * used. It is a violation of the Ion terms of use to use the default key in a + * deployed application. + */ + cesiumIonAccessToken?: string; + /** + * True to use Bing Maps from Cesium ion (Cesium World Imagery). By default, + * Ion will be used, unless the `bingMapsKey` property is specified, in which + * case that will be used instead. To disable the Bing Maps layers entirely, + * set this property to false and set `bingMapsKey` to null. + */ + useCesiumIonBingImagery?: boolean; + /** + * The OAuth2 application ID to use to allow login to Cesium ion on the "Add + * Data" panel. The referenced application must be configured on Cesium ion + * with a Redirect URI of `[TerriaMap Base + * URL]/build/TerriaJS/cesium-ion-oauth2.html`. For example, if users access + * your TerriaJS application at `https://example.com/AwesomeMap` then the + * Redirect URI must be exactly + * `https://example.com/AwesomeMap/build/TerriaJS/cesium-ion-oauth2.html`. + */ + cesiumIonOAuth2ApplicationID?: number; + /** + * Specifies where to store the Cesium ion login token. Valid values are: + * - `page` (default) - The login token is associated with the current page + * load. Even simply reloading the current page will clear the token. This + * is the safest option. + * - `sessionStorage` - The login token is associated with a browser + * session, which means it is shared/accessible from any page hosted on + * the same domain and running in the same browser tab. + * - `localStorage` - The login token is shared/accessible from any page + * hosted on the same domain, even when running in different tabs or after + * exiting and restarted the web browser. + */ + cesiumIonLoginTokenPersistence?: string; + /** + * Whether or not Cesium ion assets added via the "Add Data" panel will be + * shared with others via share links. If true, users will be asked to select + * a Cesium ion token when adding assets, and this choice must be made + * carefully to avoid exposing more Cesium ion assets than intended. If false + * (the default), the user's login token will be used, which is safe because + * this token will not be shared with others. + */ + cesiumIonAllowSharingAddedAssets?: boolean; + /** + * A [Bing Maps API + * key](https://msdn.microsoft.com/en-us/library/ff428642.aspx) used for + * requesting Bing Maps base maps and using the Bing Maps geocoder for + * searching. It is your responsibility to request a key and comply with all + * terms and conditions. + */ + bingMapsKey?: string; + hideTerriaLogo?: boolean; + /** + * An array of strings of HTML that fill up the top left logo space (see + * `brandBarSmallElements` or `displayOneBrand` for small screens). + */ + brandBarElements?: string[]; + /** + * An array of strings of HTML that fill up the top left logo space - used for + * small screens. + */ + brandBarSmallElements?: string[]; + /** + * Index of which `brandBarElements` to show for mobile header. This will be + * used if `this.brandBarSmallElements` is undefined. + */ + displayOneBrand?: number; + /** + * True to disable the "Centre map at your current location" button. + */ + disableMyLocation?: boolean; + disableSplitter?: boolean; + + disablePedestrianMode?: boolean; + + experimentalFeatures?: boolean; + magdaReferenceHeaders?: MagdaReferenceHeaders; + locationSearchBoundingBox?: number[]; + /** + * A Google API key for [Google Analytics](https://analytics.google.com). If + * specified, TerriaJS will send various events about how it's used to Google + * Analytics. + */ + googleAnalyticsKey?: string; + + /** + * Options for Google Analytics + */ + googleAnalyticsOptions?: unknown; + + /** + * Error service provider configuration. + */ + errorService?: ErrorServiceOptions; + + globalDisclaimer?: any; + /** + * True to display welcome message on startup. + */ + showWelcomeMessage?: boolean; + + // TODO: make themeing TS + /** Theme overrides, this is applied in StandardUserInterface and merged in + * order of highest priority: `StandardUserInterface.jsx` `themeOverrides` + * prop -> `theme` config parameter (this object) -> default `terriaTheme` + * (see `StandardTheme.jsx`) + */ + theme?: any; + /** + * Video to show in welcome message. + */ + welcomeMessageVideo?: any; + /** + * Video to show in Story Builder. + */ + storyVideo?: StoryVideoSettings; + /** + * True to display in-app guides. + */ + showInAppGuides?: boolean; + /** + * The content to be displayed in the help panel. + */ + helpContent?: HelpContentItem[]; + helpContentTerms?: Term[]; + /** + * + */ + languageConfiguration?: LanguageConfiguration; + /** + * Custom concurrent request limits for domains in Cesium's RequestScheduler. + * Cesium's default is 6 per domain (the maximum allowed by browsers unless + * the server supports http2). For servers supporting http2 try 12-24 to have + * more parallel requests. Setting this too high will undermine Cesium's + * prioritised request scheduling and important data may load slower. Format + * is {"domain_without_protocol:port": number}. + */ + customRequestSchedulerLimits?: Record; + + /** + * Whether to load persisted viewer mode from local storage. + */ + persistViewerMode?: boolean; + + /** + * Whether to open the add data explorer panel on load. + */ + openAddData?: boolean; + + /** + * Text showing at the top of feedback form. + */ + feedbackPreamble?: string; + + /** + * Text showing at the bottom of feedback form. + */ + feedbackPostamble?: string; + /** + * Minimum length of feedback comment. + */ + feedbackMinLength?: number; + + /** Maximum zoom level for Leaflet map */ + leafletMaxZoom: number; + + /** If undefined, then Leaflet's default attribution will be used */ + leafletAttributionPrefix?: string; + + /** + * Extra links to show in the credit line at the bottom of the map (currently + * only the Cesium map). + */ + extraCreditLinks?: ICredit[]; + + /** + * Configurable discalimer that shows up in print view + */ + printDisclaimer?: { url: string; text: string }; + + /** + * Prefix to which `:story-id` is added to fetch JSON for stories when using + * /story/:story-id routes. Should end in / + */ + storyRouteUrlPrefix?: string; + + /** + * For Console Analytics + */ + enableConsoleAnalytics?: boolean; + + relatedMaps?: RelatedMap[]; + + /** + * Optional plugin configuration + */ + plugins?: Record; + + aboutButtonHrefUrl?: string | null; + + /** + * The search bar allows requesting information from various search services + * at once. + */ + searchBarConfig?: ModelPropertiesFromTraits; + searchProviders: ModelPropertiesFromTraits[]; + + /** + * Keep catalog open when adding / removing items + */ + keepCatalogOpen: boolean; +} + +/** + * Holds all TerriaJS application configuration with defaults. + * + * Apply config from any source in any order — last write wins per field, + * `undefined` values do not overwrite existing values. + * + * @example + * const config = new TerriaConfig(); + * config.apply(await loadConfig("config.json")); + * config.apply(parseHashParams(location.hash)); + * config.apply(readLocalStorage(localStorage)); + */ +export class TerriaConfig { + @observable appName: string = "TerriaJS App"; + @observable supportEmail: string = "info@terria.io"; + @observable defaultMaximumShownFeatureInfos: number = 100; + @observable catalogIndexUrl: string | undefined = undefined; + @observable regionMappingDefinitionsUrl: string | undefined = undefined; + @observable regionMappingDefinitionsUrls: string[] = [ + "build/TerriaJS/data/regionMapping.json" + ]; + @observable proj4ServiceBaseUrl: string | undefined = "proj4def/"; + @observable corsProxyBaseUrl: string | undefined = "proxy/"; + @observable proxyableDomainsUrl: string | undefined = "proxyabledomains/"; + @observable serverConfigUrl: string | undefined = "serverconfig/"; + @observable shareUrl: string | undefined = "share"; + @observable feedbackUrl: string | undefined = undefined; + @observable initFragmentPaths: string[] = ["init/"]; + @observable storyEnabled: boolean = true; + @observable showStorySaveInstructions: boolean | undefined = false; + @observable interceptBrowserPrint: boolean | undefined = true; + @observable tabbedCatalog: boolean | undefined = false; + @observable useCesiumIonTerrain: boolean | undefined = true; + @observable cesiumTerrainUrl: string | undefined = undefined; + @observable cesiumTerrainAssetId: number | undefined = undefined; + @observable cesiumIonAccessToken: string | undefined = undefined; + @observable useCesiumIonBingImagery: boolean | undefined = undefined; + @observable cesiumIonOAuth2ApplicationID: number | undefined = undefined; + @observable cesiumIonLoginTokenPersistence: string | undefined = "page"; + @observable cesiumIonAllowSharingAddedAssets: boolean | undefined = false; + @observable bingMapsKey: string | undefined = undefined; + @observable hideTerriaLogo: boolean | undefined = false; + @observable brandBarElements: string[] | undefined = undefined; + @observable brandBarSmallElements: string[] | undefined = undefined; + @observable displayOneBrand: number | undefined = 0; + @observable disableMyLocation: boolean | undefined = undefined; + @observable disableSplitter: boolean | undefined = undefined; + @observable disablePedestrianMode: boolean | undefined = false; + @observable keepCatalogOpen: boolean = false; + @observable experimentalFeatures: boolean | undefined = undefined; + @observable magdaReferenceHeaders: MagdaReferenceHeaders | undefined = + undefined; + @observable locationSearchBoundingBox: number[] | undefined = undefined; + @observable googleAnalyticsKey: string | undefined = undefined; + @observable googleAnalyticsOptions: unknown = undefined; + @observable errorService: ErrorServiceOptions | undefined = undefined; + @observable globalDisclaimer: any = undefined; + @observable showWelcomeMessage: boolean | undefined = false; + @observable theme: any = {}; + @observable welcomeMessageVideo: any = { + videoTitle: "Getting started with the map", + videoUrl: "https://www.youtube-nocookie.com/embed/FjSxaviSLhc", + placeholderImage: "https://img.youtube.com/vi/FjSxaviSLhc/maxresdefault.jpg" + }; + @observable storyVideo: StoryVideoSettings | undefined = { + videoUrl: "https://www.youtube-nocookie.com/embed/fbiQawV8IYY" + }; + @observable showInAppGuides: boolean | undefined = false; + @observable helpContent: HelpContentItem[] | undefined = []; + @observable helpContentTerms: Term[] | undefined = defaultTerms; + @observable languageConfiguration: LanguageConfiguration | undefined = + undefined; + @observable customRequestSchedulerLimits: Record | undefined = + undefined; + @observable persistViewerMode: boolean | undefined = true; + @observable openAddData: boolean | undefined = false; + @observable feedbackPreamble: string | undefined = + "translate#feedback.feedbackPreamble"; + @observable feedbackPostamble: string | undefined = undefined; + @observable feedbackMinLength: number | undefined = 0; + @observable leafletMaxZoom: number = 18; + @observable leafletAttributionPrefix: string | undefined = undefined; + @observable extraCreditLinks: ICredit[] | undefined = [ + { + text: "map.extraCreditLinks.dataAttribution", + url: "https://terria.io/attributions" + }, + { + text: "map.extraCreditLinks.termsOfUse", + url: "https://terria.io/demo-terms" + } + ]; + @observable printDisclaimer: { url: string; text: string } | undefined = + undefined; + @observable storyRouteUrlPrefix: string | undefined = undefined; + @observable enableConsoleAnalytics: boolean | undefined = undefined; + @observable relatedMaps: RelatedMap[] | undefined = []; + @observable plugins: Record | undefined = undefined; + @observable aboutButtonHrefUrl: string | null | undefined = "about.html"; + @observable searchBarConfig: + | ModelPropertiesFromTraits + | undefined = undefined; + @observable + searchProviders: ModelPropertiesFromTraits[] = []; + + constructor() { + makeObservable(this); + } + + /** + * Merges `partial` into this config. Only fields present in + * `ConfigParameters` are accepted; `undefined` values do not overwrite + * existing values. + */ + @action + apply(partial: Partial): void { + (Object.entries(partial) as [keyof ConfigParameters, unknown][]).forEach( + ([key, value]) => { + if (key in this && value !== undefined) { + (this as any)[key] = value; + } + } + ); + } +} diff --git a/test/Map/StyledHtmlSpec.tsx b/test/Map/StyledHtmlSpec.tsx index 39b5b67a5a0..caa8a883ba9 100644 --- a/test/Map/StyledHtmlSpec.tsx +++ b/test/Map/StyledHtmlSpec.tsx @@ -43,7 +43,7 @@ describe("StyledHtml", function () { content: "data that is spatial, spluh" }; runInAction(() => { - terria.updateParameters({ + terria.applyConfig({ regionMappingDefinitionsUrl: "", initFragmentPaths: [], storyEnabled: false, diff --git a/test/ModelMixins/TableMixinSpec.ts b/test/ModelMixins/TableMixinSpec.ts index 6d4fdc0ed49..0097d18642d 100644 --- a/test/ModelMixins/TableMixinSpec.ts +++ b/test/ModelMixins/TableMixinSpec.ts @@ -601,7 +601,7 @@ describe("TableMixin", function () { ) ); - terria.updateParameters({ + terria.applyConfig({ regionMappingDefinitionsUrls: [ "additionalRegion.json", "build/TerriaJS/data/regionMapping.json" @@ -647,7 +647,7 @@ describe("TableMixin", function () { ) ); - terria.updateParameters({ + terria.applyConfig({ regionMappingDefinitionsUrl: "build/TerriaJS/data/regionMapping.json", regionMappingDefinitionsUrls: [ "additionalRegion.json", diff --git a/test/Models/TerriaConfigSpec.ts b/test/Models/TerriaConfigSpec.ts new file mode 100644 index 00000000000..89a5defb621 --- /dev/null +++ b/test/Models/TerriaConfigSpec.ts @@ -0,0 +1,82 @@ +import { TerriaConfig } from "../../lib/Models/TerriaConfig"; + +describe("TerriaConfig", function () { + describe("defaults", () => { + it("has the expected default appName", function () { + const config = new TerriaConfig(); + expect(config.appName).toBe("TerriaJS App"); + }); + + it("has the expected default supportEmail", function () { + const config = new TerriaConfig(); + expect(config.supportEmail).toBe("info@terria.io"); + }); + + it("has the expected default initFragmentPaths", function () { + const config = new TerriaConfig(); + expect(config.initFragmentPaths).toEqual(["init/"]); + }); + + it("has the expected default storyEnabled", function () { + const config = new TerriaConfig(); + expect(config.storyEnabled).toBe(true); + }); + + it("has the expected default persistViewerMode", function () { + const config = new TerriaConfig(); + expect(config.persistViewerMode).toBe(true); + }); + + it("has the expected default leafletMaxZoom", function () { + const config = new TerriaConfig(); + expect(config.leafletMaxZoom).toBe(18); + }); + }); + + describe("apply config", function () { + it("updates only the specified fields, leaving others at default", function () { + const config = new TerriaConfig(); + config.apply({ appName: "My App" }); + + expect(config.appName).toBe("My App"); + expect(config.supportEmail).toBe("info@terria.io"); // default unchanged + }); + + it("does not overwrite existing value when undefined is applied", function () { + const config = new TerriaConfig(); + config.apply({ appName: undefined }); + + expect(config.appName).toBe("TerriaJS App"); + }); + + it("uses last value when the same field is applied twice", function () { + const config = new TerriaConfig(); + config.apply({ appName: "First" }); + config.apply({ appName: "Second" }); + + expect(config.appName).toBe("Second"); + }); + + it("silently ignores unknown keys", function () { + const config = new TerriaConfig(); + expect(() => config.apply({ unknownKey: "value" } as any)).not.toThrow(); + expect((config as any).unknownKey).toBeUndefined(); + }); + + it("accumulates multiple sequential applies", function () { + const config = new TerriaConfig(); + config.apply({ appName: "My App" }); + config.apply({ supportEmail: "support@example.com" }); + + expect(config.appName).toBe("My App"); + expect(config.supportEmail).toBe("support@example.com"); + }); + + it("can override array fields", function () { + const config = new TerriaConfig(); + config.apply({ initFragmentPaths: ["custom/", "fallback/"] }); + + expect(config.initFragmentPaths).toEqual(["custom/", "fallback/"]); + }); + }); +}); diff --git a/test/Models/TerriaSpec.ts b/test/Models/TerriaSpec.ts index 5054d38aeb7..7bd845bf82b 100644 --- a/test/Models/TerriaSpec.ts +++ b/test/Models/TerriaSpec.ts @@ -728,7 +728,7 @@ describe("TerriaSpec", function () { beforeEach(function () { // These specs must run with a Terria constructed with "appBaseHref": "/" // to make the specs work with browser runner - terria.updateParameters({ + terria.applyConfig({ storyRouteUrlPrefix: "test/stories/TerriaJS%20App/" }); @@ -1856,4 +1856,22 @@ describe("TerriaSpec", function () { }); }); }); + + describe("applyConfig()", function () { + it("applies known config parameters", function () { + terria.applyConfig({ appName: "MyMap" }); + expect(terria.appName).toBe("MyMap"); + }); + + it("updates configParameters properties", function () { + terria.applyConfig({ supportEmail: "help@example.com" }); + expect(terria.configParameters.supportEmail).toBe("help@example.com"); + }); + + it("ignores unknown keys", function () { + expect(() => + terria.applyConfig({ unknownKey: "value" } as any) + ).not.toThrow(); + }); + }); }); diff --git a/test/ReactViews/StandardUserInterface/TrainerBar/TrainerBarSpec.tsx b/test/ReactViews/StandardUserInterface/TrainerBar/TrainerBarSpec.tsx index 16a35fb8bd6..7c697a68e9a 100644 --- a/test/ReactViews/StandardUserInterface/TrainerBar/TrainerBarSpec.tsx +++ b/test/ReactViews/StandardUserInterface/TrainerBar/TrainerBarSpec.tsx @@ -29,7 +29,7 @@ describe("TrainerBar", function () { it("renders nothing when setTrainerBarVisible is false", function () { runInAction(() => { - terria.updateParameters({ + terria.applyConfig({ regionMappingDefinitionsUrl: "", initFragmentPaths: [], storyEnabled: false, @@ -47,7 +47,7 @@ describe("TrainerBar", function () { it("renders a button to toggle visibility", function () { runInAction(() => { - terria.updateParameters({ + terria.applyConfig({ regionMappingDefinitionsUrl: "", initFragmentPaths: [], storyEnabled: false,