import { sortBy, uniqBy } from "lodash-es"; import { runInAction } from "mobx"; import { observer } from "mobx-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import styled from "styled-components"; import createGuid from "terriajs-cesium/Source/Core/createGuid"; import defined from "terriajs-cesium/Source/Core/defined"; import SplitDirection from "terriajs-cesium/Source/Scene/SplitDirection"; import { Category, DataSourceAction } from "../../../Core/Analytics/analyticEvents"; import TerriaError from "../../../Core/TerriaError"; import filterOutUndefined from "../../../Core/filterOutUndefined"; import getDereferencedIfExists from "../../../Core/getDereferencedIfExists"; import getPath from "../../../Core/getPath"; import CatalogMemberMixin, { getName } from "../../../ModelMixins/CatalogMemberMixin"; import DiffableMixin from "../../../ModelMixins/DiffableMixin"; import ExportableMixin from "../../../ModelMixins/ExportableMixin"; import MappableMixin from "../../../ModelMixins/MappableMixin"; import SearchableItemMixin from "../../../ModelMixins/SearchableItemMixin"; import TimeVarying from "../../../ModelMixins/TimeVarying"; import SplitItemReference from "../../../Models/Catalog/CatalogReferences/SplitItemReference"; import addUserCatalogMember from "../../../Models/Catalog/addUserCatalogMember"; import CommonStrata from "../../../Models/Definition/CommonStrata"; import { BaseModel } from "../../../Models/Definition/Model"; import hasTraits from "../../../Models/Definition/hasTraits"; import { ViewingControl } from "../../../Models/ViewingControls"; import getAncestors from "../../../Models/getAncestors"; import ViewState from "../../../ReactViewModels/ViewState"; import AnimatedSpinnerIcon from "../../../Styled/AnimatedSpinnerIcon"; import Box from "../../../Styled/Box"; import { RawButton } from "../../../Styled/Button"; import Icon, { StyledIcon } from "../../../Styled/Icon"; import Ul from "../../../Styled/List"; import SplitterTraits from "../../../Traits/TraitsClasses/SplitterTraits"; import { exportData } from "../../Preview/ExportData"; import LazyItemSearchTool from "../../Tools/ItemSearchTool/LazyItemSearchTool"; import WorkbenchButton from "../WorkbenchButton"; import { WorkbenchControls, enableAllControls, isControlEnabled } from "./WorkbenchControls"; const BoxViewingControl = styled(Box).attrs({ centered: true, left: true, justifySpaceBetween: true })``; const ViewingControlMenuButton = styled(RawButton).attrs({ // primaryHover: true })` color: ${(props) => props.theme.textDarker}; background-color: ${(props) => props.theme.textLight}; ${StyledIcon} { width: 35px; } svg { fill: ${(props) => props.theme.textDarker}; width: 18px; height: 18px; } & > span { // position: absolute; // left: 37px; } border-radius: 0; width: 124px; // ensure we support long strings min-height: 32px; display: block; &:hover, &:focus { color: ${(props) => props.theme.textLight}; background-color: ${(props) => props.theme.colorPrimary}; svg { fill: ${(props) => props.theme.textLight}; } } `; interface PropsType { viewState: ViewState; item: BaseModel; controls?: WorkbenchControls; } const ViewingControls: React.FC = observer((props) => { const { viewState, item, controls = enableAllControls } = props; const { t } = useTranslation(); const [isMenuOpen, setIsOpen] = useState(false); const [isMapZoomingToCatalogItem, setIsMapZoomingToCatalogItem] = useState(false); useEffect(() => { const hideMenu = () => { setIsOpen(false); }; window.addEventListener("click", hideMenu); return () => window.removeEventListener("click", hideMenu); }, [viewState]); const removeFromMap = useCallback(() => { const terria = viewState.terria; terria.workbench.remove(item); terria.removeSelectedFeaturesForModel(item); if (TimeVarying.is(item)) viewState.terria.timelineStack.remove(item); viewState.terria.analytics.logEvent( Category.dataSource, DataSourceAction.removeFromWorkbench, getPath(item) ); }, [item, viewState]); const zoomTo = useCallback(() => { const viewer = viewState.terria.currentViewer; if (!MappableMixin.isMixedInto(item)) return; setIsMapZoomingToCatalogItem(true); viewer.zoomTo(item).finally(() => { setIsMapZoomingToCatalogItem(false); }); }, [item, viewState]); const splitItem = useCallback(() => { const terria = item.terria; const splitRef = new SplitItemReference(createGuid(), terria); runInAction(async () => { if (!hasTraits(item, SplitterTraits, "splitDirection")) return; if (item.splitDirection === SplitDirection.NONE) { item.setTrait( CommonStrata.user, "splitDirection", SplitDirection.RIGHT ); } splitRef.setTrait(CommonStrata.user, "splitSourceItemId", item.uniqueId); terria.addModel(splitRef); terria.showSplitter = true; await splitRef.loadReference(); runInAction(() => { const target = splitRef.target; if (target) { target.setTrait( CommonStrata.user, "name", t("splitterTool.workbench.copyName", { name: getName(item) }) ); // Set a direction opposite to the original item target.setTrait( CommonStrata.user, "splitDirection", item.splitDirection === SplitDirection.LEFT ? SplitDirection.RIGHT : SplitDirection.LEFT ); } }); // Add it to terria.catalog, which is required so the new item can be shared. addUserCatalogMember(terria, splitRef, { open: false }); }); }, [item, t]); const openDiffTool = useCallback(() => { viewState.openTool({ toolName: "Difference", getToolComponent: () => import("../../Tools/DiffTool/DiffTool").then((m) => m.default), params: { sourceItem: item } }); }, [item, viewState]); const searchItem = useCallback(() => { runInAction(() => { if (!SearchableItemMixin.isMixedInto(item)) return; let itemSearchProvider; try { itemSearchProvider = item.createItemSearchProvider(); } catch (error) { viewState.terria.raiseErrorToUser(error); return; } viewState.openTool({ toolName: "Search Item", getToolComponent: () => LazyItemSearchTool, params: { item, itemSearchProvider, viewState } }); }); }, [item, viewState]); const previewItem = useCallback(async () => { // Open up all the parents (doesn't matter that this sets it to enabled as well because it already is). getAncestors(item) .map((item) => getDereferencedIfExists(item)) .forEach((group) => { runInAction(() => { group.setTrait(CommonStrata.user, "isOpen", true); }); }); viewState .viewCatalogMember(item) .then((result) => result.raiseError(viewState.terria)); }, [item, viewState]); const exportDataClicked = useCallback(() => { if (!ExportableMixin.isMixedInto(item)) return; exportData(item).catch((e) => { item.terria.raiseErrorToUser(e); }); }, [item]); const viewingControls = useMemo(() => { if (!CatalogMemberMixin.isMixedInto(item)) { return []; } // Global viewing controls (usually defined by plugins). const globalViewingControls = filterOutUndefined( viewState.globalViewingControlOptions.map( (generateViewingControlForItem) => { try { return generateViewingControlForItem(item); } catch (err) { TerriaError.from(err).log(); return undefined; } } ) ); // Item specific viewing controls const itemViewingControls: ViewingControl[] = item.viewingControls; // Collate list, unique by id and sorted by name return sortBy( uniqBy([...itemViewingControls, ...globalViewingControls], "id"), "name" ).filter(({ id }) => { // Exclude disabled controls return isControlEnabled(controls, id); }); }, [item, controls, viewState.globalViewingControlOptions]); const renderViewingControlsMenu = () => { const canSplit = controls.compare && !item.terria.configParameters.disableSplitter && hasTraits(item, SplitterTraits, "splitDirection") && hasTraits(item, SplitterTraits, "disableSplitter") && !item.disableSplitter && defined(item.splitDirection) && item.terria.currentViewer.canShowSplitter; const handleOnClick = (viewingControl: ViewingControl) => { try { viewingControl.onClick(viewState); } catch (err) { viewState.terria.raiseErrorToUser(TerriaError.from(err)); } }; return ( ); }; return ( {isMenuOpen && ( {renderViewingControlsMenu()} )} ); }); ViewingControls.displayName = "ViewingControls"; export default ViewingControls;