terriajs
Version:
Geospatial data visualization platform.
358 lines • 19.1 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { action, computed, makeObservable, observable, runInAction, toJS } from "mobx";
import { observer } from "mobx-react";
import { createRef, Component } from "react";
import Sortable from "react-anything-sortable";
import { Trans, useTranslation, withTranslation } from "react-i18next";
import styled, { withTheme } from "styled-components";
import combine from "terriajs-cesium/Source/Core/combine";
import createGuid from "terriajs-cesium/Source/Core/createGuid";
import dataStoriesImg from "../../../wwwroot/images/data-stories-getting-started.jpg";
import { Category, StoryAction } from "../../Core/AnalyticEvents/analyticEvents";
import triggerResize from "../../Core/triggerResize";
import Box from "../../Styled/Box";
import Button, { RawButton } from "../../Styled/Button";
import Icon, { StyledIcon } from "../../Styled/Icon";
import Spacing from "../../Styled/Spacing";
import Text, { TextSpan } from "../../Styled/Text";
import { withViewState } from "../Context";
import measureElement from "../HOCs/measureElement";
import VideoGuide from "../Map/Panels/HelpPanel/VideoGuide";
import { getShareData } from "../Map/Panels/SharePanel/BuildShareLink";
import SharePanel from "../Map/Panels/SharePanel/SharePanel";
import Story from "./Story";
import StoryEditor from "./StoryEditor";
import Styles from "./story-builder.scss";
const STORY_VIDEO = "storyVideo";
let StoryBuilder = class StoryBuilder extends Component {
storiesWrapperRef = createRef();
refToMeasure;
clearRecaptureSuccessTimeout;
constructor(props) {
super(props);
makeObservable(this);
this.state = {
editingMode: false,
currentStory: undefined,
recaptureSuccessId: undefined,
clearRecaptureSuccessTimeout: undefined,
showVideoGuide: false,
isRemoving: false,
isSharing: false,
storyToRemove: undefined,
storyRemoveIndex: undefined,
storyWithOpenMenuId: undefined
};
}
removeStory = (index, story) => {
this.setState({
isSharing: false,
isRemoving: true,
storyToRemove: story,
storyRemoveIndex: index
});
};
removeAction() {
if (this.state.storyToRemove && this.state.storyRemoveIndex !== undefined) {
this.props.viewState.terria.stories =
this.props.viewState.terria.stories.filter((st) => st.id !== this.state.storyToRemove.id);
if (this.state.storyRemoveIndex < this.props.viewState.currentStoryId) {
this.props.viewState.currentStoryId -= 1;
}
}
else {
this.removeAllStories();
}
this.setState({
storyToRemove: undefined,
storyRemoveIndex: undefined
});
}
toggleRemoveDialog = () => {
this.setState({
isSharing: false,
isRemoving: !this.state.isRemoving,
storyToRemove: undefined,
storyRemoveIndex: undefined
});
};
removeAllStories() {
this.props.viewState.terria.stories = [];
}
onSave(_story) {
const story = {
title: _story.title,
text: _story.text,
id: _story.id ? _story.id : createGuid()
};
this.props.viewState.terria.analytics?.logEvent(Category.story, StoryAction.saveStory, JSON.stringify(story));
const storyIndex = (this.props.viewState.terria.stories || []).findIndex((story) => story.id === _story.id);
if (storyIndex >= 0) {
const oldStory = this.props.viewState.terria.stories[storyIndex];
// replace the old story, we need to replace the stories array so that
// it is observable
this.props.viewState.terria.stories = [
...this.props.viewState.terria.stories.slice(0, storyIndex),
combine(story, oldStory),
...this.props.viewState.terria.stories.slice(storyIndex + 1)
];
}
else {
this.captureStory(story);
}
this.setState({
editingMode: false
});
}
captureStory(story) {
const shareData = toJS(getShareData(this.props.viewState.terria, this.props.viewState, {
includeStories: false
}));
this.props.viewState.terria.stories.push({ ...story, shareData });
}
recaptureScene(story) {
const { t } = this.props;
this.closeShareRemoving();
this.clearRecaptureSuccessTimeout?.();
const storyIndex = (this.props.viewState.terria.stories || []).findIndex((st) => st.id === story.id);
if (storyIndex >= 0) {
story.shareData = JSON.parse(JSON.stringify(getShareData(this.props.viewState.terria, this.props.viewState, {
includeStories: false
})));
this.props.viewState.terria.stories = [
...this.props.viewState.terria.stories.slice(0, storyIndex),
story,
...this.props.viewState.terria.stories.slice(storyIndex + 1)
];
this.setState({
recaptureSuccessId: story.id
});
const timeout = setTimeout(this.resetReCaptureStatus, 2000);
this.clearRecaptureSuccessTimeout = () => clearTimeout(timeout);
}
else {
throw new Error(t("story.doesNotExist"));
}
}
resetReCaptureStatus = () => {
this.setState({
recaptureSuccessId: undefined
});
};
closeShareRemoving = () => {
this.setState({
isRemoving: false,
isSharing: false
});
};
runStories = () => {
this.closeShareRemoving();
this.props.viewState.runStories();
};
editStory(story) {
this.closeShareRemoving();
this.props.viewState.storyShown = false;
this.setState({
editingMode: true,
currentStory: story
});
}
viewStory(index) {
this.closeShareRemoving();
this.props.viewState.currentStoryId = index;
this.runStories();
}
onSort(sortedArray, _currentDraggingSortData, _currentDraggingIndex) {
this.props.viewState.terria.stories = sortedArray;
}
viewState;
get shareDataStringSize() {
if (!this.viewState)
return undefined;
const terria = this.viewState.terria;
const stories = terria.stories;
const validStories = stories.filter((story) => story.shareData.initSources.length > 0).length;
return JSON.stringify(getShareData(terria, this.viewState, {
includeStories: validStories > 0
})).length;
}
componentDidMount() {
runInAction(() => {
this.viewState = this.props.viewState;
});
}
componentDidUpdate(prevProps) {
if (prevProps.viewState !== this.props.viewState) {
runInAction(() => {
this.viewState = this.props.viewState;
});
}
}
componentWillUnmount() {
this.clearRecaptureSuccessTimeout?.();
}
renderIntro() {
const { t } = this.props;
return (_jsxs(Box, { column: true, children: [_jsx(VideoGuide, { viewState: this.props.viewState, videoLink: this.props.viewState.terria.configParameters.storyVideo?.videoUrl ||
"https://www.youtube-nocookie.com/embed/fbiQawV8IYY", background: dataStoriesImg, videoName: STORY_VIDEO }), _jsx(StoryButton, { title: t("story.gettingStartedTitle"), btnText: t("story.gettingStarted"), onClick: () => {
this.props.viewState.setVideoGuideVisible(STORY_VIDEO);
}, children: _jsx(StyledIcon, { glyph: Icon.GLYPHS.play, light: true, styledWidth: "20px" }) }), _jsx(Spacing, { bottom: 2 }), _jsx(CaptureScene, { disabled: this.state.isRemoving, onClickCapture: this.onClickCapture })] }));
}
toggleSharePanel = () => {
this.setState({
isRemoving: false,
isSharing: !this.state.isSharing
});
};
renderPlayShare() {
const { t } = this.props;
return (_jsxs(Box, { justifySpaceBetween: true, children: [_jsx(StoryButton, { fullWidth: true, disabled: this.state.editingMode, title: t("story.preview"), btnText: t("story.play"), onClick: this.runStories, children: _jsx(StyledIcon, { glyph: Icon.GLYPHS.playStory, light: true, styledWidth: "20px" }) }), _jsx(Spacing, { right: 1 }), _jsx(SharePanel, { storyShare: true, btnDisabled: this.state.editingMode, terria: this.props.viewState.terria, viewState: this.props.viewState, modalWidth: (this.props.widthFromMeasureElementHOC ?? 100) - 22, onUserClick: this.toggleSharePanel })] }));
}
openMenu(storyId) {
this.setState({
storyWithOpenMenuId: storyId
});
}
renderStories() {
const { t, i18n } = this.props;
const stories = this.props.viewState.terria.stories || [];
const storyName = this.state.storyToRemove
? this.state.storyToRemove.title.length
? this.state.storyToRemove.title
: t("story.untitledScene")
: "";
return (_jsxs(_Fragment, { children: [_jsxs(Box, { justifySpaceBetween: true, verticalCenter: true, paddedRatio: 2, css: `
border-top: 1px solid ${this.props.theme.darkLighter};
border-bottom: 1px solid ${this.props.theme.darkLighter};
`, children: [_jsxs(TextSpan, { textLight: true, uppercase: true, overflowHide: true, overflowEllipsis: true, children: [t("story.badgeBarLabel"), " ", `(${this.props.viewState.terria.stories.length})`] }), _jsxs(RawButton, { type: "button", onClick: this.toggleRemoveDialog, textLight: true, className: Styles.removeButton, children: [_jsx(Icon, { glyph: Icon.GLYPHS.cancel }), _jsx(TextSpan, { isLink: true, children: t("story.removeAllStories") })] })] }), this.props.viewState.terria.configParameters
.showStorySaveInstructions && (_jsxs(Box, { verticalCenter: true, paddedRatio: 2, css: `
border-bottom: 1px solid ${this.props.theme.darkLighter};
`, children: [_jsx(StyledIcon, { glyph: Icon.GLYPHS.info, styledWidth: "16px", fillColor: this.props.theme.infoColor, css: `
flex-shrink: 0;
` }), _jsx(Spacing, { right: 1 }), _jsx(TextSpan, { medium: true, color: this.props.theme.infoColor, children: t("story.saveInstructions") })] })), _jsx(Spacing, { bottom: 2 }), _jsxs(Box, { column: true, paddedHorizontally: 2, flex: 1, styledMinHeight: "0", children: [this.state.isRemoving && (_jsx(RemoveDialog, { theme: this.props.theme, text: this.state.storyToRemove ? (_jsx(Text, { textLight: true, large: true, children: _jsxs(Trans, { i18nKey: "story.removeStoryDialog", i18n: i18n, children: ["Are you sure you wish to delete", _jsx(TextSpan, { textLight: true, large: true, bold: true, children: { storyName } }), "?"] }) })) : (_jsx(Text, { textLight: true, large: true, children: t("story.removeAllStoriesDialog", {
count: this.props.viewState.terria.stories.length
}) })), onConfirm: this.removeAction, closeDialog: this.toggleRemoveDialog })), _jsxs(Box, { column: true, styledHeight: "100%", css: `
${(this.state.isRemoving || this.state.isSharing) &&
`opacity: 0.3`}
`, children: [_jsx(Box, { column: true, scroll: true, overflowY: "auto", styledMaxHeight: "100%", ref: this.storiesWrapperRef, css: `
min-height: 130px;
margin-right: -10px;
`, children: _jsx(Sortable, { onSort: this.onSort, direction: "vertical", dynamic: true, css: `
margin-right: 10px;
`, children: stories.map((story, index) => (_jsx(Story, { story: story, sortData: story, deleteStory: () => this.removeStory(index, story), recaptureStory: () => this.recaptureScene(story), recaptureStorySuccessful: Boolean(story.id === this.state.recaptureSuccessId), viewStory: () => this.viewStory(index), menuOpen: this.state.storyWithOpenMenuId === story.id, openMenu: () => this.openMenu(story.id), closeMenu: () => this.openMenu(undefined), editStory: () => this.editStory(story), parentRef: this.storiesWrapperRef, index: index }, `${story.id}`))) }) }), _jsx(Spacing, { bottom: 2 }), _jsx(CaptureScene, { disabled: this.state.isRemoving, onClickCapture: this.onClickCapture }), _jsx(Spacing, { bottom: 2 })] })] })] }));
}
onClickCapture = () => {
this.setState({
editingMode: true,
currentStory: undefined
});
};
hideStoryBuilder = () => {
this.props.viewState.toggleStoryBuilder();
this.props.viewState.terria.currentViewer.notifyRepaintRequired();
// Allow any animations to finish, then trigger a resize.
setTimeout(function () {
triggerResize();
}, this.props.animationDuration || 1);
this.props.viewState.toggleFeaturePrompt("story", false, true);
};
render() {
const { t } = this.props;
const hasStories = this.props.viewState.terria.stories.length > 0;
const shareDataSize = this.shareDataStringSize;
const shareMaxRequestSize = this.props.viewState.terria.shareDataService?.shareMaxRequestSize;
const shareMaxRequestSizeBytes = this.props.viewState.terria.shareDataService?.shareMaxRequestSizeBytes;
// Disable the warning if map owners use custom server that does not return shareMaxRequestSize:
const shareDataTooLong = shareDataSize && shareMaxRequestSizeBytes
? shareDataSize > shareMaxRequestSizeBytes
: false;
return (_jsxs(Panel, { ref: (component) => (this.refToMeasure = component), isVisible: this.props.isVisible, isHidden: !this.props.isVisible, styledWidth: "320px", styledMinWidth: "320px", backgroundColor: this.props.theme.dark, column: true, children: [_jsx(Box, { right: true, children: _jsx(RawButton, { css: `
padding: 15px;
`, onClick: this.hideStoryBuilder, children: _jsx(StyledIcon, { styledWidth: "16px", fillColor: this.props.theme.textLightDimmed, opacity: 0.5, glyph: Icon.GLYPHS.closeLight }) }) }), _jsxs(Box, { centered: true, paddedHorizontally: 2, displayInlineBlock: true, children: [_jsx(Text, { bold: true, extraExtraLarge: true, textLight: true, children: t("story.panelTitle") }), _jsx(Spacing, { bottom: 2 }), _jsx(Text, { medium: true, color: this.props.theme.textLightDimmed, highlightLinks: true, children: `${t("story.panelBody")}${shareMaxRequestSize
? ` ${t("story.panelBodyCapped", { shareMaxRequestSize })}`
: ""}` }), _jsx(Spacing, { bottom: 3 }), !hasStories && this.renderIntro(), hasStories && this.renderPlayShare()] }), _jsx(Spacing, { bottom: 2 }), shareDataTooLong && (_jsx(Box, { paddedHorizontally: 2, children: _jsx(Text, { small: true, color: this.props.theme.textWarning, highlightLinks: true, children: t("story.storiesTooLong") }) })), hasStories && this.renderStories(), this.state.editingMode && (_jsx(StoryEditor, { removeStory: this.removeStory, exitEditingMode: () => this.setState({ editingMode: false }), story: this.state.currentStory, saveStory: this.onSave, terria: this.props.viewState.terria }))] }));
}
};
__decorate([
action.bound
], StoryBuilder.prototype, "removeAction", null);
__decorate([
action.bound
], StoryBuilder.prototype, "removeAllStories", null);
__decorate([
action.bound
], StoryBuilder.prototype, "onSave", null);
__decorate([
action
], StoryBuilder.prototype, "captureStory", null);
__decorate([
action
], StoryBuilder.prototype, "recaptureScene", null);
__decorate([
action
], StoryBuilder.prototype, "editStory", null);
__decorate([
action
], StoryBuilder.prototype, "viewStory", null);
__decorate([
action.bound
], StoryBuilder.prototype, "onSort", null);
__decorate([
observable
], StoryBuilder.prototype, "viewState", void 0);
__decorate([
computed
], StoryBuilder.prototype, "shareDataStringSize", null);
StoryBuilder = __decorate([
observer
], StoryBuilder);
const Panel = styled(Box) `
transition: all 0.25s;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
width: 320px;
min-width: 320px;
height: 100vh;
${(props) => props.isVisible &&
`
visibility: visible;
margin-right: 0;
`}
${(props) => props.isHidden &&
`
visibility: hidden;
margin-right: -100%;
`}
`;
const CaptureScene = (props) => {
const { t } = useTranslation();
return (_jsx(StoryButton, { title: t("story.captureSceneTitle"), btnText: t("story.captureScene"), onClick: props.onClickCapture, disabled: props.disabled, fullWidth: true, children: _jsx(StyledIcon, { glyph: Icon.GLYPHS.story, light: true, styledWidth: "20px" }) }));
};
export const StoryButton = (props) => {
const { btnText, ...rest } = props;
return (_jsx(Button, { primary: true, renderIcon: props.children && (() => props.children), textProps: {
large: true
}, ...rest, children: btnText ? btnText : "" }));
};
const RemoveDialog = (props) => {
const { t } = useTranslation();
return (_jsxs(Box, { backgroundColor: props.theme.darkLighter, position: "absolute", rounded: true, paddedVertically: 3, paddedHorizontally: 2, column: true, css: `
width: calc(100% - 20px);
`, children: [props.text, _jsx(Spacing, { bottom: 2 }), _jsxs(Box, { children: [_jsx(Button, { denyButton: true, fullWidth: true, textProps: {
large: true,
semiBold: true
}, onClick: props.closeDialog, children: t("general.cancel") }), _jsx(Spacing, { right: 2 }), _jsx(Button, { primary: true, fullWidth: true, textProps: {
large: true,
semiBold: true
}, onClick: () => {
props.onConfirm();
props.closeDialog();
}, children: t("general.confirm") })] })] }));
};
export default withViewState(withTranslation()(withTheme(measureElement(StoryBuilder))));
//# sourceMappingURL=StoryBuilder.js.map