UNPKG

terriajs

Version:

Geospatial data visualization platform.

346 lines (338 loc) 17 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; 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/AnalyticEvents/analyticEvents"; import TerriaError from "../../../Core/TerriaError"; import filterOutUndefined from "../../../Core/filterOutUndefined"; import getDereferencedIfExists from "../../../Core/getDereferencedIfExists"; import getPath from "../../../Core/getPath"; import isDefined from "../../../Core/isDefined"; 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 CameraView from "../../../Models/CameraView"; import SplitItemReference from "../../../Models/Catalog/CatalogReferences/SplitItemReference"; import addUserCatalogMember from "../../../Models/Catalog/addUserCatalogMember"; import CommonStrata from "../../../Models/Definition/CommonStrata"; import hasTraits from "../../../Models/Definition/hasTraits"; import getAncestors from "../../../Models/getAncestors"; 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 { 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}; } } `; const ViewingControls = 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; let zoomToView = item; function vectorToJson(vector) { if (typeof vector?.x === "number" && typeof vector?.y === "number" && typeof vector?.z === "number") { return { x: vector.x, y: vector.y, z: vector.z }; } else { return undefined; } } // camera is likely used more often than lookAt. const theWest = item?.idealZoom?.camera?.west; const theEast = item?.idealZoom?.camera?.east; const theNorth = item?.idealZoom?.camera?.north; const theSouth = item?.idealZoom?.camera?.south; if (isDefined(item.idealZoom?.lookAt?.targetLongitude) && isDefined(item.idealZoom?.lookAt?.targetLatitude) && (item.idealZoom?.lookAt?.range ?? 0) >= 0) { // No value checking here. Improper values can lead to unexpected results. const lookAt = { targetLongitude: item.idealZoom.lookAt.targetLongitude, targetLatitude: item.idealZoom.lookAt.targetLatitude, targetHeight: item.idealZoom.lookAt.targetHeight, heading: item.idealZoom.lookAt.heading, pitch: item.idealZoom.lookAt.pitch, range: item.idealZoom.lookAt.range }; // In the case of 2D viewer, it zooms to rectangle area approximated by the camera view parameters. zoomToView = CameraView.fromJson({ lookAt: lookAt }); } else if (theWest && theEast && theNorth && theSouth) { const thePosition = vectorToJson(item?.idealZoom?.camera?.position); const theDirection = vectorToJson(item?.idealZoom?.camera?.direction); const theUp = vectorToJson(item?.idealZoom?.camera?.up); // No value checking here. Improper values can lead to unexpected results. const camera = { west: theWest, east: theEast, north: theNorth, south: theSouth, position: thePosition, direction: theDirection, up: theUp }; zoomToView = CameraView.fromJson(camera); } else if (item.rectangle?.east !== undefined && item.rectangle?.west !== undefined && item.rectangle.east - item.rectangle.west >= 360) { zoomToView = viewState.terria.mainViewer.homeCamera; console.log("Extent is wider than world so using homeCamera."); } setIsMapZoomingToCatalogItem(true); viewer.zoomTo(zoomToView).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 = 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) => { try { viewingControl.onClick(viewState); } catch (err) { viewState.terria.raiseErrorToUser(TerriaError.from(err)); } }; return (_jsxs("ul", { children: [viewingControls.map((viewingControl) => (_jsx("li", { children: _jsx(ViewingControlMenuButton, { onClick: () => handleOnClick(viewingControl), title: viewingControl.iconTitle, children: _jsxs(BoxViewingControl, { children: [_jsx(StyledIcon, { ...viewingControl.icon }), _jsx("span", { children: viewingControl.name })] }) }) }, viewingControl.id))), canSplit ? (_jsx("li", { children: _jsx(ViewingControlMenuButton, { onClick: splitItem, title: t("workbench.splitItemTitle"), children: _jsxs(BoxViewingControl, { children: [_jsx(StyledIcon, { glyph: Icon.GLYPHS.compare }), _jsx("span", { children: t("workbench.splitItem") })] }) }) }, "workbench.splitItem")) : null, controls.difference && viewState.useSmallScreenInterface === false && DiffableMixin.isMixedInto(item) && !item.isShowingDiff && item.canDiffImages ? (_jsx("li", { children: _jsx(ViewingControlMenuButton, { onClick: openDiffTool, title: t("workbench.diffImageTitle"), children: _jsxs(BoxViewingControl, { children: [_jsx(StyledIcon, { glyph: Icon.GLYPHS.difference }), _jsx("span", { children: t("workbench.diffImage") })] }) }) }, "workbench.diffImage")) : null, controls.exportData && viewState.useSmallScreenInterface === false && ExportableMixin.isMixedInto(item) && item.canExportData ? (_jsx("li", { children: _jsx(ViewingControlMenuButton, { onClick: exportDataClicked, title: t("workbench.exportDataTitle"), children: _jsxs(BoxViewingControl, { children: [_jsx(StyledIcon, { glyph: Icon.GLYPHS.upload }), _jsx("span", { children: t("workbench.exportData") })] }) }) }, "workbench.exportData")) : null, controls.search && viewState.useSmallScreenInterface === false && SearchableItemMixin.isMixedInto(item) && item.canSearch ? (_jsx("li", { children: _jsx(ViewingControlMenuButton, { onClick: searchItem, title: t("workbench.searchItemTitle"), children: _jsxs(BoxViewingControl, { children: [_jsx(StyledIcon, { glyph: Icon.GLYPHS.search }), _jsx("span", { children: t("workbench.searchItem") })] }) }) }, "workbench.searchItem")) : null, _jsx("li", { children: _jsx(ViewingControlMenuButton, { onClick: removeFromMap, title: t("workbench.removeFromMapTitle"), children: _jsxs(BoxViewingControl, { children: [_jsx(StyledIcon, { glyph: Icon.GLYPHS.cancel }), _jsx("span", { children: t("workbench.removeFromMap") })] }) }) }, "workbench.removeFromMap")] })); }; return (_jsxs(Box, { children: [_jsxs(Ul, { css: ` list-style: none; padding-left: 0; margin: 0; width: 100%; position: relative; display: flex; justify-content: space-between; li { display: block; float: left; box-sizing: border-box; } & > button:last-child { margin-right: 0; } `, gap: 2, children: [_jsx(WorkbenchButton, { onClick: zoomTo, title: t("workbench.zoomToTitle"), disabled: !controls.idealZoom || // disabled if the item cannot be zoomed to or if a zoom is already in progress (MappableMixin.isMixedInto(item) && item.disableZoomTo) || isMapZoomingToCatalogItem === true, iconElement: () => isMapZoomingToCatalogItem ? (_jsx(AnimatedSpinnerIcon, {})) : (_jsx(Icon, { glyph: Icon.GLYPHS.search })), children: t("workbench.zoomTo") }), _jsx(WorkbenchButton, { onClick: previewItem, title: t("workbench.previewItemTitle"), iconElement: () => _jsx(Icon, { glyph: Icon.GLYPHS.about }), disabled: !controls.aboutData || (CatalogMemberMixin.isMixedInto(item) && item.disableAboutData), children: t("workbench.previewItem") }), _jsx(WorkbenchButton, { css: "flex-grow:0;", onClick: (e) => { e.stopPropagation(); if (isMenuOpen) { setIsOpen(false); } else { setIsOpen(true); } }, title: t("workbench.showMoreActionsTitle"), iconOnly: true, iconElement: () => _jsx(Icon, { glyph: Icon.GLYPHS.menuDotted }) })] }), isMenuOpen && (_jsx(Box, { css: ` position: absolute; z-index: 100; right: 0; top: 0; top: 32px; top: 42px; padding: 0; margin: 0; ul { list-style: none; } `, children: renderViewingControlsMenu() }))] })); }); ViewingControls.displayName = "ViewingControls"; export default ViewingControls; //# sourceMappingURL=ViewingControls.js.map