import dateFormat from "dateformat"; import { runInAction } from "mobx"; import { observer } from "mobx-react"; import { ChangeEvent, 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 ImageryProvider from "terriajs-cesium/Source/Scene/ImageryProvider"; import SplitDirection from "terriajs-cesium/Source/Scene/SplitDirection"; import LatLonHeight from "../../../Core/LatLonHeight"; import filterOutUndefined from "../../../Core/filterOutUndefined"; import isDefined from "../../../Core/isDefined"; import PickedFeatures from "../../../Map/PickedFeatures/PickedFeatures"; 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, { HasTrait } from "../../../Models/Definition/hasTraits"; import updateModelFromJson from "../../../Models/Definition/updateModelFromJson"; import TerriaFeature from "../../../Models/Feature/Feature"; import { getMarkerLocation, removeMarker } from "../../../Models/LocationMarkerUtils"; import Terria from "../../../Models/Terria"; import ViewState from "../../../ReactViewModels/ViewState"; 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, { IDatePickerHandle } from "./DatePicker"; import LocationPicker from "./LocationPicker"; type DiffableItem = DiffableMixin.Instance; interface PropsType { viewState: ViewState; sourceItem: DiffableItem; } const DiffTool: React.FC = observer((props: PropsType) => { const [leftItem, setLeftItem] = useState(); const [rightItem, setRightItem] = useState(); const [userSelectedSourceItem, setUserSelectedSourceItem] = useState(); const viewState = useViewState(); const changeSourceItem = (sourceItem: DiffableItem) => { 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 (
); } return null; }); DiffTool.displayName = "DiffTool"; interface MainPropsType extends PropsType { terria: Terria; leftItem: DiffableItem; rightItem: DiffableItem; changeSourceItem: (newSourceItem: DiffableItem) => void; } const Main: React.FC = 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((): DiffableItem[] => { return terria.workbench.items.filter( (item) => DiffableMixin.isMixedInto(item) && item.canDiffImages ) as DiffableItem[]; }, [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: DiffableItem) => { runInAction(() => { if (hasOpacity(model)) { model.setTrait(CommonStrata.user, "opacity", 0.8); } }); }, []); const hideItem = useCallback((model: DiffableItem) => { runInAction(() => { if (hasOpacity(model)) { model.setTrait(CommonStrata.user, "opacity", 0); } }); }, []); const handleChangeSourceItem = useCallback( (e: ChangeEvent) => { const newSourceItem = diffableItemsInWorkbench.find( (item) => item.uniqueId === e.target.value ); if (newSourceItem) props.changeSourceItem(newSourceItem); }, [diffableItemsInWorkbench, props] ); const handleChangePreviewStyle = useCallback( (e: ChangeEvent) => { 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: ChangeEvent) => { runInAction(() => { diffItem.setTrait(CommonStrata.user, "diffStyleId", e.target.value); }); }, [diffItem] ); const onUserPickingLocation = useCallback( (_pickingLocation: LatLonHeight) => { setIsPickingNewLocation(true); }, [] ); const onUserPickLocation = useCallback( (pickedFeatures: PickedFeatures, pickedLocationValue: LatLonHeight) => { 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 ( { resetTool(); closePanel(); }} >
{isShowingDiff && ( {t("diffTool.differenceResultsTitle")} )} {t("diffTool.instructions.paneDescription")} {t("diffTool.labels.area")}:
{location ? t("diffTool.locationDisplay.locationSelected.title") : t("diffTool.locationDisplay.noLocationSelected.title")} {location ? t("diffTool.locationDisplay.locationSelected.description") : t( "diffTool.locationDisplay.noLocationSelected.description" )}
{t("diffTool.labels.dates")}: {currentLeftDate && ( (A){" "} {dateFormat( JulianDate.toDate(currentLeftDate), "dd/mm/yyyy" )} )} {!currentLeftDate && ( leftDatePickerHandle.current?.open()} > {t("diffTool.instructions.setDateA")} )} {currentRightDate && ( (B){" "} {dateFormat( JulianDate.toDate(currentRightDate), "dd/mm/yyyy" )} )} {!currentRightDate && ( rightDatePickerHandle.current?.open()} > {t("diffTool.instructions.setDateB")} )} {isShowingDiff === false && currentLeftDate && currentRightDate && ( {t("diffTool.instructions.changeDates")} )}
{!isShowingDiff && ( {diffableItemsInWorkbench.map((item) => ( ))} )} {!isShowingDiff && ( {diffItem.styleSelectableDimensions?.[0]?.options?.map( (style) => ( ) )} {previewLegendUrl && ( )} )} {availableDiffStyles.map((style) => ( ))} {isShowingDiff && diffLegendUrl && ( )} {!isShowingDiff && (
{t("diffTool.labels.generateDiffButtonText")} {!isReadyToGenerateDiff && (
{t("diffTool.labels.disabledButtonPrompt")}
)}
)} {isShowingDiff && ( {t("general.back")} )} {!isShowingDiff && ( )} {!isShowingDiff && ReactDOM.createPortal( showItem(leftItem)} /> 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: any) => ( ); const AreaFilterSelection = (props: { location?: LatLonHeight; isPickingNewLocation: boolean; theme?: any; }) => { 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 ( props.theme.md}px) { width: 100%; } `} > {t("diffTool.labels.areaFilterSelection")} {isPickingNewLocation ? ( theme.textLight}; `} > ) : ( {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: any) { return ( (e.currentTarget.style.display = "block")} onError={(e) => (e.currentTarget.style.display = "none")} /> ); }; async function createSplitItem( sourceItem: DiffableItem, splitDirection: SplitDirection ): Promise { 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 as DiffableItem; 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: DiffableItem) { 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: DiffableItem) { const terria = item.terria; terria.overlays.remove(item); if (item.sourceReference && terria.workbench.contains(item) === false) { terria.removeModelReferences(item.sourceReference); } } function doesFeatureBelongToItem( feature: TerriaFeature, item: DiffableItem ): boolean { 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: DiffableItem, location: LatLonHeight, im: ImageryProvider ): Promise { 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: any ): model is HasTrait { return hasTraits(model, ImageryProviderTraits, "opacity"); } export default DiffTool;