import { TFunction } from "i18next"; import { action } from "mobx"; import { observer } from "mobx-react"; import Slider from "rc-slider"; import { ChangeEvent, ComponentProps, FC, MouseEvent, Ref, useState } from "react"; import { useTranslation } from "react-i18next"; import styled from "styled-components"; import SplitDirection from "terriajs-cesium/Source/Scene/SplitDirection"; import CatalogMemberMixin from "../../../ModelMixins/CatalogMemberMixin"; import MappableMixin from "../../../ModelMixins/MappableMixin"; import { BaseMapItem } from "../../../Models/BaseMaps/BaseMapsModel"; import Cesium from "../../../Models/Cesium"; import Terria from "../../../Models/Terria"; import ViewerMode, { MapViewers, getViewerType, isViewerMode, setViewerMode } from "../../../Models/ViewerMode"; import Box from "../../../Styled/Box"; import Button, { RawButton } from "../../../Styled/Button"; import Checkbox from "../../../Styled/Checkbox"; import { GLYPHS, StyledIcon } from "../../../Styled/Icon"; import Spacing from "../../../Styled/Spacing"; import Text, { TextSpan } from "../../../Styled/Text"; import TerriaViewer from "../../../ViewModels/TerriaViewer"; import { useViewState } from "../../Context"; import { useRefForTerria } from "../../Hooks/useRefForTerria"; import MenuPanel from "../../StandardUserInterface/customizable/MenuPanel"; import Styles from "./setting-panel.scss"; const sides = { left: "settingPanel.terrain.left", both: "settingPanel.terrain.both", right: "settingPanel.terrain.right" }; const SettingPanel: FC = observer(() => { const { t } = useTranslation(); const viewState = useViewState(); const { terria } = viewState; const settingButtonRef: Ref = useRefForTerria( SETTING_PANEL_NAME, viewState ); const [hoverBaseMap, setHoverBaseMap] = useState< MappableMixin.Instance | undefined >(); const activeMap = hoverBaseMap ?? terria.mainViewer.baseMap; const activeMapName = activeMap ? mapDisplayName(activeMap) : "(None)"; const selectBaseMap = ( baseMap: MappableMixin.Instance, event: MouseEvent ) => { event.stopPropagation(); if (!MappableMixin.isMixedInto(baseMap)) return; const currentViewerMode = terria.mainViewer.viewerMode; const newViewerMode = baseMap.preferredViewerMode ? getViewerType(baseMap.preferredViewerMode) : undefined; terria.mainViewer.setBaseMap(baseMap).then(() => { const switchedViewerMode = newViewerMode && currentViewerMode && newViewerMode !== currentViewerMode && terria.mainViewer.viewerMode === newViewerMode; if (switchedViewerMode) { notifyViewerModeSwitch( baseMap, currentViewerMode, newViewerMode, terria, t ); } }); // We store the user's chosen basemap for future use, but it's up to the instance to decide // whether to use that at start up. if (baseMap) { saveBaseMapPreference(terria, baseMap); } }; const mouseEnterBaseMap = (baseMap: BaseMapItem) => { setHoverBaseMap(baseMap.item); }; const mouseLeaveBaseMap = () => { setHoverBaseMap(undefined); }; const selectViewer = action( (viewer: keyof typeof MapViewers, event: MouseEvent) => { const mainViewer = terria.mainViewer; event.stopPropagation(); showTerrainOnSide(sides.both, undefined); setViewerMode(viewer, mainViewer); // Ensure a base map that is compatible with the new viewer mode const prevBaseMap = mainViewer.baseMap; mainViewer.useViewerCompatibleBaseMap().then((baseMapChanged) => { if (baseMapChanged && mainViewer.baseMap) { saveBaseMapPreference(terria, mainViewer.baseMap); if (prevBaseMap) { notifyBaseMapSwitch( terria, mainViewer, prevBaseMap, mainViewer.baseMap, t ); } } }); // We store the user's chosen viewer mode for future use. terria.setLocalProperty("viewermode", viewer); terria.currentViewer.notifyRepaintRequired(); } ); const showTerrainOnSide = action( (side: any, event?: MouseEvent) => { event?.stopPropagation(); switch (side) { case sides.left: terria.terrainSplitDirection = SplitDirection.LEFT; terria.showSplitter = true; break; case sides.right: terria.terrainSplitDirection = SplitDirection.RIGHT; terria.showSplitter = true; break; case sides.both: terria.terrainSplitDirection = SplitDirection.NONE; break; } terria.currentViewer.notifyRepaintRequired(); } ); const toggleDepthTestAgainstTerrainEnabled = action( (event: ChangeEvent) => { event.stopPropagation(); terria.depthTestAgainstTerrainEnabled = !terria.depthTestAgainstTerrainEnabled; terria.currentViewer.notifyRepaintRequired(); } ); const onBaseMaximumScreenSpaceErrorChange = (bmsse: number) => { terria.setBaseMaximumScreenSpaceError(bmsse); terria.setLocalProperty("baseMaximumScreenSpaceError", bmsse.toString()); }; const toggleUseNativeResolution = () => { terria.setUseNativeResolution(!terria.useNativeResolution); terria.setLocalProperty("useNativeResolution", terria.useNativeResolution); }; const qualityLabels = { 0: t("settingPanel.qualityLabels.maximumPerformance"), 1: t("settingPanel.qualityLabels.balancedPerformance"), 2: t("settingPanel.qualityLabels.lowerPerformance") }; const currentViewer = terria.mainViewer.viewerMode === ViewerMode.Cesium ? terria.mainViewer.viewerOptions.useTerrain ? "3d" : "3dsmooth" : "2d"; const useNativeResolution = terria.useNativeResolution; const nativeResolutionLabel = t("settingPanel.nativeResolutionLabel", { resolution1: useNativeResolution ? t("settingPanel.native") : t("settingPanel.screen"), resolution2: useNativeResolution ? t("settingPanel.screen") : t("settingPanel.native") }); const dropdownTheme = { inner: Styles.dropdownInner, icon: "map" }; const isCesiumWithTerrain = terria.mainViewer.viewerMode === ViewerMode.Cesium && terria.mainViewer.viewerOptions.useTerrain && terria.currentViewer && terria.currentViewer instanceof Cesium && terria.currentViewer.scene && terria.currentViewer.scene.globe; const supportsDepthTestAgainstTerrain = isCesiumWithTerrain; const depthTestAgainstTerrainEnabled = supportsDepthTestAgainstTerrain && terria.depthTestAgainstTerrainEnabled; const depthTestAgainstTerrainLabel = depthTestAgainstTerrainEnabled ? t("settingPanel.terrain.showUndergroundFeatures") : t("settingPanel.terrain.hideUndergroundFeatures"); if ( terria.configParameters.useCesiumIonTerrain || terria.configParameters.cesiumTerrainUrl ) { MapViewers["3d"].available = true; } const supportsSide = isCesiumWithTerrain; let currentSide = sides.both; if (supportsSide) { switch (terria.terrainSplitDirection) { case SplitDirection.LEFT: currentSide = sides.left; break; case SplitDirection.RIGHT: currentSide = sides.right; break; } } const timelineStack = terria.timelineStack; const alwaysShowTimelineLabel = timelineStack.alwaysShowingTimeline ? t("settingPanel.timeline.alwaysShowLabel") : t("settingPanel.timeline.hideLabel"); const baseMapStatusMessage = terria.mainViewer.baseMap && getBaseMapStatusMessage(terria.mainViewer.baseMap, t); return ( //@ts-expect-error - not yet ready to tackle tsfying MenuPanel { viewState.settingsPanelIsVisible = isOpen; })} > {t("settingPanel.mapView")} {Object.entries(MapViewers).map(([key, viewerMode]) => ( selectViewer(key as any, event)} > {t(viewerMode.label)} ))} {!!supportsSide && ( <> {t("settingPanel.terrain.sideLabel")} {Object.values(sides).map((side: any) => ( showTerrainOnSide(side, event)} > {t(side)} ))} {!!supportsDepthTestAgainstTerrain && ( <> {t("settingPanel.terrain.hideUnderground")} )} )} <> {t("settingPanel.baseMap")} {activeMapName} {terria.baseMapsModel.baseMapItems.map((baseMap, i) => ( selectBaseMap(baseMap.item, event)} onMouseEnter={() => mouseEnterBaseMap(baseMap)} onMouseLeave={mouseLeaveBaseMap} onFocus={() => mouseEnterBaseMap(baseMap)} data-test-id={`baseMap-${i}`} data-current-basemap={ baseMap.item === terria.mainViewer.baseMap ? true : undefined } > {baseMap.item === terria.mainViewer.baseMap ? ( ) : null} ))} {baseMapStatusMessage && ( {baseMapStatusMessage} )} <> {t("settingPanel.timeline.title")} { timelineStack.setAlwaysShowTimeline( !timelineStack.alwaysShowingTimeline ); }} > {t("settingPanel.timeline.alwaysShow")} {terria.mainViewer.viewerMode !== ViewerMode.Leaflet && ( <> {t("settingPanel.imageOptimisation")} toggleUseNativeResolution()} > {t("settingPanel.nativeResolutionHeader")} {t("settingPanel.mapQuality")} {t("settingPanel.qualityLabel")} onBaseMaximumScreenSpaceErrorChange(val)} marks={{ 2: "" }} aria-valuetext={qualityLabels} css={` margin: 0 10px; margin-top: 5px; `} /> {t("settingPanel.performanceLabel")} )} ); }); /** * Return name + CRS if the base map specifies one */ function mapDisplayName(baseMap: MappableMixin.Instance): string { const name = (CatalogMemberMixin.isMixedInto(baseMap) ? baseMap.name : undefined) ?? ""; return name; } /** * Save user's base map preference */ function saveBaseMapPreference( terria: Terria, baseMap: MappableMixin.Instance ) { if (baseMap.uniqueId) { terria.setLocalProperty("basemap", baseMap.uniqueId); } } /** * Notify user about switching base map to a viewer compatible base map */ function notifyBaseMapSwitch( terria: Terria, viewer: TerriaViewer, currentBaseMap: MappableMixin.Instance, newBaseMap: MappableMixin.Instance, t: TFunction ) { const viewerMode = viewer.viewerMode; terria.notificationState.addNotificationToQueue({ title: t("settingPanel.baseMapSwitched.title"), message: t("settingPanel.baseMapSwitched.message", { currentBaseMap: mapDisplayName(currentBaseMap), newBaseMap: mapDisplayName(newBaseMap), mapMode: viewer.viewerMode === ViewerMode.Cesium ? "3D" : "2D" }), ignore: () => // auto-dismiss notification if state changes viewer.baseMap !== newBaseMap || viewer.viewerMode !== viewerMode, showAsToast: true, toastVisibleDuration: 10 }); } function notifyViewerModeSwitch( newBaseMap: MappableMixin.Instance, currentViewerMode: ViewerMode, newViewerMode: ViewerMode, terria: Terria, t: TFunction ) { const viewer = terria.mainViewer; terria.notificationState.addNotificationToQueue({ title: t("settingPanel.viewerModeSwitched.title"), message: t("settingPanel.viewerModeSwitched.message", { newBaseMap: mapDisplayName(newBaseMap), currentMode: currentViewerMode === ViewerMode.Cesium ? "3D" : "2D", newMode: newViewerMode === ViewerMode.Cesium ? "3D" : "2D" }), ignore: () => // auto-dismiss notification if state changes viewer.viewerMode !== newViewerMode || viewer.baseMap !== newBaseMap, showAsToast: true, toastVisibleDuration: 15 }); } /** * Returns a status message to show when the given base map is selected. * * Currently this returns a message if the base map requires a specific viewer * mode. */ function getBaseMapStatusMessage( baseMap: MappableMixin.Instance, t: TFunction ): string | undefined { const viewerMode = baseMap.preferredViewerMode && isViewerMode(baseMap.preferredViewerMode) ? baseMap.preferredViewerMode.slice(0, 2).toLocaleUpperCase() : undefined; if (viewerMode) { return t("settingPanel.baseMapStatus.requiresViewerMode", { viewerMode }); } } export const SETTING_PANEL_NAME = "MenuBarMapSettingsButton"; export default SettingPanel; type IFlexGrid = { gap: number; elementsNo: number; }; const FlexGrid = styled(Box).attrs({ flexWrap: true })` gap: ${(props) => props.gap * 5}px; > * { flex: ${(props) => `1 0 ${getCalcWidth(props.elementsNo, props.gap)}`}; max-width: ${(props) => getCalcWidth(props.elementsNo, props.gap)}; } `; const getCalcWidth = (elementsNo: number, gap: number) => `calc(${100 / elementsNo}% - ${gap * 5}px)`; type IButtonProps = { isActive: boolean; }; const SettingsButton = styled(Button)` background-color: ${(props) => props.theme.overlay}; border: 1px solid ${(props) => (props.isActive ? "rgba(255, 255, 255, 0.5)" : "transparent")}; `; const StyledBasemapButton = styled(RawButton)` border-radius: 4px; position: relative; border: 2px solid ${(props) => props.isActive ? props.theme.turquoiseBlue : "rgba(255, 255, 255, 0.5)"}; `; const StyledImage = styled(Box).attrs({ as: "img" })>` border-radius: inherit; `; const BaseMapStatus = styled(Text)` font-size: 11px; padding: ${(p) => p.theme.padding} 0; `;