terriajs
Version:
Geospatial data visualization platform.
449 lines • 25.9 kB
JavaScript
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