UNPKG

terriajs

Version:

Geospatial data visualization platform.

449 lines 25.9 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import dateFormat from "dateformat"; import { runInAction } from "mobx"; import { observer } from "mobx-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import ReactDOM from "react-dom"; import { useTranslation } from "react-i18next"; import styled, { useTheme } from "styled-components"; import Cartographic from "terriajs-cesium/Source/Core/Cartographic"; import JulianDate from "terriajs-cesium/Source/Core/JulianDate"; import CesiumMath from "terriajs-cesium/Source/Core/Math"; import createGuid from "terriajs-cesium/Source/Core/createGuid"; import SplitDirection from "terriajs-cesium/Source/Scene/SplitDirection"; import filterOutUndefined from "../../../Core/filterOutUndefined"; import isDefined from "../../../Core/isDefined"; import prettifyCoordinates from "../../../Map/Vector/prettifyCoordinates"; import DiffableMixin from "../../../ModelMixins/DiffableMixin"; import MappableMixin, { ImageryParts } from "../../../ModelMixins/MappableMixin"; import SplitItemReference from "../../../Models/Catalog/CatalogReferences/SplitItemReference"; import CommonStrata from "../../../Models/Definition/CommonStrata"; import hasTraits from "../../../Models/Definition/hasTraits"; import updateModelFromJson from "../../../Models/Definition/updateModelFromJson"; import { getMarkerLocation, removeMarker } from "../../../Models/LocationMarkerUtils"; import Box, { BoxSpan } from "../../../Styled/Box"; import Button, { RawButton } from "../../../Styled/Button"; import { GLYPHS, StyledIcon } from "../../../Styled/Icon"; import Select from "../../../Styled/Select"; import Spacing from "../../../Styled/Spacing"; import Text, { TextSpan } from "../../../Styled/Text"; import ImageryProviderTraits from "../../../Traits/TraitsClasses/ImageryProviderTraits"; import { useViewState } from "../../Context"; import { parseCustomMarkdownToReactWithOptions } from "../../Custom/parseCustomMarkdownToReact"; import Loader from "../../Loader"; import WorkflowPanel from "../../Workflow/WorkflowPanel"; import DatePicker from "./DatePicker"; import LocationPicker from "./LocationPicker"; const DiffTool = observer((props) => { const [leftItem, setLeftItem] = useState(); const [rightItem, setRightItem] = useState(); const [userSelectedSourceItem, setUserSelectedSourceItem] = useState(); const viewState = useViewState(); const changeSourceItem = (sourceItem) => { setUserSelectedSourceItem(sourceItem); }; const sourceItem = useMemo(() => userSelectedSourceItem || props.sourceItem, [props.sourceItem, userSelectedSourceItem]); useEffect(() => { const terria = viewState.terria; const originalSettings = { showSplitter: terria.showSplitter, isMapFullScreen: viewState.isMapFullScreen }; runInAction(() => { terria.showSplitter = true; sourceItem.setTrait(CommonStrata.user, "show", false); terria.elements.set("timeline", { visible: false }); }); const itemsPromise = Promise.all([ createSplitItem(sourceItem, SplitDirection.LEFT), createSplitItem(sourceItem, SplitDirection.RIGHT) ]); itemsPromise .then(([lItem, rItem]) => { setLeftItem(lItem); setRightItem(rItem); }) .catch(); return () => { runInAction(() => { terria.showSplitter = originalSettings.showSplitter; viewState.setIsMapFullScreen(originalSettings.isMapFullScreen); sourceItem.setTrait(CommonStrata.user, "show", true); terria.elements.set("timeline", { visible: true }); }); itemsPromise.then(([lItem, rItem]) => { if (lItem) removeSplitItem(lItem); if (rItem) removeSplitItem(rItem); }); }; }, [sourceItem, viewState]); if (leftItem && rightItem) { return (_jsx(Main, { ...props, terria: props.viewState.terria, sourceItem: sourceItem, changeSourceItem: changeSourceItem, leftItem: leftItem, rightItem: rightItem })); } return null; }); DiffTool.displayName = "DiffTool"; const Main = observer((props) => { const { terria, viewState, sourceItem, leftItem, rightItem } = props; const { t } = useTranslation(); const theme = useTheme(); const [location, setLocation] = useState(); const [, setLocationPickError] = useState(false); const [isPickingNewLocation, setIsPickingNewLocation] = useState(false); const leftDatePickerHandle = useRef(null); const rightDatePickerHandle = useRef(null); const diffItem = useMemo(() => { return leftItem; }, [leftItem]); const currentLeftDate = useMemo(() => { return leftItem.currentDiscreteJulianDate; }, [leftItem.currentDiscreteJulianDate]); const currentRightDate = useMemo(() => { return rightItem.currentDiscreteJulianDate; }, [rightItem.currentDiscreteJulianDate]); const diffItemName = useMemo(() => { const name = sourceItem.name || ""; const format = "yyyy/mm/dd"; if (!currentLeftDate || !currentRightDate) { return name; } else { const d1 = dateFormat(JulianDate.toDate(currentLeftDate), format); const d2 = dateFormat(JulianDate.toDate(currentRightDate), format); return `${name} - difference for dates ${d1}, ${d2}`; } }, [currentLeftDate, currentRightDate, sourceItem.name]); const diffableItemsInWorkbench = useMemo(() => { return terria.workbench.items.filter((item) => DiffableMixin.isMixedInto(item) && item.canDiffImages); }, [terria.workbench.items]); const previewStyle = useMemo(() => { return diffItem.styleSelectableDimensions?.[0]?.selectedId; }, [diffItem.styleSelectableDimensions]); const currentDiffStyle = useMemo(() => { return diffItem.diffStyleId; }, [diffItem.diffStyleId]); const availableDiffStyles = useMemo(() => { return filterOutUndefined(diffItem.availableDiffStyles.map((diffStyleId) => diffItem.styleSelectableDimensions?.[0]?.options?.find((style) => style.id === diffStyleId))); }, [diffItem.availableDiffStyles, diffItem.styleSelectableDimensions]); const diffLegendUrl = useMemo(() => { return (currentDiffStyle && currentLeftDate && currentRightDate && diffItem.getLegendUrlForStyle(currentDiffStyle, currentLeftDate, currentRightDate)); }, [currentDiffStyle, currentLeftDate, currentRightDate, diffItem]); const previewLegendUrl = useMemo(() => { return previewStyle && diffItem.getLegendUrlForStyle(previewStyle); }, [previewStyle, diffItem]); const showItem = useCallback((model) => { runInAction(() => { if (hasOpacity(model)) { model.setTrait(CommonStrata.user, "opacity", 0.8); } }); }, []); const hideItem = useCallback((model) => { runInAction(() => { if (hasOpacity(model)) { model.setTrait(CommonStrata.user, "opacity", 0); } }); }, []); const handleChangeSourceItem = useCallback((e) => { const newSourceItem = diffableItemsInWorkbench.find((item) => item.uniqueId === e.target.value); if (newSourceItem) props.changeSourceItem(newSourceItem); }, [diffableItemsInWorkbench, props]); const handleChangePreviewStyle = useCallback((e) => { const styleId = e.target.value; runInAction(() => { leftItem.styleSelectableDimensions?.[0]?.setDimensionValue(CommonStrata.user, styleId); rightItem.styleSelectableDimensions?.[0]?.setDimensionValue(CommonStrata.user, styleId); }); }, [leftItem.styleSelectableDimensions, rightItem.styleSelectableDimensions]); const handleChangeDiffStyle = useCallback((e) => { runInAction(() => { diffItem.setTrait(CommonStrata.user, "diffStyleId", e.target.value); }); }, [diffItem]); const onUserPickingLocation = useCallback((_pickingLocation) => { setIsPickingNewLocation(true); }, []); const onUserPickLocation = useCallback((pickedFeatures, pickedLocationValue) => { const feature = pickedFeatures.features.find((f) => doesFeatureBelongToItem(f, leftItem) || doesFeatureBelongToItem(f, rightItem)); runInAction(() => { if (feature) { leftItem.setTimeFilterFeature(feature, pickedFeatures.providerCoords); rightItem.setTimeFilterFeature(feature, pickedFeatures.providerCoords); setLocation(pickedLocationValue); setLocationPickError(false); } else { setLocationPickError(true); } setIsPickingNewLocation(false); }); }, [leftItem, rightItem]); const unsetDates = useCallback(() => { runInAction(() => { leftItem.setTrait(CommonStrata.user, "currentTime", null); rightItem.setTrait(CommonStrata.user, "currentTime", null); }); hideItem(leftItem); hideItem(rightItem); }, [leftItem, rightItem, hideItem]); const generateDiff = useCallback(() => { if (currentLeftDate === undefined || currentRightDate === undefined || currentDiffStyle === undefined) { return; } runInAction(() => { terria.overlays.remove(leftItem); terria.overlays.remove(rightItem); terria.workbench.add(diffItem); diffItem.setTrait(CommonStrata.user, "name", diffItemName); diffItem.showDiffImage(currentLeftDate, currentRightDate, currentDiffStyle); const diffItemProperties = diffItem.diffItemProperties; if (diffItemProperties) { updateModelFromJson(diffItem, CommonStrata.user, diffItemProperties); } terria.showSplitter = false; }); }, [ currentDiffStyle, currentLeftDate, currentRightDate, diffItem, diffItemName, leftItem, rightItem, terria ]); const resetTool = useCallback(() => { runInAction(() => { diffItem.clearDiffImage(); setDefaultDiffStyle(diffItem); terria.overlays.add(leftItem); terria.overlays.add(rightItem); terria.workbench.remove(diffItem); terria.showSplitter = true; leftItem.setTrait(CommonStrata.user, "splitDirection", SplitDirection.LEFT); rightItem.setTrait(CommonStrata.user, "splitDirection", SplitDirection.RIGHT); }); }, [diffItem, leftItem, rightItem, terria]); const setLocationFromActiveSearch = useCallback(async () => { const markerLocation = getMarkerLocation(terria); if (markerLocation && MappableMixin.isMixedInto(sourceItem)) { const part = sourceItem.mapItems.find((p) => ImageryParts.is(p)); const imageryProvider = part && ImageryParts.is(part) && part.imageryProvider; if (imageryProvider) { const promises = [ setTimeFilterFromLocation(leftItem, markerLocation, imageryProvider), setTimeFilterFromLocation(rightItem, markerLocation, imageryProvider) ]; const someSuccessful = (await Promise.all(promises)).some((ok) => ok); if (someSuccessful) { runInAction(() => setLocation(markerLocation)); } else { // If we cannot resolve imagery at the marker location, remove it runInAction(() => removeMarker(terria)); } } } }, [leftItem, rightItem, sourceItem, terria]); useEffect(() => { const { latitude, longitude, height } = diffItem.timeFilterCoordinates; if (latitude !== undefined && longitude !== undefined) { setLocation({ latitude, longitude, height }); // Assuming removeMarker is an action or handles its own MobX transactions removeMarker(terria); } else { setLocationFromActiveSearch(); } }, [diffItem.timeFilterCoordinates, setLocationFromActiveSearch, terria]); const closePanel = useCallback(() => { viewState.closeTool(); }, [viewState]); const isShowingDiff = diffItem.isShowingDiff; const datesSelected = currentLeftDate && currentRightDate; const isReadyToGenerateDiff = location && datesSelected && currentDiffStyle !== undefined; return (_jsx(WorkflowPanel, { viewState: viewState, title: t("diffTool.title"), icon: GLYPHS.difference, onClose: () => { resetTool(); closePanel(); }, children: _jsxs("div", { css: ` display: flex; flex-direction: column; height: 100%; padding: 15px; gap: 20px; `, children: [isShowingDiff && (_jsx(Text, { medium: true, textLight: true, children: t("diffTool.differenceResultsTitle") })), _jsx(Text, { textLight: true, children: t("diffTool.instructions.paneDescription") }), _jsx(Group, { children: _jsxs(LocationAndDatesDisplayBox, { children: [_jsxs(Box, { children: [_jsxs(Text, { medium: true, children: [t("diffTool.labels.area"), ":"] }), _jsxs("div", { children: [_jsx(Text, { bold: true, textLight: true, children: location ? t("diffTool.locationDisplay.locationSelected.title") : t("diffTool.locationDisplay.noLocationSelected.title") }), _jsx(Text, { light: true, textLight: true, small: true, children: location ? t("diffTool.locationDisplay.locationSelected.description") : t("diffTool.locationDisplay.noLocationSelected.description") })] })] }), _jsxs(Box, { children: [_jsxs(Text, { medium: true, children: [t("diffTool.labels.dates"), ":"] }), _jsxs(Box, { column: true, alignItemsFlexStart: true, children: [currentLeftDate && (_jsxs(Text, { large: true, children: ["(A)", " ", dateFormat(JulianDate.toDate(currentLeftDate), "dd/mm/yyyy")] })), !currentLeftDate && (_jsx(RawButton, { onClick: () => leftDatePickerHandle.current?.open(), children: _jsx(TextSpan, { isLink: true, small: true, bold: true, children: t("diffTool.instructions.setDateA") }) })), currentRightDate && (_jsxs(Text, { large: true, children: ["(B)", " ", dateFormat(JulianDate.toDate(currentRightDate), "dd/mm/yyyy")] })), !currentRightDate && (_jsx(RawButton, { onClick: () => rightDatePickerHandle.current?.open(), children: _jsx(TextSpan, { isLink: true, small: true, bold: true, children: t("diffTool.instructions.setDateB") }) })), isShowingDiff === false && currentLeftDate && currentRightDate && (_jsx(RawButton, { onClick: unsetDates, children: _jsx(TextSpan, { isLink: true, small: true, children: t("diffTool.instructions.changeDates") }) }))] })] })] }) }), !isShowingDiff && (_jsx(Group, { children: _jsxs(Selector, { viewState: viewState, value: sourceItem.uniqueId, onChange: handleChangeSourceItem, label: t("diffTool.labels.sourceDataset"), children: [_jsx("option", { disabled: true, children: "Select source item" }), diffableItemsInWorkbench.map((item) => (_jsx("option", { value: item.uniqueId, children: item.name }, item.uniqueId)))] }) })), !isShowingDiff && (_jsxs(Group, { children: [_jsxs(Selector, { viewState: viewState, spacingBottom: true, value: previewStyle, onChange: handleChangePreviewStyle, label: t("diffTool.labels.previewStyle"), children: [_jsx("option", { disabled: true, value: "", children: t("diffTool.choosePreview") }), diffItem.styleSelectableDimensions?.[0]?.options?.map((style) => (_jsx("option", { value: style.id, children: style.name }, style.id)))] }), previewLegendUrl && (_jsx(LegendImage, { width: "100%", src: previewLegendUrl }))] })), _jsxs(Group, { children: [_jsxs(Selector, { viewState: viewState, value: currentDiffStyle || "", onChange: handleChangeDiffStyle, label: t("diffTool.labels.differenceOutput"), children: [_jsx("option", { disabled: true, value: "", children: t("diffTool.chooseDifference") }), availableDiffStyles.map((style) => (_jsx("option", { value: style.id, children: style.name }, style.id)))] }), isShowingDiff && diffLegendUrl && (_jsx(LegendImage, { width: "100%", src: diffLegendUrl }))] }), !isShowingDiff && (_jsxs("div", { children: [_jsx(GenerateButton, { onClick: generateDiff, disabled: !isReadyToGenerateDiff, "aria-describedby": "TJSDifferenceDisabledButtonPrompt", children: _jsx(TextSpan, { large: true, children: t("diffTool.labels.generateDiffButtonText") }) }), !isReadyToGenerateDiff && (_jsxs("div", { css: ` display: flex; flex-direction: row; padding: 5px; `, children: [_jsx("div", { css: ` margin-right: 10px; `, children: _jsx(StyledIcon, { fillColor: "#ccc", styledWidth: "16px", styledHeight: "16px", glyph: GLYPHS.info }) }), _jsx(Text, { small: true, light: true, textLight: true, id: "TJSDifferenceDisabledButtonPrompt", children: t("diffTool.labels.disabledButtonPrompt") })] }))] })), isShowingDiff && (_jsxs(Box, { centered: true, left: true, children: [_jsx(BackButton, { css: ` color: ${theme.textLight}; border-color: ${theme.textLight}; `, transparentBg: true, onClick: resetTool, children: _jsxs(BoxSpan, { centered: true, children: [_jsx(StyledIcon, { css: "transform:rotate(90deg);", light: true, styledWidth: "16px", glyph: GLYPHS.arrowDown }), _jsx(TextSpan, { medium: true, children: t("general.back") })] }) }), _jsx(Button, { primary: true, onClick: closePanel, css: ` flex-grow: 1; margin-left: 10px; `, children: _jsx(TextSpan, { medium: true, children: t("diffTool.labels.saveToWorkbench") }) })] })), !isShowingDiff && (_jsx(LocationPicker, { terria: terria, location: location, onPicking: onUserPickingLocation, onPicked: onUserPickLocation })), !isShowingDiff && ReactDOM.createPortal(_jsxs(Box, { centered: true, fullWidth: true, flexWrap: true, backgroundColor: theme.dark, children: [_jsx(DatePicker, { ref: leftDatePickerHandle, heading: t("diffTool.labels.dateComparisonA"), item: leftItem, onDateSet: () => showItem(leftItem) }), _jsx(AreaFilterSelection, { location: location, isPickingNewLocation: isPickingNewLocation }), _jsx(DatePicker, { ref: rightDatePickerHandle, heading: t("diffTool.labels.dateComparisonB"), item: rightItem, onDateSet: () => showItem(rightItem) })] }), document.getElementById("TJS-BottomDockLastPortal"))] }) })); }); Main.displayName = "Main"; const BackButton = styled(Button).attrs({ secondary: true }) ``; const GenerateButton = styled(Button).attrs({ primary: true, fullWidth: true }) ``; const Selector = (props) => (_jsx(Box, { fullWidth: true, column: true, children: _jsxs("label", { children: [_jsx(Text, { textLight: true, css: "p {margin: 0;}", children: parseCustomMarkdownToReactWithOptions(`${props.label}:`, { injectTermsAsTooltips: true, tooltipTerms: props.viewState.terria.configParameters.helpContentTerms }) }), _jsx(Spacing, { bottom: 1 }), _jsx(Select, { ...props, children: props.children }), props.spacingBottom && _jsx(Spacing, { bottom: 2 })] }) })); const AreaFilterSelection = (props) => { const { location, isPickingNewLocation } = props; const { t } = useTranslation(); let locationText = "-"; if (location) { const { longitude, latitude } = prettifyCoordinates(location.longitude, location.latitude, { digits: 2 }); locationText = `${longitude} ${latitude}`; } return (_jsxs(Box, { column: true, centered: true, styledMinWidth: "230px", css: ` @media (max-width: ${(props) => props.theme.md}px) { width: 100%; } `, children: [_jsxs(Box, { centered: true, children: [_jsx(StyledIcon, { light: true, styledWidth: "16px", glyph: GLYPHS.location2 }), _jsx(Spacing, { right: 2 }), _jsx(Text, { textLight: true, extraLarge: true, children: t("diffTool.labels.areaFilterSelection") })] }), _jsx(Spacing, { bottom: 3 }), _jsx(Box, { styledMinHeight: "40px", children: isPickingNewLocation ? (_jsx(Text, { textLight: true, extraExtraLarge: true, bold: true, // Using legacy Loader.jsx means we override at a higher level to inherit // this fills tyle css: ` fill: ${({ theme }) => theme.textLight}; `, children: _jsx(Loader, { light: true, message: `Querying ${location ? "new" : ""} position...` }) })) : (_jsx(Text, { textLight: true, bold: true, heading: true, textAlignCenter: true, children: locationText })) })] })); }; const Group = styled.div ` background-color: ${(p) => p.theme.darkWithOverlay}; padding: 15px; border-radius: 5px; `; const LocationAndDatesDisplayBox = styled(Box).attrs({ column: true }) ` color: ${(p) => p.theme.textLight}; padding: 15px; > ${Box}:first-child { margin-bottom: 13px; } > div > div:first-child { /* The labels */ margin-right: 5px; min-width: 50px; } `; const LegendImage = function (props) { return (_jsx("img", { ...props, // Show the legend only if it loads successfully, so we start out hidden style: { display: "none", marginTop: "10px" }, onLoad: (e) => (e.currentTarget.style.display = "block"), onError: (e) => (e.currentTarget.style.display = "none") })); }; async function createSplitItem(sourceItem, splitDirection) { const terria = sourceItem.terria; const ref = new SplitItemReference(createGuid(), terria); ref.setTrait(CommonStrata.user, "splitSourceItemId", sourceItem.uniqueId); terria.addModel(ref); await ref.loadReference(); return runInAction(() => { if (ref.target === undefined) { throw Error("failed to split item"); } const newItem = ref.target; newItem.setTrait(CommonStrata.user, "show", true); newItem.setTrait(CommonStrata.user, "splitDirection", splitDirection); newItem.setTrait(CommonStrata.user, "currentTime", null); newItem.setTrait(CommonStrata.user, "initialTimeSource", "none"); if (hasOpacity(newItem)) { // We want to show the item on the map only after date selection. At the // same time we cannot set `show` to false because if we // do so, date picking which relies on feature picking, will not work. So // we simply set the opacity of the item to 0. newItem.setTrait(CommonStrata.user, "opacity", 0); } // Override feature info template as the parent featureInfoTemplate might // not be relevant for the difference item. This has to be done in the user // stratum to override template set in definition stratum. updateModelFromJson(newItem, CommonStrata.user, { featureInfoTemplate: { template: "" } }); setDefaultDiffStyle(newItem); // Set the default style to true color style if it exists const trueColor = newItem.styleSelectableDimensions?.[0]?.options?.find((style) => isDefined(style.name) && style.name.search(/true/i) >= 0); if (trueColor?.id) { newItem.styleSelectableDimensions?.[0]?.setDimensionValue(CommonStrata.user, trueColor.id); } terria.overlays.add(newItem); return newItem; }); } /** * If the item has only one available diff style, auto-select it */ function setDefaultDiffStyle(item) { if (item.diffStyleId !== undefined) { return; } const availableStyles = filterOutUndefined(item.availableDiffStyles.map((diffStyleId) => item.styleSelectableDimensions?.[0]?.options?.find((style) => style.id === diffStyleId))); if (availableStyles.length === 1) { item.setTrait(CommonStrata.user, "diffStyleId", availableStyles[0].id); } } function removeSplitItem(item) { const terria = item.terria; terria.overlays.remove(item); if (item.sourceReference && terria.workbench.contains(item) === false) { terria.removeModelReferences(item.sourceReference); } } function doesFeatureBelongToItem(feature, item) { if (!MappableMixin.isMixedInto(item)) return false; const imageryProvider = feature.imageryLayer?.imageryProvider; if (imageryProvider === undefined) return false; return (item.mapItems.find((m) => ImageryParts.is(m) && m.imageryProvider === imageryProvider) !== undefined); } function setTimeFilterFromLocation(item, location, im) { const carto = new Cartographic(CesiumMath.toRadians(location.longitude), CesiumMath.toRadians(location.latitude)); // We just need to set this to a high enough level supported by the service const level = 30; const tile = im.tilingScheme.positionToTileXY(carto, level); return item.setTimeFilterFromLocation({ position: { latitude: location.latitude, longitude: location.longitude, height: location.height }, tileCoords: { x: tile.x, y: tile.y, level } }); } function hasOpacity(model) { return hasTraits(model, ImageryProviderTraits, "opacity"); } export default DiffTool; //# sourceMappingURL=DiffTool.js.map