UNPKG

terriajs

Version:

Geospatial data visualization platform.

358 lines 19.1 kB
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