From d7d220958f33871ac4fd8b40c58af94dde3efb25 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 27 Feb 2024 11:02:27 -0800 Subject: [PATCH 01/56] refactor: clean up AnimationPanelContainer --- src/components/AnimationPanel/AnimationPanelContainer.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/AnimationPanel/AnimationPanelContainer.tsx b/src/components/AnimationPanel/AnimationPanelContainer.tsx index bd70a65..9296636 100644 --- a/src/components/AnimationPanel/AnimationPanelContainer.tsx +++ b/src/components/AnimationPanel/AnimationPanelContainer.tsx @@ -39,12 +39,16 @@ const AnimationPanelContainer: React.FC = ({ mapView }: Props) => { waybackItems4AnimationSelector ); - return isAnimationModeOn && waybackItems4Animation.length ? ( + if (!isAnimationModeOn || !waybackItems4Animation.length) { + return null; + } + + return ( - ) : null; + ); }; export default AnimationPanelContainer; From 2283686befd545b100795f73462b03bc2f864b8e Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 27 Feb 2024 13:34:03 -0800 Subject: [PATCH 02/56] feat: use MediaLayer for Animation --- .../AnimationControls/AnimationControls.tsx | 40 +- .../AnimationLayer/AnimationLayer.tsx | 166 ++++++ .../AnimationLayer/useMediaLayerAnimation.tsx | 126 +++++ .../useMediaLayerImageElement.tsx | 124 +++++ .../AnimationPanel/AnimationPanel.tsx | 516 +++++++++--------- .../AnimationPanelContainer.tsx | 108 ++-- .../AnimationPanel/generateFrames4GIF.ts | 23 +- src/components/AppLayout/AppLayout.tsx | 6 +- src/components/CloseButton/CloseButton.css | 5 + src/components/CloseButton/CloseButton.tsx | 42 ++ src/components/CloseButton/index.ts | 16 + src/components/index.ts | 2 +- src/store/AnimationMode/reducer.ts | 210 ++----- src/utils/snippets/getNormalizedExtent.ts | 32 ++ 14 files changed, 922 insertions(+), 494 deletions(-) create mode 100644 src/components/AnimationLayer/AnimationLayer.tsx create mode 100644 src/components/AnimationLayer/useMediaLayerAnimation.tsx create mode 100644 src/components/AnimationLayer/useMediaLayerImageElement.tsx create mode 100644 src/components/CloseButton/CloseButton.css create mode 100644 src/components/CloseButton/CloseButton.tsx create mode 100644 src/components/CloseButton/index.ts create mode 100644 src/utils/snippets/getNormalizedExtent.ts diff --git a/src/components/AnimationControls/AnimationControls.tsx b/src/components/AnimationControls/AnimationControls.tsx index e381bb3..b27ddd2 100644 --- a/src/components/AnimationControls/AnimationControls.tsx +++ b/src/components/AnimationControls/AnimationControls.tsx @@ -29,18 +29,22 @@ import { waybackItems4AnimationLoaded, // rNum4AnimationFramesSelector, rNum2ExcludeSelector, - toggleAnimationFrame, + // toggleAnimationFrame, rNum2ExcludeReset, // animationSpeedChanged, animationSpeedSelector, - isAnimationPlayingToggled, - isAnimationPlayingSelector, - startAnimation, - stopAnimation, - updateAnimationSpeed, + // isAnimationPlayingToggled, + // isAnimationPlayingSelector, + // startAnimation, + // stopAnimation, + // updateAnimationSpeed, indexOfCurrentAnimationFrameSelector, waybackItem4CurrentAnimationFrameSelector, - setActiveFrameByReleaseNum, + animationSpeedChanged, + selectAnimationStatus, + animationStatusChanged, + indexOfActiveAnimationFrameChanged, + // setActiveFrameByReleaseNum, } from '@store/AnimationMode/reducer'; import { IWaybackItem } from '@typings/index'; @@ -70,23 +74,25 @@ const AnimationControls = () => { const animationSpeed = useSelector(animationSpeedSelector); - const isPlaying = useSelector(isAnimationPlayingSelector); + // const isPlaying = useSelector(isAnimationPlayingSelector); + + const animationStatus = useSelector(selectAnimationStatus); const waybackItem4CurrentAnimationFrame = useSelector( waybackItem4CurrentAnimationFrameSelector ); const speedOnChange = useCallback((speed: number) => { - dispatch(updateAnimationSpeed(speed)); + dispatch(animationSpeedChanged(speed)); }, []); const playPauseBtnOnClick = useCallback(() => { - if (isPlaying) { - dispatch(stopAnimation()); + if (animationStatus === 'playing') { + dispatch(animationStatusChanged('pausing')); } else { - dispatch(startAnimation()); + dispatch(animationStatusChanged('playing')); } - }, [isPlaying]); + }, [animationStatus]); const getContent = () => { if ( @@ -117,7 +123,7 @@ const AnimationControls = () => { }} > @@ -133,15 +139,15 @@ const AnimationControls = () => { // rNum4AnimationFrames={rNum4AnimationFrames} rNum2Exclude={rNum2ExcludeFromAnimation} setActiveFrame={(rNum) => { - dispatch(setActiveFrameByReleaseNum(rNum)); + dispatch(indexOfActiveAnimationFrameChanged(rNum)); }} toggleFrame={(rNum) => { - dispatch(toggleAnimationFrame(rNum)); + // dispatch(toggleAnimationFrame(rNum)); }} waybackItem4CurrentAnimationFrame={ waybackItem4CurrentAnimationFrame } - isButtonDisabled={isPlaying} + isButtonDisabled={animationStatus === 'playing'} /> ); diff --git a/src/components/AnimationLayer/AnimationLayer.tsx b/src/components/AnimationLayer/AnimationLayer.tsx new file mode 100644 index 0000000..9446d91 --- /dev/null +++ b/src/components/AnimationLayer/AnimationLayer.tsx @@ -0,0 +1,166 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MapView from '@arcgis/core/views/MapView'; +import MediaLayer from '@arcgis/core/layers/MediaLayer'; +import React, { FC, useCallback, useEffect, useRef } from 'react'; +import { useDispatch } from 'react-redux'; +import { useSelector } from 'react-redux'; +import { + animationSpeedChanged, + animationSpeedSelector, + animationStatusChanged, + selectAnimationStatus, + toggleAnimationMode, + waybackItems4AnimationSelector, +} from '@store/AnimationMode/reducer'; +import classNames from 'classnames'; +import { CloseButton } from '@components/CloseButton'; +import { useMediaLayerImageElement } from './useMediaLayerImageElement'; +import useMediaLayerAnimation from './useMediaLayerAnimation'; + +type Props = { + mapView?: MapView; +}; + +export const AnimationLayer: FC = ({ mapView }: Props) => { + const dispatch = useDispatch(); + + const mediaLayerRef = useRef(); + + const animationStatus = useSelector(selectAnimationStatus); + + const animationSpeed = useSelector(animationSpeedSelector); + + const waybackItems = useSelector(waybackItems4AnimationSelector); + + /** + * Array of Imagery Elements for each scene in `sortedQueryParams4ScenesInAnimationMode` + */ + const mediaLayerElements = useMediaLayerImageElement({ + mapView, + animationStatus, + waybackItems, + }); + + /** + * This is a callback function that will be called each time the active frame (Image Element) in the animation layer is changed. + */ + const activeFrameOnChange = useCallback( + (indexOfActiveFrame: number) => { + console.log(`activeFrameOnChange`, indexOfActiveFrame); + + // const queryParamsOfActiveFrame = + // sortedQueryParams4ScenesInAnimationMode[indexOfActiveFrame]; + + // dispatch( + // selectedItemIdOfQueryParamsListChanged( + // queryParamsOfActiveFrame?.uniqueId + // ) + // ); + }, + [waybackItems] + ); + + useMediaLayerAnimation({ + animationStatus, + animationSpeed: animationSpeed * 1000, + mediaLayerElements, + activeFrameOnChange, + }); + + const initMediaLayer = async () => { + mediaLayerRef.current = new MediaLayer({ + visible: true, + // effect: LandCoverLayerEffect, + // blendMode: LandCoverLayerBlendMode, + }); + + mapView.map.add(mediaLayerRef.current); + }; + + useEffect(() => { + if (!mediaLayerRef.current) { + initMediaLayer(); + return; + } + + const source = mediaLayerRef.current.source as any; + + if (!mediaLayerElements) { + // animation is not started or just stopped + // just clear all elements in media layer + source.elements.removeAll(); + } else { + source.elements.addMany(mediaLayerElements); + // media layer elements are ready, change animation mode to playing to start the animation + dispatch(animationStatusChanged('playing')); + } + }, [mediaLayerElements, mapView]); + + // useEffect(() => { + // // We only need to save animation window information when the animation is in progress. + // // Additionally, we should always reset the animation window information in the hash parameters + // // when the animation stops. Resetting the animation window information is crucial + // // as it ensures that the animation window information is not used if the user manually starts the animation. + // // Animation window information from the hash parameters should only be utilized by users + // // who open the application in animation mode through a link shared by others. + // const extent = animationStatus === 'playing' ? mapView.extent : null; + + // const width = animationStatus === 'playing' ? mapView.width : null; + + // const height = animationStatus === 'playing' ? mapView.height : null; + + // saveAnimationWindowInfoToHashParams(extent, width, height); + // }, [animationStatus]); + + // useEffect(() => { + // // should close download animation panel whenever user exits the animation mode + // if (animationStatus === null) { + // dispatch(showDownloadAnimationPanelChanged(false)); + // } + // }, [animationStatus]); + + if (!animationStatus) { + return null; + } + + return ( +
+ {animationStatus === 'loading' && ( + + )} + + { + dispatch(toggleAnimationMode()); + }} + /> + + {/* */} +
+ ); +}; diff --git a/src/components/AnimationLayer/useMediaLayerAnimation.tsx b/src/components/AnimationLayer/useMediaLayerAnimation.tsx new file mode 100644 index 0000000..06ab35f --- /dev/null +++ b/src/components/AnimationLayer/useMediaLayerAnimation.tsx @@ -0,0 +1,126 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC, useEffect, useRef, useState } from 'react'; +import IImageElement from '@arcgis/core/layers/support/ImageElement'; +import { AnimationStatus } from '@store/AnimationMode/reducer'; + +type Props = { + /** + * status of the animation mode + */ + animationStatus: AnimationStatus; + /** + * animation speed in millisecond + */ + animationSpeed: number; + /** + * array of image elements to be animated + */ + mediaLayerElements: IImageElement[]; + /** + * Fires when the active frame changes + * @param indexOfActiveFrame index of the active frame + * @returns void + */ + activeFrameOnChange: (indexOfActiveFrame: number) => void; +}; + +/** + * This is a custom hook that handles the animation of input media layer elements + * @param animationStatus status of the animation + * @param mediaLayerElements Image Elements added to a media layer that will be animated + */ +const useMediaLayerAnimation = ({ + animationStatus, + animationSpeed, + mediaLayerElements, + activeFrameOnChange, +}: Props) => { + const isPlayingRef = useRef(false); + + const timeLastFrameDisplayed = useRef(performance.now()); + + const indexOfNextFrame = useRef(0); + + const activeFrameOnChangeRef = useRef(); + + const animationSpeedRef = useRef(animationSpeed); + + const showNextFrame = () => { + // use has stopped animation, no need to show next frame + if (!isPlayingRef.current) { + return; + } + + // get current performance time + const now = performance.now(); + + const millisecondsSinceLastFrame = now - timeLastFrameDisplayed.current; + + // if last frame was shown within the time window, no need to display next frame + if (millisecondsSinceLastFrame < animationSpeedRef.current) { + requestAnimationFrame(showNextFrame); + return; + } + + timeLastFrameDisplayed.current = now; + + // reset index of next frame to 0 if it is out of range. + // this can happen when a frame gets removed after previous animation is stopped + if (indexOfNextFrame.current >= mediaLayerElements.length) { + indexOfNextFrame.current = 0; + } + + activeFrameOnChangeRef.current(indexOfNextFrame.current); + + for (let i = 0; i < mediaLayerElements.length; i++) { + const opacity = i === indexOfNextFrame.current ? 1 : 0; + mediaLayerElements[i].opacity = opacity; + } + + // update indexOfNextFrame using the index of next element + // when hit the end of the array, use 0 instead + indexOfNextFrame.current = + (indexOfNextFrame.current + 1) % mediaLayerElements.length; + + // call showNextFrame recursively to play the animation as long as + // animationMode is 'playing' + requestAnimationFrame(showNextFrame); + }; + + useEffect(() => { + isPlayingRef.current = animationStatus === 'playing'; + + // cannot animate layers if the list is empty + if (!mediaLayerElements || !mediaLayerElements?.length) { + return; + } + + if (mediaLayerElements && animationStatus === 'playing') { + requestAnimationFrame(showNextFrame); + } + }, [animationStatus, mediaLayerElements]); + + useEffect(() => { + activeFrameOnChangeRef.current = activeFrameOnChange; + }, [activeFrameOnChange]); + + useEffect(() => { + animationSpeedRef.current = animationSpeed; + }, [animationSpeed]); +}; + +export default useMediaLayerAnimation; diff --git a/src/components/AnimationLayer/useMediaLayerImageElement.tsx b/src/components/AnimationLayer/useMediaLayerImageElement.tsx new file mode 100644 index 0000000..8a7eee3 --- /dev/null +++ b/src/components/AnimationLayer/useMediaLayerImageElement.tsx @@ -0,0 +1,124 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MapView from '@arcgis/core/views/MapView'; +import { AnimationStatus } from '@store/AnimationMode/reducer'; +import React, { useEffect, useRef, useState } from 'react'; +import ImageElement from '@arcgis/core/layers/support/ImageElement'; +import ExtentAndRotationGeoreference from '@arcgis/core/layers/support/ExtentAndRotationGeoreference'; +import { IWaybackItem } from '@typings/index'; +import { getNormalizedExtent } from '@utils/snippets/getNormalizedExtent'; +import { + FrameData, + generateFrames, +} from '@components/AnimationPanel/generateFrames4GIF'; +import { PARENT_CONTAINER_LEFT_OFFSET } from '@components/AnimationPanel/AnimationPanel'; + +type Props = { + mapView?: MapView; + animationStatus: AnimationStatus; + waybackItems: IWaybackItem[]; +}; + +export const useMediaLayerImageElement = ({ + mapView, + animationStatus, + waybackItems, +}: Props) => { + const [imageElements, setImageElements] = useState(null); + + const abortControllerRef = useRef(); + + // const animationStatus = useSelector(selectAnimationStatus); + + const loadFrameData = async () => { + if (!mapView) { + return; + } + + // use a new abort controller so the pending requests can be cancelled + // if user quits animation mode before the responses are returned + abortControllerRef.current = new AbortController(); + + try { + const extent = getNormalizedExtent(mapView.extent); + const width = mapView.width; + const height = mapView.height; + + const { xmin, ymin, xmax, ymax } = extent; + + const container = mapView.container as HTMLDivElement; + + const elemRect = container.getBoundingClientRect(); + // console.log(elemRect) + + const frameData = await generateFrames({ + frameRect: { + screenX: elemRect.left - PARENT_CONTAINER_LEFT_OFFSET, + screenY: elemRect.top, + width, + height, + }, + mapView, + waybackItems, + }); + + // once responses are received, get array of image elements using the binary data returned from export image requests + const imageElements = frameData.map((d: FrameData) => { + return new ImageElement({ + image: URL.createObjectURL(d.frameBlob), + georeference: new ExtentAndRotationGeoreference({ + extent: { + spatialReference: { + wkid: 102100, + }, + xmin, + ymin, + xmax, + ymax, + }, + }), + opacity: 1, + }); + }); + + setImageElements(imageElements); + } catch (err) { + console.error(err); + } + }; + + useEffect(() => { + if (!animationStatus || !waybackItems.length) { + // call abort so all pending requests can be cancelled + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + // call revokeObjectURL so these image elements can be freed from the memory + if (imageElements) { + for (const elem of imageElements) { + URL.revokeObjectURL(elem.image as string); + } + } + + setImageElements(null); + } else if (animationStatus === 'loading') { + loadFrameData(); + } + }, [animationStatus, waybackItems]); + + return imageElements; +}; diff --git a/src/components/AnimationPanel/AnimationPanel.tsx b/src/components/AnimationPanel/AnimationPanel.tsx index 4e30954..180d28c 100644 --- a/src/components/AnimationPanel/AnimationPanel.tsx +++ b/src/components/AnimationPanel/AnimationPanel.tsx @@ -1,260 +1,260 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useCallback, useEffect, useRef, useState } from 'react'; - -import MapView from '@arcgis/core/views/MapView'; - -import { FrameData, generateFrames } from './generateFrames4GIF'; - -import Resizable from './Resizable'; -import ImageAutoPlay from './ImageAutoPlay'; -import LoadingIndicator from './LoadingIndicator'; -import DownloadGIFDialog from './DownloadGIFDialog'; -import CloseBtn from './CloseBtn'; - -// import { whenFalse } from '@arcgis/core/core/watchUtils'; -import { IWaybackItem } from '@typings/index'; -import { useDispatch, useSelector } from 'react-redux'; - -import { - animationSpeedSelector, - // indexOfCurrentFrameChanged, - // isAnimationModeOnSelector, - isDownloadGIFDialogOnSelector, - rNum2ExcludeSelector, - // startAnimation, - toggleIsLoadingFrameData, -} from '@store/AnimationMode/reducer'; -import Background from './Background'; -import { watch } from '@arcgis/core/core/reactiveUtils'; - -type Props = { - waybackItems4Animation: IWaybackItem[]; - mapView?: MapView; -}; - -type GetFramesParams = { - waybackItems: IWaybackItem[]; - // releaseNums:number[], - container: HTMLDivElement; - mapView: MapView; -}; - -type GetFramesResponse = { - data: FrameData[]; - taskInfo: string; -}; - -type GetTaskFingerPrintParams = { - container: HTMLDivElement; - mapView: MapView; -}; - -// width of Gutter and Side Bar, need to calculate this dynamically +// /* Copyright 2024 Esri +// * +// * Licensed under the Apache License Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ + +// import React, { useCallback, useEffect, useRef, useState } from 'react'; + +// import MapView from '@arcgis/core/views/MapView'; + +// import { FrameData, generateFrames } from './generateFrames4GIF'; + +// import Resizable from './Resizable'; +// import ImageAutoPlay from './ImageAutoPlay'; +// import LoadingIndicator from './LoadingIndicator'; +// import DownloadGIFDialog from './DownloadGIFDialog'; +// import CloseBtn from './CloseBtn'; + +// // import { whenFalse } from '@arcgis/core/core/watchUtils'; +// import { IWaybackItem } from '@typings/index'; +// import { useDispatch, useSelector } from 'react-redux'; + +// import { +// animationSpeedSelector, +// // indexOfCurrentFrameChanged, +// // isAnimationModeOnSelector, +// isDownloadGIFDialogOnSelector, +// rNum2ExcludeSelector, +// // startAnimation, +// toggleIsLoadingFrameData, +// } from '@store/AnimationMode/reducer'; +// import Background from './Background'; +// import { watch } from '@arcgis/core/core/reactiveUtils'; + +// type Props = { +// waybackItems4Animation: IWaybackItem[]; +// mapView?: MapView; +// }; + +// type GetFramesParams = { +// waybackItems: IWaybackItem[]; +// // releaseNums:number[], +// container: HTMLDivElement; +// mapView: MapView; +// }; + +// type GetFramesResponse = { +// data: FrameData[]; +// taskInfo: string; +// }; + +// type GetTaskFingerPrintParams = { +// container: HTMLDivElement; +// mapView: MapView; +// }; + +// // width of Gutter and Side Bar, need to calculate this dynamically export const PARENT_CONTAINER_LEFT_OFFSET = 350; -const getFrames = async ({ - waybackItems, - // releaseNums, - container, - mapView, -}: GetFramesParams): Promise => { - const taskInfo = getAnimationTaskInfo({ - container, - mapView, - }); - - const elemRect = container.getBoundingClientRect(); - // console.log(elemRect) - - const { offsetHeight, offsetWidth } = container; - // const releaseNums = waybackItems.map(d=>d.releaseNum) - - const data = await generateFrames({ - frameRect: { - screenX: elemRect.left - PARENT_CONTAINER_LEFT_OFFSET, - screenY: elemRect.top, - width: offsetWidth, - height: offsetHeight, - }, - mapView, - waybackItems, - }); - - return { - data, - taskInfo, - }; -}; - -// get a string that represent current Map (extent) and UI State (size, position of the Resize component) -// this string will be used as a finger print to check if frame data returned by getFrames match the current Map and UI state, -// sometimes multiple getFrames calls can be triggered (zoom the map after resizing the window) and we should only show the response from the last request -const getAnimationTaskInfo = ({ - container, - mapView, -}: GetTaskFingerPrintParams): string => { - if (!mapView || !container) { - return ''; - } - - const { xmax, xmin, ymax, ymin } = mapView.extent; - - const { left, top } = container.getBoundingClientRect(); - - const { offsetHeight, offsetWidth } = container; - - return [xmax, xmin, ymax, ymin, left, top, offsetHeight, offsetWidth].join( - '#' - ); -}; - -const containerRef = React.createRef(); - -const AnimationPanel: React.FC = ({ - waybackItems4Animation, - mapView, -}: Props) => { - const dispatch = useDispatch(); - - // array of frame images as dataURI string - const [frameData, setFrameData] = useState(null); - - const loadingWaybackItems4AnimationRef = useRef(false); - - const getAnimationFramesDelay = useRef(); - - const waybackItems4AnimationRef = useRef(); - - const isDownloadGIFDialogOn = useSelector(isDownloadGIFDialogOnSelector); - - // in second - const animationSpeed = useSelector(animationSpeedSelector); - - // release numbers for the frames to be excluded from animation - const rNum2Exclude = useSelector(rNum2ExcludeSelector); - - const getAnimationFrames = useCallback(() => { - // in milliseconds - const DELAY_TIME = 1500; - - clearTimeout(getAnimationFramesDelay.current); - - getAnimationFramesDelay.current = setTimeout(async () => { - try { - const waybackItems = waybackItems4AnimationRef.current; - - const container = containerRef.current; - - if ( - !waybackItems || - !waybackItems.length || - loadingWaybackItems4AnimationRef.current - ) { - return; - } - - const { data, taskInfo } = await getFrames({ - waybackItems, - container, - mapView, - }); - - if (taskInfo !== getAnimationTaskInfo({ mapView, container })) { - console.error( - "animation task info doesn't match current map or UI state, ignore frame data returned by this task" - ); - return; - } - - setFrameData(data); - } catch (err) { - console.error(err); - } - }, DELAY_TIME); - }, [waybackItems4Animation]); - - const resizableOnChange = useCallback(() => { - setFrameData(null); - - getAnimationFrames(); - }, []); - - useEffect(() => { - waybackItems4AnimationRef.current = waybackItems4Animation; - - loadingWaybackItems4AnimationRef.current = false; - - getAnimationFrames(); - }, [waybackItems4Animation]); - - useEffect(() => { - // const onUpdating = whenFalse(mapView, 'stationary', () => { - // loadingWaybackItems4AnimationRef.current = true; - // setFrameData(null); - // }); - - watch( - () => mapView.stationary, - () => { - if (!mapView.stationary) { - loadingWaybackItems4AnimationRef.current = true; - setFrameData(null); - } - } - ); - - // return () => { - // // onStationary.remove(); - // onUpdating.remove(); - // }; - }, []); - - useEffect(() => { - const isLoading = frameData === null; - dispatch(toggleIsLoadingFrameData(isLoading)); - }, [frameData]); - - return ( - <> - - - - {frameData && frameData.length ? ( - - ) : ( - - )} - - - - - {isDownloadGIFDialogOn ? ( - - ) : null} - - ); -}; - -export default AnimationPanel; +// const getFrames = async ({ +// waybackItems, +// // releaseNums, +// container, +// mapView, +// }: GetFramesParams): Promise => { +// const taskInfo = getAnimationTaskInfo({ +// container, +// mapView, +// }); + +// const elemRect = container.getBoundingClientRect(); +// // console.log(elemRect) + +// const { offsetHeight, offsetWidth } = container; +// // const releaseNums = waybackItems.map(d=>d.releaseNum) + +// const data = await generateFrames({ +// frameRect: { +// screenX: elemRect.left - PARENT_CONTAINER_LEFT_OFFSET, +// screenY: elemRect.top, +// width: offsetWidth, +// height: offsetHeight, +// }, +// mapView, +// waybackItems, +// }); + +// return { +// data, +// taskInfo, +// }; +// }; + +// // get a string that represent current Map (extent) and UI State (size, position of the Resize component) +// // this string will be used as a finger print to check if frame data returned by getFrames match the current Map and UI state, +// // sometimes multiple getFrames calls can be triggered (zoom the map after resizing the window) and we should only show the response from the last request +// const getAnimationTaskInfo = ({ +// container, +// mapView, +// }: GetTaskFingerPrintParams): string => { +// if (!mapView || !container) { +// return ''; +// } + +// const { xmax, xmin, ymax, ymin } = mapView.extent; + +// const { left, top } = container.getBoundingClientRect(); + +// const { offsetHeight, offsetWidth } = container; + +// return [xmax, xmin, ymax, ymin, left, top, offsetHeight, offsetWidth].join( +// '#' +// ); +// }; + +// const containerRef = React.createRef(); + +// const AnimationPanel: React.FC = ({ +// waybackItems4Animation, +// mapView, +// }: Props) => { +// const dispatch = useDispatch(); + +// // array of frame images as dataURI string +// const [frameData, setFrameData] = useState(null); + +// const loadingWaybackItems4AnimationRef = useRef(false); + +// const getAnimationFramesDelay = useRef(); + +// const waybackItems4AnimationRef = useRef(); + +// const isDownloadGIFDialogOn = useSelector(isDownloadGIFDialogOnSelector); + +// // in second +// const animationSpeed = useSelector(animationSpeedSelector); + +// // release numbers for the frames to be excluded from animation +// const rNum2Exclude = useSelector(rNum2ExcludeSelector); + +// const getAnimationFrames = useCallback(() => { +// // in milliseconds +// const DELAY_TIME = 1500; + +// clearTimeout(getAnimationFramesDelay.current); + +// getAnimationFramesDelay.current = setTimeout(async () => { +// try { +// const waybackItems = waybackItems4AnimationRef.current; + +// const container = containerRef.current; + +// if ( +// !waybackItems || +// !waybackItems.length || +// loadingWaybackItems4AnimationRef.current +// ) { +// return; +// } + +// const { data, taskInfo } = await getFrames({ +// waybackItems, +// container, +// mapView, +// }); + +// if (taskInfo !== getAnimationTaskInfo({ mapView, container })) { +// console.error( +// "animation task info doesn't match current map or UI state, ignore frame data returned by this task" +// ); +// return; +// } + +// setFrameData(data); +// } catch (err) { +// console.error(err); +// } +// }, DELAY_TIME); +// }, [waybackItems4Animation]); + +// const resizableOnChange = useCallback(() => { +// setFrameData(null); + +// getAnimationFrames(); +// }, []); + +// useEffect(() => { +// waybackItems4AnimationRef.current = waybackItems4Animation; + +// loadingWaybackItems4AnimationRef.current = false; + +// getAnimationFrames(); +// }, [waybackItems4Animation]); + +// useEffect(() => { +// // const onUpdating = whenFalse(mapView, 'stationary', () => { +// // loadingWaybackItems4AnimationRef.current = true; +// // setFrameData(null); +// // }); + +// watch( +// () => mapView.stationary, +// () => { +// if (!mapView.stationary) { +// loadingWaybackItems4AnimationRef.current = true; +// setFrameData(null); +// } +// } +// ); + +// // return () => { +// // // onStationary.remove(); +// // onUpdating.remove(); +// // }; +// }, []); + +// useEffect(() => { +// const isLoading = frameData === null; +// dispatch(toggleIsLoadingFrameData(isLoading)); +// }, [frameData]); + +// return ( +// <> +// + +// +// {frameData && frameData.length ? ( +// +// ) : ( +// +// )} +// + +// + +// {isDownloadGIFDialogOn ? ( +// +// ) : null} +// +// ); +// }; + +// export default AnimationPanel; diff --git a/src/components/AnimationPanel/AnimationPanelContainer.tsx b/src/components/AnimationPanel/AnimationPanelContainer.tsx index 9296636..43e3e2a 100644 --- a/src/components/AnimationPanel/AnimationPanelContainer.tsx +++ b/src/components/AnimationPanel/AnimationPanelContainer.tsx @@ -1,54 +1,54 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useEffect } from 'react'; - -import { useSelector } from 'react-redux'; - -import { - isAnimationModeOnSelector, - waybackItems4AnimationSelector, -} from '@store/AnimationMode/reducer'; - -// import IMapView from 'esri/views/MapView'; - -import AnimationPanel from './AnimationPanel'; -import { IWaybackItem } from '@typings/index'; -import MapView from '@arcgis/core/views/MapView'; - -type Props = { - mapView?: MapView; -}; - -const AnimationPanelContainer: React.FC = ({ mapView }: Props) => { - const isAnimationModeOn = useSelector(isAnimationModeOnSelector); - - const waybackItems4Animation: IWaybackItem[] = useSelector( - waybackItems4AnimationSelector - ); - - if (!isAnimationModeOn || !waybackItems4Animation.length) { - return null; - } - - return ( - - ); -}; - -export default AnimationPanelContainer; +// /* Copyright 2024 Esri +// * +// * Licensed under the Apache License Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ + +// import React, { useEffect } from 'react'; + +// import { useSelector } from 'react-redux'; + +// import { +// isAnimationModeOnSelector, +// waybackItems4AnimationSelector, +// } from '@store/AnimationMode/reducer'; + +// // import IMapView from 'esri/views/MapView'; + +// import AnimationPanel from './AnimationPanel'; +// import { IWaybackItem } from '@typings/index'; +// import MapView from '@arcgis/core/views/MapView'; + +// type Props = { +// mapView?: MapView; +// }; + +// const AnimationPanelContainer: React.FC = ({ mapView }: Props) => { +// const isAnimationModeOn = useSelector(isAnimationModeOnSelector); + +// const waybackItems4Animation: IWaybackItem[] = useSelector( +// waybackItems4AnimationSelector +// ); + +// if (!isAnimationModeOn || !waybackItems4Animation.length) { +// return null; +// } + +// return ( +// +// ); +// }; + +// export default AnimationPanelContainer; diff --git a/src/components/AnimationPanel/generateFrames4GIF.ts b/src/components/AnimationPanel/generateFrames4GIF.ts index 7a9a0d6..695dadd 100644 --- a/src/components/AnimationPanel/generateFrames4GIF.ts +++ b/src/components/AnimationPanel/generateFrames4GIF.ts @@ -58,7 +58,8 @@ export type FrameData = { releaseNum: number; waybackItem: IWaybackItem; frameCanvas: HTMLCanvasElement; - frameDataURI: string; + frameDataURI?: string; + frameBlob: Blob; height: number; width: number; center: CenterLocationForFrameRect; @@ -87,7 +88,7 @@ export const generateFrames = async ({ for (const item of waybackItems) { const { releaseNum } = item; - const { frameCanvas, frameDataURI } = await generateFrame({ + const { frameCanvas, frameBlob } = await generateFrame({ frameRect, tiles, releaseNum, @@ -97,7 +98,8 @@ export const generateFrames = async ({ releaseNum, waybackItem: item, frameCanvas, - frameDataURI, + frameDataURI: '', + frameBlob, width: frameRect.width, height: frameRect.height, center, @@ -119,7 +121,7 @@ const generateFrame = async ({ releaseNum: number; }): Promise<{ frameCanvas: HTMLCanvasElement; - frameDataURI: string; + frameBlob: Blob; }> => { return new Promise((resolve, reject) => { const { screenX, screenY, height, width } = frameRect; @@ -160,9 +162,16 @@ const generateFrame = async ({ // all tiles are drawn to canvas, return the canvas as D if (tilesProcessed === tiles.length) { // resolve(canvas.toDataURL('image/png')); - resolve({ - frameCanvas: canvas, - frameDataURI: canvas.toDataURL('image/png'), + // resolve({ + // frameCanvas: canvas, + // frameDataURI: canvas.toDataURL('image/png'), + // }); + + canvas.toBlob((frameBlob: Blob) => { + resolve({ + frameCanvas: canvas, + frameBlob, + }); }); } }); diff --git a/src/components/AppLayout/AppLayout.tsx b/src/components/AppLayout/AppLayout.tsx index 8165cbe..ec6af2c 100644 --- a/src/components/AppLayout/AppLayout.tsx +++ b/src/components/AppLayout/AppLayout.tsx @@ -43,7 +43,7 @@ import { TilePreviewWindow, // Title4ActiveItem, WaybackLayer, - AnimationPanel, + // AnimationPanel, AnimationModeToggleBtn, ZoomWidget, OpenDownloadPanelBtn, @@ -53,6 +53,7 @@ import { import { getServiceUrl } from '@utils/Tier'; import useCurrenPageBecomesVisible from '@hooks/useCurrenPageBecomesVisible'; import { revalidateToken } from '@utils/Esri-OAuth'; +import { AnimationLayer } from '@components/AnimationLayer/AnimationLayer'; const AppLayout: React.FC = () => { // const { onPremises } = React.useContext(AppContext); @@ -102,7 +103,8 @@ const AppLayout: React.FC = () => { - + {/* */} + diff --git a/src/components/CloseButton/CloseButton.css b/src/components/CloseButton/CloseButton.css new file mode 100644 index 0000000..84b5cff --- /dev/null +++ b/src/components/CloseButton/CloseButton.css @@ -0,0 +1,5 @@ +.close-button::before { + @apply absolute top-0 right-0 w-52 h-52 pointer-events-none; + content: ' '; + background: linear-gradient(215deg, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 40%); +} \ No newline at end of file diff --git a/src/components/CloseButton/CloseButton.tsx b/src/components/CloseButton/CloseButton.tsx new file mode 100644 index 0000000..1c0f970 --- /dev/null +++ b/src/components/CloseButton/CloseButton.tsx @@ -0,0 +1,42 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import './CloseButton.css'; +import React, { FC } from 'react'; + +type Props = { + onClick: () => void; +}; + +export const CloseButton: FC = ({ onClick }: Props) => { + return ( +
+ + + + +
+ ); +}; diff --git a/src/components/CloseButton/index.ts b/src/components/CloseButton/index.ts new file mode 100644 index 0000000..0507326 --- /dev/null +++ b/src/components/CloseButton/index.ts @@ -0,0 +1,16 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { CloseButton } from './CloseButton'; diff --git a/src/components/index.ts b/src/components/index.ts index efba292..a30bf4c 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -47,7 +47,7 @@ export { default as LayerSelector } from './LayerSelector/LayerSelector'; export { default as ZoomWidget } from './ZoomWidget/ZoomWidget'; export { default as TilePreviewWindow } from './PreviewWindow/PreviewWindowContainer'; -export { default as AnimationPanel } from './AnimationPanel/AnimationPanelContainer'; +// export { default as AnimationPanel } from './AnimationPanel/AnimationPanelContainer'; export { OpenDownloadPanelBtn } from './OpenDownloadPanelBtn/OpenDownloadPanelBtn'; export { DownloadDialog } from './DownloadDialog'; diff --git a/src/store/AnimationMode/reducer.ts b/src/store/AnimationMode/reducer.ts index 1672f0d..a11987a 100644 --- a/src/store/AnimationMode/reducer.ts +++ b/src/store/AnimationMode/reducer.ts @@ -28,7 +28,13 @@ import { RootState, StoreDispatch, StoreGetState } from '../configureStore'; import { isSwipeWidgetOpenToggled } from '../Swipe/reducer'; +export type AnimationStatus = 'loading' | 'playing' | 'pausing'; + export type AnimationModeState = { + /** + * status of the Animation mode + */ + animationStatus?: AnimationStatus; isAnimationModeOn: boolean; isDownloadGIFDialogOn: boolean; // rNum4AnimationFrames: number[], @@ -37,7 +43,7 @@ export type AnimationModeState = { rNum2Exclude: number[]; // animation speed in second animationSpeed: number; - isPlaying: boolean; + // isPlaying: boolean; indexOfCurrentFrame: number; isLoadingFrameData: boolean; }; @@ -45,13 +51,14 @@ export type AnimationModeState = { export const DEFAULT_ANIMATION_SPEED_IN_SECONDS = 1; export const initialAnimationModeState = { + animationStatus: null, isAnimationModeOn: false, isDownloadGIFDialogOn: false, // rNum4AnimationFrames: [], waybackItems4Animation: [], rNum2Exclude: [], animationSpeed: DEFAULT_ANIMATION_SPEED_IN_SECONDS, - isPlaying: true, + // isPlaying: true, indexOfCurrentFrame: 0, isLoadingFrameData: true, } as AnimationModeState; @@ -63,6 +70,12 @@ const slice = createSlice({ isAnimationModeOnToggled: (state: AnimationModeState) => { state.isAnimationModeOn = !state.isAnimationModeOn; }, + animationStatusChanged: ( + state, + action: PayloadAction + ) => { + state.animationStatus = action.payload; + }, isDownloadGIFDialogOnToggled: (state: AnimationModeState) => { state.isDownloadGIFDialogOn = !state.isDownloadGIFDialogOn; }, @@ -98,13 +111,13 @@ const slice = createSlice({ ) => { state.animationSpeed = action.payload; }, - isAnimationPlayingToggled: ( - state: AnimationModeState, - action: PayloadAction - ) => { - state.isPlaying = action.payload; - }, - indexOfCurrentFrameChanged: ( + // isAnimationPlayingToggled: ( + // state: AnimationModeState, + // action: PayloadAction + // ) => { + // state.isPlaying = action.payload; + // }, + indexOfActiveAnimationFrameChanged: ( state: AnimationModeState, action: PayloadAction ) => { @@ -117,8 +130,8 @@ const slice = createSlice({ state.isLoadingFrameData = action.payload; }, resetAnimationMode: (state: AnimationModeState) => { - state.isPlaying = true; - state.animationSpeed = DEFAULT_ANIMATION_SPEED_IN_SECONDS; + // state.isPlaying = true; + // state.animationSpeed = DEFAULT_ANIMATION_SPEED_IN_SECONDS; state.rNum2Exclude = []; }, }, @@ -127,37 +140,38 @@ const slice = createSlice({ const { reducer } = slice; export const { + animationStatusChanged, isAnimationModeOnToggled, isDownloadGIFDialogOnToggled, waybackItems4AnimationLoaded, rNum2ExcludeToggled, rNum2ExcludeReset, animationSpeedChanged, - isAnimationPlayingToggled, - indexOfCurrentFrameChanged, + // isAnimationPlayingToggled, + indexOfActiveAnimationFrameChanged, isLoadingFrameDataToggled, resetAnimationMode, } = slice.actions; -export const toggleIsLoadingFrameData = - (isLoading: boolean) => - (dispatch: StoreDispatch, getState: StoreGetState) => { - const { AnimationMode } = getState(); +// export const toggleIsLoadingFrameData = +// (isLoading: boolean) => +// (dispatch: StoreDispatch, getState: StoreGetState) => { +// const { AnimationMode } = getState(); - const { isPlaying } = AnimationMode; +// const { isPlaying } = AnimationMode; - batch(() => { - dispatch(isLoadingFrameDataToggled(isLoading)); +// batch(() => { +// dispatch(isLoadingFrameDataToggled(isLoading)); - if (isLoading) { - dispatch(indexOfCurrentFrameChanged(0)); - } +// if (isLoading) { +// dispatch(indexOfCurrentFrameChanged(0)); +// } - if (!isLoading && isPlaying) { - dispatch(startAnimation()); - } - }); - }; +// if (!isLoading && isPlaying) { +// dispatch(startAnimation()); +// } +// }); +// }; export const toggleAnimationMode = () => (dispatch: StoreDispatch, getState: StoreGetState) => { @@ -182,132 +196,18 @@ export const toggleAnimationMode = ); dispatch(isAnimationModeOnToggled()); - }; - -let interval4Animation: NodeJS.Timeout; - -export const startAnimation = - () => (dispatch: StoreDispatch, getState: StoreGetState) => { - const { AnimationMode } = getState(); - - let { animationSpeed } = AnimationMode; - - animationSpeed = animationSpeed || 0.1; - - dispatch(isAnimationPlayingToggled(true)); - - clearInterval(interval4Animation); - - interval4Animation = setInterval(() => { - dispatch(showNextFrame()); - }, animationSpeed * 1000); - }; - -export const stopAnimation = - () => (dispatch: StoreDispatch, getState: StoreGetState) => { - dispatch(isAnimationPlayingToggled(false)); - clearInterval(interval4Animation); - }; - -export const setActiveFrameByReleaseNum = - (releaseNum: number) => - (dispatch: StoreDispatch, getState: StoreGetState) => { - const { AnimationMode } = getState(); - - const { isPlaying, isLoadingFrameData, waybackItems4Animation } = - AnimationMode; - - if (isPlaying || isLoadingFrameData) { - return; - } - - let targetIdx = 0; - - for (let i = 0; i < waybackItems4Animation.length; i++) { - if (waybackItems4Animation[i].releaseNum === releaseNum) { - targetIdx = i; - break; - } - } - dispatch(indexOfCurrentFrameChanged(targetIdx)); - }; - -const showNextFrame = - () => (dispatch: StoreDispatch, getState: StoreGetState) => { - const { AnimationMode } = getState(); - - const { - rNum2Exclude, - waybackItems4Animation, - indexOfCurrentFrame, - isLoadingFrameData, - } = AnimationMode; - - if (isLoadingFrameData) { - return; - } - - const rNum2ExcludeSet = new Set(rNum2Exclude); - - let idx4NextFrame = indexOfCurrentFrame; - - // loop through the circular array to find next item to show - for ( - let i = indexOfCurrentFrame + 1; - i < indexOfCurrentFrame + waybackItems4Animation.length; - i++ - ) { - const targetIdx = i % waybackItems4Animation.length; - - const targetItem = waybackItems4Animation[targetIdx]; - - if (!rNum2ExcludeSet.has(targetItem.releaseNum)) { - idx4NextFrame = targetIdx; - break; - } - } - - dispatch(indexOfCurrentFrameChanged(idx4NextFrame)); - }; - -export const updateAnimationSpeed = - (speedInSeconds: number) => - (dispatch: StoreDispatch, getState: StoreGetState) => { - const { AnimationMode } = getState(); - - const { isPlaying, animationSpeed } = AnimationMode; - - if (speedInSeconds == animationSpeed) { - return; - } - - saveAnimationSpeedInURLQueryParam(true, speedInSeconds); - - batch(() => { - dispatch(animationSpeedChanged(speedInSeconds)); - - if (isPlaying) { - dispatch(startAnimation()); - } - }); + dispatch( + animationStatusChanged( + willAnimationModeBeTurnedOn ? 'loading' : null + ) + ); }; -export const toggleAnimationFrame = - (releaseNum: number) => - (dispatch: StoreDispatch, getState: StoreGetState) => { - const { AnimationMode } = getState(); - - const { isPlaying } = AnimationMode; - - batch(() => { - dispatch(rNum2ExcludeToggled(releaseNum)); - - if (isPlaying) { - dispatch(startAnimation()); - } - }); - }; +export const selectAnimationStatus = createSelector( + (state: RootState) => state.AnimationMode.animationStatus, + (animationStatus) => animationStatus +); export const isAnimationModeOnSelector = createSelector( (state: RootState) => state.AnimationMode.isAnimationModeOn, @@ -334,10 +234,10 @@ export const animationSpeedSelector = createSelector( (animationSpeed) => animationSpeed ); -export const isAnimationPlayingSelector = createSelector( - (state: RootState) => state.AnimationMode.isPlaying, - (isPlaying) => isPlaying -); +// export const isAnimationPlayingSelector = createSelector( +// (state: RootState) => state.AnimationMode.isPlaying, +// (isPlaying) => isPlaying +// ); export const indexOfCurrentAnimationFrameSelector = createSelector( (state: RootState) => state.AnimationMode.indexOfCurrentFrame, diff --git a/src/utils/snippets/getNormalizedExtent.ts b/src/utils/snippets/getNormalizedExtent.ts new file mode 100644 index 0000000..c64ae46 --- /dev/null +++ b/src/utils/snippets/getNormalizedExtent.ts @@ -0,0 +1,32 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Extent } from '@arcgis/core/geometry'; + +/** + * Returns an Extent that's been shifted to within +/- 180. + * The map view's extent can go out of range after the user drags the map + * across the international date line. + * + * Therefore, normalizing the Extent is essential to ensure the coordinates are within the + * correct range. This function takes an Extent object as input and shifts it to ensure + * corrdinate values are within the range of -180 to +180 degrees (or equivelant projected values). + * + * @param extent - The Extent object representing the geographic area. + * @returns {Extent} - The normalized Extent object with longitude values within the range of +/- 180 degrees. + */ +export const getNormalizedExtent = (extent: Extent): Extent => { + return extent.clone().normalize()[0]; +}; From 9e9d039f4498047d093e7bf11eb3ffbc3e57ffcb Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 27 Feb 2024 14:11:41 -0800 Subject: [PATCH 03/56] feat: update AnimationMode redux slice add releaseNumberOfActiveAnimationFrame to AnimationModeState remove props that are no longer needed --- .../AnimationControls/AnimationControls.tsx | 22 +- .../AnimationControls/DonwloadGifButton.tsx | 13 +- .../AnimationControls/FramesSeletor.tsx | 8 +- .../AnimationLayer/AnimationLayer.tsx | 19 +- .../AnimationPanel/DownloadGIFDialog.tsx | 684 +++++++++--------- .../AnimationPanel/ImageAutoPlay.tsx | 108 +-- src/store/AnimationMode/reducer.ts | 132 ++-- src/store/getPreloadedState.ts | 1 + 8 files changed, 479 insertions(+), 508 deletions(-) diff --git a/src/components/AnimationControls/AnimationControls.tsx b/src/components/AnimationControls/AnimationControls.tsx index b27ddd2..a5007fc 100644 --- a/src/components/AnimationControls/AnimationControls.tsx +++ b/src/components/AnimationControls/AnimationControls.tsx @@ -38,12 +38,13 @@ import { // startAnimation, // stopAnimation, // updateAnimationSpeed, - indexOfCurrentAnimationFrameSelector, - waybackItem4CurrentAnimationFrameSelector, + // indexOfCurrentAnimationFrameSelector, + // waybackItem4CurrentAnimationFrameSelector, animationSpeedChanged, selectAnimationStatus, animationStatusChanged, - indexOfActiveAnimationFrameChanged, + // indexOfActiveAnimationFrameChanged, + selectReleaseNumberOfActiveAnimationFrame, // setActiveFrameByReleaseNum, } from '@store/AnimationMode/reducer'; @@ -78,8 +79,12 @@ const AnimationControls = () => { const animationStatus = useSelector(selectAnimationStatus); - const waybackItem4CurrentAnimationFrame = useSelector( - waybackItem4CurrentAnimationFrameSelector + // const waybackItem4CurrentAnimationFrame = useSelector( + // waybackItem4CurrentAnimationFrameSelector + // ); + + const releaseNum4ActiveFrame = useSelector( + selectReleaseNumberOfActiveAnimationFrame ); const speedOnChange = useCallback((speed: number) => { @@ -139,14 +144,13 @@ const AnimationControls = () => { // rNum4AnimationFrames={rNum4AnimationFrames} rNum2Exclude={rNum2ExcludeFromAnimation} setActiveFrame={(rNum) => { - dispatch(indexOfActiveAnimationFrameChanged(rNum)); + // dispatch(indexOfActiveAnimationFrameChanged(rNum)); + console.log(rNum); }} toggleFrame={(rNum) => { // dispatch(toggleAnimationFrame(rNum)); }} - waybackItem4CurrentAnimationFrame={ - waybackItem4CurrentAnimationFrame - } + releaseNum4ActiveFrame={releaseNum4ActiveFrame} isButtonDisabled={animationStatus === 'playing'} /> diff --git a/src/components/AnimationControls/DonwloadGifButton.tsx b/src/components/AnimationControls/DonwloadGifButton.tsx index 81d0081..09096a3 100644 --- a/src/components/AnimationControls/DonwloadGifButton.tsx +++ b/src/components/AnimationControls/DonwloadGifButton.tsx @@ -17,21 +17,24 @@ import React, { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import classnames from 'classnames'; import { - isDownloadGIFDialogOnToggled, - isLoadingFrameDataSelector, + showDownloadAnimationPanelToggled, + selectAnimationStatus, + // isLoadingFrameDataSelector, } from '@store/AnimationMode/reducer'; const DonwloadGifButton = () => { const dispatch = useDispatch(); - const isLoadingFrameData = useSelector(isLoadingFrameDataSelector); + // const isLoadingFrameData = useSelector(isLoadingFrameDataSelector); + + const animationStatus = useSelector(selectAnimationStatus); const onClickHandler = useCallback(() => { - dispatch(isDownloadGIFDialogOnToggled()); + dispatch(showDownloadAnimationPanelToggled()); }, []); const classNames = classnames('btn btn-fill', { - 'btn-disabled': isLoadingFrameData, + 'btn-disabled': animationStatus === 'loading', }); return ( diff --git a/src/components/AnimationControls/FramesSeletor.tsx b/src/components/AnimationControls/FramesSeletor.tsx index 4c7678e..5828b35 100644 --- a/src/components/AnimationControls/FramesSeletor.tsx +++ b/src/components/AnimationControls/FramesSeletor.tsx @@ -24,7 +24,7 @@ type Props = { // rNum4AnimationFrames: number[]; waybackItemsWithLocalChanges: IWaybackItem[]; rNum2Exclude: number[]; - waybackItem4CurrentAnimationFrame: IWaybackItem; + releaseNum4ActiveFrame: number; // activeItem: IWaybackItem; isButtonDisabled: boolean; setActiveFrame: (rNum: number) => void; @@ -36,7 +36,7 @@ const FramesSeletor: React.FC = ({ // rNum4AnimationFrames, waybackItemsWithLocalChanges, rNum2Exclude, - waybackItem4CurrentAnimationFrame, + releaseNum4ActiveFrame, // activeItem, isButtonDisabled, setActiveFrame, @@ -95,9 +95,7 @@ const FramesSeletor: React.FC = ({ // onClick={onSelect.bind(this, d)} onClick={setActiveFrame.bind(this, releaseNum)} showBoarderOnLeft={ - waybackItem4CurrentAnimationFrame && - waybackItem4CurrentAnimationFrame.releaseNum === - releaseNum + releaseNum4ActiveFrame === releaseNum } disableCursorPointer={isButtonDisabled} > diff --git a/src/components/AnimationLayer/AnimationLayer.tsx b/src/components/AnimationLayer/AnimationLayer.tsx index 9446d91..d3ad98c 100644 --- a/src/components/AnimationLayer/AnimationLayer.tsx +++ b/src/components/AnimationLayer/AnimationLayer.tsx @@ -19,9 +19,11 @@ import React, { FC, useCallback, useEffect, useRef } from 'react'; import { useDispatch } from 'react-redux'; import { useSelector } from 'react-redux'; import { - animationSpeedChanged, + // animationSpeedChanged, animationSpeedSelector, animationStatusChanged, + // indexOfActiveAnimationFrameChanged, + releaseNumberOfActiveAnimationFrameChanged, selectAnimationStatus, toggleAnimationMode, waybackItems4AnimationSelector, @@ -60,16 +62,13 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { */ const activeFrameOnChange = useCallback( (indexOfActiveFrame: number) => { - console.log(`activeFrameOnChange`, indexOfActiveFrame); + dispatch( + releaseNumberOfActiveAnimationFrameChanged( + waybackItems[indexOfActiveFrame]?.releaseNum + ) + ); - // const queryParamsOfActiveFrame = - // sortedQueryParams4ScenesInAnimationMode[indexOfActiveFrame]; - - // dispatch( - // selectedItemIdOfQueryParamsListChanged( - // queryParamsOfActiveFrame?.uniqueId - // ) - // ); + // console.log(waybackItems[indexOfActiveFrame]) }, [waybackItems] ); diff --git a/src/components/AnimationPanel/DownloadGIFDialog.tsx b/src/components/AnimationPanel/DownloadGIFDialog.tsx index 0679e8c..26c2f32 100644 --- a/src/components/AnimationPanel/DownloadGIFDialog.tsx +++ b/src/components/AnimationPanel/DownloadGIFDialog.tsx @@ -1,342 +1,342 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useState, useCallback, useRef } from 'react'; - -import { useDispatch } from 'react-redux'; -import { isDownloadGIFDialogOnToggled } from '@store/AnimationMode/reducer'; -import { FrameData } from './generateFrames4GIF'; - -import LoadingSpinner from './LoadingSpinner'; - -import classnames from 'classnames'; - -// import gifshot from 'gifshot'; -import GifStream from '@entryline/gifstream'; - -type Props = { - frameData: FrameData[]; - rNum2Exclude: number[]; - speed?: number; // animation speed in second -}; - -// type CreateGIFCallBack = (response: { -// // image - Base 64 image -// image: string; -// // error - Boolean that determines if an error occurred -// error: boolean; -// // errorCode - Helpful error label -// errorCode: string; -// // errorMsg - Helpful error message -// errorMsg: string; -// }) => void; - -type SaveAsGIFParams = { - frameData: FrameData[]; - // outputFileName: string; - speed: number; -}; - -type ResponseCreateGIF = { - error: string; - blob: Blob; -}; - -type ImagesCreateGIF = { - src: string; - delay: number; -}; - -const gifStream = new GifStream(); - -const donwload = (blob: Blob, fileName = ''): void => { - const url = URL.createObjectURL(blob); - - const link = document.createElement('a'); - link.download = fileName; - link.href = url; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - - URL.revokeObjectURL(url); -}; - -const saveAsGIF = async ({ - frameData, - // outputFileName, - speed, -}: SaveAsGIFParams): Promise => { - // if the speed is zero, it means user wants to have the fastest speed, so let's use 100 millisecond - speed = speed || 0.1; - - return new Promise((resolve, reject) => { - const images: ImagesCreateGIF[] = frameData.map((d) => { - const { frameCanvas, height, width, waybackItem, center } = d; - - const { releaseDateLabel } = waybackItem; - - const releaseData = `Wayback ${releaseDateLabel}`; - const sourceInfo = - 'Esri, Maxar, Earthstar Geographics, GIS Community'; - const locationInfo = `${center.latitude.toFixed( - 3 - )}, ${center.longitude.toFixed(3)}`; - const HorizontalPadding = 4; - const SpaceBetween = 4; - - const context = frameCanvas.getContext('2d'); - context.font = '10px Avenir Next'; - - const metrics4ReleaseDate = context.measureText(releaseData); - const metrics4LocationInfo = context.measureText(locationInfo); - const metrics4SourceInfo = context.measureText(sourceInfo); - - const shouldWrap = - metrics4ReleaseDate.width + - metrics4LocationInfo.width + - metrics4SourceInfo.width + - SpaceBetween * 2 + - HorizontalPadding * 2 > - width; - - // draw the gradient background rect - const gradientRectHeight = shouldWrap ? 28 : 16; - // const gradient = context.createLinearGradient(0, 0, 0, gradientRectHeight); - // gradient.addColorStop(0, "rgba(0,0,0,0)"); - // gradient.addColorStop(0.5, "rgba(0,0,0,.3)"); - // gradient.addColorStop(1, "rgba(0,0,0,.6)"); - context.fillStyle = 'rgba(0,0,0,.2)'; - context.rect( - 0, - height - gradientRectHeight, - width, - gradientRectHeight - ); - context.fill(); - - // draw the watermark text - context.shadowColor = 'black'; - context.shadowBlur = 5; - context.fillStyle = 'rgba(255,255,255,.9)'; - - if (shouldWrap) { - let y = height - 4; - const horizontalPadding = - (width - Math.ceil(metrics4SourceInfo.width)) / 2; - context.fillText(sourceInfo, horizontalPadding, y); - - y = height - 16; - context.fillText(releaseData, horizontalPadding, y); - - const xPos4LocationInfo = - width - (metrics4LocationInfo.width + horizontalPadding); - context.fillText(locationInfo, xPos4LocationInfo, y); - } else { - const y = height - 4; - - context.fillText(releaseData, HorizontalPadding, y); - - const xPos4SourceInfo = - width - (metrics4SourceInfo.width + HorizontalPadding); - context.fillText(sourceInfo, xPos4SourceInfo, y); - - let xPos4LocationInfo = - metrics4ReleaseDate.width + HorizontalPadding; - const availWidth = xPos4SourceInfo - xPos4LocationInfo; - const leftPadding4LocationInfo = - (availWidth - metrics4LocationInfo.width) / 2; - - xPos4LocationInfo = - xPos4LocationInfo + leftPadding4LocationInfo; - context.fillText(locationInfo, xPos4LocationInfo, y); - } - - return { - src: frameCanvas.toDataURL(), - delay: speed * 1000, - }; - }); - - gifStream.createGIF( - { - gifWidth: frameData[0].width, - gifHeight: frameData[0].height, - images, - progressCallback: (progress: number) => { - // console.log(progress) - }, - }, - (res: ResponseCreateGIF) => { - // this.onGifComplete(obj, width, height); - // console.log(res) - - if (res.error) { - reject(res.error); - } - - // donwload(res.blob, outputFileName) - resolve(res.blob); - } - ); - }); -}; - -const DownloadGIFDialog: React.FC = ({ - frameData, - speed, - rNum2Exclude, -}) => { - const dispatch = useDispatch(); - - const [isDownloading, setIsDownloading] = useState(false); - - const [outputFileName, setOutputFileName] = useState( - 'wayback-imagery-animation' - ); - - const isCancelled = useRef(false); - - const closeDialog = useCallback(() => { - dispatch(isDownloadGIFDialogOnToggled()); - }, []); - - const downloadBtnOnClick = async () => { - setIsDownloading(true); - - const data = !rNum2Exclude.length - ? frameData - : frameData.filter( - (d) => rNum2Exclude.indexOf(d.releaseNum) === -1 - ); - - try { - const blob = await saveAsGIF({ - frameData: data, - // outputFileName, - speed, - }); - - if (isCancelled.current) { - console.log('gif task has been cancelled'); - return; - } - - donwload(blob, outputFileName); - - closeDialog(); - } catch (err) { - console.error(err); - } - }; - - const getContent = () => { - if (isDownloading) { - return ( - <> -
-

- Generating animated GIF file... -

-

Your download will begin shortly.

-
- -
-
{ - gifStream.cancel(); - isCancelled.current = true; - closeDialog(); - }} - > - Cancel -
-
- -
- -
- - ); - } - - return ( - <> -
- Download GIF -
- -
-
- File name: - -
-
- -
-
- Cancel -
-
- Download -
-
- - ); - }; - - return ( -
-
- {getContent()} -
-
- ); -}; - -export default DownloadGIFDialog; +// /* Copyright 2024 Esri +// * +// * Licensed under the Apache License Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ + +// import React, { useState, useCallback, useRef } from 'react'; + +// import { useDispatch } from 'react-redux'; +// import { isDownloadGIFDialogOnToggled } from '@store/AnimationMode/reducer'; +// import { FrameData } from './generateFrames4GIF'; + +// import LoadingSpinner from './LoadingSpinner'; + +// import classnames from 'classnames'; + +// // import gifshot from 'gifshot'; +// import GifStream from '@entryline/gifstream'; + +// type Props = { +// frameData: FrameData[]; +// rNum2Exclude: number[]; +// speed?: number; // animation speed in second +// }; + +// // type CreateGIFCallBack = (response: { +// // // image - Base 64 image +// // image: string; +// // // error - Boolean that determines if an error occurred +// // error: boolean; +// // // errorCode - Helpful error label +// // errorCode: string; +// // // errorMsg - Helpful error message +// // errorMsg: string; +// // }) => void; + +// type SaveAsGIFParams = { +// frameData: FrameData[]; +// // outputFileName: string; +// speed: number; +// }; + +// type ResponseCreateGIF = { +// error: string; +// blob: Blob; +// }; + +// type ImagesCreateGIF = { +// src: string; +// delay: number; +// }; + +// const gifStream = new GifStream(); + +// const donwload = (blob: Blob, fileName = ''): void => { +// const url = URL.createObjectURL(blob); + +// const link = document.createElement('a'); +// link.download = fileName; +// link.href = url; +// document.body.appendChild(link); +// link.click(); +// document.body.removeChild(link); + +// URL.revokeObjectURL(url); +// }; + +// const saveAsGIF = async ({ +// frameData, +// // outputFileName, +// speed, +// }: SaveAsGIFParams): Promise => { +// // if the speed is zero, it means user wants to have the fastest speed, so let's use 100 millisecond +// speed = speed || 0.1; + +// return new Promise((resolve, reject) => { +// const images: ImagesCreateGIF[] = frameData.map((d) => { +// const { frameCanvas, height, width, waybackItem, center } = d; + +// const { releaseDateLabel } = waybackItem; + +// const releaseData = `Wayback ${releaseDateLabel}`; +// const sourceInfo = +// 'Esri, Maxar, Earthstar Geographics, GIS Community'; +// const locationInfo = `${center.latitude.toFixed( +// 3 +// )}, ${center.longitude.toFixed(3)}`; +// const HorizontalPadding = 4; +// const SpaceBetween = 4; + +// const context = frameCanvas.getContext('2d'); +// context.font = '10px Avenir Next'; + +// const metrics4ReleaseDate = context.measureText(releaseData); +// const metrics4LocationInfo = context.measureText(locationInfo); +// const metrics4SourceInfo = context.measureText(sourceInfo); + +// const shouldWrap = +// metrics4ReleaseDate.width + +// metrics4LocationInfo.width + +// metrics4SourceInfo.width + +// SpaceBetween * 2 + +// HorizontalPadding * 2 > +// width; + +// // draw the gradient background rect +// const gradientRectHeight = shouldWrap ? 28 : 16; +// // const gradient = context.createLinearGradient(0, 0, 0, gradientRectHeight); +// // gradient.addColorStop(0, "rgba(0,0,0,0)"); +// // gradient.addColorStop(0.5, "rgba(0,0,0,.3)"); +// // gradient.addColorStop(1, "rgba(0,0,0,.6)"); +// context.fillStyle = 'rgba(0,0,0,.2)'; +// context.rect( +// 0, +// height - gradientRectHeight, +// width, +// gradientRectHeight +// ); +// context.fill(); + +// // draw the watermark text +// context.shadowColor = 'black'; +// context.shadowBlur = 5; +// context.fillStyle = 'rgba(255,255,255,.9)'; + +// if (shouldWrap) { +// let y = height - 4; +// const horizontalPadding = +// (width - Math.ceil(metrics4SourceInfo.width)) / 2; +// context.fillText(sourceInfo, horizontalPadding, y); + +// y = height - 16; +// context.fillText(releaseData, horizontalPadding, y); + +// const xPos4LocationInfo = +// width - (metrics4LocationInfo.width + horizontalPadding); +// context.fillText(locationInfo, xPos4LocationInfo, y); +// } else { +// const y = height - 4; + +// context.fillText(releaseData, HorizontalPadding, y); + +// const xPos4SourceInfo = +// width - (metrics4SourceInfo.width + HorizontalPadding); +// context.fillText(sourceInfo, xPos4SourceInfo, y); + +// let xPos4LocationInfo = +// metrics4ReleaseDate.width + HorizontalPadding; +// const availWidth = xPos4SourceInfo - xPos4LocationInfo; +// const leftPadding4LocationInfo = +// (availWidth - metrics4LocationInfo.width) / 2; + +// xPos4LocationInfo = +// xPos4LocationInfo + leftPadding4LocationInfo; +// context.fillText(locationInfo, xPos4LocationInfo, y); +// } + +// return { +// src: frameCanvas.toDataURL(), +// delay: speed * 1000, +// }; +// }); + +// gifStream.createGIF( +// { +// gifWidth: frameData[0].width, +// gifHeight: frameData[0].height, +// images, +// progressCallback: (progress: number) => { +// // console.log(progress) +// }, +// }, +// (res: ResponseCreateGIF) => { +// // this.onGifComplete(obj, width, height); +// // console.log(res) + +// if (res.error) { +// reject(res.error); +// } + +// // donwload(res.blob, outputFileName) +// resolve(res.blob); +// } +// ); +// }); +// }; + +// const DownloadGIFDialog: React.FC = ({ +// frameData, +// speed, +// rNum2Exclude, +// }) => { +// const dispatch = useDispatch(); + +// const [isDownloading, setIsDownloading] = useState(false); + +// const [outputFileName, setOutputFileName] = useState( +// 'wayback-imagery-animation' +// ); + +// const isCancelled = useRef(false); + +// const closeDialog = useCallback(() => { +// dispatch(isDownloadGIFDialogOnToggled()); +// }, []); + +// const downloadBtnOnClick = async () => { +// setIsDownloading(true); + +// const data = !rNum2Exclude.length +// ? frameData +// : frameData.filter( +// (d) => rNum2Exclude.indexOf(d.releaseNum) === -1 +// ); + +// try { +// const blob = await saveAsGIF({ +// frameData: data, +// // outputFileName, +// speed, +// }); + +// if (isCancelled.current) { +// console.log('gif task has been cancelled'); +// return; +// } + +// donwload(blob, outputFileName); + +// closeDialog(); +// } catch (err) { +// console.error(err); +// } +// }; + +// const getContent = () => { +// if (isDownloading) { +// return ( +// <> +//
+//

+// Generating animated GIF file... +//

+//

Your download will begin shortly.

+//
+ +//
+//
{ +// gifStream.cancel(); +// isCancelled.current = true; +// closeDialog(); +// }} +// > +// Cancel +//
+//
+ +//
+// +//
+// +// ); +// } + +// return ( +// <> +//
+// Download GIF +//
+ +//
+//
+// File name: +// +//
+//
+ +//
+//
+// Cancel +//
+//
+// Download +//
+//
+// +// ); +// }; + +// return ( +//
+//
+// {getContent()} +//
+//
+// ); +// }; + +// export default DownloadGIFDialog; diff --git a/src/components/AnimationPanel/ImageAutoPlay.tsx b/src/components/AnimationPanel/ImageAutoPlay.tsx index 2d39229..7a78bf2 100644 --- a/src/components/AnimationPanel/ImageAutoPlay.tsx +++ b/src/components/AnimationPanel/ImageAutoPlay.tsx @@ -1,54 +1,54 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useEffect, useRef, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { indexOfCurrentAnimationFrameSelector } from '@store/AnimationMode/reducer'; - -import { FrameData } from './generateFrames4GIF'; - -type Props = { - frameData: FrameData[]; -}; - -const ImageAutoPlay: React.FC = ({ frameData }: Props) => { - const idx = useSelector(indexOfCurrentAnimationFrameSelector); - - // const isPlaying = useSelector(isAnimationPlayingSelector) - - const getCurrentFrame = () => { - if (!frameData || !frameData.length) { - return null; - } - - const { frameDataURI } = frameData[idx] || frameData[0]; - - return ( -
- ); - }; - - return getCurrentFrame(); -}; - -export default ImageAutoPlay; +// /* Copyright 2024 Esri +// * +// * Licensed under the Apache License Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ + +// import React, { useEffect, useRef, useState } from 'react'; +// import { useSelector } from 'react-redux'; +// import { indexOfCurrentAnimationFrameSelector } from '@store/AnimationMode/reducer'; + +// import { FrameData } from './generateFrames4GIF'; + +// type Props = { +// frameData: FrameData[]; +// }; + +// const ImageAutoPlay: React.FC = ({ frameData }: Props) => { +// const idx = useSelector(indexOfCurrentAnimationFrameSelector); + +// // const isPlaying = useSelector(isAnimationPlayingSelector) + +// const getCurrentFrame = () => { +// if (!frameData || !frameData.length) { +// return null; +// } + +// const { frameDataURI } = frameData[idx] || frameData[0]; + +// return ( +//
+// ); +// }; + +// return getCurrentFrame(); +// }; + +// export default ImageAutoPlay; diff --git a/src/store/AnimationMode/reducer.ts b/src/store/AnimationMode/reducer.ts index a11987a..6718082 100644 --- a/src/store/AnimationMode/reducer.ts +++ b/src/store/AnimationMode/reducer.ts @@ -36,16 +36,17 @@ export type AnimationModeState = { */ animationStatus?: AnimationStatus; isAnimationModeOn: boolean; - isDownloadGIFDialogOn: boolean; - // rNum4AnimationFrames: number[], + showDownloadAnimationPanel: boolean; waybackItems4Animation: IWaybackItem[]; - // array of release numbers for items to be excluded from the animation + /** + * array of release numbers for items to be excluded from the animation + */ rNum2Exclude: number[]; - // animation speed in second + /** + * animation speed in second + */ animationSpeed: number; - // isPlaying: boolean; - indexOfCurrentFrame: number; - isLoadingFrameData: boolean; + releaseNumberOfActiveAnimationFrame: number; }; export const DEFAULT_ANIMATION_SPEED_IN_SECONDS = 1; @@ -53,14 +54,15 @@ export const DEFAULT_ANIMATION_SPEED_IN_SECONDS = 1; export const initialAnimationModeState = { animationStatus: null, isAnimationModeOn: false, - isDownloadGIFDialogOn: false, + showDownloadAnimationPanel: false, // rNum4AnimationFrames: [], waybackItems4Animation: [], rNum2Exclude: [], animationSpeed: DEFAULT_ANIMATION_SPEED_IN_SECONDS, // isPlaying: true, indexOfCurrentFrame: 0, - isLoadingFrameData: true, + // isLoadingFrameData: true, + releaseNumberOfActiveAnimationFrame: null, } as AnimationModeState; const slice = createSlice({ @@ -76,12 +78,10 @@ const slice = createSlice({ ) => { state.animationStatus = action.payload; }, - isDownloadGIFDialogOnToggled: (state: AnimationModeState) => { - state.isDownloadGIFDialogOn = !state.isDownloadGIFDialogOn; + showDownloadAnimationPanelToggled: (state: AnimationModeState) => { + state.showDownloadAnimationPanel = + !state.showDownloadAnimationPanel; }, - // rNum4AnimationFramesLoaded: (state:AnimationModeState, action:PayloadAction)=>{ - // state.rNum4AnimationFrames = action.payload - // }, waybackItems4AnimationLoaded: ( state: AnimationModeState, action: PayloadAction @@ -111,29 +111,17 @@ const slice = createSlice({ ) => { state.animationSpeed = action.payload; }, - // isAnimationPlayingToggled: ( - // state: AnimationModeState, - // action: PayloadAction - // ) => { - // state.isPlaying = action.payload; - // }, - indexOfActiveAnimationFrameChanged: ( - state: AnimationModeState, - action: PayloadAction - ) => { - state.indexOfCurrentFrame = action.payload; - }, - isLoadingFrameDataToggled: ( - state: AnimationModeState, - action: PayloadAction - ) => { - state.isLoadingFrameData = action.payload; - }, resetAnimationMode: (state: AnimationModeState) => { // state.isPlaying = true; // state.animationSpeed = DEFAULT_ANIMATION_SPEED_IN_SECONDS; state.rNum2Exclude = []; }, + releaseNumberOfActiveAnimationFrameChanged: ( + state: AnimationModeState, + action: PayloadAction + ) => { + state.releaseNumberOfActiveAnimationFrame = action.payload; + }, }, }); @@ -142,37 +130,15 @@ const { reducer } = slice; export const { animationStatusChanged, isAnimationModeOnToggled, - isDownloadGIFDialogOnToggled, + showDownloadAnimationPanelToggled, waybackItems4AnimationLoaded, rNum2ExcludeToggled, rNum2ExcludeReset, animationSpeedChanged, - // isAnimationPlayingToggled, - indexOfActiveAnimationFrameChanged, - isLoadingFrameDataToggled, resetAnimationMode, + releaseNumberOfActiveAnimationFrameChanged, } = slice.actions; -// export const toggleIsLoadingFrameData = -// (isLoading: boolean) => -// (dispatch: StoreDispatch, getState: StoreGetState) => { -// const { AnimationMode } = getState(); - -// const { isPlaying } = AnimationMode; - -// batch(() => { -// dispatch(isLoadingFrameDataToggled(isLoading)); - -// if (isLoading) { -// dispatch(indexOfCurrentFrameChanged(0)); -// } - -// if (!isLoading && isPlaying) { -// dispatch(startAnimation()); -// } -// }); -// }; - export const toggleAnimationMode = () => (dispatch: StoreDispatch, getState: StoreGetState) => { const { SwipeView, AnimationMode } = getState(); @@ -196,12 +162,6 @@ export const toggleAnimationMode = ); dispatch(isAnimationModeOnToggled()); - - dispatch( - animationStatusChanged( - willAnimationModeBeTurnedOn ? 'loading' : null - ) - ); }; export const selectAnimationStatus = createSelector( @@ -214,9 +174,9 @@ export const isAnimationModeOnSelector = createSelector( (isAnimationModeOn) => isAnimationModeOn ); -export const isDownloadGIFDialogOnSelector = createSelector( - (state: RootState) => state.AnimationMode.isDownloadGIFDialogOn, - (isDownloadGIFDialogOn) => isDownloadGIFDialogOn +export const selectShouldShowDownloadPanel = createSelector( + (state: RootState) => state.AnimationMode.showDownloadAnimationPanel, + (showDownloadAnimationPanelToggled) => showDownloadAnimationPanelToggled ); export const waybackItems4AnimationSelector = createSelector( @@ -239,27 +199,33 @@ export const animationSpeedSelector = createSelector( // (isPlaying) => isPlaying // ); -export const indexOfCurrentAnimationFrameSelector = createSelector( - (state: RootState) => state.AnimationMode.indexOfCurrentFrame, - (indexOfCurrentFrame) => indexOfCurrentFrame -); +// export const indexOfCurrentAnimationFrameSelector = createSelector( +// (state: RootState) => state.AnimationMode.indexOfCurrentFrame, +// (indexOfCurrentFrame) => indexOfCurrentFrame +// ); -export const waybackItem4CurrentAnimationFrameSelector = createSelector( - (state: RootState) => state.AnimationMode.indexOfCurrentFrame, - (state: RootState) => state.AnimationMode.waybackItems4Animation, - (state: RootState) => state.AnimationMode.isLoadingFrameData, - (indexOfCurrentFrame, waybackItems4Animation, isLoadingFrameData) => { - if (!waybackItems4Animation.length || isLoadingFrameData) { - return null; - } +// export const waybackItem4CurrentAnimationFrameSelector = createSelector( +// (state: RootState) => state.AnimationMode.indexOfCurrentFrame, +// (state: RootState) => state.AnimationMode.waybackItems4Animation, +// (state: RootState) => state.AnimationMode.isLoadingFrameData, +// (indexOfCurrentFrame, waybackItems4Animation, isLoadingFrameData) => { +// if (!waybackItems4Animation.length || isLoadingFrameData) { +// return null; +// } + +// return waybackItems4Animation[indexOfCurrentFrame]; +// } +// ); - return waybackItems4Animation[indexOfCurrentFrame]; - } -); +// export const isLoadingFrameDataSelector = createSelector( +// (state: RootState) => state.AnimationMode.isLoadingFrameData, +// (isLoadingFrameData) => isLoadingFrameData +// ); -export const isLoadingFrameDataSelector = createSelector( - (state: RootState) => state.AnimationMode.isLoadingFrameData, - (isLoadingFrameData) => isLoadingFrameData +export const selectReleaseNumberOfActiveAnimationFrame = createSelector( + (state: RootState) => + state.AnimationMode.releaseNumberOfActiveAnimationFrame, + (releaseNumberOfActiveAnimationFrame) => releaseNumberOfActiveAnimationFrame ); export default reducer; diff --git a/src/store/getPreloadedState.ts b/src/store/getPreloadedState.ts index 846e95c..ca74ee5 100644 --- a/src/store/getPreloadedState.ts +++ b/src/store/getPreloadedState.ts @@ -147,6 +147,7 @@ const getPreloadedState4AnimationMode = ( const state: AnimationModeState = { ...initialAnimationModeState, isAnimationModeOn: true, + animationStatus: 'loading', animationSpeed, rNum2Exclude: rNum4FramesToExclude, }; From 845c975d2bfa42ac2d0328e3648872eb3c0480c4 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 27 Feb 2024 14:30:03 -0800 Subject: [PATCH 04/56] feat: add mode to Map slice of Redux Store --- .../AnimationLayer/AnimationLayer.tsx | 11 ++++ src/store/AnimationMode/reducer.ts | 55 +++++++++++-------- src/store/Map/reducer.ts | 13 +++++ src/store/Swipe/reducer.ts | 33 ++++++----- src/store/getPreloadedState.ts | 21 ++++--- 5 files changed, 88 insertions(+), 45 deletions(-) diff --git a/src/components/AnimationLayer/AnimationLayer.tsx b/src/components/AnimationLayer/AnimationLayer.tsx index d3ad98c..5604b48 100644 --- a/src/components/AnimationLayer/AnimationLayer.tsx +++ b/src/components/AnimationLayer/AnimationLayer.tsx @@ -22,6 +22,7 @@ import { // animationSpeedChanged, animationSpeedSelector, animationStatusChanged, + isAnimationModeOnSelector, // indexOfActiveAnimationFrameChanged, releaseNumberOfActiveAnimationFrameChanged, selectAnimationStatus, @@ -42,6 +43,8 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { const mediaLayerRef = useRef(); + const isAnimationModeOn = useSelector(isAnimationModeOnSelector); + const animationStatus = useSelector(selectAnimationStatus); const animationSpeed = useSelector(animationSpeedSelector); @@ -109,6 +112,14 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { } }, [mediaLayerElements, mapView]); + useEffect(() => { + if (isAnimationModeOn) { + dispatch(animationStatusChanged('loading')); + } else { + dispatch(animationStatusChanged(null)); + } + }, [isAnimationModeOn]); + // useEffect(() => { // // We only need to save animation window information when the animation is in progress. // // Additionally, we should always reset the animation window information in the hash parameters diff --git a/src/store/AnimationMode/reducer.ts b/src/store/AnimationMode/reducer.ts index 6718082..e03031f 100644 --- a/src/store/AnimationMode/reducer.ts +++ b/src/store/AnimationMode/reducer.ts @@ -25,8 +25,9 @@ import { saveAnimationSpeedInURLQueryParam } from '@utils/UrlSearchParam'; // import { IWaybackItem } from '@typings/index'; import { RootState, StoreDispatch, StoreGetState } from '../configureStore'; +import { MapMode, mapModeChanged, selectMapMode } from '@store/Map/reducer'; -import { isSwipeWidgetOpenToggled } from '../Swipe/reducer'; +// import { isSwipeWidgetOpenToggled } from '../Swipe/reducer'; export type AnimationStatus = 'loading' | 'playing' | 'pausing'; @@ -35,7 +36,7 @@ export type AnimationModeState = { * status of the Animation mode */ animationStatus?: AnimationStatus; - isAnimationModeOn: boolean; + // isAnimationModeOn: boolean; showDownloadAnimationPanel: boolean; waybackItems4Animation: IWaybackItem[]; /** @@ -53,7 +54,7 @@ export const DEFAULT_ANIMATION_SPEED_IN_SECONDS = 1; export const initialAnimationModeState = { animationStatus: null, - isAnimationModeOn: false, + // isAnimationModeOn: false, showDownloadAnimationPanel: false, // rNum4AnimationFrames: [], waybackItems4Animation: [], @@ -69,9 +70,9 @@ const slice = createSlice({ name: 'AnimationMode', initialState: initialAnimationModeState, reducers: { - isAnimationModeOnToggled: (state: AnimationModeState) => { - state.isAnimationModeOn = !state.isAnimationModeOn; - }, + // isAnimationModeOnToggled: (state: AnimationModeState) => { + // state.isAnimationModeOn = !state.isAnimationModeOn; + // }, animationStatusChanged: ( state, action: PayloadAction @@ -129,7 +130,7 @@ const { reducer } = slice; export const { animationStatusChanged, - isAnimationModeOnToggled, + // isAnimationModeOnToggled, showDownloadAnimationPanelToggled, waybackItems4AnimationLoaded, rNum2ExcludeToggled, @@ -141,27 +142,33 @@ export const { export const toggleAnimationMode = () => (dispatch: StoreDispatch, getState: StoreGetState) => { - const { SwipeView, AnimationMode } = getState(); + const mode = selectMapMode(getState()); + + const newMode: MapMode = mode === 'animation' ? 'explore' : 'animation'; + + dispatch(mapModeChanged(newMode)); + + // const { SwipeView, AnimationMode } = getState(); - const { isAnimationModeOn, animationSpeed } = AnimationMode; + // const { isAnimationModeOn, animationSpeed } = AnimationMode; - const willAnimationModeBeTurnedOn = !isAnimationModeOn; + // const willAnimationModeBeTurnedOn = !isAnimationModeOn; - if (SwipeView.isSwipeWidgetOpen && willAnimationModeBeTurnedOn) { - dispatch(isSwipeWidgetOpenToggled()); - } + // if (SwipeView.isSwipeWidgetOpen && willAnimationModeBeTurnedOn) { + // dispatch(isSwipeWidgetOpenToggled()); + // } - if (isAnimationModeOn) { - console.log('reset animation mode'); - dispatch(resetAnimationMode()); - } + // if (isAnimationModeOn) { + // console.log('reset animation mode'); + // dispatch(resetAnimationMode()); + // } - saveAnimationSpeedInURLQueryParam( - willAnimationModeBeTurnedOn, - animationSpeed - ); + // saveAnimationSpeedInURLQueryParam( + // willAnimationModeBeTurnedOn, + // animationSpeed + // ); - dispatch(isAnimationModeOnToggled()); + // dispatch(isAnimationModeOnToggled()); }; export const selectAnimationStatus = createSelector( @@ -170,8 +177,8 @@ export const selectAnimationStatus = createSelector( ); export const isAnimationModeOnSelector = createSelector( - (state: RootState) => state.AnimationMode.isAnimationModeOn, - (isAnimationModeOn) => isAnimationModeOn + (state: RootState) => state.Map.mode, + (mode) => mode === 'animation' ); export const selectShouldShowDownloadPanel = createSelector( diff --git a/src/store/Map/reducer.ts b/src/store/Map/reducer.ts index d2a3d4b..1bf97e0 100644 --- a/src/store/Map/reducer.ts +++ b/src/store/Map/reducer.ts @@ -32,7 +32,10 @@ export type MapCenter = { lat: number; }; +export type MapMode = 'explore' | 'swipe' | 'animation'; + export type MapState = { + mode: MapMode; mapExtent: IExtentGeomety; metadataQueryResult: IWaybackMetadataQueryResult; metadataPopupAnchor: IScreenPoint; @@ -48,6 +51,7 @@ export type MapState = { }; export const initialMapState: MapState = { + mode: 'explore', mapExtent: null, metadataQueryResult: null, metadataPopupAnchor: null, @@ -60,6 +64,9 @@ const slice = createSlice({ name: 'Map', initialState: initialMapState, reducers: { + mapModeChanged: (state: MapState, action: PayloadAction) => { + state.mode = action.payload; + }, mapExtentUpdated: ( state: MapState, action: PayloadAction @@ -93,6 +100,7 @@ const slice = createSlice({ const { reducer } = slice; export const { + mapModeChanged, mapExtentUpdated, metadataQueryResultUpdated, metadataPopupAnchorUpdated, @@ -101,6 +109,11 @@ export const { zoomUpdated, } = slice.actions; +export const selectMapMode = createSelector( + (state: RootState) => state.Map.mode, + (mode) => mode +); + export const mapExtentSelector = createSelector( (state: RootState) => state.Map.mapExtent, (mapExtent) => mapExtent diff --git a/src/store/Swipe/reducer.ts b/src/store/Swipe/reducer.ts index e915ed9..bcbb54e 100644 --- a/src/store/Swipe/reducer.ts +++ b/src/store/Swipe/reducer.ts @@ -24,16 +24,17 @@ import { import { RootState, StoreDispatch, StoreGetState } from '../configureStore'; import { toggleAnimationMode } from '../AnimationMode/reducer'; +import { MapMode, mapModeChanged, selectMapMode } from '@store/Map/reducer'; export type SwipeViewState = { - isSwipeWidgetOpen: boolean; + // isSwipeWidgetOpen: boolean; releaseNum4LeadingLayer: number; releaseNum4TrailingLayer: number; swipePosition: number; }; export const initialSwipeViewState = { - isSwipeWidgetOpen: false, + // isSwipeWidgetOpen: false, releaseNum4LeadingLayer: null, releaseNum4TrailingLayer: null, swipePosition: 50, @@ -43,9 +44,9 @@ const slice = createSlice({ name: 'SwipeView', initialState: initialSwipeViewState, reducers: { - isSwipeWidgetOpenToggled: (state: SwipeViewState) => { - state.isSwipeWidgetOpen = !state.isSwipeWidgetOpen; - }, + // isSwipeWidgetOpenToggled: (state: SwipeViewState) => { + // state.isSwipeWidgetOpen = !state.isSwipeWidgetOpen; + // }, releaseNum4LeadingLayerUpdated: ( state: SwipeViewState, action: PayloadAction @@ -70,7 +71,7 @@ const slice = createSlice({ const { reducer } = slice; export const { - isSwipeWidgetOpenToggled, + // isSwipeWidgetOpenToggled, releaseNum4LeadingLayerUpdated, releaseNum4TrailingLayerUpdated, swipePositionUpdated, @@ -78,18 +79,24 @@ export const { export const toggleSwipeWidget = () => (dispatch: StoreDispatch, getState: StoreGetState) => { - const { AnimationMode } = getState(); + const mode = selectMapMode(getState()); + + const newMode: MapMode = mode === 'swipe' ? 'explore' : 'swipe'; + + dispatch(mapModeChanged(newMode)); + + // const { AnimationMode } = getState(); - if (AnimationMode.isAnimationModeOn) { - dispatch(toggleAnimationMode()); - } + // if (AnimationMode.isAnimationModeOn) { + // dispatch(toggleAnimationMode()); + // } - dispatch(isSwipeWidgetOpenToggled()); + // dispatch(isSwipeWidgetOpenToggled()); }; export const isSwipeWidgetOpenSelector = createSelector( - (state: RootState) => state.SwipeView.isSwipeWidgetOpen, - (isSwipeWidgetOpen) => isSwipeWidgetOpen + (state: RootState) => state.Map.mode, + (mode) => mode === 'swipe' ); export const swipeWidgetLeadingLayerSelector = createSelector( diff --git a/src/store/getPreloadedState.ts b/src/store/getPreloadedState.ts index ca74ee5..8298706 100644 --- a/src/store/getPreloadedState.ts +++ b/src/store/getPreloadedState.ts @@ -19,7 +19,7 @@ import { initialUIState, UIState } from './UI/reducer'; import { initialWaybackItemsState, WaybackItemsState } from './Wayback/reducer'; import { initialSwipeViewState, SwipeViewState } from './Swipe/reducer'; import { IURLParamData, IWaybackItem } from '../types'; -import { initialMapState, MapState } from './Map/reducer'; +import { initialMapState, MapMode, MapState } from './Map/reducer'; import { decodeURLParams, getMapCenterFromHashParams, @@ -47,10 +47,6 @@ import { const isMobile = miscFns.isMobileDevice(); const getPreloadedState4UI = (urlParams: IURLParamData): UIState => { - // const shouldOnlyShowItemsWithLocalChange = true - // urlParams.shouldOnlyShowItemsWithLocalChange || - // getShouldShowUpdatesWithLocalChanges(); - const state: UIState = { ...initialUIState, // shouldOnlyShowItemsWithLocalChange, @@ -103,7 +99,7 @@ const getPreloadedState4SwipeView = ( const state: SwipeViewState = { ...initialSwipeViewState, - isSwipeWidgetOpen, + // isSwipeWidgetOpen, releaseNum4LeadingLayer: rNum4SwipeWidgetLeadingLayer || rNum4ActiveWaybackItem || @@ -117,12 +113,21 @@ const getPreloadedState4SwipeView = ( }; const getPreloadedState4Map = (urlParams: IURLParamData): MapState => { - const { mapExtent } = urlParams; + const { mapExtent, animationSpeed, isSwipeWidgetOpen } = urlParams; const { center, zoom } = getMapCenterFromHashParams() || {}; + let mode: MapMode = 'explore'; + + if (isSwipeWidgetOpen) { + mode = 'swipe'; + } else if (animationSpeed) { + mode = 'animation'; + } + const state: MapState = { ...initialMapState, + mode, mapExtent, center, zoom, @@ -146,7 +151,7 @@ const getPreloadedState4AnimationMode = ( const state: AnimationModeState = { ...initialAnimationModeState, - isAnimationModeOn: true, + // isAnimationModeOn: true, animationStatus: 'loading', animationSpeed, rNum2Exclude: rNum4FramesToExclude, From 137ec61c586d388f59ed1cc89fd195d3edea5603 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 27 Feb 2024 14:43:56 -0800 Subject: [PATCH 05/56] feat: remove wyabckItems4Animation from AnimationMode --- .../AnimationControls/AnimationControls.tsx | 44 +++++-------------- .../AnimationLayer/AnimationLayer.tsx | 36 +++++---------- src/store/AnimationMode/reducer.ts | 43 +++++++++--------- src/utils/UrlSearchParam/index.ts | 7 +-- 4 files changed, 48 insertions(+), 82 deletions(-) diff --git a/src/components/AnimationControls/AnimationControls.tsx b/src/components/AnimationControls/AnimationControls.tsx index a5007fc..5096a55 100644 --- a/src/components/AnimationControls/AnimationControls.tsx +++ b/src/components/AnimationControls/AnimationControls.tsx @@ -26,7 +26,7 @@ import { } from '@store/Wayback/reducer'; import { - waybackItems4AnimationLoaded, + // waybackItems4AnimationLoaded, // rNum4AnimationFramesSelector, rNum2ExcludeSelector, // toggleAnimationFrame, @@ -54,8 +54,11 @@ import DonwloadGifButton from './DonwloadGifButton'; import FramesSeletor from './FramesSeletor'; import SpeedSelector from './SpeedSelector'; import PlayPauseBtn from './PlayPauseBtn'; -import { usePrevious } from '@hooks/usePrevious'; -import { saveFrames2ExcludeInURLQueryParam } from '@utils/UrlSearchParam'; +// import { usePrevious } from '@hooks/usePrevious'; +import { + saveAnimationSpeedInURLQueryParam, + saveFrames2ExcludeInURLQueryParam, +} from '@utils/UrlSearchParam'; const AnimationControls = () => { const dispatch = useDispatch(); @@ -63,26 +66,14 @@ const AnimationControls = () => { const rNum2ExcludeFromAnimation: number[] = useSelector(rNum2ExcludeSelector); - // const activeItem:IWaybackItem = useSelector(activeWaybackItemSelector); - const waybackItemsWithLocalChanges: IWaybackItem[] = useSelector( waybackItemsWithLocalChangesSelector ); - const prevWaybackItemsWithLocalChanges = usePrevious( - waybackItemsWithLocalChanges - ); - const animationSpeed = useSelector(animationSpeedSelector); - // const isPlaying = useSelector(isAnimationPlayingSelector); - const animationStatus = useSelector(selectAnimationStatus); - // const waybackItem4CurrentAnimationFrame = useSelector( - // waybackItem4CurrentAnimationFrameSelector - // ); - const releaseNum4ActiveFrame = useSelector( selectReleaseNumberOfActiveAnimationFrame ); @@ -140,8 +131,6 @@ const AnimationControls = () => { { // dispatch(indexOfActiveAnimationFrameChanged(rNum)); @@ -157,26 +146,17 @@ const AnimationControls = () => { ); }; - useEffect(() => { - batch(() => { - dispatch( - waybackItems4AnimationLoaded(waybackItemsWithLocalChanges) - ); - - if ( - prevWaybackItemsWithLocalChanges && - prevWaybackItemsWithLocalChanges.length - ) { - dispatch(rNum2ExcludeReset()); - } - }); - }, [waybackItemsWithLocalChanges]); - useEffect(() => { // console.log(rNum2ExcludeFromAnimation) saveFrames2ExcludeInURLQueryParam(rNum2ExcludeFromAnimation); }, [rNum2ExcludeFromAnimation]); + useEffect(() => { + saveAnimationSpeedInURLQueryParam( + animationStatus !== null ? animationSpeed : undefined + ); + }, [animationSpeed, animationStatus]); + return ( <>
= ({ mapView }: Props) => { const animationSpeed = useSelector(animationSpeedSelector); - const waybackItems = useSelector(waybackItems4AnimationSelector); + const waybackItems = useSelector(waybackItemsWithLocalChangesSelector); /** * Array of Imagery Elements for each scene in `sortedQueryParams4ScenesInAnimationMode` @@ -117,31 +120,16 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { dispatch(animationStatusChanged('loading')); } else { dispatch(animationStatusChanged(null)); + dispatch(rNum2ExcludeReset()); } }, [isAnimationModeOn]); - // useEffect(() => { - // // We only need to save animation window information when the animation is in progress. - // // Additionally, we should always reset the animation window information in the hash parameters - // // when the animation stops. Resetting the animation window information is crucial - // // as it ensures that the animation window information is not used if the user manually starts the animation. - // // Animation window information from the hash parameters should only be utilized by users - // // who open the application in animation mode through a link shared by others. - // const extent = animationStatus === 'playing' ? mapView.extent : null; - - // const width = animationStatus === 'playing' ? mapView.width : null; - - // const height = animationStatus === 'playing' ? mapView.height : null; - - // saveAnimationWindowInfoToHashParams(extent, width, height); - // }, [animationStatus]); - - // useEffect(() => { - // // should close download animation panel whenever user exits the animation mode - // if (animationStatus === null) { - // dispatch(showDownloadAnimationPanelChanged(false)); - // } - // }, [animationStatus]); + useEffect(() => { + // should close download animation panel whenever user exits the animation mode + if (animationStatus === null) { + dispatch(showDownloadAnimationPanelToggled(false)); + } + }, [animationStatus]); if (!animationStatus) { return null; diff --git a/src/store/AnimationMode/reducer.ts b/src/store/AnimationMode/reducer.ts index e03031f..f6dc1f1 100644 --- a/src/store/AnimationMode/reducer.ts +++ b/src/store/AnimationMode/reducer.ts @@ -36,9 +36,11 @@ export type AnimationModeState = { * status of the Animation mode */ animationStatus?: AnimationStatus; - // isAnimationModeOn: boolean; + /** + * if true, show download animation panel + */ showDownloadAnimationPanel: boolean; - waybackItems4Animation: IWaybackItem[]; + // waybackItems4Animation: IWaybackItem[]; /** * array of release numbers for items to be excluded from the animation */ @@ -47,6 +49,9 @@ export type AnimationModeState = { * animation speed in second */ animationSpeed: number; + /** + * release number of wayback item that is being displayed as current animation frame + */ releaseNumberOfActiveAnimationFrame: number; }; @@ -79,16 +84,18 @@ const slice = createSlice({ ) => { state.animationStatus = action.payload; }, - showDownloadAnimationPanelToggled: (state: AnimationModeState) => { - state.showDownloadAnimationPanel = - !state.showDownloadAnimationPanel; - }, - waybackItems4AnimationLoaded: ( + showDownloadAnimationPanelToggled: ( state: AnimationModeState, - action: PayloadAction + action: PayloadAction ) => { - state.waybackItems4Animation = action.payload; + state.showDownloadAnimationPanel = action.payload; }, + // waybackItems4AnimationLoaded: ( + // state: AnimationModeState, + // action: PayloadAction + // ) => { + // state.waybackItems4Animation = action.payload; + // }, rNum2ExcludeToggled: ( state: AnimationModeState, action: PayloadAction @@ -112,11 +119,6 @@ const slice = createSlice({ ) => { state.animationSpeed = action.payload; }, - resetAnimationMode: (state: AnimationModeState) => { - // state.isPlaying = true; - // state.animationSpeed = DEFAULT_ANIMATION_SPEED_IN_SECONDS; - state.rNum2Exclude = []; - }, releaseNumberOfActiveAnimationFrameChanged: ( state: AnimationModeState, action: PayloadAction @@ -132,11 +134,10 @@ export const { animationStatusChanged, // isAnimationModeOnToggled, showDownloadAnimationPanelToggled, - waybackItems4AnimationLoaded, + // waybackItems4AnimationLoaded, rNum2ExcludeToggled, rNum2ExcludeReset, animationSpeedChanged, - resetAnimationMode, releaseNumberOfActiveAnimationFrameChanged, } = slice.actions; @@ -183,13 +184,13 @@ export const isAnimationModeOnSelector = createSelector( export const selectShouldShowDownloadPanel = createSelector( (state: RootState) => state.AnimationMode.showDownloadAnimationPanel, - (showDownloadAnimationPanelToggled) => showDownloadAnimationPanelToggled + (showDownloadAnimationPanel) => showDownloadAnimationPanel ); -export const waybackItems4AnimationSelector = createSelector( - (state: RootState) => state.AnimationMode.waybackItems4Animation, - (waybackItems4Animation) => waybackItems4Animation -); +// export const waybackItems4AnimationSelector = createSelector( +// (state: RootState) => state.AnimationMode.waybackItems4Animation, +// (waybackItems4Animation) => waybackItems4Animation +// ); export const rNum2ExcludeSelector = createSelector( (state: RootState) => state.AnimationMode.rNum2Exclude, diff --git a/src/utils/UrlSearchParam/index.ts b/src/utils/UrlSearchParam/index.ts index 815da8d..424bf69 100644 --- a/src/utils/UrlSearchParam/index.ts +++ b/src/utils/UrlSearchParam/index.ts @@ -132,12 +132,9 @@ const saveSwipeWidgetInfoInURLQueryParam: SaveSwipeWidgetInfoInURLQueryParam = updateHashParams(key, value); }; -const saveAnimationSpeedInURLQueryParam = ( - isAnimationOn: boolean, - speed: number -): void => { +const saveAnimationSpeedInURLQueryParam = (speed?: number): void => { const key: ParamKey = 'animationSpeed'; - const value = isAnimationOn ? speed.toString() : null; + const value = speed !== undefined ? speed.toString() : null; updateHashParams(key, value); }; From ee7015b330ce7086698f92646071d19243e6c897 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 27 Feb 2024 14:59:25 -0800 Subject: [PATCH 06/56] feat: update Animation Controls show loading indicator in PlayPauseButton toggleFrame of FrameSelector should call rNum2ExcludeToggled --- .../AnimationControls/AnimationControls.tsx | 4 +- .../AnimationControls/PlayPauseBtn.tsx | 16 ++++- src/store/AnimationMode/reducer.ts | 70 ------------------- 3 files changed, 17 insertions(+), 73 deletions(-) diff --git a/src/components/AnimationControls/AnimationControls.tsx b/src/components/AnimationControls/AnimationControls.tsx index 5096a55..87acbbb 100644 --- a/src/components/AnimationControls/AnimationControls.tsx +++ b/src/components/AnimationControls/AnimationControls.tsx @@ -45,6 +45,7 @@ import { animationStatusChanged, // indexOfActiveAnimationFrameChanged, selectReleaseNumberOfActiveAnimationFrame, + rNum2ExcludeToggled, // setActiveFrameByReleaseNum, } from '@store/AnimationMode/reducer'; @@ -120,6 +121,7 @@ const AnimationControls = () => { > @@ -137,7 +139,7 @@ const AnimationControls = () => { console.log(rNum); }} toggleFrame={(rNum) => { - // dispatch(toggleAnimationFrame(rNum)); + dispatch(rNum2ExcludeToggled(rNum)); }} releaseNum4ActiveFrame={releaseNum4ActiveFrame} isButtonDisabled={animationStatus === 'playing'} diff --git a/src/components/AnimationControls/PlayPauseBtn.tsx b/src/components/AnimationControls/PlayPauseBtn.tsx index 05dad90..4f78f51 100644 --- a/src/components/AnimationControls/PlayPauseBtn.tsx +++ b/src/components/AnimationControls/PlayPauseBtn.tsx @@ -16,6 +16,7 @@ import React, { useEffect } from 'react'; type Props = { + isLoading: boolean; isPlaying: boolean; onClick: () => void; }; @@ -44,7 +45,18 @@ const PauseBtn = ( ); -const PlayPauseBtn: React.FC = ({ isPlaying, onClick }: Props) => { +const PlayPauseBtn: React.FC = ({ + isLoading, + isPlaying, + onClick, +}: Props) => { + const getIcon = () => { + if (isLoading) { + return ; + } + + return isPlaying ? PauseBtn : PlayBtn; + }; return (
= ({ isPlaying, onClick }: Props) => { }} onClick={onClick} > - {isPlaying ? PauseBtn : PlayBtn} + {getIcon()}
); }; diff --git a/src/store/AnimationMode/reducer.ts b/src/store/AnimationMode/reducer.ts index f6dc1f1..98de1dc 100644 --- a/src/store/AnimationMode/reducer.ts +++ b/src/store/AnimationMode/reducer.ts @@ -40,7 +40,6 @@ export type AnimationModeState = { * if true, show download animation panel */ showDownloadAnimationPanel: boolean; - // waybackItems4Animation: IWaybackItem[]; /** * array of release numbers for items to be excluded from the animation */ @@ -59,15 +58,10 @@ export const DEFAULT_ANIMATION_SPEED_IN_SECONDS = 1; export const initialAnimationModeState = { animationStatus: null, - // isAnimationModeOn: false, showDownloadAnimationPanel: false, - // rNum4AnimationFrames: [], waybackItems4Animation: [], rNum2Exclude: [], animationSpeed: DEFAULT_ANIMATION_SPEED_IN_SECONDS, - // isPlaying: true, - indexOfCurrentFrame: 0, - // isLoadingFrameData: true, releaseNumberOfActiveAnimationFrame: null, } as AnimationModeState; @@ -75,9 +69,6 @@ const slice = createSlice({ name: 'AnimationMode', initialState: initialAnimationModeState, reducers: { - // isAnimationModeOnToggled: (state: AnimationModeState) => { - // state.isAnimationModeOn = !state.isAnimationModeOn; - // }, animationStatusChanged: ( state, action: PayloadAction @@ -90,12 +81,6 @@ const slice = createSlice({ ) => { state.showDownloadAnimationPanel = action.payload; }, - // waybackItems4AnimationLoaded: ( - // state: AnimationModeState, - // action: PayloadAction - // ) => { - // state.waybackItems4Animation = action.payload; - // }, rNum2ExcludeToggled: ( state: AnimationModeState, action: PayloadAction @@ -148,28 +133,6 @@ export const toggleAnimationMode = const newMode: MapMode = mode === 'animation' ? 'explore' : 'animation'; dispatch(mapModeChanged(newMode)); - - // const { SwipeView, AnimationMode } = getState(); - - // const { isAnimationModeOn, animationSpeed } = AnimationMode; - - // const willAnimationModeBeTurnedOn = !isAnimationModeOn; - - // if (SwipeView.isSwipeWidgetOpen && willAnimationModeBeTurnedOn) { - // dispatch(isSwipeWidgetOpenToggled()); - // } - - // if (isAnimationModeOn) { - // console.log('reset animation mode'); - // dispatch(resetAnimationMode()); - // } - - // saveAnimationSpeedInURLQueryParam( - // willAnimationModeBeTurnedOn, - // animationSpeed - // ); - - // dispatch(isAnimationModeOnToggled()); }; export const selectAnimationStatus = createSelector( @@ -187,11 +150,6 @@ export const selectShouldShowDownloadPanel = createSelector( (showDownloadAnimationPanel) => showDownloadAnimationPanel ); -// export const waybackItems4AnimationSelector = createSelector( -// (state: RootState) => state.AnimationMode.waybackItems4Animation, -// (waybackItems4Animation) => waybackItems4Animation -// ); - export const rNum2ExcludeSelector = createSelector( (state: RootState) => state.AnimationMode.rNum2Exclude, (rNum2Exclude) => rNum2Exclude @@ -202,34 +160,6 @@ export const animationSpeedSelector = createSelector( (animationSpeed) => animationSpeed ); -// export const isAnimationPlayingSelector = createSelector( -// (state: RootState) => state.AnimationMode.isPlaying, -// (isPlaying) => isPlaying -// ); - -// export const indexOfCurrentAnimationFrameSelector = createSelector( -// (state: RootState) => state.AnimationMode.indexOfCurrentFrame, -// (indexOfCurrentFrame) => indexOfCurrentFrame -// ); - -// export const waybackItem4CurrentAnimationFrameSelector = createSelector( -// (state: RootState) => state.AnimationMode.indexOfCurrentFrame, -// (state: RootState) => state.AnimationMode.waybackItems4Animation, -// (state: RootState) => state.AnimationMode.isLoadingFrameData, -// (indexOfCurrentFrame, waybackItems4Animation, isLoadingFrameData) => { -// if (!waybackItems4Animation.length || isLoadingFrameData) { -// return null; -// } - -// return waybackItems4Animation[indexOfCurrentFrame]; -// } -// ); - -// export const isLoadingFrameDataSelector = createSelector( -// (state: RootState) => state.AnimationMode.isLoadingFrameData, -// (isLoadingFrameData) => isLoadingFrameData -// ); - export const selectReleaseNumberOfActiveAnimationFrame = createSelector( (state: RootState) => state.AnimationMode.releaseNumberOfActiveAnimationFrame, From eaede68783553b322f4aba0fb9deb97d05479a73 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 27 Feb 2024 15:17:23 -0800 Subject: [PATCH 07/56] feat: hide Map Controls when Animation Mode is on --- src/components/MapView/CustomMapViewStyle.css | 4 ++++ src/components/MapView/MapViewConatiner.tsx | 13 ++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 src/components/MapView/CustomMapViewStyle.css diff --git a/src/components/MapView/CustomMapViewStyle.css b/src/components/MapView/CustomMapViewStyle.css new file mode 100644 index 0000000..bebc9f1 --- /dev/null +++ b/src/components/MapView/CustomMapViewStyle.css @@ -0,0 +1,4 @@ +.hide-map-control .esri-zoom, +.hide-map-control .esri-search { + display: none; +} \ No newline at end of file diff --git a/src/components/MapView/MapViewConatiner.tsx b/src/components/MapView/MapViewConatiner.tsx index fa20ff2..0bf9713 100644 --- a/src/components/MapView/MapViewConatiner.tsx +++ b/src/components/MapView/MapViewConatiner.tsx @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import './CustomMapViewStyle.css'; import React, { useContext, useEffect, useMemo } from 'react'; import { useSelector, useDispatch } from 'react-redux'; @@ -43,6 +43,10 @@ import { } from '@utils/UrlSearchParam'; import { batch } from 'react-redux'; import { getWaybackItemsWithLocalChanges } from '@vannizhang/wayback-core'; +import { + isAnimationModeOnSelector, + selectAnimationStatus, +} from '@store/AnimationMode/reducer'; type Props = { children?: React.ReactNode; @@ -75,6 +79,8 @@ const MapViewConatiner: React.FC = ({ children }) => { const mapExtent = useSelector(mapExtentSelector); + const isAnimationModeOn = useSelector(isAnimationModeOnSelector); + const { center, zoom } = useSelector(selectMapCenterAndZoom); const defaultMapExtent = useMemo((): IExtentGeomety => { @@ -124,6 +130,11 @@ const MapViewConatiner: React.FC = ({ children }) => { saveMapCenterToHashParams(center, zoom); }, [center, zoom]); + useEffect(() => { + // adding this class will hide map zoom widget when animation mode is on + document.body.classList.toggle('hide-map-control', isAnimationModeOn); + }, [isAnimationModeOn]); + return ( Date: Tue, 27 Feb 2024 15:27:31 -0800 Subject: [PATCH 08/56] chore: remove Animation Panel folder --- .../generateAnimationFrames.ts} | 26 +- .../useMediaLayerImageElement.tsx | 12 +- .../AnimationPanel/AnimationPanel.tsx | 260 ------------- .../AnimationPanelContainer.tsx | 54 --- src/components/AnimationPanel/Background.tsx | 59 --- src/components/AnimationPanel/CloseBtn.tsx | 56 --- .../AnimationPanel/DownloadGIFDialog.tsx | 342 ------------------ .../AnimationPanel/ImageAutoPlay.tsx | 54 --- .../AnimationPanel/LoadingIndicator.tsx | 49 --- .../AnimationPanel/LoadingSpinner.css | 26 -- .../AnimationPanel/LoadingSpinner.tsx | 56 --- src/components/AnimationPanel/Resizable.tsx | 216 ----------- 12 files changed, 18 insertions(+), 1192 deletions(-) rename src/components/{AnimationPanel/generateFrames4GIF.ts => AnimationLayer/generateAnimationFrames.ts} (94%) delete mode 100644 src/components/AnimationPanel/AnimationPanel.tsx delete mode 100644 src/components/AnimationPanel/AnimationPanelContainer.tsx delete mode 100644 src/components/AnimationPanel/Background.tsx delete mode 100644 src/components/AnimationPanel/CloseBtn.tsx delete mode 100644 src/components/AnimationPanel/DownloadGIFDialog.tsx delete mode 100644 src/components/AnimationPanel/ImageAutoPlay.tsx delete mode 100644 src/components/AnimationPanel/LoadingIndicator.tsx delete mode 100644 src/components/AnimationPanel/LoadingSpinner.css delete mode 100644 src/components/AnimationPanel/LoadingSpinner.tsx delete mode 100644 src/components/AnimationPanel/Resizable.tsx diff --git a/src/components/AnimationPanel/generateFrames4GIF.ts b/src/components/AnimationLayer/generateAnimationFrames.ts similarity index 94% rename from src/components/AnimationPanel/generateFrames4GIF.ts rename to src/components/AnimationLayer/generateAnimationFrames.ts index 695dadd..3e1968d 100644 --- a/src/components/AnimationPanel/generateFrames4GIF.ts +++ b/src/components/AnimationLayer/generateAnimationFrames.ts @@ -55,19 +55,19 @@ type CenterLocationForFrameRect = { }; export type FrameData = { - releaseNum: number; - waybackItem: IWaybackItem; + // releaseNum: number; + // waybackItem: IWaybackItem; frameCanvas: HTMLCanvasElement; - frameDataURI?: string; frameBlob: Blob; - height: number; - width: number; - center: CenterLocationForFrameRect; + // frameDataURI?: string; + // height: number; + // width: number; + // center: CenterLocationForFrameRect; }; const WaybackImageBaseURL = getServiceUrl('wayback-imagery-base'); -export const generateFrames = async ({ +export const generateAnimationFrames = async ({ frameRect, mapView, waybackItems, @@ -95,14 +95,14 @@ export const generateFrames = async ({ }); frames.push({ - releaseNum, - waybackItem: item, + // releaseNum, + // waybackItem: item, frameCanvas, - frameDataURI: '', + // frameDataURI: '', frameBlob, - width: frameRect.width, - height: frameRect.height, - center, + // width: frameRect.width, + // height: frameRect.height, + // center, }); } diff --git a/src/components/AnimationLayer/useMediaLayerImageElement.tsx b/src/components/AnimationLayer/useMediaLayerImageElement.tsx index 8a7eee3..36be3a4 100644 --- a/src/components/AnimationLayer/useMediaLayerImageElement.tsx +++ b/src/components/AnimationLayer/useMediaLayerImageElement.tsx @@ -20,11 +20,9 @@ import ImageElement from '@arcgis/core/layers/support/ImageElement'; import ExtentAndRotationGeoreference from '@arcgis/core/layers/support/ExtentAndRotationGeoreference'; import { IWaybackItem } from '@typings/index'; import { getNormalizedExtent } from '@utils/snippets/getNormalizedExtent'; -import { - FrameData, - generateFrames, -} from '@components/AnimationPanel/generateFrames4GIF'; -import { PARENT_CONTAINER_LEFT_OFFSET } from '@components/AnimationPanel/AnimationPanel'; +import { generateAnimationFrames, FrameData } from './generateAnimationFrames'; + +export const MAP_CONTAINER_LEFT_OFFSET = 350; type Props = { mapView?: MapView; @@ -64,9 +62,9 @@ export const useMediaLayerImageElement = ({ const elemRect = container.getBoundingClientRect(); // console.log(elemRect) - const frameData = await generateFrames({ + const frameData = await generateAnimationFrames({ frameRect: { - screenX: elemRect.left - PARENT_CONTAINER_LEFT_OFFSET, + screenX: elemRect.left - MAP_CONTAINER_LEFT_OFFSET, screenY: elemRect.top, width, height, diff --git a/src/components/AnimationPanel/AnimationPanel.tsx b/src/components/AnimationPanel/AnimationPanel.tsx deleted file mode 100644 index 180d28c..0000000 --- a/src/components/AnimationPanel/AnimationPanel.tsx +++ /dev/null @@ -1,260 +0,0 @@ -// /* Copyright 2024 Esri -// * -// * Licensed under the Apache License Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. -// */ - -// import React, { useCallback, useEffect, useRef, useState } from 'react'; - -// import MapView from '@arcgis/core/views/MapView'; - -// import { FrameData, generateFrames } from './generateFrames4GIF'; - -// import Resizable from './Resizable'; -// import ImageAutoPlay from './ImageAutoPlay'; -// import LoadingIndicator from './LoadingIndicator'; -// import DownloadGIFDialog from './DownloadGIFDialog'; -// import CloseBtn from './CloseBtn'; - -// // import { whenFalse } from '@arcgis/core/core/watchUtils'; -// import { IWaybackItem } from '@typings/index'; -// import { useDispatch, useSelector } from 'react-redux'; - -// import { -// animationSpeedSelector, -// // indexOfCurrentFrameChanged, -// // isAnimationModeOnSelector, -// isDownloadGIFDialogOnSelector, -// rNum2ExcludeSelector, -// // startAnimation, -// toggleIsLoadingFrameData, -// } from '@store/AnimationMode/reducer'; -// import Background from './Background'; -// import { watch } from '@arcgis/core/core/reactiveUtils'; - -// type Props = { -// waybackItems4Animation: IWaybackItem[]; -// mapView?: MapView; -// }; - -// type GetFramesParams = { -// waybackItems: IWaybackItem[]; -// // releaseNums:number[], -// container: HTMLDivElement; -// mapView: MapView; -// }; - -// type GetFramesResponse = { -// data: FrameData[]; -// taskInfo: string; -// }; - -// type GetTaskFingerPrintParams = { -// container: HTMLDivElement; -// mapView: MapView; -// }; - -// // width of Gutter and Side Bar, need to calculate this dynamically -export const PARENT_CONTAINER_LEFT_OFFSET = 350; - -// const getFrames = async ({ -// waybackItems, -// // releaseNums, -// container, -// mapView, -// }: GetFramesParams): Promise => { -// const taskInfo = getAnimationTaskInfo({ -// container, -// mapView, -// }); - -// const elemRect = container.getBoundingClientRect(); -// // console.log(elemRect) - -// const { offsetHeight, offsetWidth } = container; -// // const releaseNums = waybackItems.map(d=>d.releaseNum) - -// const data = await generateFrames({ -// frameRect: { -// screenX: elemRect.left - PARENT_CONTAINER_LEFT_OFFSET, -// screenY: elemRect.top, -// width: offsetWidth, -// height: offsetHeight, -// }, -// mapView, -// waybackItems, -// }); - -// return { -// data, -// taskInfo, -// }; -// }; - -// // get a string that represent current Map (extent) and UI State (size, position of the Resize component) -// // this string will be used as a finger print to check if frame data returned by getFrames match the current Map and UI state, -// // sometimes multiple getFrames calls can be triggered (zoom the map after resizing the window) and we should only show the response from the last request -// const getAnimationTaskInfo = ({ -// container, -// mapView, -// }: GetTaskFingerPrintParams): string => { -// if (!mapView || !container) { -// return ''; -// } - -// const { xmax, xmin, ymax, ymin } = mapView.extent; - -// const { left, top } = container.getBoundingClientRect(); - -// const { offsetHeight, offsetWidth } = container; - -// return [xmax, xmin, ymax, ymin, left, top, offsetHeight, offsetWidth].join( -// '#' -// ); -// }; - -// const containerRef = React.createRef(); - -// const AnimationPanel: React.FC = ({ -// waybackItems4Animation, -// mapView, -// }: Props) => { -// const dispatch = useDispatch(); - -// // array of frame images as dataURI string -// const [frameData, setFrameData] = useState(null); - -// const loadingWaybackItems4AnimationRef = useRef(false); - -// const getAnimationFramesDelay = useRef(); - -// const waybackItems4AnimationRef = useRef(); - -// const isDownloadGIFDialogOn = useSelector(isDownloadGIFDialogOnSelector); - -// // in second -// const animationSpeed = useSelector(animationSpeedSelector); - -// // release numbers for the frames to be excluded from animation -// const rNum2Exclude = useSelector(rNum2ExcludeSelector); - -// const getAnimationFrames = useCallback(() => { -// // in milliseconds -// const DELAY_TIME = 1500; - -// clearTimeout(getAnimationFramesDelay.current); - -// getAnimationFramesDelay.current = setTimeout(async () => { -// try { -// const waybackItems = waybackItems4AnimationRef.current; - -// const container = containerRef.current; - -// if ( -// !waybackItems || -// !waybackItems.length || -// loadingWaybackItems4AnimationRef.current -// ) { -// return; -// } - -// const { data, taskInfo } = await getFrames({ -// waybackItems, -// container, -// mapView, -// }); - -// if (taskInfo !== getAnimationTaskInfo({ mapView, container })) { -// console.error( -// "animation task info doesn't match current map or UI state, ignore frame data returned by this task" -// ); -// return; -// } - -// setFrameData(data); -// } catch (err) { -// console.error(err); -// } -// }, DELAY_TIME); -// }, [waybackItems4Animation]); - -// const resizableOnChange = useCallback(() => { -// setFrameData(null); - -// getAnimationFrames(); -// }, []); - -// useEffect(() => { -// waybackItems4AnimationRef.current = waybackItems4Animation; - -// loadingWaybackItems4AnimationRef.current = false; - -// getAnimationFrames(); -// }, [waybackItems4Animation]); - -// useEffect(() => { -// // const onUpdating = whenFalse(mapView, 'stationary', () => { -// // loadingWaybackItems4AnimationRef.current = true; -// // setFrameData(null); -// // }); - -// watch( -// () => mapView.stationary, -// () => { -// if (!mapView.stationary) { -// loadingWaybackItems4AnimationRef.current = true; -// setFrameData(null); -// } -// } -// ); - -// // return () => { -// // // onStationary.remove(); -// // onUpdating.remove(); -// // }; -// }, []); - -// useEffect(() => { -// const isLoading = frameData === null; -// dispatch(toggleIsLoadingFrameData(isLoading)); -// }, [frameData]); - -// return ( -// <> -// - -// -// {frameData && frameData.length ? ( -// -// ) : ( -// -// )} -// - -// - -// {isDownloadGIFDialogOn ? ( -// -// ) : null} -// -// ); -// }; - -// export default AnimationPanel; diff --git a/src/components/AnimationPanel/AnimationPanelContainer.tsx b/src/components/AnimationPanel/AnimationPanelContainer.tsx deleted file mode 100644 index 43e3e2a..0000000 --- a/src/components/AnimationPanel/AnimationPanelContainer.tsx +++ /dev/null @@ -1,54 +0,0 @@ -// /* Copyright 2024 Esri -// * -// * Licensed under the Apache License Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. -// */ - -// import React, { useEffect } from 'react'; - -// import { useSelector } from 'react-redux'; - -// import { -// isAnimationModeOnSelector, -// waybackItems4AnimationSelector, -// } from '@store/AnimationMode/reducer'; - -// // import IMapView from 'esri/views/MapView'; - -// import AnimationPanel from './AnimationPanel'; -// import { IWaybackItem } from '@typings/index'; -// import MapView from '@arcgis/core/views/MapView'; - -// type Props = { -// mapView?: MapView; -// }; - -// const AnimationPanelContainer: React.FC = ({ mapView }: Props) => { -// const isAnimationModeOn = useSelector(isAnimationModeOnSelector); - -// const waybackItems4Animation: IWaybackItem[] = useSelector( -// waybackItems4AnimationSelector -// ); - -// if (!isAnimationModeOn || !waybackItems4Animation.length) { -// return null; -// } - -// return ( -// -// ); -// }; - -// export default AnimationPanelContainer; diff --git a/src/components/AnimationPanel/Background.tsx b/src/components/AnimationPanel/Background.tsx deleted file mode 100644 index 4f1834c..0000000 --- a/src/components/AnimationPanel/Background.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useCallback } from 'react'; - -const Background = () => { - // const dispatch = useDispatch(); - - // const onClickHandler = useCallback(() => { - // dispatch(toggleAnimationMode()); - // }, []); - - return ( -
- {/*
- - - -
*/} -
- ); -}; - -export default Background; diff --git a/src/components/AnimationPanel/CloseBtn.tsx b/src/components/AnimationPanel/CloseBtn.tsx deleted file mode 100644 index a6ad793..0000000 --- a/src/components/AnimationPanel/CloseBtn.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useCallback } from 'react'; - -import { useDispatch } from 'react-redux'; -import { toggleAnimationMode } from '@store/AnimationMode/reducer'; - -const CloseBtn = () => { - const dispatch = useDispatch(); - - const onClickHandler = useCallback(() => { - dispatch(toggleAnimationMode()); - }, []); - - return ( -
- - - - -
- ); -}; - -export default CloseBtn; diff --git a/src/components/AnimationPanel/DownloadGIFDialog.tsx b/src/components/AnimationPanel/DownloadGIFDialog.tsx deleted file mode 100644 index 26c2f32..0000000 --- a/src/components/AnimationPanel/DownloadGIFDialog.tsx +++ /dev/null @@ -1,342 +0,0 @@ -// /* Copyright 2024 Esri -// * -// * Licensed under the Apache License Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. -// */ - -// import React, { useState, useCallback, useRef } from 'react'; - -// import { useDispatch } from 'react-redux'; -// import { isDownloadGIFDialogOnToggled } from '@store/AnimationMode/reducer'; -// import { FrameData } from './generateFrames4GIF'; - -// import LoadingSpinner from './LoadingSpinner'; - -// import classnames from 'classnames'; - -// // import gifshot from 'gifshot'; -// import GifStream from '@entryline/gifstream'; - -// type Props = { -// frameData: FrameData[]; -// rNum2Exclude: number[]; -// speed?: number; // animation speed in second -// }; - -// // type CreateGIFCallBack = (response: { -// // // image - Base 64 image -// // image: string; -// // // error - Boolean that determines if an error occurred -// // error: boolean; -// // // errorCode - Helpful error label -// // errorCode: string; -// // // errorMsg - Helpful error message -// // errorMsg: string; -// // }) => void; - -// type SaveAsGIFParams = { -// frameData: FrameData[]; -// // outputFileName: string; -// speed: number; -// }; - -// type ResponseCreateGIF = { -// error: string; -// blob: Blob; -// }; - -// type ImagesCreateGIF = { -// src: string; -// delay: number; -// }; - -// const gifStream = new GifStream(); - -// const donwload = (blob: Blob, fileName = ''): void => { -// const url = URL.createObjectURL(blob); - -// const link = document.createElement('a'); -// link.download = fileName; -// link.href = url; -// document.body.appendChild(link); -// link.click(); -// document.body.removeChild(link); - -// URL.revokeObjectURL(url); -// }; - -// const saveAsGIF = async ({ -// frameData, -// // outputFileName, -// speed, -// }: SaveAsGIFParams): Promise => { -// // if the speed is zero, it means user wants to have the fastest speed, so let's use 100 millisecond -// speed = speed || 0.1; - -// return new Promise((resolve, reject) => { -// const images: ImagesCreateGIF[] = frameData.map((d) => { -// const { frameCanvas, height, width, waybackItem, center } = d; - -// const { releaseDateLabel } = waybackItem; - -// const releaseData = `Wayback ${releaseDateLabel}`; -// const sourceInfo = -// 'Esri, Maxar, Earthstar Geographics, GIS Community'; -// const locationInfo = `${center.latitude.toFixed( -// 3 -// )}, ${center.longitude.toFixed(3)}`; -// const HorizontalPadding = 4; -// const SpaceBetween = 4; - -// const context = frameCanvas.getContext('2d'); -// context.font = '10px Avenir Next'; - -// const metrics4ReleaseDate = context.measureText(releaseData); -// const metrics4LocationInfo = context.measureText(locationInfo); -// const metrics4SourceInfo = context.measureText(sourceInfo); - -// const shouldWrap = -// metrics4ReleaseDate.width + -// metrics4LocationInfo.width + -// metrics4SourceInfo.width + -// SpaceBetween * 2 + -// HorizontalPadding * 2 > -// width; - -// // draw the gradient background rect -// const gradientRectHeight = shouldWrap ? 28 : 16; -// // const gradient = context.createLinearGradient(0, 0, 0, gradientRectHeight); -// // gradient.addColorStop(0, "rgba(0,0,0,0)"); -// // gradient.addColorStop(0.5, "rgba(0,0,0,.3)"); -// // gradient.addColorStop(1, "rgba(0,0,0,.6)"); -// context.fillStyle = 'rgba(0,0,0,.2)'; -// context.rect( -// 0, -// height - gradientRectHeight, -// width, -// gradientRectHeight -// ); -// context.fill(); - -// // draw the watermark text -// context.shadowColor = 'black'; -// context.shadowBlur = 5; -// context.fillStyle = 'rgba(255,255,255,.9)'; - -// if (shouldWrap) { -// let y = height - 4; -// const horizontalPadding = -// (width - Math.ceil(metrics4SourceInfo.width)) / 2; -// context.fillText(sourceInfo, horizontalPadding, y); - -// y = height - 16; -// context.fillText(releaseData, horizontalPadding, y); - -// const xPos4LocationInfo = -// width - (metrics4LocationInfo.width + horizontalPadding); -// context.fillText(locationInfo, xPos4LocationInfo, y); -// } else { -// const y = height - 4; - -// context.fillText(releaseData, HorizontalPadding, y); - -// const xPos4SourceInfo = -// width - (metrics4SourceInfo.width + HorizontalPadding); -// context.fillText(sourceInfo, xPos4SourceInfo, y); - -// let xPos4LocationInfo = -// metrics4ReleaseDate.width + HorizontalPadding; -// const availWidth = xPos4SourceInfo - xPos4LocationInfo; -// const leftPadding4LocationInfo = -// (availWidth - metrics4LocationInfo.width) / 2; - -// xPos4LocationInfo = -// xPos4LocationInfo + leftPadding4LocationInfo; -// context.fillText(locationInfo, xPos4LocationInfo, y); -// } - -// return { -// src: frameCanvas.toDataURL(), -// delay: speed * 1000, -// }; -// }); - -// gifStream.createGIF( -// { -// gifWidth: frameData[0].width, -// gifHeight: frameData[0].height, -// images, -// progressCallback: (progress: number) => { -// // console.log(progress) -// }, -// }, -// (res: ResponseCreateGIF) => { -// // this.onGifComplete(obj, width, height); -// // console.log(res) - -// if (res.error) { -// reject(res.error); -// } - -// // donwload(res.blob, outputFileName) -// resolve(res.blob); -// } -// ); -// }); -// }; - -// const DownloadGIFDialog: React.FC = ({ -// frameData, -// speed, -// rNum2Exclude, -// }) => { -// const dispatch = useDispatch(); - -// const [isDownloading, setIsDownloading] = useState(false); - -// const [outputFileName, setOutputFileName] = useState( -// 'wayback-imagery-animation' -// ); - -// const isCancelled = useRef(false); - -// const closeDialog = useCallback(() => { -// dispatch(isDownloadGIFDialogOnToggled()); -// }, []); - -// const downloadBtnOnClick = async () => { -// setIsDownloading(true); - -// const data = !rNum2Exclude.length -// ? frameData -// : frameData.filter( -// (d) => rNum2Exclude.indexOf(d.releaseNum) === -1 -// ); - -// try { -// const blob = await saveAsGIF({ -// frameData: data, -// // outputFileName, -// speed, -// }); - -// if (isCancelled.current) { -// console.log('gif task has been cancelled'); -// return; -// } - -// donwload(blob, outputFileName); - -// closeDialog(); -// } catch (err) { -// console.error(err); -// } -// }; - -// const getContent = () => { -// if (isDownloading) { -// return ( -// <> -//
-//

-// Generating animated GIF file... -//

-//

Your download will begin shortly.

-//
- -//
-//
{ -// gifStream.cancel(); -// isCancelled.current = true; -// closeDialog(); -// }} -// > -// Cancel -//
-//
- -//
-// -//
-// -// ); -// } - -// return ( -// <> -//
-// Download GIF -//
- -//
-//
-// File name: -// -//
-//
- -//
-//
-// Cancel -//
-//
-// Download -//
-//
-// -// ); -// }; - -// return ( -//
-//
-// {getContent()} -//
-//
-// ); -// }; - -// export default DownloadGIFDialog; diff --git a/src/components/AnimationPanel/ImageAutoPlay.tsx b/src/components/AnimationPanel/ImageAutoPlay.tsx deleted file mode 100644 index 7a78bf2..0000000 --- a/src/components/AnimationPanel/ImageAutoPlay.tsx +++ /dev/null @@ -1,54 +0,0 @@ -// /* Copyright 2024 Esri -// * -// * Licensed under the Apache License Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. -// */ - -// import React, { useEffect, useRef, useState } from 'react'; -// import { useSelector } from 'react-redux'; -// import { indexOfCurrentAnimationFrameSelector } from '@store/AnimationMode/reducer'; - -// import { FrameData } from './generateFrames4GIF'; - -// type Props = { -// frameData: FrameData[]; -// }; - -// const ImageAutoPlay: React.FC = ({ frameData }: Props) => { -// const idx = useSelector(indexOfCurrentAnimationFrameSelector); - -// // const isPlaying = useSelector(isAnimationPlayingSelector) - -// const getCurrentFrame = () => { -// if (!frameData || !frameData.length) { -// return null; -// } - -// const { frameDataURI } = frameData[idx] || frameData[0]; - -// return ( -//
-// ); -// }; - -// return getCurrentFrame(); -// }; - -// export default ImageAutoPlay; diff --git a/src/components/AnimationPanel/LoadingIndicator.tsx b/src/components/AnimationPanel/LoadingIndicator.tsx deleted file mode 100644 index 0291a57..0000000 --- a/src/components/AnimationPanel/LoadingIndicator.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; - -import LoadingSpinner from './LoadingSpinner'; - -const LoadingIndicator = () => { - return ( -
-
- -
- - Loading imagery -
- ); -}; - -export default LoadingIndicator; diff --git a/src/components/AnimationPanel/LoadingSpinner.css b/src/components/AnimationPanel/LoadingSpinner.css deleted file mode 100644 index b627cde..0000000 --- a/src/components/AnimationPanel/LoadingSpinner.css +++ /dev/null @@ -1,26 +0,0 @@ -@keyframes bounce { - 0% { - margin-left:-25%; - } - 50% { - margin-left:100%; - } -} - -.spinner-wrap { - position: relative; - /* top: -1px; - left: 0; */ - width: 100%; - height: 3px; - overflow: hidden; -} - -.spinner-line { - background-color: #fff; - width: 30%; - height: 100%; - margin-top: 0; - margin-left: -25%; - animation: bounce 2s infinite ease-in; -} \ No newline at end of file diff --git a/src/components/AnimationPanel/LoadingSpinner.tsx b/src/components/AnimationPanel/LoadingSpinner.tsx deleted file mode 100644 index 266166e..0000000 --- a/src/components/AnimationPanel/LoadingSpinner.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import './LoadingSpinner.css'; -import React from 'react'; - -// import styled, { keyframes } from 'styled-components'; - -// const bounce = keyframes` -// 0% { -// margin-left:-25%; -// } -// 50% { -// margin-left:100%; -// } -// `; - -// const SpinnerWrap = styled.div` -// position: relative; -// /* top: -1px; -// left: 0; */ -// width: 100%; -// height: 3px; -// overflow: hidden; -// `; - -// const SpinnerLine = styled.div` -// background-color: #fff; -// width: 30%; -// height: 100%; -// margin-top: 0; -// margin-left: -25%; -// animation: ${bounce} 2s infinite ease-in; -// `; - -const LoadingSpinner = () => { - return ( -
-
-
- ); -}; - -export default LoadingSpinner; diff --git a/src/components/AnimationPanel/Resizable.tsx b/src/components/AnimationPanel/Resizable.tsx deleted file mode 100644 index 5a0e0be..0000000 --- a/src/components/AnimationPanel/Resizable.tsx +++ /dev/null @@ -1,216 +0,0 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { - useRef, - // useMemo, - useState, - useEffect, - useCallback, -} from 'react'; -import { - PREVIEW_WINDOW_HEIGHT, - PREVIEW_WINDOW_WIDTH, -} from '../PreviewWindow/PreviewWindow'; - -import { PARENT_CONTAINER_LEFT_OFFSET } from './AnimationPanel'; - -type Position = { - top: number; - left: number; -}; - -type Size = { - height: number; - width: number; -}; - -type Props = { - containerRef: React.RefObject; - children?: React.ReactNode; - // eithe size or position is updated - onChange?: () => void; -}; - -const CONTAINER_MIN_SIZE = 260; - -// const CONTAINER_DEFAULT_SIZE = 300; - -const Resizable: React.FC = ({ - containerRef, - onChange, - children, -}: Props) => { - // const containerRef = useRef(); - - // const resizeBtnRef = useRef(); - - const [position, setPosition] = useState(); - - const [size, setSize] = useState({ - width: PREVIEW_WINDOW_WIDTH, - height: PREVIEW_WINDOW_HEIGHT, - }); - - // when the container is being dragged, we keep updating it's position using current mouse position, - // by default, the top left corner of the container will be snapped to the new position, - // but this is not an ideal user experience, consider user dragging the container by holding the bottom right corner of the container, - // let's say user moves the mouse to the right by 50px, and what they want by doing that is moving the top left corner of the container by 50px, - // instead of moving the top left corner down to the current mouse position - // therefore we need to know/save the offset between container's top left corner and mouse position when the drag event is started, - // and we will use it when calculate the new position for the container - const positionOffset = useRef(null); - - const mouseOnMoveHandler = useCallback((evt: MouseEvent) => { - const { clientX, clientY } = evt; - - const { offsetLeft, offsetTop, offsetHeight, offsetWidth } = - containerRef.current; - - if (!positionOffset.current) { - positionOffset.current = { - top: clientY - offsetTop, - left: clientX - offsetLeft, - }; - } - - let left = clientX - positionOffset.current.left; - - if (left < 0) { - left = 0; - } - - // reach to the right end of view port - if ( - left + PARENT_CONTAINER_LEFT_OFFSET + offsetWidth >= - window.innerWidth - ) { - left = - window.innerWidth - offsetWidth - PARENT_CONTAINER_LEFT_OFFSET; - } - - let top = clientY - positionOffset.current.top; - - if (top < 0) { - top = 0; - } else if (top + offsetHeight > window.innerHeight) { - top = window.innerHeight - offsetHeight; - } - - setPosition({ - top, - left, - }); - - onChange(); - }, []); - - const resize = useCallback((evt: MouseEvent) => { - // console.log('resizing') - - const { clientX, clientY } = evt; - - const { offsetLeft, offsetTop } = containerRef.current; - - const newWidth = clientX - PARENT_CONTAINER_LEFT_OFFSET - offsetLeft; - const newHeight = clientY - offsetTop; - - setSize({ - width: - newWidth >= CONTAINER_MIN_SIZE ? newWidth : CONTAINER_MIN_SIZE, - height: - newHeight >= CONTAINER_MIN_SIZE - ? newHeight - : CONTAINER_MIN_SIZE, - }); - - onChange(); - }, []); - - const addUpdatePositionHanlder = useCallback( - (evt: React.MouseEvent) => { - window.addEventListener('mousemove', mouseOnMoveHandler); - }, - [] - ); - - const removeUpdatePositionHanlder = useCallback(() => { - positionOffset.current = null; - window.removeEventListener('mousemove', mouseOnMoveHandler); - }, []); - - const addResizeHandler = useCallback((evt: any) => { - evt.stopPropagation(); - window.addEventListener('mousemove', resize); - }, []); - - const removeResizeHandler = useCallback((evt: any) => { - window.removeEventListener('mousemove', resize); - }, []); - - useEffect(() => { - window.addEventListener('mouseup', removeUpdatePositionHanlder); - window.addEventListener('mouseup', removeResizeHandler); - - return () => { - window.removeEventListener('mouseup', removeUpdatePositionHanlder); - window.removeEventListener('mouseup', removeResizeHandler); - }; - }, []); - - return ( -
- {children} - -
-
- ); -}; - -export default Resizable; From 8e1aa924354e7b6a1bc3f76801c84ea351e0aa5e Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Tue, 27 Feb 2024 15:51:03 -0800 Subject: [PATCH 09/56] feat: useMediaLayerImageElement should return ImageElementData[] --- .../AnimationLayer/AnimationLayer.tsx | 20 +++++++++++----- .../AnimationLayer/generateAnimationFrames.ts | 4 ++-- .../AnimationLayer/useMediaLayerAnimation.tsx | 19 +++++++-------- .../useMediaLayerImageElement.tsx | 23 ++++++++++++++++--- 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/src/components/AnimationLayer/AnimationLayer.tsx b/src/components/AnimationLayer/AnimationLayer.tsx index 3d28982..98baf14 100644 --- a/src/components/AnimationLayer/AnimationLayer.tsx +++ b/src/components/AnimationLayer/AnimationLayer.tsx @@ -24,6 +24,7 @@ import { animationStatusChanged, isAnimationModeOnSelector, rNum2ExcludeReset, + rNum2ExcludeSelector, // indexOfActiveAnimationFrameChanged, releaseNumberOfActiveAnimationFrameChanged, selectAnimationStatus, @@ -55,9 +56,14 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { const waybackItems = useSelector(waybackItemsWithLocalChangesSelector); /** - * Array of Imagery Elements for each scene in `sortedQueryParams4ScenesInAnimationMode` + * release num of wayback items to be excluded from the animation */ - const mediaLayerElements = useMediaLayerImageElement({ + const releaseNumOfItems2Exclude = useSelector(rNum2ExcludeSelector); + + /** + * Array of Imagery Element Data + */ + const imageElementsData = useMediaLayerImageElement({ mapView, animationStatus, waybackItems, @@ -82,7 +88,7 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { useMediaLayerAnimation({ animationStatus, animationSpeed: animationSpeed * 1000, - mediaLayerElements, + imageElementsData, activeFrameOnChange, }); @@ -104,16 +110,18 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { const source = mediaLayerRef.current.source as any; - if (!mediaLayerElements) { + if (!imageElementsData) { // animation is not started or just stopped // just clear all elements in media layer source.elements.removeAll(); } else { - source.elements.addMany(mediaLayerElements); + source.elements.addMany( + imageElementsData.map((d) => d.imageElement) + ); // media layer elements are ready, change animation mode to playing to start the animation dispatch(animationStatusChanged('playing')); } - }, [mediaLayerElements, mapView]); + }, [imageElementsData, mapView]); useEffect(() => { if (isAnimationModeOn) { diff --git a/src/components/AnimationLayer/generateAnimationFrames.ts b/src/components/AnimationLayer/generateAnimationFrames.ts index 3e1968d..c7c9f1c 100644 --- a/src/components/AnimationLayer/generateAnimationFrames.ts +++ b/src/components/AnimationLayer/generateAnimationFrames.ts @@ -55,7 +55,7 @@ type CenterLocationForFrameRect = { }; export type FrameData = { - // releaseNum: number; + releaseNum: number; // waybackItem: IWaybackItem; frameCanvas: HTMLCanvasElement; frameBlob: Blob; @@ -95,7 +95,7 @@ export const generateAnimationFrames = async ({ }); frames.push({ - // releaseNum, + releaseNum, // waybackItem: item, frameCanvas, // frameDataURI: '', diff --git a/src/components/AnimationLayer/useMediaLayerAnimation.tsx b/src/components/AnimationLayer/useMediaLayerAnimation.tsx index 06ab35f..e0544bb 100644 --- a/src/components/AnimationLayer/useMediaLayerAnimation.tsx +++ b/src/components/AnimationLayer/useMediaLayerAnimation.tsx @@ -16,6 +16,7 @@ import React, { FC, useEffect, useRef, useState } from 'react'; import IImageElement from '@arcgis/core/layers/support/ImageElement'; import { AnimationStatus } from '@store/AnimationMode/reducer'; +import { ImageElementData } from './useMediaLayerImageElement'; type Props = { /** @@ -29,7 +30,7 @@ type Props = { /** * array of image elements to be animated */ - mediaLayerElements: IImageElement[]; + imageElementsData: ImageElementData[]; /** * Fires when the active frame changes * @param indexOfActiveFrame index of the active frame @@ -46,7 +47,7 @@ type Props = { const useMediaLayerAnimation = ({ animationStatus, animationSpeed, - mediaLayerElements, + imageElementsData, activeFrameOnChange, }: Props) => { const isPlayingRef = useRef(false); @@ -80,21 +81,21 @@ const useMediaLayerAnimation = ({ // reset index of next frame to 0 if it is out of range. // this can happen when a frame gets removed after previous animation is stopped - if (indexOfNextFrame.current >= mediaLayerElements.length) { + if (indexOfNextFrame.current >= imageElementsData.length) { indexOfNextFrame.current = 0; } activeFrameOnChangeRef.current(indexOfNextFrame.current); - for (let i = 0; i < mediaLayerElements.length; i++) { + for (let i = 0; i < imageElementsData.length; i++) { const opacity = i === indexOfNextFrame.current ? 1 : 0; - mediaLayerElements[i].opacity = opacity; + imageElementsData[i].imageElement.opacity = opacity; } // update indexOfNextFrame using the index of next element // when hit the end of the array, use 0 instead indexOfNextFrame.current = - (indexOfNextFrame.current + 1) % mediaLayerElements.length; + (indexOfNextFrame.current + 1) % imageElementsData.length; // call showNextFrame recursively to play the animation as long as // animationMode is 'playing' @@ -105,14 +106,14 @@ const useMediaLayerAnimation = ({ isPlayingRef.current = animationStatus === 'playing'; // cannot animate layers if the list is empty - if (!mediaLayerElements || !mediaLayerElements?.length) { + if (!imageElementsData || !imageElementsData?.length) { return; } - if (mediaLayerElements && animationStatus === 'playing') { + if (imageElementsData?.length && animationStatus === 'playing') { requestAnimationFrame(showNextFrame); } - }, [animationStatus, mediaLayerElements]); + }, [animationStatus, imageElementsData]); useEffect(() => { activeFrameOnChangeRef.current = activeFrameOnChange; diff --git a/src/components/AnimationLayer/useMediaLayerImageElement.tsx b/src/components/AnimationLayer/useMediaLayerImageElement.tsx index 36be3a4..5b44dd5 100644 --- a/src/components/AnimationLayer/useMediaLayerImageElement.tsx +++ b/src/components/AnimationLayer/useMediaLayerImageElement.tsx @@ -30,12 +30,24 @@ type Props = { waybackItems: IWaybackItem[]; }; +export type ImageElementData = { + /** + * wayback release number associated with this image element + */ + releaseNumber: number; + /** + * image element to be used in the media layer + */ + imageElement: ImageElement; +}; + export const useMediaLayerImageElement = ({ mapView, animationStatus, waybackItems, }: Props) => { - const [imageElements, setImageElements] = useState(null); + const [imageElements, setImageElements] = + useState(null); const abortControllerRef = useRef(); @@ -75,7 +87,7 @@ export const useMediaLayerImageElement = ({ // once responses are received, get array of image elements using the binary data returned from export image requests const imageElements = frameData.map((d: FrameData) => { - return new ImageElement({ + const imageElement = new ImageElement({ image: URL.createObjectURL(d.frameBlob), georeference: new ExtentAndRotationGeoreference({ extent: { @@ -90,6 +102,11 @@ export const useMediaLayerImageElement = ({ }), opacity: 1, }); + + return { + releaseNumber: d.releaseNum, + imageElement, + } as ImageElementData; }); setImageElements(imageElements); @@ -108,7 +125,7 @@ export const useMediaLayerImageElement = ({ // call revokeObjectURL so these image elements can be freed from the memory if (imageElements) { for (const elem of imageElements) { - URL.revokeObjectURL(elem.image as string); + URL.revokeObjectURL(elem.imageElement.image as string); } } From f1015823170bf11e1befb53aa32f21be8a1f8ad7 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 28 Feb 2024 10:07:28 -0800 Subject: [PATCH 10/56] feat: add useSaveAppState2URLHashParams hook --- .../AnimationControls/AnimationControls.tsx | 6 ---- src/components/AppLayout/AppLayout.tsx | 3 ++ src/hooks/useSaveAppState2URLHashParams.tsx | 35 +++++++++++++++++++ 3 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 src/hooks/useSaveAppState2URLHashParams.tsx diff --git a/src/components/AnimationControls/AnimationControls.tsx b/src/components/AnimationControls/AnimationControls.tsx index 87acbbb..e58faa9 100644 --- a/src/components/AnimationControls/AnimationControls.tsx +++ b/src/components/AnimationControls/AnimationControls.tsx @@ -153,12 +153,6 @@ const AnimationControls = () => { saveFrames2ExcludeInURLQueryParam(rNum2ExcludeFromAnimation); }, [rNum2ExcludeFromAnimation]); - useEffect(() => { - saveAnimationSpeedInURLQueryParam( - animationStatus !== null ? animationSpeed : undefined - ); - }, [animationSpeed, animationStatus]); - return ( <>
{ // const { onPremises } = React.useContext(AppContext); const currentPageIsVisibleAgain = useCurrenPageBecomesVisible(); + useSaveAppState2URLHashParams(); + useEffect(() => { if (!currentPageIsVisibleAgain) { return; diff --git a/src/hooks/useSaveAppState2URLHashParams.tsx b/src/hooks/useSaveAppState2URLHashParams.tsx new file mode 100644 index 0000000..3dd24ac --- /dev/null +++ b/src/hooks/useSaveAppState2URLHashParams.tsx @@ -0,0 +1,35 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useEffect } from 'react'; +import { useSelector } from 'react-redux'; + +import { + animationSpeedSelector, + selectAnimationStatus, +} from '@store/AnimationMode/reducer'; +import { saveAnimationSpeedInURLQueryParam } from '@utils/UrlSearchParam'; + +export const useSaveAppState2URLHashParams = () => { + const animationSpeed = useSelector(animationSpeedSelector); + + const animationStatus = useSelector(selectAnimationStatus); + + useEffect(() => { + saveAnimationSpeedInURLQueryParam( + animationStatus !== null ? animationSpeed : undefined + ); + }, [animationSpeed, animationStatus]); +}; From 9c3975d47543499d4427ee7473c726f411cca800 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 28 Feb 2024 10:28:23 -0800 Subject: [PATCH 11/56] feat: add releaseNumOfItems2Exclude prop to useMediaLayerAnimation --- .../AnimationLayer/AnimationLayer.tsx | 1 + .../AnimationLayer/useMediaLayerAnimation.tsx | 27 ++++++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/components/AnimationLayer/AnimationLayer.tsx b/src/components/AnimationLayer/AnimationLayer.tsx index 98baf14..d6d6419 100644 --- a/src/components/AnimationLayer/AnimationLayer.tsx +++ b/src/components/AnimationLayer/AnimationLayer.tsx @@ -89,6 +89,7 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { animationStatus, animationSpeed: animationSpeed * 1000, imageElementsData, + releaseNumOfItems2Exclude, activeFrameOnChange, }); diff --git a/src/components/AnimationLayer/useMediaLayerAnimation.tsx b/src/components/AnimationLayer/useMediaLayerAnimation.tsx index e0544bb..a28c143 100644 --- a/src/components/AnimationLayer/useMediaLayerAnimation.tsx +++ b/src/components/AnimationLayer/useMediaLayerAnimation.tsx @@ -31,6 +31,10 @@ type Props = { * array of image elements to be animated */ imageElementsData: ImageElementData[]; + /** + * list of release number of wayback items to exclude from the animation + */ + releaseNumOfItems2Exclude: number[]; /** * Fires when the active frame changes * @param indexOfActiveFrame index of the active frame @@ -48,18 +52,21 @@ const useMediaLayerAnimation = ({ animationStatus, animationSpeed, imageElementsData, + releaseNumOfItems2Exclude, activeFrameOnChange, }: Props) => { const isPlayingRef = useRef(false); const timeLastFrameDisplayed = useRef(performance.now()); - const indexOfNextFrame = useRef(0); + const indexOfActiveFrame = useRef(0); const activeFrameOnChangeRef = useRef(); const animationSpeedRef = useRef(animationSpeed); + const releaseNumOfItems2ExcludeRef = useRef([]); + const showNextFrame = () => { // use has stopped animation, no need to show next frame if (!isPlayingRef.current) { @@ -81,21 +88,23 @@ const useMediaLayerAnimation = ({ // reset index of next frame to 0 if it is out of range. // this can happen when a frame gets removed after previous animation is stopped - if (indexOfNextFrame.current >= imageElementsData.length) { - indexOfNextFrame.current = 0; + if (indexOfActiveFrame.current >= imageElementsData.length) { + indexOfActiveFrame.current = 0; } - activeFrameOnChangeRef.current(indexOfNextFrame.current); + activeFrameOnChangeRef.current(indexOfActiveFrame.current); for (let i = 0; i < imageElementsData.length; i++) { - const opacity = i === indexOfNextFrame.current ? 1 : 0; + const opacity = i === indexOfActiveFrame.current ? 1 : 0; imageElementsData[i].imageElement.opacity = opacity; } + const indexOfNextFrame = + (indexOfActiveFrame.current + 1) % imageElementsData.length; + // update indexOfNextFrame using the index of next element // when hit the end of the array, use 0 instead - indexOfNextFrame.current = - (indexOfNextFrame.current + 1) % imageElementsData.length; + indexOfActiveFrame.current = indexOfNextFrame; // call showNextFrame recursively to play the animation as long as // animationMode is 'playing' @@ -122,6 +131,10 @@ const useMediaLayerAnimation = ({ useEffect(() => { animationSpeedRef.current = animationSpeed; }, [animationSpeed]); + + useEffect(() => { + releaseNumOfItems2ExcludeRef.current = releaseNumOfItems2Exclude; + }, [releaseNumOfItems2Exclude]); }; export default useMediaLayerAnimation; From 5119ede9ffbaebd22b69a386ac33c53f33a3d5c8 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 28 Feb 2024 12:40:29 -0800 Subject: [PATCH 12/56] feat: useMediaLayerAnimation should skip frames for items in releaseNumOfItems2Exclude --- .../AnimationLayer/useMediaLayerAnimation.tsx | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/components/AnimationLayer/useMediaLayerAnimation.tsx b/src/components/AnimationLayer/useMediaLayerAnimation.tsx index a28c143..1f7c09b 100644 --- a/src/components/AnimationLayer/useMediaLayerAnimation.tsx +++ b/src/components/AnimationLayer/useMediaLayerAnimation.tsx @@ -99,9 +99,32 @@ const useMediaLayerAnimation = ({ imageElementsData[i].imageElement.opacity = opacity; } - const indexOfNextFrame = + // set index of active frame to -1 so that all frames wil be exclude from the animation + if ( + releaseNumOfItems2ExcludeRef.current.length === + imageElementsData.length + ) { + indexOfActiveFrame.current = -1; + requestAnimationFrame(showNextFrame); + return; + } + + // get the index of animation frame that will become active next + let indexOfNextFrame = (indexOfActiveFrame.current + 1) % imageElementsData.length; + // check if the next frame should be excluded from the animation, + // if so, update the indexOfNextFrame to skip the frame that should be excluded + while ( + indexOfNextFrame !== indexOfActiveFrame.current && + releaseNumOfItems2ExcludeRef.current.includes( + imageElementsData[indexOfNextFrame].releaseNumber + ) + ) { + indexOfNextFrame = + (indexOfNextFrame + 1) % imageElementsData.length; + } + // update indexOfNextFrame using the index of next element // when hit the end of the array, use 0 instead indexOfActiveFrame.current = indexOfNextFrame; From 715ef2b8c36b2c34e2e7d1ef0db53ed4a45533b1 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 28 Feb 2024 12:53:32 -0800 Subject: [PATCH 13/56] fix: reset values when Animation is stopped --- src/components/AnimationControls/AnimationControls.tsx | 7 ++++--- src/components/AnimationLayer/AnimationLayer.tsx | 1 + src/components/AnimationLayer/useMediaLayerAnimation.tsx | 5 +++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/AnimationControls/AnimationControls.tsx b/src/components/AnimationControls/AnimationControls.tsx index e58faa9..815bde5 100644 --- a/src/components/AnimationControls/AnimationControls.tsx +++ b/src/components/AnimationControls/AnimationControls.tsx @@ -30,7 +30,7 @@ import { // rNum4AnimationFramesSelector, rNum2ExcludeSelector, // toggleAnimationFrame, - rNum2ExcludeReset, + // rNum2ExcludeReset, // animationSpeedChanged, animationSpeedSelector, // isAnimationPlayingToggled, @@ -46,6 +46,7 @@ import { // indexOfActiveAnimationFrameChanged, selectReleaseNumberOfActiveAnimationFrame, rNum2ExcludeToggled, + // releaseNumberOfActiveAnimationFrameChanged, // setActiveFrameByReleaseNum, } from '@store/AnimationMode/reducer'; @@ -135,8 +136,8 @@ const AnimationControls = () => { waybackItemsWithLocalChanges={waybackItemsWithLocalChanges} rNum2Exclude={rNum2ExcludeFromAnimation} setActiveFrame={(rNum) => { - // dispatch(indexOfActiveAnimationFrameChanged(rNum)); - console.log(rNum); + // dispatch(releaseNumberOfActiveAnimationFrameChanged(rNum)); + // console.log(rNum); }} toggleFrame={(rNum) => { dispatch(rNum2ExcludeToggled(rNum)); diff --git a/src/components/AnimationLayer/AnimationLayer.tsx b/src/components/AnimationLayer/AnimationLayer.tsx index d6d6419..868397f 100644 --- a/src/components/AnimationLayer/AnimationLayer.tsx +++ b/src/components/AnimationLayer/AnimationLayer.tsx @@ -130,6 +130,7 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { } else { dispatch(animationStatusChanged(null)); dispatch(rNum2ExcludeReset()); + dispatch(releaseNumberOfActiveAnimationFrameChanged(null)); } }, [isAnimationModeOn]); diff --git a/src/components/AnimationLayer/useMediaLayerAnimation.tsx b/src/components/AnimationLayer/useMediaLayerAnimation.tsx index 1f7c09b..4c9abb3 100644 --- a/src/components/AnimationLayer/useMediaLayerAnimation.tsx +++ b/src/components/AnimationLayer/useMediaLayerAnimation.tsx @@ -137,6 +137,11 @@ const useMediaLayerAnimation = ({ useEffect(() => { isPlayingRef.current = animationStatus === 'playing'; + // reset the index of active frame when animation is stopped + if (animationStatus === null) { + indexOfActiveFrame.current = 0; + } + // cannot animate layers if the list is empty if (!imageElementsData || !imageElementsData?.length) { return; From 45eaf83ad397669816810d422b60b670c19195e0 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 28 Feb 2024 13:19:04 -0800 Subject: [PATCH 14/56] feat: update Wayback slice of Redux Store add queryLocalChanges thunk function add isLoading to the state --- .../AnimationLayer/AnimationLayer.tsx | 13 +++++- .../useMediaLayerImageElement.tsx | 9 +++- src/components/MapView/MapViewConatiner.tsx | 24 +++++----- src/store/Wayback/reducer.ts | 17 +++++++ src/store/Wayback/thunks.ts | 44 +++++++++++++++++++ 5 files changed, 91 insertions(+), 16 deletions(-) create mode 100644 src/store/Wayback/thunks.ts diff --git a/src/components/AnimationLayer/AnimationLayer.tsx b/src/components/AnimationLayer/AnimationLayer.tsx index 868397f..c42f4c9 100644 --- a/src/components/AnimationLayer/AnimationLayer.tsx +++ b/src/components/AnimationLayer/AnimationLayer.tsx @@ -36,7 +36,10 @@ import classNames from 'classnames'; import { CloseButton } from '@components/CloseButton'; import { useMediaLayerImageElement } from './useMediaLayerImageElement'; import useMediaLayerAnimation from './useMediaLayerAnimation'; -import { waybackItemsWithLocalChangesSelector } from '@store/Wayback/reducer'; +import { + selectIsLoadingWaybackItems, + waybackItemsWithLocalChangesSelector, +} from '@store/Wayback/reducer'; type Props = { mapView?: MapView; @@ -53,6 +56,9 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { const animationSpeed = useSelector(animationSpeedSelector); + /** + * wayback items with local changes + */ const waybackItems = useSelector(waybackItemsWithLocalChangesSelector); /** @@ -60,6 +66,10 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { */ const releaseNumOfItems2Exclude = useSelector(rNum2ExcludeSelector); + const isLoadingWaybackItemsWithLoalChanges = useSelector( + selectIsLoadingWaybackItems + ); + /** * Array of Imagery Element Data */ @@ -67,6 +77,7 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { mapView, animationStatus, waybackItems, + isLoading: isLoadingWaybackItemsWithLoalChanges, }); /** diff --git a/src/components/AnimationLayer/useMediaLayerImageElement.tsx b/src/components/AnimationLayer/useMediaLayerImageElement.tsx index 5b44dd5..5ca1ad3 100644 --- a/src/components/AnimationLayer/useMediaLayerImageElement.tsx +++ b/src/components/AnimationLayer/useMediaLayerImageElement.tsx @@ -28,6 +28,10 @@ type Props = { mapView?: MapView; animationStatus: AnimationStatus; waybackItems: IWaybackItem[]; + /** + * if true, it is in process of loading wayback items + */ + isLoading: boolean; }; export type ImageElementData = { @@ -45,6 +49,7 @@ export const useMediaLayerImageElement = ({ mapView, animationStatus, waybackItems, + isLoading, }: Props) => { const [imageElements, setImageElements] = useState(null); @@ -116,7 +121,7 @@ export const useMediaLayerImageElement = ({ }; useEffect(() => { - if (!animationStatus || !waybackItems.length) { + if (!animationStatus || !waybackItems.length || isLoading) { // call abort so all pending requests can be cancelled if (abortControllerRef.current) { abortControllerRef.current.abort(); @@ -133,7 +138,7 @@ export const useMediaLayerImageElement = ({ } else if (animationStatus === 'loading') { loadFrameData(); } - }, [animationStatus, waybackItems]); + }, [animationStatus, waybackItems, isLoading]); return imageElements; }; diff --git a/src/components/MapView/MapViewConatiner.tsx b/src/components/MapView/MapViewConatiner.tsx index 0bf9713..60f6079 100644 --- a/src/components/MapView/MapViewConatiner.tsx +++ b/src/components/MapView/MapViewConatiner.tsx @@ -27,6 +27,7 @@ import { } from '@store/Map/reducer'; import { + isLoadingWaybackItemsToggled, // activeWaybackItemSelector, releaseNum4ItemsWithLocalChangesUpdated, // previewWaybackItemSelector @@ -47,6 +48,8 @@ import { isAnimationModeOnSelector, selectAnimationStatus, } from '@store/AnimationMode/reducer'; +import { queryLocalChanges } from '@store/Wayback/thunks'; +import { Point } from '@arcgis/core/geometry'; type Props = { children?: React.ReactNode; @@ -96,19 +99,14 @@ const MapViewConatiner: React.FC = ({ children }) => { mapCenterPoint: IMapPointInfo ) => { try { - const waybackItems = await getWaybackItemsWithLocalChanges( - { - longitude: mapCenterPoint.longitude, - latitude: mapCenterPoint.latitude, - }, - mapCenterPoint.zoom - ); - console.log(waybackItems); - - const rNums = waybackItems.map((d) => d.releaseNum); - - // console.log(rNums); - dispatch(releaseNum4ItemsWithLocalChangesUpdated(rNums)); + const { longitude, latitude, zoom } = mapCenterPoint; + + const point = new Point({ + longitude, + latitude, + }); + + dispatch(queryLocalChanges(point, zoom)); } catch (err) { console.error('failed to query local changes', err); } diff --git a/src/store/Wayback/reducer.ts b/src/store/Wayback/reducer.ts index a7968e6..f41e4d4 100644 --- a/src/store/Wayback/reducer.ts +++ b/src/store/Wayback/reducer.ts @@ -43,6 +43,10 @@ export type WaybackItemsState = { releaseNum4ActiveWaybackItem: number; releaseNum4PreviewWaybackItem: number; releaseNum4AlternativePreviewWaybackItem: number; + /** + * if ture, it is in process of loading wayback items or items with local changes + */ + isLoading: boolean; }; export const initialWaybackItemsState = { @@ -53,6 +57,7 @@ export const initialWaybackItemsState = { releaseNum4ActiveWaybackItem: null, releaseNum4PreviewWaybackItem: null, releaseNum4AlternativePreviewWaybackItem: null, + isLoading: false, } as WaybackItemsState; const slice = createSlice({ @@ -127,6 +132,12 @@ const slice = createSlice({ ) => { state.releaseNum4AlternativePreviewWaybackItem = action.payload; }, + isLoadingWaybackItemsToggled: ( + state: WaybackItemsState, + action: PayloadAction + ) => { + state.isLoading = action.payload; + }, }, }); @@ -140,6 +151,7 @@ export const { releaseNum4ActiveWaybackItemUpdated, releaseNum4PreviewWaybackItemUpdated, releaseNum4AlternativePreviewWaybackItemUpdated, + isLoadingWaybackItemsToggled, } = slice.actions; let delay4SetPreviewWaybackItem: NodeJS.Timeout; @@ -271,4 +283,9 @@ export const selectWaybackItemsByReleaseNum = createSelector( (byReleaseNumber) => byReleaseNumber ); +export const selectIsLoadingWaybackItems = createSelector( + (state: RootState) => state.WaybackItems.isLoading, + (isLoading) => isLoading +); + export default reducer; diff --git a/src/store/Wayback/thunks.ts b/src/store/Wayback/thunks.ts new file mode 100644 index 0000000..b3dd39b --- /dev/null +++ b/src/store/Wayback/thunks.ts @@ -0,0 +1,44 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Point } from '@arcgis/core/geometry'; +import { StoreDispatch, StoreGetState } from '../configureStore'; +import { + isLoadingWaybackItemsToggled, + releaseNum4ItemsWithLocalChangesUpdated, +} from './reducer'; +import { getWaybackItemsWithLocalChanges } from '@vannizhang/wayback-core'; + +export const queryLocalChanges = + (point: Point, zoom: number) => + async (dispatch: StoreDispatch, getState: StoreGetState) => { + dispatch(isLoadingWaybackItemsToggled(true)); + + const waybackItems = await getWaybackItemsWithLocalChanges( + { + longitude: point.longitude, + latitude: point.latitude, + }, + zoom + ); + // console.log(waybackItems); + + const rNums = waybackItems.map((d) => d.releaseNum); + + // console.log(rNums); + dispatch(releaseNum4ItemsWithLocalChangesUpdated(rNums)); + + dispatch(isLoadingWaybackItemsToggled(false)); + }; From 4a4d83e8b495a29cc40d845f913ad0c66a9b8172 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 28 Feb 2024 13:53:06 -0800 Subject: [PATCH 15/56] fix: update queryLocalChanges thunk function use abortController to cancel pending getWaybackItemsWithLocalChanges task --- package-lock.json | 14 ++++++------- package.json | 2 +- src/store/Wayback/thunks.ts | 40 +++++++++++++++++++++++++------------ 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6fb2279..ddb6dfe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@esri/arcgis-rest-feature-service": "^4.0.4", "@esri/arcgis-rest-request": "^4.2.0", "@reduxjs/toolkit": "^1.9.5", - "@vannizhang/wayback-core": "^1.0.5", + "@vannizhang/wayback-core": "^1.0.6", "axios": "^1.6.2", "classnames": "^2.2.6", "d3": "^7.8.5", @@ -4427,9 +4427,9 @@ "dev": true }, "node_modules/@vannizhang/wayback-core": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@vannizhang/wayback-core/-/wayback-core-1.0.5.tgz", - "integrity": "sha512-tSorkvDRvojionT6RksdW9W1zhSCbxnaynRBn1+q5aX0hW9d/3XPdk3X6yxGv1TT7bbu50fY5m7cG3ygMkVEHw==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@vannizhang/wayback-core/-/wayback-core-1.0.6.tgz", + "integrity": "sha512-Ee5oaqy2vSUyxid+ZoYY0ZEUrNVWxi0wXwp8eU4jpelEneIt9HWSOOtT8Y5RVEAWgMhQQfWsqJZhh7KZxgduVg==" }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", @@ -20231,9 +20231,9 @@ "dev": true }, "@vannizhang/wayback-core": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@vannizhang/wayback-core/-/wayback-core-1.0.5.tgz", - "integrity": "sha512-tSorkvDRvojionT6RksdW9W1zhSCbxnaynRBn1+q5aX0hW9d/3XPdk3X6yxGv1TT7bbu50fY5m7cG3ygMkVEHw==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@vannizhang/wayback-core/-/wayback-core-1.0.6.tgz", + "integrity": "sha512-Ee5oaqy2vSUyxid+ZoYY0ZEUrNVWxi0wXwp8eU4jpelEneIt9HWSOOtT8Y5RVEAWgMhQQfWsqJZhh7KZxgduVg==" }, "@webassemblyjs/ast": { "version": "1.11.6", diff --git a/package.json b/package.json index 4f8b749..9e57759 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "@esri/arcgis-rest-feature-service": "^4.0.4", "@esri/arcgis-rest-request": "^4.2.0", "@reduxjs/toolkit": "^1.9.5", - "@vannizhang/wayback-core": "^1.0.5", + "@vannizhang/wayback-core": "^1.0.6", "axios": "^1.6.2", "classnames": "^2.2.6", "d3": "^7.8.5", diff --git a/src/store/Wayback/thunks.ts b/src/store/Wayback/thunks.ts index b3dd39b..eb80f71 100644 --- a/src/store/Wayback/thunks.ts +++ b/src/store/Wayback/thunks.ts @@ -21,24 +21,38 @@ import { } from './reducer'; import { getWaybackItemsWithLocalChanges } from '@vannizhang/wayback-core'; +let abortController: AbortController = null; + export const queryLocalChanges = (point: Point, zoom: number) => async (dispatch: StoreDispatch, getState: StoreGetState) => { - dispatch(isLoadingWaybackItemsToggled(true)); + try { + if (abortController) { + abortController.abort(); + } + + abortController = new AbortController(); + + dispatch(isLoadingWaybackItemsToggled(true)); + + const waybackItems = await getWaybackItemsWithLocalChanges( + { + longitude: point.longitude, + latitude: point.latitude, + }, + zoom, + abortController + ); - const waybackItems = await getWaybackItemsWithLocalChanges( - { - longitude: point.longitude, - latitude: point.latitude, - }, - zoom - ); - // console.log(waybackItems); + // console.log(waybackItems); - const rNums = waybackItems.map((d) => d.releaseNum); + const rNums = waybackItems.map((d) => d.releaseNum); - // console.log(rNums); - dispatch(releaseNum4ItemsWithLocalChangesUpdated(rNums)); + // console.log(rNums); + dispatch(releaseNum4ItemsWithLocalChangesUpdated(rNums)); - dispatch(isLoadingWaybackItemsToggled(false)); + dispatch(isLoadingWaybackItemsToggled(false)); + } catch (err) { + console.log(err); + } }; From ac0e81cc6225a6e1d59b882d7d9c847f3a0ecdc1 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 28 Feb 2024 14:27:12 -0800 Subject: [PATCH 16/56] fix: generateAnimationFrames should be cancellable --- .../AnimationLayer/generateAnimationFrames.ts | 41 +++++++++++-------- .../useMediaLayerImageElement.tsx | 1 + 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/components/AnimationLayer/generateAnimationFrames.ts b/src/components/AnimationLayer/generateAnimationFrames.ts index c7c9f1c..549f52a 100644 --- a/src/components/AnimationLayer/generateAnimationFrames.ts +++ b/src/components/AnimationLayer/generateAnimationFrames.ts @@ -47,6 +47,7 @@ type GenerateFramesParams = { frameRect: FrameRectInfo; mapView: MapView; waybackItems: IWaybackItem[]; + abortController?: AbortController; }; type CenterLocationForFrameRect = { @@ -71,6 +72,7 @@ export const generateAnimationFrames = async ({ frameRect, mapView, waybackItems, + abortController, }: GenerateFramesParams): Promise => { const frames: FrameData[] = []; @@ -80,34 +82,39 @@ export const generateAnimationFrames = async ({ mapView, }); - const center = getCenterLocationForFrameRect({ - frameRect, - mapView, - }); - - for (const item of waybackItems) { + const generateFrameRequests = waybackItems.map((item) => { const { releaseNum } = item; - const { frameCanvas, frameBlob } = await generateFrame({ + return generateFrame({ frameRect, tiles, releaseNum, + abortController, }); + }); + + const responses = await Promise.all(generateFrameRequests); + + for (let i = 0; i < responses.length; i++) { + const res = responses[i]; + + const { releaseNum } = waybackItems[i]; frames.push({ + ...res, releaseNum, - // waybackItem: item, - frameCanvas, - // frameDataURI: '', - frameBlob, - // width: frameRect.width, - // height: frameRect.height, - // center, }); } - // console.log(frameDataURL) - return frames; + return new Promise((resolve, reject) => { + if (abortController && abortController.signal.aborted) { + reject( + 'The task to generate animation frames has been cancelled by the user' + ); + } + + resolve(frames); + }); }; // get data URL from canvas with map tiles that conver the entire container @@ -115,10 +122,12 @@ const generateFrame = async ({ frameRect, tiles, releaseNum, + abortController, }: { frameRect: FrameRectInfo; tiles: TileInfo[]; releaseNum: number; + abortController: AbortController; }): Promise<{ frameCanvas: HTMLCanvasElement; frameBlob: Blob; diff --git a/src/components/AnimationLayer/useMediaLayerImageElement.tsx b/src/components/AnimationLayer/useMediaLayerImageElement.tsx index 5ca1ad3..0131ebe 100644 --- a/src/components/AnimationLayer/useMediaLayerImageElement.tsx +++ b/src/components/AnimationLayer/useMediaLayerImageElement.tsx @@ -88,6 +88,7 @@ export const useMediaLayerImageElement = ({ }, mapView, waybackItems, + abortController: abortControllerRef.current, }); // once responses are received, get array of image elements using the binary data returned from export image requests From 29e63eacde5c95f18aaaa1f4a92cdba6fe54505f Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 28 Feb 2024 14:40:20 -0800 Subject: [PATCH 17/56] fix: AnimationLayer components should not be rendered when AnimationMode is off --- src/components/AnimationLayer/AnimationLayer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AnimationLayer/AnimationLayer.tsx b/src/components/AnimationLayer/AnimationLayer.tsx index c42f4c9..967f424 100644 --- a/src/components/AnimationLayer/AnimationLayer.tsx +++ b/src/components/AnimationLayer/AnimationLayer.tsx @@ -152,7 +152,7 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { } }, [animationStatus]); - if (!animationStatus) { + if (!isAnimationModeOn) { return null; } From c1f88e993bba9567c1f63e05431cc0c1c33d8e41 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 28 Feb 2024 14:47:09 -0800 Subject: [PATCH 18/56] fix: rename DownloadGIFButton as DonwloadAnimationButton --- src/components/AnimationControls/AnimationControls.tsx | 4 ++-- .../{DonwloadGifButton.tsx => DonwloadAnimationButton.tsx} | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) rename src/components/AnimationControls/{DonwloadGifButton.tsx => DonwloadAnimationButton.tsx} (93%) diff --git a/src/components/AnimationControls/AnimationControls.tsx b/src/components/AnimationControls/AnimationControls.tsx index 815bde5..c601f4e 100644 --- a/src/components/AnimationControls/AnimationControls.tsx +++ b/src/components/AnimationControls/AnimationControls.tsx @@ -52,7 +52,7 @@ import { import { IWaybackItem } from '@typings/index'; -import DonwloadGifButton from './DonwloadGifButton'; +import { DonwloadAnimationButton } from './DonwloadAnimationButton'; import FramesSeletor from './FramesSeletor'; import SpeedSelector from './SpeedSelector'; import PlayPauseBtn from './PlayPauseBtn'; @@ -108,7 +108,7 @@ const AnimationControls = () => { return ( <> - +
Animation Speed diff --git a/src/components/AnimationControls/DonwloadGifButton.tsx b/src/components/AnimationControls/DonwloadAnimationButton.tsx similarity index 93% rename from src/components/AnimationControls/DonwloadGifButton.tsx rename to src/components/AnimationControls/DonwloadAnimationButton.tsx index 09096a3..33bf245 100644 --- a/src/components/AnimationControls/DonwloadGifButton.tsx +++ b/src/components/AnimationControls/DonwloadAnimationButton.tsx @@ -22,7 +22,7 @@ import { // isLoadingFrameDataSelector, } from '@store/AnimationMode/reducer'; -const DonwloadGifButton = () => { +export const DonwloadAnimationButton = () => { const dispatch = useDispatch(); // const isLoadingFrameData = useSelector(isLoadingFrameDataSelector); @@ -39,9 +39,7 @@ const DonwloadGifButton = () => { return (
- Download GIF + Download Animation
); }; - -export default DonwloadGifButton; From fd81c01f06951daa84adc1f207dd90dbf8627397 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 28 Feb 2024 15:04:03 -0800 Subject: [PATCH 19/56] fix: update setActiveFrame prop of Animation Frame Selector call releaseNumberOfActiveAnimationFrameChanged --- src/components/AnimationControls/AnimationControls.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/AnimationControls/AnimationControls.tsx b/src/components/AnimationControls/AnimationControls.tsx index c601f4e..119f444 100644 --- a/src/components/AnimationControls/AnimationControls.tsx +++ b/src/components/AnimationControls/AnimationControls.tsx @@ -46,6 +46,7 @@ import { // indexOfActiveAnimationFrameChanged, selectReleaseNumberOfActiveAnimationFrame, rNum2ExcludeToggled, + releaseNumberOfActiveAnimationFrameChanged, // releaseNumberOfActiveAnimationFrameChanged, // setActiveFrameByReleaseNum, } from '@store/AnimationMode/reducer'; @@ -136,7 +137,13 @@ const AnimationControls = () => { waybackItemsWithLocalChanges={waybackItemsWithLocalChanges} rNum2Exclude={rNum2ExcludeFromAnimation} setActiveFrame={(rNum) => { - // dispatch(releaseNumberOfActiveAnimationFrameChanged(rNum)); + if (animationStatus !== 'pausing') { + return; + } + + dispatch( + releaseNumberOfActiveAnimationFrameChanged(rNum) + ); // console.log(rNum); }} toggleFrame={(rNum) => { From b80315620997e7a6c7f6497aed0f88ee5e5025dc Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Wed, 28 Feb 2024 15:28:10 -0800 Subject: [PATCH 20/56] fix: update useMediaLayerAnimation hook to show active frame selected by the user when the animation is paused --- .../AnimationLayer/AnimationLayer.tsx | 6 +++++ .../AnimationLayer/useMediaLayerAnimation.tsx | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/components/AnimationLayer/AnimationLayer.tsx b/src/components/AnimationLayer/AnimationLayer.tsx index 967f424..893b7a2 100644 --- a/src/components/AnimationLayer/AnimationLayer.tsx +++ b/src/components/AnimationLayer/AnimationLayer.tsx @@ -28,6 +28,7 @@ import { // indexOfActiveAnimationFrameChanged, releaseNumberOfActiveAnimationFrameChanged, selectAnimationStatus, + selectReleaseNumberOfActiveAnimationFrame, showDownloadAnimationPanelToggled, toggleAnimationMode, // waybackItems4AnimationSelector, @@ -70,6 +71,10 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { selectIsLoadingWaybackItems ); + const releaseNumOfActiveFrame = useSelector( + selectReleaseNumberOfActiveAnimationFrame + ); + /** * Array of Imagery Element Data */ @@ -101,6 +106,7 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { animationSpeed: animationSpeed * 1000, imageElementsData, releaseNumOfItems2Exclude, + releaseNumOfActiveFrame, activeFrameOnChange, }); diff --git a/src/components/AnimationLayer/useMediaLayerAnimation.tsx b/src/components/AnimationLayer/useMediaLayerAnimation.tsx index 4c9abb3..11e5d1f 100644 --- a/src/components/AnimationLayer/useMediaLayerAnimation.tsx +++ b/src/components/AnimationLayer/useMediaLayerAnimation.tsx @@ -35,6 +35,8 @@ type Props = { * list of release number of wayback items to exclude from the animation */ releaseNumOfItems2Exclude: number[]; + + releaseNumOfActiveFrame: number; /** * Fires when the active frame changes * @param indexOfActiveFrame index of the active frame @@ -53,6 +55,7 @@ const useMediaLayerAnimation = ({ animationSpeed, imageElementsData, releaseNumOfItems2Exclude, + releaseNumOfActiveFrame, activeFrameOnChange, }: Props) => { const isPlayingRef = useRef(false); @@ -163,6 +166,27 @@ const useMediaLayerAnimation = ({ useEffect(() => { releaseNumOfItems2ExcludeRef.current = releaseNumOfItems2Exclude; }, [releaseNumOfItems2Exclude]); + + useEffect(() => { + // should not do anything if the animation is playing or loading + if (animationStatus !== 'pausing') { + return; + } + + // find the index of active frame using the release number + // why is this necessary? when the animation is paused, the user can click on the + // frame list card to view a frame and decide whether or not to include this frame + // in the animation + indexOfActiveFrame.current = imageElementsData.findIndex( + (d) => d.releaseNumber === releaseNumOfActiveFrame + ); + + // adjust opacity of image elements to show the one that is currently active + for (let i = 0; i < imageElementsData.length; i++) { + const opacity = i === indexOfActiveFrame.current ? 1 : 0; + imageElementsData[i].imageElement.opacity = opacity; + } + }, [animationStatus, releaseNumOfActiveFrame, imageElementsData]); }; export default useMediaLayerAnimation; From cdd386bbce62c115ce3ddc22291c4155e0bd33c5 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Thu, 29 Feb 2024 09:52:13 -0800 Subject: [PATCH 21/56] feat: add Download Animation Panel component --- package-lock.json | 11 + package.json | 1 + .../DownloadJobStatus.tsx | 85 +++++++ .../DownloadOptionsList.tsx | 139 ++++++++++++ .../AnimationDownloadPanel/DownloadPanel.tsx | 211 ++++++++++++++++++ .../OpenDownloadPanelButton.tsx | 46 ++++ .../AnimationDownloadPanel/PreviewWindow.tsx | 92 ++++++++ .../AnimationDownloadPanel/config.ts | 38 ++++ .../AnimationDownloadPanel/index.ts | 16 ++ .../AnimationLayer/AnimationLayer.tsx | 22 +- .../useFrameDataForDownloadJob.tsx | 88 ++++++++ src/constants/strings.ts | 2 +- src/store/AnimationMode/reducer.ts | 6 +- src/store/Map/reducer.ts | 5 + src/utils/snippets/downloadBlob.ts | 29 +++ src/utils/snippets/loadImage.ts | 27 +++ 16 files changed, 808 insertions(+), 10 deletions(-) create mode 100644 src/components/AnimationDownloadPanel/DownloadJobStatus.tsx create mode 100644 src/components/AnimationDownloadPanel/DownloadOptionsList.tsx create mode 100644 src/components/AnimationDownloadPanel/DownloadPanel.tsx create mode 100644 src/components/AnimationDownloadPanel/OpenDownloadPanelButton.tsx create mode 100644 src/components/AnimationDownloadPanel/PreviewWindow.tsx create mode 100644 src/components/AnimationDownloadPanel/config.ts create mode 100644 src/components/AnimationDownloadPanel/index.ts create mode 100644 src/components/AnimationLayer/useFrameDataForDownloadJob.tsx create mode 100644 src/utils/snippets/downloadBlob.ts create mode 100644 src/utils/snippets/loadImage.ts diff --git a/package-lock.json b/package-lock.json index ddb6dfe..9cc1942 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@esri/arcgis-rest-feature-service": "^4.0.4", "@esri/arcgis-rest-request": "^4.2.0", "@reduxjs/toolkit": "^1.9.5", + "@vannizhang/images-to-video-converter-client": "^1.1.10", "@vannizhang/wayback-core": "^1.0.6", "axios": "^1.6.2", "classnames": "^2.2.6", @@ -4426,6 +4427,11 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@vannizhang/images-to-video-converter-client": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@vannizhang/images-to-video-converter-client/-/images-to-video-converter-client-1.1.10.tgz", + "integrity": "sha512-OjE4aLaXZbxq8XIRE/Pa/MYt1BFz+j1CPZinoF2pT4rG6eNFmkBkYCdVFTgB+X1KhpsSapTpbrt1PFCL9z/CsA==" + }, "node_modules/@vannizhang/wayback-core": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@vannizhang/wayback-core/-/wayback-core-1.0.6.tgz", @@ -20230,6 +20236,11 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "@vannizhang/images-to-video-converter-client": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@vannizhang/images-to-video-converter-client/-/images-to-video-converter-client-1.1.10.tgz", + "integrity": "sha512-OjE4aLaXZbxq8XIRE/Pa/MYt1BFz+j1CPZinoF2pT4rG6eNFmkBkYCdVFTgB+X1KhpsSapTpbrt1PFCL9z/CsA==" + }, "@vannizhang/wayback-core": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@vannizhang/wayback-core/-/wayback-core-1.0.6.tgz", diff --git a/package.json b/package.json index 9e57759..a29a037 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "@esri/arcgis-rest-feature-service": "^4.0.4", "@esri/arcgis-rest-request": "^4.2.0", "@reduxjs/toolkit": "^1.9.5", + "@vannizhang/images-to-video-converter-client": "^1.1.10", "@vannizhang/wayback-core": "^1.0.6", "axios": "^1.6.2", "classnames": "^2.2.6", diff --git a/src/components/AnimationDownloadPanel/DownloadJobStatus.tsx b/src/components/AnimationDownloadPanel/DownloadJobStatus.tsx new file mode 100644 index 0000000..83d9e70 --- /dev/null +++ b/src/components/AnimationDownloadPanel/DownloadJobStatus.tsx @@ -0,0 +1,85 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC } from 'react'; +import { DownloadJobStatus } from './DownloadPanel'; +import classNames from 'classnames'; + +type Props = { + status: DownloadJobStatus; + /** + * emits when user clicks on cancel button to cancel the pending download job + * @returns + */ + cancelButtonOnClick: () => void; + /** + * emit when user clicks on the close button to hide the notification message + * @returns + */ + closeButtonOnClick: () => void; +}; + +export const DownloadJobStatusInfo: FC = ({ + status, + cancelButtonOnClick, + closeButtonOnClick, +}) => { + if (!status) { + return null; + } + + return ( +
+ {status === 'pending' && ( +
+ + Creating MP4. + + Cancel + +
+ )} + + {(status === 'finished' || status === 'failed') && ( +
+

+ {status === 'finished' + ? 'Complete! Check browser downloads for the MP4 file.' + : 'Failed to create MP4.'} +

+ + +
+ )} +
+ ); +}; diff --git a/src/components/AnimationDownloadPanel/DownloadOptionsList.tsx b/src/components/AnimationDownloadPanel/DownloadOptionsList.tsx new file mode 100644 index 0000000..0e3bd20 --- /dev/null +++ b/src/components/AnimationDownloadPanel/DownloadOptionsList.tsx @@ -0,0 +1,139 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC, useRef } from 'react'; +import { VIDEO_SIZE_OPTIONS } from './config'; +import classNames from 'classnames'; + +type DimensionInfoProps = { + width: number; + height: number; + onClick: () => void; + onMouseEnter: () => void; + onMouseLeave: () => void; +}; + +/** + * size of the icon that will be used to preview the dimension + */ +const DIMENSION_ICON_SIZE = 32; + +/** + * this is the max size of the output video that we support + */ +const MAX_SIZE_OUTPUT_VIDEO = 1920; + +const DimensionInfo: FC = ({ + width, + height, + onClick, + onMouseEnter, + onMouseLeave, +}) => { + const getDimensionIcon = () => { + return ( +
+
+
+ ); + }; + + return ( +
+ {getDimensionIcon()} + + {width} x {height} + +
+ ); +}; + +type Props = { + /** + * emits when user hovers an option list item + * @param size [width, height] of output video + * @returns void + */ + onMouseEnter: (sizes?: number[]) => void; + onMouseLeave: () => void; + /** + * emits when user clicks an option list item + * @param size [width, height] of output video + * @returns void + */ + onClick: (sizes?: number[]) => void; +}; + +export const DownloadOptionsList: FC = ({ + onMouseEnter, + onMouseLeave, + onClick, +}) => { + return ( +
+
+ {VIDEO_SIZE_OPTIONS.map((d) => { + const { title, dimensions } = d; + + return ( +
+

+ {title} +

+
+ {dimensions.map((size) => { + const [w, h] = size; + + return ( + + ); + })} +
+
+ ); + })} +
+
+ ); +}; diff --git a/src/components/AnimationDownloadPanel/DownloadPanel.tsx b/src/components/AnimationDownloadPanel/DownloadPanel.tsx new file mode 100644 index 0000000..e6bed3a --- /dev/null +++ b/src/components/AnimationDownloadPanel/DownloadPanel.tsx @@ -0,0 +1,211 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC, useEffect, useRef, useState } from 'react'; +import IImageElement from '@arcgis/core/layers/support/ImageElement'; +import { downloadBlob } from '@utils/snippets/downloadBlob'; +import { DownloadOptionsList } from './DownloadOptionsList'; +import { Dimension, PreviewWindow } from './PreviewWindow'; +import { useSelector } from 'react-redux'; +import { DownloadJobStatusInfo } from './DownloadJobStatus'; +import { CloseButton } from '../CloseButton'; +import { useDispatch } from 'react-redux'; +// import { selectMapCenter } from '@shared/store/Map/selectors'; +import { OpenDownloadPanelButton } from './OpenDownloadPanelButton'; +import { + convertImages2Video, + AnimationFrameData, +} from '@vannizhang/images-to-video-converter-client'; +import { APP_TITLE } from '@constants/strings'; +import { + selectShouldShowDownloadPanel, + showDownloadAnimationPanelToggled, +} from '@store/AnimationMode/reducer'; + +// /** +// * This object contains the data for each animation frame. +// */ +// export type AnimationFrameData4DownloadJob = { +// /** +// * The image element representing the median layer for this frame. +// */ +// mediaLayerElement: IImageElement; +// /** +// * Additional information about this frame. +// */ +// info: string; +// }; + +type Props = { + /** + * An array containing data representing the animation frames. + */ + frameData4DownloadJob: AnimationFrameData[]; + /** + * animation speed in millisecond + */ + animationSpeed: number; + /** + * size of the map view window + */ + mapViewWindowSize: Dimension; +}; + +/** + * status of job to download animation as MP4 + */ +export type DownloadJobStatus = 'pending' | 'finished' | 'cancelled' | 'failed'; + +export const AnimationDownloadPanel: FC = ({ + frameData4DownloadJob, + animationSpeed, + mapViewWindowSize, +}) => { + const dispatch = useDispatch(); + + const shouldShowDownloadPanel = useSelector(selectShouldShowDownloadPanel); + + const [previewWindowSize, setPreviewWindowSize] = useState(null); + + const [downloadJobStatus, setDownloadJobStatus] = + useState(null); + + const abortController = useRef(); + + const downloadAnimation = async (outputVideoDimension: Dimension) => { + setDownloadJobStatus('pending'); + + const { width, height } = outputVideoDimension; + + try { + if (abortController.current) { + abortController.current.abort(); + } + + abortController.current = new AbortController(); + + const { filename, fileContent } = await convertImages2Video({ + data: frameData4DownloadJob, + animationSpeed, + outputWidth: width, + outputHeight: height, + authoringApp: APP_TITLE, + abortController: abortController.current, + }); + + downloadBlob(fileContent, filename); + + setDownloadJobStatus('finished'); + } catch (err) { + console.log(err); + + // no need to set status to failed if error + // is caused by the user aborting the pending job + if (err.name === 'AbortError') { + return; + } + + setDownloadJobStatus('failed'); + } + }; + + useEffect(() => { + if (!shouldShowDownloadPanel) { + setPreviewWindowSize(null); + setDownloadJobStatus(null); + + if (abortController.current) { + abortController.current.abort(); + } + } + }, [shouldShowDownloadPanel]); + + if (!frameData4DownloadJob || !frameData4DownloadJob?.length) { + return null; + } + + return ( + <> +
+ {/* Download Button that opens the Download Animation Panel */} + {shouldShowDownloadPanel === false && ( + + )} + + {downloadJobStatus !== null && ( + { + // close animation download panel will also cancel any + // pending tasks + dispatch(showDownloadAnimationPanelToggled(false)); + }} + closeButtonOnClick={() => { + dispatch(showDownloadAnimationPanelToggled(false)); + }} + /> + )} + + {shouldShowDownloadPanel && downloadJobStatus === null && ( + <> + { + if (!size) { + return; + } + + const [width, height] = size; + + setPreviewWindowSize({ + width, + height, + }); + // console.log(size); + }} + onMouseLeave={setPreviewWindowSize.bind(null, null)} + onClick={(size) => { + if (!size) { + return; + } + + const [width, height] = size; + + downloadAnimation({ + width, + height, + }); + }} + /> + + { + dispatch( + showDownloadAnimationPanelToggled(false) + ); + }} + /> + + )} +
+ + {previewWindowSize && ( + + )} + + ); +}; diff --git a/src/components/AnimationDownloadPanel/OpenDownloadPanelButton.tsx b/src/components/AnimationDownloadPanel/OpenDownloadPanelButton.tsx new file mode 100644 index 0000000..6df1f49 --- /dev/null +++ b/src/components/AnimationDownloadPanel/OpenDownloadPanelButton.tsx @@ -0,0 +1,46 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { showDownloadAnimationPanelToggled } from '@store/AnimationMode/reducer'; +import classNames from 'classnames'; +import React from 'react'; +import { useDispatch } from 'react-redux'; + +export const OpenDownloadPanelButton = () => { + const dispatch = useDispatch(); + + return ( +
{ + dispatch(showDownloadAnimationPanelToggled(true)); + }} + > + {/* download-to icon: https://esri.github.io/calcite-ui-icons/#download-to */} + + + + +
+ ); +}; diff --git a/src/components/AnimationDownloadPanel/PreviewWindow.tsx b/src/components/AnimationDownloadPanel/PreviewWindow.tsx new file mode 100644 index 0000000..1935d0d --- /dev/null +++ b/src/components/AnimationDownloadPanel/PreviewWindow.tsx @@ -0,0 +1,92 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC, useMemo } from 'react'; + +export type Dimension = { + width: number; + height: number; +}; + +type Props = { + /** + * dimension of the preview window + */ + previewWindowSize: Dimension; + /** + * dimension of the map view window + */ + mapViewWindowSize: Dimension; +}; + +/** + * Preview Window component helps user to visually see the portion of animation that will be included in the output mp4 file + * @param param0 + * @returns + */ +export const PreviewWindow: FC = ({ + previewWindowSize, + mapViewWindowSize, +}) => { + /** + * useMemo Hook for Adjusting Window Size + * + * This `useMemo` hook adjusts the size of a preview window (represented by the `size` variable) + * to make sure it can fit within the preview window (represented by the `mapViewWindowSize` variable). + * + * @param {WindowSize} size - The original size of the preview window to be adjusted. + * @param {WindowSize} mapViewWindowSize - The size of the map view window. + * @returns {WindowSize | null} - The adjusted window size, or null if `size` is falsy. + */ + const adjustedSize: Dimension = useMemo(() => { + if (!previewWindowSize) { + return null; + } + + // Calculate the aspect ratio of the user selected output size size. + const aspectRatio = previewWindowSize.width / previewWindowSize.height; + + const previewWindowHeight = mapViewWindowSize.height; + const previewWindowWidth = previewWindowHeight * aspectRatio; + + if (previewWindowWidth > mapViewWindowSize.width) { + return { + width: mapViewWindowSize.width, + height: mapViewWindowSize.width * (1 / aspectRatio), + }; + } + + return { + height: previewWindowHeight, + width: previewWindowWidth, + }; + }, [previewWindowSize]); + + if (!adjustedSize) { + return null; + } + + return ( +
+
+
+ ); +}; diff --git a/src/components/AnimationDownloadPanel/config.ts b/src/components/AnimationDownloadPanel/config.ts new file mode 100644 index 0000000..9f57891 --- /dev/null +++ b/src/components/AnimationDownloadPanel/config.ts @@ -0,0 +1,38 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const VIDEO_SIZE_OPTIONS = [ + { + title: 'horizontal', + dimensions: [ + [1920, 1080], // aspect ratio 16:9 + [1080, 720], // aspect ratio 3:2 + ], + }, + { + title: 'square', + dimensions: [ + [1080, 1080], // aspect ratio 1:1 + [720, 720], // aspect ratio 1:1 + ], + }, + { + title: 'vertical', + dimensions: [ + [1080, 1920], // aspect ratio 9:16 + [720, 1080], // aspect ratio 2:3 + ], + }, +]; diff --git a/src/components/AnimationDownloadPanel/index.ts b/src/components/AnimationDownloadPanel/index.ts new file mode 100644 index 0000000..e1cbb80 --- /dev/null +++ b/src/components/AnimationDownloadPanel/index.ts @@ -0,0 +1,16 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { AnimationDownloadPanel } from './DownloadPanel'; diff --git a/src/components/AnimationLayer/AnimationLayer.tsx b/src/components/AnimationLayer/AnimationLayer.tsx index 893b7a2..a0df56f 100644 --- a/src/components/AnimationLayer/AnimationLayer.tsx +++ b/src/components/AnimationLayer/AnimationLayer.tsx @@ -41,6 +41,8 @@ import { selectIsLoadingWaybackItems, waybackItemsWithLocalChangesSelector, } from '@store/Wayback/reducer'; +import { AnimationDownloadPanel } from '@components/AnimationDownloadPanel'; +import { useFrameDataForDownloadJob } from './useFrameDataForDownloadJob'; type Props = { mapView?: MapView; @@ -55,7 +57,9 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { const animationStatus = useSelector(selectAnimationStatus); - const animationSpeed = useSelector(animationSpeedSelector); + const animationSpeedInSeconds = useSelector(animationSpeedSelector); + + const animationSpeedInMilliseconds = animationSpeedInSeconds * 1000; /** * wayback items with local changes @@ -85,6 +89,12 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { isLoading: isLoadingWaybackItemsWithLoalChanges, }); + const frameData = useFrameDataForDownloadJob({ + waybackItems, + imageElements: imageElementsData, + releaseNumOfItems2Exclude, + }); + /** * This is a callback function that will be called each time the active frame (Image Element) in the animation layer is changed. */ @@ -103,7 +113,7 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { useMediaLayerAnimation({ animationStatus, - animationSpeed: animationSpeed * 1000, + animationSpeed: animationSpeedInMilliseconds, imageElementsData, releaseNumOfItems2Exclude, releaseNumOfActiveFrame, @@ -178,14 +188,14 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { }} /> - {/* */} + />
); }; diff --git a/src/components/AnimationLayer/useFrameDataForDownloadJob.tsx b/src/components/AnimationLayer/useFrameDataForDownloadJob.tsx new file mode 100644 index 0000000..a6c0808 --- /dev/null +++ b/src/components/AnimationLayer/useFrameDataForDownloadJob.tsx @@ -0,0 +1,88 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import ImageElement from '@arcgis/core/layers/support/ImageElement'; +import { selectMapCenter } from '@store/Map/reducer'; +import { IWaybackItem } from '@typings/index'; +import { loadImageAsHTMLIMageElement } from '@utils/snippets/loadImage'; +import { AnimationFrameData } from '@vannizhang/images-to-video-converter-client'; +import React, { FC, useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { ImageElementData } from './useMediaLayerImageElement'; +// import { AnimationFrameData4DownloadJob } from '../AnimationDownloadPanel/DownloadPanel'; + +/** + * Represents the properties required by the custom hook `useFrameDataForDownloadJob`. + */ +type Props = { + /** + * An array wayback items associated with the media layer elements in animation mode. + */ + waybackItems: IWaybackItem[]; + /** + * An array of ImageElement objects representing media layer elements. + */ + imageElements: ImageElementData[]; + /** + * list of release number of wayback items to exclude from the animation. + */ + releaseNumOfItems2Exclude: number[]; +}; + +/** + * This custom hook returns an array of `AnimationFrameData` objects that + * can be used by the Animation Download task. + * @param {Props} - The properties required by the hook. + * @returns An array of `AnimationFrameData4DownloadJob` objects. + */ +export const useFrameDataForDownloadJob = ({ + waybackItems, + imageElements, +}: Props) => { + const { lon, lat } = useSelector(selectMapCenter) || {}; + + const [frameData, setFrameData] = useState([]); + + useEffect(() => { + (async () => { + if (!waybackItems?.length || !imageElements?.length) { + setFrameData([]); + return; + } + + // load media layer elements as an array of HTML Image Elements + const images = await Promise.all( + imageElements.map((d) => + loadImageAsHTMLIMageElement(d.imageElement.image as string) + ) + ); + + const data: AnimationFrameData[] = images.map((image, index) => { + const item = waybackItems[index]; + + return { + image, + imageInfo: `${item.releaseDateLabel} | x ${lon.toFixed( + 3 + )} y ${lat.toFixed(3)}`, + } as AnimationFrameData; + }); + + setFrameData(data); + })(); + }, [imageElements]); + + return frameData; +}; diff --git a/src/constants/strings.ts b/src/constants/strings.ts index 140d543..9547d3c 100644 --- a/src/constants/strings.ts +++ b/src/constants/strings.ts @@ -13,4 +13,4 @@ * limitations under the License. */ -export const APP_TITLE = 'World Imagery Wayback'; +export const APP_TITLE = 'ESRI | World Imagery Wayback'; diff --git a/src/store/AnimationMode/reducer.ts b/src/store/AnimationMode/reducer.ts index 98de1dc..11b8857 100644 --- a/src/store/AnimationMode/reducer.ts +++ b/src/store/AnimationMode/reducer.ts @@ -19,9 +19,9 @@ import { PayloadAction, // createAsyncThunk } from '@reduxjs/toolkit'; -import { batch } from 'react-redux'; -import { IWaybackItem } from '@typings/index'; -import { saveAnimationSpeedInURLQueryParam } from '@utils/UrlSearchParam'; +// import { batch } from 'react-redux'; +// import { IWaybackItem } from '@typings/index'; +// import { saveAnimationSpeedInURLQueryParam } from '@utils/UrlSearchParam'; // import { IWaybackItem } from '@typings/index'; import { RootState, StoreDispatch, StoreGetState } from '../configureStore'; diff --git a/src/store/Map/reducer.ts b/src/store/Map/reducer.ts index 1bf97e0..3b5defb 100644 --- a/src/store/Map/reducer.ts +++ b/src/store/Map/reducer.ts @@ -150,4 +150,9 @@ export const selectMapCenterAndZoom = createSelector( } ); +export const selectMapCenter = createSelector( + (state: RootState) => state.Map.center, + (center) => center +); + export default reducer; diff --git a/src/utils/snippets/downloadBlob.ts b/src/utils/snippets/downloadBlob.ts new file mode 100644 index 0000000..ea821d9 --- /dev/null +++ b/src/utils/snippets/downloadBlob.ts @@ -0,0 +1,29 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const downloadBlob = (blob: Blob, outputFileName: string): void => { + // const blob = new Blob(chunks, { type: 'video/webm' }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = outputFileName; //'test.webm'; + document.body.appendChild(a); + a.click(); + setTimeout(() => { + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }, 100); +}; diff --git a/src/utils/snippets/loadImage.ts b/src/utils/snippets/loadImage.ts new file mode 100644 index 0000000..537aa45 --- /dev/null +++ b/src/utils/snippets/loadImage.ts @@ -0,0 +1,27 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const loadImageAsHTMLIMageElement = async ( + imageURL: string +): Promise => { + const image = new Image(); + image.src = imageURL; + + return new Promise((resolve) => { + image.onload = () => { + resolve(image); + }; + }); +}; From 54f9deb960d7f1aefdf3496ece7cfda2e37c2f8b Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Thu, 29 Feb 2024 10:08:44 -0800 Subject: [PATCH 22/56] fix: update style of the Animation Download Panel --- .../AnimationControls/DonwloadAnimationButton.tsx | 2 +- src/components/AnimationDownloadPanel/DownloadJobStatus.tsx | 2 +- .../AnimationDownloadPanel/DownloadOptionsList.tsx | 2 +- tailwind.config.js | 6 ++++++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/AnimationControls/DonwloadAnimationButton.tsx b/src/components/AnimationControls/DonwloadAnimationButton.tsx index 33bf245..894ae57 100644 --- a/src/components/AnimationControls/DonwloadAnimationButton.tsx +++ b/src/components/AnimationControls/DonwloadAnimationButton.tsx @@ -30,7 +30,7 @@ export const DonwloadAnimationButton = () => { const animationStatus = useSelector(selectAnimationStatus); const onClickHandler = useCallback(() => { - dispatch(showDownloadAnimationPanelToggled()); + dispatch(showDownloadAnimationPanelToggled(true)); }, []); const classNames = classnames('btn btn-fill', { diff --git a/src/components/AnimationDownloadPanel/DownloadJobStatus.tsx b/src/components/AnimationDownloadPanel/DownloadJobStatus.tsx index 83d9e70..6eb67e1 100644 --- a/src/components/AnimationDownloadPanel/DownloadJobStatus.tsx +++ b/src/components/AnimationDownloadPanel/DownloadJobStatus.tsx @@ -45,7 +45,7 @@ export const DownloadJobStatusInfo: FC = ({ className={classNames( 'absolute top-0 right-0 w-[220px] h-[72px] px-4', 'flex items-center', - 'theme-background text-xs' + 'bg-custom-background text-custom-foreground text-xs' )} > {status === 'pending' && ( diff --git a/src/components/AnimationDownloadPanel/DownloadOptionsList.tsx b/src/components/AnimationDownloadPanel/DownloadOptionsList.tsx index 0e3bd20..b2dd723 100644 --- a/src/components/AnimationDownloadPanel/DownloadOptionsList.tsx +++ b/src/components/AnimationDownloadPanel/DownloadOptionsList.tsx @@ -99,7 +99,7 @@ export const DownloadOptionsList: FC = ({ return (
diff --git a/tailwind.config.js b/tailwind.config.js index 5682f5f..06c3477 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -20,6 +20,12 @@ module.exports = { 'light': '#56a5d8', 'dark': '#1A3D60' } + }, + 'background': { + DEFAULT: '#121212' + }, + 'foreground': { + DEFAULT: '#ccc' } } }, From 7588245865069d651aa902f18dc26737d143cd21 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Thu, 29 Feb 2024 12:00:09 -0800 Subject: [PATCH 23/56] fix: useFrameDataForDownloadJob should ignore items in the exclusion list --- .../useFrameDataForDownloadJob.tsx | 30 ++++++++++++------- .../useMediaLayerImageElement.tsx | 5 ++-- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/components/AnimationLayer/useFrameDataForDownloadJob.tsx b/src/components/AnimationLayer/useFrameDataForDownloadJob.tsx index a6c0808..a1bf80a 100644 --- a/src/components/AnimationLayer/useFrameDataForDownloadJob.tsx +++ b/src/components/AnimationLayer/useFrameDataForDownloadJob.tsx @@ -50,6 +50,7 @@ type Props = { export const useFrameDataForDownloadJob = ({ waybackItems, imageElements, + releaseNumOfItems2Exclude, }: Props) => { const { lon, lat } = useSelector(selectMapCenter) || {}; @@ -62,27 +63,34 @@ export const useFrameDataForDownloadJob = ({ return; } - // load media layer elements as an array of HTML Image Elements - const images = await Promise.all( - imageElements.map((d) => - loadImageAsHTMLIMageElement(d.imageElement.image as string) - ) - ); + const data: AnimationFrameData[] = []; - const data: AnimationFrameData[] = images.map((image, index) => { - const item = waybackItems[index]; + for (let i = 0; i < imageElements.length; i++) { + const item = waybackItems[i]; - return { + // should not include the frame of the items in the exlusion list + if (releaseNumOfItems2Exclude.includes(item.releaseNum)) { + continue; + } + + // load media layer elements as an array of HTML Image Elements + const image = await loadImageAsHTMLIMageElement( + imageElements[i].imageElement.image as string + ); + + const frameData = { image, imageInfo: `${item.releaseDateLabel} | x ${lon.toFixed( 3 )} y ${lat.toFixed(3)}`, } as AnimationFrameData; - }); + + data.push(frameData); + } setFrameData(data); })(); - }, [imageElements]); + }, [imageElements, releaseNumOfItems2Exclude]); return frameData; }; diff --git a/src/components/AnimationLayer/useMediaLayerImageElement.tsx b/src/components/AnimationLayer/useMediaLayerImageElement.tsx index 0131ebe..5c7e2d3 100644 --- a/src/components/AnimationLayer/useMediaLayerImageElement.tsx +++ b/src/components/AnimationLayer/useMediaLayerImageElement.tsx @@ -51,8 +51,7 @@ export const useMediaLayerImageElement = ({ waybackItems, isLoading, }: Props) => { - const [imageElements, setImageElements] = - useState(null); + const [imageElements, setImageElements] = useState([]); const abortControllerRef = useRef(); @@ -135,7 +134,7 @@ export const useMediaLayerImageElement = ({ } } - setImageElements(null); + setImageElements([]); } else if (animationStatus === 'loading') { loadFrameData(); } From f294f9a667e0ef11f6a4ca4d21aec4721ead1556 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Thu, 29 Feb 2024 12:23:48 -0800 Subject: [PATCH 24/56] fix: useMediaLayerImageElement should hold off updating image elements if wayback items are in loading --- .../AnimationLayer/AnimationLayer.tsx | 4 ++-- .../useMediaLayerImageElement.tsx | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/components/AnimationLayer/AnimationLayer.tsx b/src/components/AnimationLayer/AnimationLayer.tsx index a0df56f..5318e98 100644 --- a/src/components/AnimationLayer/AnimationLayer.tsx +++ b/src/components/AnimationLayer/AnimationLayer.tsx @@ -86,7 +86,7 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { mapView, animationStatus, waybackItems, - isLoading: isLoadingWaybackItemsWithLoalChanges, + isLoadingWaybackItemsWithLoalChanges, }); const frameData = useFrameDataForDownloadJob({ @@ -138,7 +138,7 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { const source = mediaLayerRef.current.source as any; - if (!imageElementsData) { + if (!imageElementsData || !imageElementsData?.length) { // animation is not started or just stopped // just clear all elements in media layer source.elements.removeAll(); diff --git a/src/components/AnimationLayer/useMediaLayerImageElement.tsx b/src/components/AnimationLayer/useMediaLayerImageElement.tsx index 5c7e2d3..c927315 100644 --- a/src/components/AnimationLayer/useMediaLayerImageElement.tsx +++ b/src/components/AnimationLayer/useMediaLayerImageElement.tsx @@ -26,12 +26,18 @@ export const MAP_CONTAINER_LEFT_OFFSET = 350; type Props = { mapView?: MapView; + /** + * status of the animation + */ animationStatus: AnimationStatus; + /** + * wayback items with local changes + */ waybackItems: IWaybackItem[]; /** * if true, it is in process of loading wayback items */ - isLoading: boolean; + isLoadingWaybackItemsWithLoalChanges: boolean; }; export type ImageElementData = { @@ -49,7 +55,7 @@ export const useMediaLayerImageElement = ({ mapView, animationStatus, waybackItems, - isLoading, + isLoadingWaybackItemsWithLoalChanges, }: Props) => { const [imageElements, setImageElements] = useState([]); @@ -121,7 +127,11 @@ export const useMediaLayerImageElement = ({ }; useEffect(() => { - if (!animationStatus || !waybackItems.length || isLoading) { + if (isLoadingWaybackItemsWithLoalChanges) { + return; + } + + if (!animationStatus || !waybackItems.length) { // call abort so all pending requests can be cancelled if (abortControllerRef.current) { abortControllerRef.current.abort(); @@ -138,7 +148,7 @@ export const useMediaLayerImageElement = ({ } else if (animationStatus === 'loading') { loadFrameData(); } - }, [animationStatus, waybackItems, isLoading]); + }, [animationStatus, waybackItems, isLoadingWaybackItemsWithLoalChanges]); return imageElements; }; From eebaa1ffd02d11d2f3453453181668b2641c2417 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 15 Mar 2024 09:21:44 -0700 Subject: [PATCH 25/56] fix: add drop-shadow effect to Download Animation Buttons --- .../AnimationDownloadPanel/OpenDownloadPanelButton.tsx | 1 + src/style/index.css | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/components/AnimationDownloadPanel/OpenDownloadPanelButton.tsx b/src/components/AnimationDownloadPanel/OpenDownloadPanelButton.tsx index 6df1f49..478d208 100644 --- a/src/components/AnimationDownloadPanel/OpenDownloadPanelButton.tsx +++ b/src/components/AnimationDownloadPanel/OpenDownloadPanelButton.tsx @@ -34,6 +34,7 @@ export const OpenDownloadPanelButton = () => { viewBox="0 0 32 32" height={64} width={64} + className="with-drop-shadow" > Date: Fri, 15 Mar 2024 09:23:17 -0700 Subject: [PATCH 26/56] fix: add drop-shadow effect to Close Button --- src/components/CloseButton/CloseButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CloseButton/CloseButton.tsx b/src/components/CloseButton/CloseButton.tsx index 1c0f970..c0548d7 100644 --- a/src/components/CloseButton/CloseButton.tsx +++ b/src/components/CloseButton/CloseButton.tsx @@ -28,7 +28,7 @@ export const CloseButton: FC = ({ onClick }: Props) => { viewBox="0 0 32 32" height="64" width="64" - className="absolute top-1 right-1 cursor-pointer" + className="absolute top-1 right-1 cursor-pointer with-drop-shadow" onClick={onClick} > Date: Fri, 15 Mar 2024 09:46:25 -0700 Subject: [PATCH 27/56] fix: style of SwipeWidgetLayerSelector --- .../SwipeWidgetLayerSelector.tsx | 58 ++----------------- 1 file changed, 6 insertions(+), 52 deletions(-) diff --git a/src/components/SwipeWidgetLayerSelector/SwipeWidgetLayerSelector.tsx b/src/components/SwipeWidgetLayerSelector/SwipeWidgetLayerSelector.tsx index a0f96c0..74d9a04 100644 --- a/src/components/SwipeWidgetLayerSelector/SwipeWidgetLayerSelector.tsx +++ b/src/components/SwipeWidgetLayerSelector/SwipeWidgetLayerSelector.tsx @@ -20,7 +20,7 @@ import classnames from 'classnames'; import { LayerSelector } from '../'; -export const SwipeWidgetLayerSelectorWidth = 210; +export const SwipeWidgetLayerSelectorWidth = 220; export type SwipeWidgetLayer = 'leading' | 'trailing'; @@ -54,43 +54,6 @@ const SwipeWidgetLayerSelector: React.FC = ({ const { releaseDateLabel, itemID } = d; const isSelected = selectedItem && selectedItem.itemID === itemID; - const classNames = classnames( - 'swipe-widget-layer-selector-item', - { - 'is-selected': isSelected, - 'is-arrow-on-left': targetLayerType === 'trailing', - } - ); - // return ( - //
- // {releaseDateLabel} - //
- // ); return ( = ({ }); return ( -
+
Versions with{' '} @@ -127,13 +86,7 @@ const SwipeWidgetLayerSelector: React.FC = ({ } return ( -
+

{targetLayerType === 'leading' ? 'Left' : 'Right'} Selection

@@ -170,6 +123,7 @@ const SwipeWidgetLayerSelector: React.FC = ({ return (
= ({ backgroundColor: '#121212', padding: '1rem', boxSizing: 'border-box', - display: 'flex', - alignItems: 'center', + // display: 'flex', + // alignItems: 'center', }} > {getTitle()} From bdf268d1e89e768f15004a52021a9b155d221ecd Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 15 Mar 2024 10:18:11 -0700 Subject: [PATCH 28/56] fix: style of MobileFooter --- src/components/MobileFooter/MobileFooter.tsx | 13 ++++++----- .../MobileFooter/MobileFooterContainer.tsx | 8 +++++-- src/components/Sidebar/Sidebar.tsx | 22 ++++++++++++------- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/components/MobileFooter/MobileFooter.tsx b/src/components/MobileFooter/MobileFooter.tsx index 7e9918b..7ec7a95 100644 --- a/src/components/MobileFooter/MobileFooter.tsx +++ b/src/components/MobileFooter/MobileFooter.tsx @@ -28,6 +28,7 @@ const MobileFooter: React.FC = ({ isGutterHide, OnClick }: Props) => { return (
= ({ isGutterHide, OnClick }: Props) => { // width: '100%', right: 0, background: DEFAULT_BACKGROUND_COLOR, - // height: '50px' + minHeight: '100px', padding: '.5rem 0', }} onClick={OnClick} > - +
+ +
{ const isGutterHide = useSelector(isGutterHideSelector); const isSideBarHide = useSelector(isSideBarHideSelector); - return isSideBarHide ? ( + if (!isSideBarHide) { + return null; + } + + return ( { dispatch(isSideBarHideToggled()); }} /> - ) : null; + ); }; export default MobileFooterContainer; diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index 69fecf0..8c7af2a 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -41,7 +41,7 @@ const Sidebar: React.FC = ({ right: 0, left: isGutterHide ? 0 : GUTTER_WIDTH, width: isGutterHide ? '100%' : 'calc(100% - 50px)', - maxHeight: 300, + maxHeight: 400, padding: '.5rem 0', }; @@ -65,15 +65,21 @@ const Sidebar: React.FC = ({ alignItems: 'stretch', }; - return isMobile - ? ({ - ...defaultStyle, - ...mobileStyle, - } as React.CSSProperties) - : defaultStyle; + if (isMobile) { + return { + ...defaultStyle, + ...mobileStyle, + } as React.CSSProperties; + } + + return defaultStyle; }; - return !isHide ?
{children}
: null; + if (isHide) { + return null; + } + + return
{children}
; }; export default Sidebar; From 575fb34a6976aaf2e7d49cbb5f5a49cd91d8cd5f Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 15 Mar 2024 11:04:08 -0700 Subject: [PATCH 29/56] fix: Animation Speed Control should use ANIMATION_SPEED_OPTIONS_IN_MILLISECONDS --- .../AnimationControls/SpeedSelector.tsx | 46 +++++++++++++------ .../AnimationLayer/AnimationLayer.tsx | 4 +- src/store/AnimationMode/reducer.ts | 16 +++++-- src/store/getPreloadedState.ts | 15 +++++- 4 files changed, 60 insertions(+), 21 deletions(-) diff --git a/src/components/AnimationControls/SpeedSelector.tsx b/src/components/AnimationControls/SpeedSelector.tsx index 87a938e..364ddac 100644 --- a/src/components/AnimationControls/SpeedSelector.tsx +++ b/src/components/AnimationControls/SpeedSelector.tsx @@ -13,22 +13,35 @@ * limitations under the License. */ +import { ANIMATION_SPEED_OPTIONS_IN_MILLISECONDS } from '@store/AnimationMode/reducer'; import React from 'react'; type Props = { defaultVal: number; - onChange: (speed: number) => void; + onChange: (speedInMilliseonds: number) => void; }; -const MIN_VAL = 0.0; // min animation speed is .5 second -const MAX_VAL = 2; // max animation speed is 3 second -const SLIDER_STEP = 0.25; +// const MIN_VAL = 0.0; // min animation speed is .5 second +// const MAX_VAL = 2; // max animation speed is 3 second +// const SLIDER_STEP = 0.25; const SpeedSelector: React.FC = ({ defaultVal, onChange }: Props) => { const sliderRef = React.useRef(); const onChangeDely = React.useRef(); + const calcSliderDefaultValue = () => { + const idx = ANIMATION_SPEED_OPTIONS_IN_MILLISECONDS.indexOf(defaultVal); + + if (idx === -1) { + return Math.floor( + ANIMATION_SPEED_OPTIONS_IN_MILLISECONDS.length / 2 + ); + } + + return idx; + }; + React.useEffect(() => { sliderRef.current.addEventListener( 'calciteSliderChange', @@ -39,13 +52,18 @@ const SpeedSelector: React.FC = ({ defaultVal, onChange }: Props) => { // console.log('slider on change', evt.target.value) // onChange(+evt.target.value) - const tickVal = Math.floor(+evt.target.value * 100) / 100; + // const tickVal = Math.floor(+evt.target.value * 100) / 100; + + // // the max val indciates fastes time and min val indicates slowest, therefore we need to use max val to minus the tick val + // // to get the actual animation speed, let's say the tick val is 2 and max val is 3, that gives a current speed of 1 second + // const val = MAX_VAL - tickVal; + + const index = evt.target.value; - // the max val indciates fastes time and min val indicates slowest, therefore we need to use max val to minus the tick val - // to get the actual animation speed, let's say the tick val is 2 and max val is 3, that gives a current speed of 1 second - const val = MAX_VAL - tickVal; + const speed = + ANIMATION_SPEED_OPTIONS_IN_MILLISECONDS[index]; - onChange(val); + onChange(speed); }, 500); } ); @@ -77,12 +95,12 @@ const SpeedSelector: React.FC = ({ defaultVal, onChange }: Props) => { >
diff --git a/src/components/AnimationLayer/AnimationLayer.tsx b/src/components/AnimationLayer/AnimationLayer.tsx index 5318e98..9d2d3b2 100644 --- a/src/components/AnimationLayer/AnimationLayer.tsx +++ b/src/components/AnimationLayer/AnimationLayer.tsx @@ -57,9 +57,9 @@ export const AnimationLayer: FC = ({ mapView }: Props) => { const animationStatus = useSelector(selectAnimationStatus); - const animationSpeedInSeconds = useSelector(animationSpeedSelector); + // const animationSpeedInSeconds = useSelector(animationSpeedSelector); - const animationSpeedInMilliseconds = animationSpeedInSeconds * 1000; + const animationSpeedInMilliseconds = useSelector(animationSpeedSelector); /** * wayback items with local changes diff --git a/src/store/AnimationMode/reducer.ts b/src/store/AnimationMode/reducer.ts index 11b8857..647b37b 100644 --- a/src/store/AnimationMode/reducer.ts +++ b/src/store/AnimationMode/reducer.ts @@ -45,7 +45,7 @@ export type AnimationModeState = { */ rNum2Exclude: number[]; /** - * animation speed in second + * animation speed in milliseconds */ animationSpeed: number; /** @@ -54,14 +54,24 @@ export type AnimationModeState = { releaseNumberOfActiveAnimationFrame: number; }; -export const DEFAULT_ANIMATION_SPEED_IN_SECONDS = 1; +/** + * list of animation speed in milliseconds + */ +export const ANIMATION_SPEED_OPTIONS_IN_MILLISECONDS = [ + 2000, 1000, 800, 600, 400, 200, 100, 20, 0, +]; + +export const DEFAULT_ANIMATION_SPEED_IN_MILLISECONDS = + ANIMATION_SPEED_OPTIONS_IN_MILLISECONDS[ + Math.floor(ANIMATION_SPEED_OPTIONS_IN_MILLISECONDS.length / 2) + ]; export const initialAnimationModeState = { animationStatus: null, showDownloadAnimationPanel: false, waybackItems4Animation: [], rNum2Exclude: [], - animationSpeed: DEFAULT_ANIMATION_SPEED_IN_SECONDS, + animationSpeed: DEFAULT_ANIMATION_SPEED_IN_MILLISECONDS, releaseNumberOfActiveAnimationFrame: null, } as AnimationModeState; diff --git a/src/store/getPreloadedState.ts b/src/store/getPreloadedState.ts index 8298706..94ebfa1 100644 --- a/src/store/getPreloadedState.ts +++ b/src/store/getPreloadedState.ts @@ -33,8 +33,10 @@ import { getDownloadJobsFromLocalStorage, } from '../utils/LocalStorage'; import { + ANIMATION_SPEED_OPTIONS_IN_MILLISECONDS, AnimationModeState, - DEFAULT_ANIMATION_SPEED_IN_SECONDS, + DEFAULT_ANIMATION_SPEED_IN_MILLISECONDS, + // DEFAULT_ANIMATION_SPEED_IN_MILLISECONDS, initialAnimationModeState, } from './AnimationMode/reducer'; @@ -139,7 +141,8 @@ const getPreloadedState4Map = (urlParams: IURLParamData): MapState => { const getPreloadedState4AnimationMode = ( urlParams: IURLParamData ): AnimationModeState => { - const { animationSpeed, rNum4FramesToExclude } = urlParams; + let { animationSpeed } = urlParams; + const { rNum4FramesToExclude } = urlParams; if ( animationSpeed === null || @@ -149,6 +152,14 @@ const getPreloadedState4AnimationMode = ( return initialAnimationModeState; } + // use default animation speed if the value from hash params is not in the list of options + if ( + ANIMATION_SPEED_OPTIONS_IN_MILLISECONDS.includes(+animationSpeed) === + false + ) { + animationSpeed = DEFAULT_ANIMATION_SPEED_IN_MILLISECONDS; + } + const state: AnimationModeState = { ...initialAnimationModeState, // isAnimationModeOn: true, From 0da6257c5349c7c3aa761ddebb0d2f58f6380807 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 15 Mar 2024 11:33:21 -0700 Subject: [PATCH 30/56] feat: add loading indicator to PopUp --- .../MetadataQueryTask/MetadataQueryTask.tsx | 12 ++++++--- .../MetadataQueryTaskContainer.tsx | 4 +++ .../PopUp/MetadataPopupContainer.tsx | 12 +++++++-- src/components/PopUp/index.tsx | 25 +++++++++++++++++-- src/store/Map/reducer.ts | 18 +++++++++++++ 5 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/components/MetadataQueryTask/MetadataQueryTask.tsx b/src/components/MetadataQueryTask/MetadataQueryTask.tsx index c40573b..a8db943 100644 --- a/src/components/MetadataQueryTask/MetadataQueryTask.tsx +++ b/src/components/MetadataQueryTask/MetadataQueryTask.tsx @@ -37,7 +37,7 @@ type Props = { isSwipeWidgetOpen: boolean; swipeWidgetPosition: number; mapView?: MapView; - + metadataQueryOnStart: () => void; metadataOnChange: (data: IWaybackMetadataQueryResult) => void; anchorPointOnChange: (data: IScreenPoint) => void; }; @@ -50,6 +50,7 @@ const MetadataQueryLayer: React.FC = ({ isSwipeWidgetOpen, swipeWidgetPosition, mapView, + metadataQueryOnStart, metadataOnChange, anchorPointOnChange, }) => { @@ -88,6 +89,10 @@ const MetadataQueryLayer: React.FC = ({ // zoom: mapView.zoom, // getCurrZoomLevel(mapView) // }); + updateScreenPoint4PopupAnchor(); + + metadataQueryOnStart(); + const res = await getMetadata( { latitude: mapPoint.latitude, @@ -96,7 +101,7 @@ const MetadataQueryLayer: React.FC = ({ mapView.zoom, // getCurrZoomLevel(mapView) releaseNum ); - console.log(res); + // console.log(res); const metadata: IWaybackMetadataQueryResult = res ? { @@ -105,11 +110,10 @@ const MetadataQueryLayer: React.FC = ({ } : null; - updateScreenPoint4PopupAnchor(); - metadataOnChange(metadata); } catch (err) { console.error(err); + metadataOnChange(null); } }; diff --git a/src/components/MetadataQueryTask/MetadataQueryTaskContainer.tsx b/src/components/MetadataQueryTask/MetadataQueryTaskContainer.tsx index 9d64ae0..0994c79 100644 --- a/src/components/MetadataQueryTask/MetadataQueryTaskContainer.tsx +++ b/src/components/MetadataQueryTask/MetadataQueryTaskContainer.tsx @@ -29,6 +29,7 @@ import { import { metadataQueryResultUpdated, metadataPopupAnchorUpdated, + isQueryingMetadataToggled, } from '@store/Map/reducer'; import MetadataQueryTask from './MetadataQueryTask'; @@ -62,6 +63,9 @@ const MetadataQueryTaskContainer: React.FC = ({ mapView }: Props) => { swipeWidgetTrailingLayer={swipeWidgetTrailingLayer} isSwipeWidgetOpen={isSwipeWidgetOpen} swipeWidgetPosition={swipeWidgetPosition} + metadataQueryOnStart={() => { + disptach(isQueryingMetadataToggled(true)); + }} metadataOnChange={(metadata) => { // console.log(metadata) disptach(metadataQueryResultUpdated(metadata)); diff --git a/src/components/PopUp/MetadataPopupContainer.tsx b/src/components/PopUp/MetadataPopupContainer.tsx index 3bf911b..a3f6c95 100644 --- a/src/components/PopUp/MetadataPopupContainer.tsx +++ b/src/components/PopUp/MetadataPopupContainer.tsx @@ -22,6 +22,7 @@ import { metadataPopupAnchorSelector, metadataQueryResultSelector, metadataQueryResultUpdated, + selectIsQueringMetadata, } from '@store/Map/reducer'; import MetadataPopUp from './index'; @@ -33,17 +34,24 @@ const MetadataPopupContainer = () => { const anchorPoint = useSelector(metadataPopupAnchorSelector); + const isQueryingMetadata = useSelector(selectIsQueringMetadata); + const isAnimationModeOn = useSelector(isAnimationModeOnSelector); - return !isAnimationModeOn ? ( + if (isAnimationModeOn) { + return null; + } + + return ( { dispatch(metadataQueryResultUpdated(null)); }} /> - ) : null; + ); }; export default MetadataPopupContainer; diff --git a/src/components/PopUp/index.tsx b/src/components/PopUp/index.tsx index 9490bcc..bcf3501 100644 --- a/src/components/PopUp/index.tsx +++ b/src/components/PopUp/index.tsx @@ -25,6 +25,10 @@ import { } from '@typings/index'; interface IProps { + /** + * if true, it is in process of querying metadata + */ + isQueryingMetadata: boolean; metadata: IWaybackMetadataQueryResult; metadataAnchorScreenPoint: IScreenPoint; onClose: () => void; @@ -54,9 +58,14 @@ class PopUp extends React.PureComponent { render() { // const { targetLayer } = this.props; - const { metadata, metadataAnchorScreenPoint } = this.props; + const { metadata, isQueryingMetadata, metadataAnchorScreenPoint } = + this.props; - if (!metadata || !metadataAnchorScreenPoint) { + if (!metadataAnchorScreenPoint) { + return null; + } + + if (!metadata && !isQueryingMetadata) { return null; } @@ -67,6 +76,18 @@ class PopUp extends React.PureComponent { width: this.Width, } as React.CSSProperties; + if (isQueryingMetadata) { + return ( +
+
+ +
+ +
+
+ ); + } + const { provider, source, resolution, accuracy, releaseDate, date } = metadata; diff --git a/src/store/Map/reducer.ts b/src/store/Map/reducer.ts index 3b5defb..5607eaf 100644 --- a/src/store/Map/reducer.ts +++ b/src/store/Map/reducer.ts @@ -37,6 +37,10 @@ export type MapMode = 'explore' | 'swipe' | 'animation'; export type MapState = { mode: MapMode; mapExtent: IExtentGeomety; + /** + * if true, it is in process of querying metadata + */ + isQueryingMetadata: boolean; metadataQueryResult: IWaybackMetadataQueryResult; metadataPopupAnchor: IScreenPoint; isReferenceLayerVisible: boolean; @@ -53,6 +57,7 @@ export type MapState = { export const initialMapState: MapState = { mode: 'explore', mapExtent: null, + isQueryingMetadata: false, metadataQueryResult: null, metadataPopupAnchor: null, isReferenceLayerVisible: true, @@ -78,6 +83,7 @@ const slice = createSlice({ action: PayloadAction ) => { state.metadataQueryResult = action.payload; + state.isQueryingMetadata = false; }, metadataPopupAnchorUpdated: ( state: MapState, @@ -88,6 +94,12 @@ const slice = createSlice({ isReferenceLayerVisibleToggled: (state: MapState) => { state.isReferenceLayerVisible = !state.isReferenceLayerVisible; }, + isQueryingMetadataToggled: ( + state: MapState, + action: PayloadAction + ) => { + state.isQueryingMetadata = action.payload; + }, mapCenterUpdated: (state, action: PayloadAction) => { state.center = action.payload; }, @@ -105,6 +117,7 @@ export const { metadataQueryResultUpdated, metadataPopupAnchorUpdated, isReferenceLayerVisibleToggled, + isQueryingMetadataToggled, mapCenterUpdated, zoomUpdated, } = slice.actions; @@ -124,6 +137,11 @@ export const isReferenceLayerVisibleSelector = createSelector( (isReferenceLayerVisible) => isReferenceLayerVisible ); +export const selectIsQueringMetadata = createSelector( + (state: RootState) => state.Map.isQueryingMetadata, + (isQueryingMetadata) => isQueryingMetadata +); + export const metadataQueryResultSelector = createSelector( (state: RootState) => state.Map.metadataQueryResult, (metadataQueryResult) => metadataQueryResult From 76f17a89aec7d98745a18dbf6b2e010455ec57ff Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 15 Mar 2024 13:06:20 -0700 Subject: [PATCH 31/56] fix: icon of Copy Link button in Gutter --- src/components/Gutter/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Gutter/index.tsx b/src/components/Gutter/index.tsx index 52223b1..b48533d 100644 --- a/src/components/Gutter/index.tsx +++ b/src/components/Gutter/index.tsx @@ -94,7 +94,7 @@ export const Gutter: FC = ({ }, 3000); }} > - +
Date: Fri, 15 Mar 2024 13:11:50 -0700 Subject: [PATCH 32/56] refactor: comment out services/wayback should use wayback-core npm package instead --- .../MetadataQueryTask/MetadataQueryTask.tsx | 11 +- src/services/wayback/ChangeDetector.ts | 628 +++++++++--------- src/services/wayback/Metadata.ts | 214 +++--- src/services/wayback/config.ts | 96 +-- src/services/wayback/helpers.ts | 68 +- src/services/wayback/index.ts | 290 ++++---- src/services/wayback/types.d.ts | 50 +- src/types/index.d.ts | 4 + 8 files changed, 684 insertions(+), 677 deletions(-) diff --git a/src/components/MetadataQueryTask/MetadataQueryTask.tsx b/src/components/MetadataQueryTask/MetadataQueryTask.tsx index a8db943..0ad7ce0 100644 --- a/src/components/MetadataQueryTask/MetadataQueryTask.tsx +++ b/src/components/MetadataQueryTask/MetadataQueryTask.tsx @@ -93,11 +93,13 @@ const MetadataQueryLayer: React.FC = ({ metadataQueryOnStart(); + const queryLocation = { + latitude: mapPoint.latitude, + longitude: mapPoint.longitude, + }; + const res = await getMetadata( - { - latitude: mapPoint.latitude, - longitude: mapPoint.longitude, - }, + queryLocation, mapView.zoom, // getCurrZoomLevel(mapView) releaseNum ); @@ -107,6 +109,7 @@ const MetadataQueryLayer: React.FC = ({ ? { ...res, releaseDate: releaseDateLabel, + queryLocation, } : null; diff --git a/src/services/wayback/ChangeDetector.ts b/src/services/wayback/ChangeDetector.ts index f09e4c2..fdd339b 100644 --- a/src/services/wayback/ChangeDetector.ts +++ b/src/services/wayback/ChangeDetector.ts @@ -1,314 +1,314 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import axios from 'axios'; -import { - queryFeatures, - IQueryFeaturesResponse, - IFeature, -} from '@esri/arcgis-rest-feature-service'; -import { geometryFns } from 'helper-toolkit-ts'; -import { IWaybackConfig, IMapPointInfo, IWaybackItem } from '@typings/index'; -import config from './config'; - -interface ICandidates { - rNum: number; - url: string; -} - -interface IParamGetTileUrl { - rNum?: number; - column: number; - row: number; - level: number; -} - -interface IOptionsWaybackChangeDetector { - waybackMapServerBaseUrl?: string; - changeDetectionLayerUrl?: string; - waybackconfig: IWaybackConfig; - shouldUseChangdeDetectorLayer?: boolean; - waybackItems: Array; -} - -interface IResponseGetImageBlob { - rNum: number; - dataUri: string; -} - -interface IResponseWaybackTilemap { - data: Array; - select: Array; - valid: boolean; - location: { - left: number; - top: number; - width: number; - height: number; - }; -} - -class WaybackChangeDetector { - // original wayback config JSON file - private waybackconfig: IWaybackConfig; - private waybackMapServerBaseUrl: string; - private changeDetectionLayerUrl: string; - private shouldUseChangdeDetectorLayer: boolean; - private waybackItems: Array; - private rNum2IndexLookup: { [key: number]: number }; - - constructor({ - waybackMapServerBaseUrl = '', - changeDetectionLayerUrl = '', - waybackconfig = null, - shouldUseChangdeDetectorLayer = false, - waybackItems = [], - }: IOptionsWaybackChangeDetector) { - this.waybackMapServerBaseUrl = waybackMapServerBaseUrl; - this.changeDetectionLayerUrl = changeDetectionLayerUrl; - this.waybackconfig = waybackconfig; - this.waybackItems = waybackItems; - this.shouldUseChangdeDetectorLayer = shouldUseChangdeDetectorLayer; - - // console.log('waybackItems', this.waybackItems); - } - - // get array of release numbers for wayback items that come with changes for input area - async findChanges(pointInfo: IMapPointInfo): Promise> { - try { - const level = +pointInfo.zoom.toFixed(0); - const column = geometryFns.long2tile(pointInfo.longitude, level); - const row = geometryFns.lat2tile(pointInfo.latitude, level); - - const candidatesRNums = this.shouldUseChangdeDetectorLayer - ? await this.getRNumsFromDetectionLayer(pointInfo, level) - : await this.getRNumsFromTilemap({ column, row, level }); - - const candidates = candidatesRNums.map((rNum) => { - return { - rNum, - url: this.getTileImageUrl({ column, row, level, rNum }), - }; - }); - // console.log(candidates) - - const rNumsNoDuplicates = await this.removeDuplicates(candidates); - // console.log(rNumsNoDuplicates) - - return rNumsNoDuplicates; - } catch (err) { - console.error('failed to find changes', err); - return null; - } - } - - getPreviousReleaseNum(rNum: number) { - if (!this.rNum2IndexLookup) { - const lookup = {}; - - this.waybackItems.forEach((item, index) => { - lookup[item.releaseNum] = index; - }); - - this.rNum2IndexLookup = lookup; - } - - const index4InputRNum = this.rNum2IndexLookup[rNum]; - - const previousReleaseNum = this.waybackItems[index4InputRNum + 1] - ? this.waybackItems[index4InputRNum + 1].releaseNum - : null; - - return previousReleaseNum; - } - - async getRNumsFromTilemap({ - column = null, - row = null, - level = null, - }: IParamGetTileUrl): Promise> { - return new Promise((resolve, reject) => { - const results: Array = []; - - const mostRecentRelease = this.waybackItems[0].releaseNum; - - const tilemapRequest = async (rNum: number) => { - try { - const requestUrl = `${this.waybackMapServerBaseUrl}/tilemap/${rNum}/${level}/${row}/${column}`; - - const response = await axios.get(requestUrl); - - const tilemapResponse: IResponseWaybackTilemap = - response.data || null; - - const lastRelease = - tilemapResponse.select && tilemapResponse.select[0] - ? +tilemapResponse.select[0] - : rNum; - - if (tilemapResponse.data[0]) { - results.push(lastRelease); - } - - const nextReleaseToCheck = tilemapResponse.data[0] - ? this.getPreviousReleaseNum(lastRelease) - : null; - - if (nextReleaseToCheck) { - tilemapRequest(nextReleaseToCheck); - } else { - resolve(results); - } - } catch (err) { - console.error(err); - reject(null); - } - }; - - tilemapRequest(mostRecentRelease); - }); - } - - async getRNumsFromDetectionLayer( - pointInfo: IMapPointInfo, - zoomLevel: number - ): Promise> { - const queryUrl = this.changeDetectionLayerUrl + '/query'; - - const fields = config['change-detection-layer'].fields; - - const FIELD_NAME_ZOOM = fields[0].fieldname; - const FIELD_NAME_RELEASE_NUM = fields[1].fieldname; - const FIELD_NAME_RELEASE_NAME = fields[2].fieldname; - - try { - const queryResponse = (await queryFeatures({ - url: queryUrl, - geometry: pointInfo.geometry, - geometryType: 'esriGeometryPoint', - spatialRel: 'esriSpatialRelIntersects', - where: `${FIELD_NAME_ZOOM} = ${zoomLevel}`, - outFields: [FIELD_NAME_RELEASE_NUM], - orderByFields: FIELD_NAME_RELEASE_NAME, - returnGeometry: false, - f: 'json', - })) as IQueryFeaturesResponse; - - const rNums: Array = - queryResponse.features && queryResponse.features.length - ? queryResponse.features.map((feature: IFeature) => { - return feature.attributes[FIELD_NAME_RELEASE_NUM]; - }) - : []; - - return rNums; - } catch (err) { - console.error(err); - return []; - } - } - - getTileImageUrl({ - column = null, - row = null, - level = null, - rNum = null, - }: IParamGetTileUrl) { - const urlTemplate = this.waybackconfig[rNum].itemURL; - return urlTemplate - .replace('{level}', level.toString()) - .replace('{row}', row.toString()) - .replace('{col}', column.toString()); - } - - async removeDuplicates( - candidates?: Array - ): Promise> { - if (!candidates.length) { - return []; - } - - const finalResults: Array = []; - - // reverse the candidates list so the wayback items will be sorted by release dates in ascending order (oldest >>> latest) - const imageDataUriRequests = candidates.reverse().map((candidate) => { - return this.getSampledImagedDataUri(candidate.url, candidate.rNum); - }); - - try { - const imageDataUriResults = await Promise.all(imageDataUriRequests); - - let dataUri4PrevRelease = ''; - - for (const { dataUri, rNum } of imageDataUriResults) { - if (dataUri === dataUri4PrevRelease) { - continue; - } - - finalResults.push(rNum); - dataUri4PrevRelease = dataUri; - } - } catch (err) { - console.error('failed to fetch all image data uri', err); - } - - return finalResults; - } - - async getSampledImagedDataUri( - imageUrl: string, - rNum: number - ): Promise { - const samplePoints = [512, 1000, 2500, 5000, 7500, 10000, 12500, 15000]; - - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.open('GET', imageUrl, true); - xhr.responseType = 'arraybuffer'; - - xhr.onload = function (e) { - if (this.status == 200) { - const uInt8Array = new Uint8Array(this.response); - let i = uInt8Array.length; - const binaryString = new Array(i); - while (i--) { - binaryString[i] = String.fromCharCode(uInt8Array[i]); - } - const data = binaryString.join(''); - const base64 = window.btoa(data); - // console.log(base64.length) - - let dataUri = ''; //base64.substr(512, 5000); - // console.log(tileImageDataUri); - - for (const point of samplePoints) { - dataUri += base64.substr(point, 500); - } - - resolve({ - rNum, - dataUri, - }); - } else { - reject(null); - } - }; - - xhr.send(); - }); - } -} - -export default WaybackChangeDetector; +// /* Copyright 2024 Esri +// * +// * Licensed under the Apache License Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ + +// import axios from 'axios'; +// import { +// queryFeatures, +// IQueryFeaturesResponse, +// IFeature, +// } from '@esri/arcgis-rest-feature-service'; +// import { geometryFns } from 'helper-toolkit-ts'; +// import { IWaybackConfig, IMapPointInfo, IWaybackItem } from '@typings/index'; +// import config from './config'; + +// interface ICandidates { +// rNum: number; +// url: string; +// } + +// interface IParamGetTileUrl { +// rNum?: number; +// column: number; +// row: number; +// level: number; +// } + +// interface IOptionsWaybackChangeDetector { +// waybackMapServerBaseUrl?: string; +// changeDetectionLayerUrl?: string; +// waybackconfig: IWaybackConfig; +// shouldUseChangdeDetectorLayer?: boolean; +// waybackItems: Array; +// } + +// interface IResponseGetImageBlob { +// rNum: number; +// dataUri: string; +// } + +// interface IResponseWaybackTilemap { +// data: Array; +// select: Array; +// valid: boolean; +// location: { +// left: number; +// top: number; +// width: number; +// height: number; +// }; +// } + +// class WaybackChangeDetector { +// // original wayback config JSON file +// private waybackconfig: IWaybackConfig; +// private waybackMapServerBaseUrl: string; +// private changeDetectionLayerUrl: string; +// private shouldUseChangdeDetectorLayer: boolean; +// private waybackItems: Array; +// private rNum2IndexLookup: { [key: number]: number }; + +// constructor({ +// waybackMapServerBaseUrl = '', +// changeDetectionLayerUrl = '', +// waybackconfig = null, +// shouldUseChangdeDetectorLayer = false, +// waybackItems = [], +// }: IOptionsWaybackChangeDetector) { +// this.waybackMapServerBaseUrl = waybackMapServerBaseUrl; +// this.changeDetectionLayerUrl = changeDetectionLayerUrl; +// this.waybackconfig = waybackconfig; +// this.waybackItems = waybackItems; +// this.shouldUseChangdeDetectorLayer = shouldUseChangdeDetectorLayer; + +// // console.log('waybackItems', this.waybackItems); +// } + +// // get array of release numbers for wayback items that come with changes for input area +// async findChanges(pointInfo: IMapPointInfo): Promise> { +// try { +// const level = +pointInfo.zoom.toFixed(0); +// const column = geometryFns.long2tile(pointInfo.longitude, level); +// const row = geometryFns.lat2tile(pointInfo.latitude, level); + +// const candidatesRNums = this.shouldUseChangdeDetectorLayer +// ? await this.getRNumsFromDetectionLayer(pointInfo, level) +// : await this.getRNumsFromTilemap({ column, row, level }); + +// const candidates = candidatesRNums.map((rNum) => { +// return { +// rNum, +// url: this.getTileImageUrl({ column, row, level, rNum }), +// }; +// }); +// // console.log(candidates) + +// const rNumsNoDuplicates = await this.removeDuplicates(candidates); +// // console.log(rNumsNoDuplicates) + +// return rNumsNoDuplicates; +// } catch (err) { +// console.error('failed to find changes', err); +// return null; +// } +// } + +// getPreviousReleaseNum(rNum: number) { +// if (!this.rNum2IndexLookup) { +// const lookup = {}; + +// this.waybackItems.forEach((item, index) => { +// lookup[item.releaseNum] = index; +// }); + +// this.rNum2IndexLookup = lookup; +// } + +// const index4InputRNum = this.rNum2IndexLookup[rNum]; + +// const previousReleaseNum = this.waybackItems[index4InputRNum + 1] +// ? this.waybackItems[index4InputRNum + 1].releaseNum +// : null; + +// return previousReleaseNum; +// } + +// async getRNumsFromTilemap({ +// column = null, +// row = null, +// level = null, +// }: IParamGetTileUrl): Promise> { +// return new Promise((resolve, reject) => { +// const results: Array = []; + +// const mostRecentRelease = this.waybackItems[0].releaseNum; + +// const tilemapRequest = async (rNum: number) => { +// try { +// const requestUrl = `${this.waybackMapServerBaseUrl}/tilemap/${rNum}/${level}/${row}/${column}`; + +// const response = await axios.get(requestUrl); + +// const tilemapResponse: IResponseWaybackTilemap = +// response.data || null; + +// const lastRelease = +// tilemapResponse.select && tilemapResponse.select[0] +// ? +tilemapResponse.select[0] +// : rNum; + +// if (tilemapResponse.data[0]) { +// results.push(lastRelease); +// } + +// const nextReleaseToCheck = tilemapResponse.data[0] +// ? this.getPreviousReleaseNum(lastRelease) +// : null; + +// if (nextReleaseToCheck) { +// tilemapRequest(nextReleaseToCheck); +// } else { +// resolve(results); +// } +// } catch (err) { +// console.error(err); +// reject(null); +// } +// }; + +// tilemapRequest(mostRecentRelease); +// }); +// } + +// async getRNumsFromDetectionLayer( +// pointInfo: IMapPointInfo, +// zoomLevel: number +// ): Promise> { +// const queryUrl = this.changeDetectionLayerUrl + '/query'; + +// const fields = config['change-detection-layer'].fields; + +// const FIELD_NAME_ZOOM = fields[0].fieldname; +// const FIELD_NAME_RELEASE_NUM = fields[1].fieldname; +// const FIELD_NAME_RELEASE_NAME = fields[2].fieldname; + +// try { +// const queryResponse = (await queryFeatures({ +// url: queryUrl, +// geometry: pointInfo.geometry, +// geometryType: 'esriGeometryPoint', +// spatialRel: 'esriSpatialRelIntersects', +// where: `${FIELD_NAME_ZOOM} = ${zoomLevel}`, +// outFields: [FIELD_NAME_RELEASE_NUM], +// orderByFields: FIELD_NAME_RELEASE_NAME, +// returnGeometry: false, +// f: 'json', +// })) as IQueryFeaturesResponse; + +// const rNums: Array = +// queryResponse.features && queryResponse.features.length +// ? queryResponse.features.map((feature: IFeature) => { +// return feature.attributes[FIELD_NAME_RELEASE_NUM]; +// }) +// : []; + +// return rNums; +// } catch (err) { +// console.error(err); +// return []; +// } +// } + +// getTileImageUrl({ +// column = null, +// row = null, +// level = null, +// rNum = null, +// }: IParamGetTileUrl) { +// const urlTemplate = this.waybackconfig[rNum].itemURL; +// return urlTemplate +// .replace('{level}', level.toString()) +// .replace('{row}', row.toString()) +// .replace('{col}', column.toString()); +// } + +// async removeDuplicates( +// candidates?: Array +// ): Promise> { +// if (!candidates.length) { +// return []; +// } + +// const finalResults: Array = []; + +// // reverse the candidates list so the wayback items will be sorted by release dates in ascending order (oldest >>> latest) +// const imageDataUriRequests = candidates.reverse().map((candidate) => { +// return this.getSampledImagedDataUri(candidate.url, candidate.rNum); +// }); + +// try { +// const imageDataUriResults = await Promise.all(imageDataUriRequests); + +// let dataUri4PrevRelease = ''; + +// for (const { dataUri, rNum } of imageDataUriResults) { +// if (dataUri === dataUri4PrevRelease) { +// continue; +// } + +// finalResults.push(rNum); +// dataUri4PrevRelease = dataUri; +// } +// } catch (err) { +// console.error('failed to fetch all image data uri', err); +// } + +// return finalResults; +// } + +// async getSampledImagedDataUri( +// imageUrl: string, +// rNum: number +// ): Promise { +// const samplePoints = [512, 1000, 2500, 5000, 7500, 10000, 12500, 15000]; + +// return new Promise((resolve, reject) => { +// const xhr = new XMLHttpRequest(); +// xhr.open('GET', imageUrl, true); +// xhr.responseType = 'arraybuffer'; + +// xhr.onload = function (e) { +// if (this.status == 200) { +// const uInt8Array = new Uint8Array(this.response); +// let i = uInt8Array.length; +// const binaryString = new Array(i); +// while (i--) { +// binaryString[i] = String.fromCharCode(uInt8Array[i]); +// } +// const data = binaryString.join(''); +// const base64 = window.btoa(data); +// // console.log(base64.length) + +// let dataUri = ''; //base64.substr(512, 5000); +// // console.log(tileImageDataUri); + +// for (const point of samplePoints) { +// dataUri += base64.substr(point, 500); +// } + +// resolve({ +// rNum, +// dataUri, +// }); +// } else { +// reject(null); +// } +// }; + +// xhr.send(); +// }); +// } +// } + +// export default WaybackChangeDetector; diff --git a/src/services/wayback/Metadata.ts b/src/services/wayback/Metadata.ts index d57dd7a..82173b4 100644 --- a/src/services/wayback/Metadata.ts +++ b/src/services/wayback/Metadata.ts @@ -1,107 +1,107 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - queryFeatures, - IQueryFeaturesResponse, - IFeature, -} from '@esri/arcgis-rest-feature-service'; -import { IWaybackConfig, IWaybackMetadataQueryResult } from '@typings/index'; -import { IParamsQueryMetadata } from './types'; -import config from './config'; - -class MetadataManager { - // original wayback config JSON file - private waybackconfig: IWaybackConfig; - - // can only get metadata when the map is between the min and max zoom level (10 <= mapZoom <= 23) - private readonly MAX_ZOOM = 23; - private readonly MIN_ZOOM = 10; - - constructor(waybackconfig: IWaybackConfig) { - this.waybackconfig = waybackconfig; - } - - // setWaybackConfig(waybackconfig:IWaybackConfig){ - // this.waybackconfig = waybackconfig; - // // console.log('set waybackconfig for metadata manager', waybackconfig); - // } - - async queryData( - params: IParamsQueryMetadata - ): Promise { - const fields = config['metadata-layer'].fields; - - const FIELD_NAME_SRC_DATE = fields[0].fieldname; - const FIELD_NAME_SRC_PROVIDER = fields[1].fieldname; - const FIELD_NAME_SRC_NAME = fields[2].fieldname; - const FIELD_NAME_SRC_RES = fields[3].fieldname; - const FIELD_NAME_SRC_ACC = fields[4].fieldname; - - const queryUrl = this.getQueryUrl(params.releaseNum, params.zoom); - - const outFields = fields.map((d) => d.fieldname); - - try { - const queryResponse = (await queryFeatures({ - url: queryUrl, - geometry: params.pointGeometry, - geometryType: 'esriGeometryPoint', - spatialRel: 'esriSpatialRelIntersects', - outFields, - returnGeometry: false, - f: 'json', - })) as IQueryFeaturesResponse; - - const feature: IFeature = - queryResponse.features && queryResponse.features.length - ? queryResponse.features[0] - : null; - - const date = feature.attributes[FIELD_NAME_SRC_DATE]; - const provider = feature.attributes[FIELD_NAME_SRC_PROVIDER]; - const source = feature.attributes[FIELD_NAME_SRC_NAME]; - const resolution = feature.attributes[FIELD_NAME_SRC_RES]; - const accuracy = feature.attributes[FIELD_NAME_SRC_ACC]; - - return { - date, - provider, - source, - resolution, - accuracy, - }; - } catch (err) { - return null; - } - } - - private getQueryUrl(releaseNum: number, zoom: number) { - const metadataLayerUrl = - this.waybackconfig[releaseNum].metadataLayerUrl; - const layerId = this.getLayerId(zoom); - return `${metadataLayerUrl}/${layerId}/query`; - } - - private getLayerId(zoom: number) { - zoom = +zoom; - const layerID = this.MAX_ZOOM - zoom; - // the service has 14 sub layers that provide metadata up to zoom level 10 (layer ID 14), if the zoom level is small that (e.g. 5), there are no metadata - const layerIdForMinZoom = this.MAX_ZOOM - this.MIN_ZOOM; - return layerID <= layerIdForMinZoom ? layerID : layerIdForMinZoom; - } -} - -export default MetadataManager; +// /* Copyright 2024 Esri +// * +// * Licensed under the Apache License Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ + +// import { +// queryFeatures, +// IQueryFeaturesResponse, +// IFeature, +// } from '@esri/arcgis-rest-feature-service'; +// import { IWaybackConfig, IWaybackMetadataQueryResult } from '@typings/index'; +// import { IParamsQueryMetadata } from './types'; +// import config from './config'; + +// class MetadataManager { +// // original wayback config JSON file +// private waybackconfig: IWaybackConfig; + +// // can only get metadata when the map is between the min and max zoom level (10 <= mapZoom <= 23) +// private readonly MAX_ZOOM = 23; +// private readonly MIN_ZOOM = 10; + +// constructor(waybackconfig: IWaybackConfig) { +// this.waybackconfig = waybackconfig; +// } + +// // setWaybackConfig(waybackconfig:IWaybackConfig){ +// // this.waybackconfig = waybackconfig; +// // // console.log('set waybackconfig for metadata manager', waybackconfig); +// // } + +// async queryData( +// params: IParamsQueryMetadata +// ): Promise { +// const fields = config['metadata-layer'].fields; + +// const FIELD_NAME_SRC_DATE = fields[0].fieldname; +// const FIELD_NAME_SRC_PROVIDER = fields[1].fieldname; +// const FIELD_NAME_SRC_NAME = fields[2].fieldname; +// const FIELD_NAME_SRC_RES = fields[3].fieldname; +// const FIELD_NAME_SRC_ACC = fields[4].fieldname; + +// const queryUrl = this.getQueryUrl(params.releaseNum, params.zoom); + +// const outFields = fields.map((d) => d.fieldname); + +// try { +// const queryResponse = (await queryFeatures({ +// url: queryUrl, +// geometry: params.pointGeometry, +// geometryType: 'esriGeometryPoint', +// spatialRel: 'esriSpatialRelIntersects', +// outFields, +// returnGeometry: false, +// f: 'json', +// })) as IQueryFeaturesResponse; + +// const feature: IFeature = +// queryResponse.features && queryResponse.features.length +// ? queryResponse.features[0] +// : null; + +// const date = feature.attributes[FIELD_NAME_SRC_DATE]; +// const provider = feature.attributes[FIELD_NAME_SRC_PROVIDER]; +// const source = feature.attributes[FIELD_NAME_SRC_NAME]; +// const resolution = feature.attributes[FIELD_NAME_SRC_RES]; +// const accuracy = feature.attributes[FIELD_NAME_SRC_ACC]; + +// return { +// date, +// provider, +// source, +// resolution, +// accuracy, +// }; +// } catch (err) { +// return null; +// } +// } + +// private getQueryUrl(releaseNum: number, zoom: number) { +// const metadataLayerUrl = +// this.waybackconfig[releaseNum].metadataLayerUrl; +// const layerId = this.getLayerId(zoom); +// return `${metadataLayerUrl}/${layerId}/query`; +// } + +// private getLayerId(zoom: number) { +// zoom = +zoom; +// const layerID = this.MAX_ZOOM - zoom; +// // the service has 14 sub layers that provide metadata up to zoom level 10 (layer ID 14), if the zoom level is small that (e.g. 5), there are no metadata +// const layerIdForMinZoom = this.MAX_ZOOM - this.MIN_ZOOM; +// return layerID <= layerIdForMinZoom ? layerID : layerIdForMinZoom; +// } +// } + +// export default MetadataManager; diff --git a/src/services/wayback/config.ts b/src/services/wayback/config.ts index 93673fd..dfe8da1 100644 --- a/src/services/wayback/config.ts +++ b/src/services/wayback/config.ts @@ -1,49 +1,49 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// /* Copyright 2024 Esri +// * +// * Licensed under the Apache License Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ -export default { - 'metadata-layer': { - fields: [ - { - fieldname: 'SRC_DATE2', - }, - { - fieldname: 'NICE_DESC', - }, - { - fieldname: 'SRC_DESC', - }, - { - fieldname: 'SAMP_RES', - }, - { - fieldname: 'SRC_ACC', - }, - ], - }, - 'change-detection-layer': { - fields: [ - { - fieldname: 'MapLevel', - }, - { - fieldname: 'ClumpID', - }, - { - fieldname: 'Clump', - }, - ], - }, -}; +// export default { +// 'metadata-layer': { +// fields: [ +// { +// fieldname: 'SRC_DATE2', +// }, +// { +// fieldname: 'NICE_DESC', +// }, +// { +// fieldname: 'SRC_DESC', +// }, +// { +// fieldname: 'SAMP_RES', +// }, +// { +// fieldname: 'SRC_ACC', +// }, +// ], +// }, +// 'change-detection-layer': { +// fields: [ +// { +// fieldname: 'MapLevel', +// }, +// { +// fieldname: 'ClumpID', +// }, +// { +// fieldname: 'Clump', +// }, +// ], +// }, +// }; diff --git a/src/services/wayback/helpers.ts b/src/services/wayback/helpers.ts index 147cc6c..0359044 100644 --- a/src/services/wayback/helpers.ts +++ b/src/services/wayback/helpers.ts @@ -1,39 +1,39 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// /* Copyright 2024 Esri +// * +// * Licensed under the Apache License Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ -const convertDateFromWaybackItemTitle = (dateString = '') => { - const dateParts = dateString.split('-'); - const year = +dateParts[0]; - const mon = +dateParts[1] - 1; - const day = +dateParts[2]; - return new Date(year, mon, day); -}; +// const convertDateFromWaybackItemTitle = (dateString = '') => { +// const dateParts = dateString.split('-'); +// const year = +dateParts[0]; +// const mon = +dateParts[1] - 1; +// const day = +dateParts[2]; +// return new Date(year, mon, day); +// }; -// the title of wayback item is like "World Imagery (Wayback 2014-02-20)", -// therefore need to call this method to extract "2014-02-20" from string -const extractDateFromWaybackItemTitle = (waybackItemTitle = '') => { - const regexpYYYYMMDD = /\d{4}-\d{2}-\d{2}/g; - const results = waybackItemTitle.match(regexpYYYYMMDD); +// // the title of wayback item is like "World Imagery (Wayback 2014-02-20)", +// // therefore need to call this method to extract "2014-02-20" from string +// const extractDateFromWaybackItemTitle = (waybackItemTitle = '') => { +// const regexpYYYYMMDD = /\d{4}-\d{2}-\d{2}/g; +// const results = waybackItemTitle.match(regexpYYYYMMDD); - const dateString = results.length ? results[0] : waybackItemTitle; - const datetime = convertDateFromWaybackItemTitle(dateString); +// const dateString = results.length ? results[0] : waybackItemTitle; +// const datetime = convertDateFromWaybackItemTitle(dateString); - return { - releaseDateLabel: dateString, - releaseDatetime: datetime.getTime(), - }; -}; +// return { +// releaseDateLabel: dateString, +// releaseDatetime: datetime.getTime(), +// }; +// }; -export { extractDateFromWaybackItemTitle }; +// export { extractDateFromWaybackItemTitle }; diff --git a/src/services/wayback/index.ts b/src/services/wayback/index.ts index ce8762f..d721c79 100644 --- a/src/services/wayback/index.ts +++ b/src/services/wayback/index.ts @@ -1,145 +1,145 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import config from '../../app-config'; -import axios from 'axios'; - -import { getServiceUrl } from '@utils/Tier'; -import { IWaybackItem, IWaybackConfig, IMapPointInfo } from '@typings/index'; -import { IParamsQueryMetadata } from './types'; -import { extractDateFromWaybackItemTitle } from './helpers'; -import MetadataManager from './Metadata'; -import ChangeDetector from './ChangeDetector'; -import { getRoundedDate } from 'helper-toolkit-ts/dist/date'; - -class WaybackManager { - // module to query the wayback metadata - private metadataManager: MetadataManager; - private changeDetector: ChangeDetector; - - // original wayback config JSON file - private waybackconfig: IWaybackConfig; - - // array of wayback items with more attributes - private waybackItems: Array; - - // constructor() {} - - async init() { - this.waybackconfig = await this.fetchWaybackConfig(); - // console.log(this.waybackconfig); - - this.waybackItems = this.getWaybackItems(); - - this.metadataManager = new MetadataManager(this.waybackconfig); - - this.changeDetector = new ChangeDetector({ - waybackMapServerBaseUrl: getServiceUrl('wayback-imagery-base'), - changeDetectionLayerUrl: getServiceUrl( - 'wayback-change-detector-layer' - ), - waybackconfig: this.waybackconfig, - waybackItems: this.waybackItems, - shouldUseChangdeDetectorLayer: - config.shouldUseWaybackFootprintsLayer, - }); - - return { - waybackItems: this.waybackItems, - }; - } - - getWaybackItems() { - const waybackItems = Object.keys(this.waybackconfig).map( - (key: string) => { - const releaseNum = +key; - - const waybackconfigItem = this.waybackconfig[+releaseNum]; - - const releaseDate = extractDateFromWaybackItemTitle( - waybackconfigItem.itemTitle - ); - - const waybackItem = { - releaseNum, - ...releaseDate, - ...waybackconfigItem, - }; - - return waybackItem; - } - ); - - waybackItems.sort((a, b) => { - return b.releaseDatetime - a.releaseDatetime; - }); - - return waybackItems; - } - - async getLocalChanges(pointInfo: IMapPointInfo) { - try { - const localChangeQueryRes = await this.changeDetector.findChanges( - pointInfo - ); - return localChangeQueryRes; - } catch (err) { - console.error(err); - return null; - } - } - - async getMetadata(params: IParamsQueryMetadata) { - try { - const metadataQueryRes = await this.metadataManager.queryData( - params - ); - return metadataQueryRes; - } catch (err) { - console.error(err); - return null; - } - } - - private fetchWaybackConfig(): Promise { - // make sure we can get the latest version of the wayback config file - const requestUrl = - getServiceUrl('wayback-config') + `?modified=${getRoundedDate(5)}`; - - return new Promise((resolve, reject) => { - axios - .get(requestUrl) - .then((response) => { - // handle success - // console.log(response); - - if (response.data) { - resolve(response.data); - } else { - reject({ - error: 'failed to fetch wayback config data', - }); - } - }) - .catch((error) => { - // handle error - console.log(error); - reject(error); - }); - }); - } -} - -export default WaybackManager; +// /* Copyright 2024 Esri +// * +// * Licensed under the Apache License Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ + +// import config from '../../app-config'; +// import axios from 'axios'; + +// import { getServiceUrl } from '@utils/Tier'; +// import { IWaybackItem, IWaybackConfig, IMapPointInfo } from '@typings/index'; +// import { IParamsQueryMetadata } from './types'; +// import { extractDateFromWaybackItemTitle } from './helpers'; +// import MetadataManager from './Metadata'; +// import ChangeDetector from './ChangeDetector'; +// import { getRoundedDate } from 'helper-toolkit-ts/dist/date'; + +// class WaybackManager { +// // module to query the wayback metadata +// private metadataManager: MetadataManager; +// private changeDetector: ChangeDetector; + +// // original wayback config JSON file +// private waybackconfig: IWaybackConfig; + +// // array of wayback items with more attributes +// private waybackItems: Array; + +// // constructor() {} + +// async init() { +// this.waybackconfig = await this.fetchWaybackConfig(); +// // console.log(this.waybackconfig); + +// this.waybackItems = this.getWaybackItems(); + +// this.metadataManager = new MetadataManager(this.waybackconfig); + +// this.changeDetector = new ChangeDetector({ +// waybackMapServerBaseUrl: getServiceUrl('wayback-imagery-base'), +// changeDetectionLayerUrl: getServiceUrl( +// 'wayback-change-detector-layer' +// ), +// waybackconfig: this.waybackconfig, +// waybackItems: this.waybackItems, +// shouldUseChangdeDetectorLayer: +// config.shouldUseWaybackFootprintsLayer, +// }); + +// return { +// waybackItems: this.waybackItems, +// }; +// } + +// getWaybackItems() { +// const waybackItems = Object.keys(this.waybackconfig).map( +// (key: string) => { +// const releaseNum = +key; + +// const waybackconfigItem = this.waybackconfig[+releaseNum]; + +// const releaseDate = extractDateFromWaybackItemTitle( +// waybackconfigItem.itemTitle +// ); + +// const waybackItem = { +// releaseNum, +// ...releaseDate, +// ...waybackconfigItem, +// }; + +// return waybackItem; +// } +// ); + +// waybackItems.sort((a, b) => { +// return b.releaseDatetime - a.releaseDatetime; +// }); + +// return waybackItems; +// } + +// async getLocalChanges(pointInfo: IMapPointInfo) { +// try { +// const localChangeQueryRes = await this.changeDetector.findChanges( +// pointInfo +// ); +// return localChangeQueryRes; +// } catch (err) { +// console.error(err); +// return null; +// } +// } + +// async getMetadata(params: IParamsQueryMetadata) { +// try { +// const metadataQueryRes = await this.metadataManager.queryData( +// params +// ); +// return metadataQueryRes; +// } catch (err) { +// console.error(err); +// return null; +// } +// } + +// private fetchWaybackConfig(): Promise { +// // make sure we can get the latest version of the wayback config file +// const requestUrl = +// getServiceUrl('wayback-config') + `?modified=${getRoundedDate(5)}`; + +// return new Promise((resolve, reject) => { +// axios +// .get(requestUrl) +// .then((response) => { +// // handle success +// // console.log(response); + +// if (response.data) { +// resolve(response.data); +// } else { +// reject({ +// error: 'failed to fetch wayback config data', +// }); +// } +// }) +// .catch((error) => { +// // handle error +// console.log(error); +// reject(error); +// }); +// }); +// } +// } + +// export default WaybackManager; diff --git a/src/services/wayback/types.d.ts b/src/services/wayback/types.d.ts index 969bb27..bd6a66e 100644 --- a/src/services/wayback/types.d.ts +++ b/src/services/wayback/types.d.ts @@ -1,28 +1,28 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// /* Copyright 2024 Esri +// * +// * Licensed under the Apache License Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ -import { - IPointGeomety, - // IMapPointInfo, - // IWaybackConfig, -} from '@typings/index'; +// import { +// IPointGeomety, +// // IMapPointInfo, +// // IWaybackConfig, +// } from '@typings/index'; -interface IParamsQueryMetadata { - pointGeometry: IPointGeomety; - zoom: number; - releaseNum: number; -} +// interface IParamsQueryMetadata { +// pointGeometry: IPointGeomety; +// zoom: number; +// releaseNum: number; +// } -export { IParamsQueryMetadata }; +// export { IParamsQueryMetadata }; diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 7fb67ed..45f3c01 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -83,6 +83,10 @@ interface IWaybackMetadataQueryResult { resolution: number; accuracy: number; releaseDate?: string; + queryLocation: { + longitude: number; + latitude: number; + }; } interface IScreenPoint { From 9146d75395deef2a9597ab010b5df67213227bf3 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 15 Mar 2024 13:21:22 -0700 Subject: [PATCH 33/56] refactor: use Function Component for Metadata Popup --- src/components/PopUp/index.tsx | 172 +++++++++++++++++---------------- 1 file changed, 88 insertions(+), 84 deletions(-) diff --git a/src/components/PopUp/index.tsx b/src/components/PopUp/index.tsx index bcf3501..9fd5021 100644 --- a/src/components/PopUp/index.tsx +++ b/src/components/PopUp/index.tsx @@ -15,7 +15,7 @@ // import { loadModules } from 'esri-loader'; import './style.css'; -import React from 'react'; +import React, { FC } from 'react'; import { dateFns } from 'helper-toolkit-ts'; import { @@ -34,16 +34,15 @@ interface IProps { onClose: () => void; } -class PopUp extends React.PureComponent { - private readonly Width = 360; - private readonly PositionOffset = 22.5; +const Width = 360; +const PositionOffset = 22.5; - constructor(props: IProps) { - super(props); - } +const PopUp: FC = (props: IProps) => { + const { metadata, isQueryingMetadata, metadataAnchorScreenPoint, onClose } = + props; - formatMetadataDate() { - const { metadata } = this.props; + const formatMetadataDate = () => { + const { metadata } = props; const { date } = metadata; const metadataDate = new Date(date); @@ -53,93 +52,98 @@ class PopUp extends React.PureComponent { const day = metadataDate.getDate(); return `${month} ${day}, ${year}`; - } - - render() { - // const { targetLayer } = this.props; - - const { metadata, isQueryingMetadata, metadataAnchorScreenPoint } = - this.props; - - if (!metadataAnchorScreenPoint) { - return null; - } + }; - if (!metadata && !isQueryingMetadata) { - return null; - } - - const containerStyle = { - position: 'absolute', - top: metadataAnchorScreenPoint.y - this.PositionOffset, - left: metadataAnchorScreenPoint.x - this.PositionOffset, - width: this.Width, - } as React.CSSProperties; + if (!metadataAnchorScreenPoint) { + return null; + } - if (isQueryingMetadata) { - return ( -
-
+ if (!metadata && !isQueryingMetadata) { + return null; + } -
- -
-
- ); - } - - const { provider, source, resolution, accuracy, releaseDate, date } = - metadata; - - // const releaseDate = 'targetLayer.releaseDateLabel'; - const formattedDate = this.formatMetadataDate(); - - const providerAndCaptureDateInfo = date ? ( - - {provider} ({source}) image captured on {formattedDate}{' '} - as shown in the {releaseDate} version of the World - Imagery map. - - ) : ( - - {provider} ({source}) imagery as shown in the{' '} - {releaseDate} version of the World Imagery map. - - ); + const containerStyle = { + position: 'absolute', + top: metadataAnchorScreenPoint.y - PositionOffset, + left: metadataAnchorScreenPoint.x - PositionOffset, + width: Width, + } as React.CSSProperties; + if (isQueryingMetadata) { return (
-
-

- {providerAndCaptureDateInfo} -

-

- Resolution: Pixels in the source image -
- represent a ground distance of{' '} - {+resolution.toFixed(2)} meters. -

-

- Accuracy: Objects displayed in this image -
- are within {+accuracy.toFixed(2)} meters of - true location. -

-
- -
- -
+
); } -} + + const { + provider, + source, + resolution, + accuracy, + releaseDate, + date, + queryLocation, + } = metadata; + + // const releaseDate = 'targetLayer.releaseDateLabel'; + const formattedDate = formatMetadataDate(); + + const providerAndCaptureDateInfo = date ? ( + + {provider} ({source}) image captured on {formattedDate} as + shown in the {releaseDate} version of the World Imagery map. + + ) : ( + + {provider} ({source}) imagery as shown in the {releaseDate}{' '} + version of the World Imagery map. + + ); + + return ( +
+
+ +
+
+

{providerAndCaptureDateInfo}

+

+ Resolution: Pixels in the source image +
+ represent a ground distance of{' '} + {+resolution.toFixed(2)} meters. +

+

+ Accuracy: Objects displayed in this image +
+ are within {+accuracy.toFixed(2)} meters of true + location. +

+ +

+ x: {queryLocation.longitude.toFixed(4)} + {' '}y: {queryLocation.latitude.toFixed(4)} +

+
+ +
+ +
+
+
+ ); +}; export default PopUp; From 012b698c6817594201fcf4f3bfd1bedee6cd173d Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 15 Mar 2024 13:28:38 -0700 Subject: [PATCH 34/56] feat: Add Long/Lat coordinates to the map popup ref #88 --- src/components/PopUp/index.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/PopUp/index.tsx b/src/components/PopUp/index.tsx index 9fd5021..4c61113 100644 --- a/src/components/PopUp/index.tsx +++ b/src/components/PopUp/index.tsx @@ -54,6 +54,15 @@ const PopUp: FC = (props: IProps) => { return `${month} ${day}, ${year}`; }; + const copyQueryLocation = () => { + const { queryLocation } = metadata; + + const text = `${queryLocation.latitude.toFixed( + 5 + )} ${queryLocation.longitude.toFixed(5)}`; + navigator.clipboard.writeText(text); + }; + if (!metadataAnchorScreenPoint) { return null; } @@ -127,8 +136,9 @@ const PopUp: FC = (props: IProps) => {

x: {queryLocation.longitude.toFixed(4)} {' '}y: {queryLocation.latitude.toFixed(4)} From 39c1d8c3c9d759a0c30bfbec7ec9f09b4f696f6c Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 15 Mar 2024 15:47:49 -0700 Subject: [PATCH 35/56] feat: add CopyLinkButton to Animation Download Panel --- .../CopiedLinkMessage.tsx | 42 +++++++++++++++++ .../AnimationDownloadPanel/CopyLinkButton.tsx | 46 +++++++++++++++++++ .../AnimationDownloadPanel/DownloadPanel.tsx | 8 +++- src/constants/UI.ts | 5 ++ src/store/AnimationMode/reducer.ts | 17 +++++++ src/store/AnimationMode/thunks.ts | 32 +++++++++++++ src/style/index.css | 2 +- src/utils/snippets/delay.ts | 20 ++++++++ 8 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 src/components/AnimationDownloadPanel/CopiedLinkMessage.tsx create mode 100644 src/components/AnimationDownloadPanel/CopyLinkButton.tsx create mode 100644 src/store/AnimationMode/thunks.ts create mode 100644 src/utils/snippets/delay.ts diff --git a/src/components/AnimationDownloadPanel/CopiedLinkMessage.tsx b/src/components/AnimationDownloadPanel/CopiedLinkMessage.tsx new file mode 100644 index 0000000..6b3892e --- /dev/null +++ b/src/components/AnimationDownloadPanel/CopiedLinkMessage.tsx @@ -0,0 +1,42 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { selectAnimationLinkIsCopied } from '@store/AnimationMode/reducer'; +import classNames from 'classnames'; +import React from 'react'; +import { useSelector } from 'react-redux'; + +export const CopiedLinkMessage = () => { + const linkIsCopied = useSelector(selectAnimationLinkIsCopied); + + if (!linkIsCopied) { + return null; + } + + return ( +

+
+ + {'link copied to clipboard'} +
+
+ ); +}; diff --git a/src/components/AnimationDownloadPanel/CopyLinkButton.tsx b/src/components/AnimationDownloadPanel/CopyLinkButton.tsx new file mode 100644 index 0000000..1a6ace9 --- /dev/null +++ b/src/components/AnimationDownloadPanel/CopyLinkButton.tsx @@ -0,0 +1,46 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { copyAnimationLink } from '@store/AnimationMode/thunks'; +import classNames from 'classnames'; +import React from 'react'; +import { useDispatch } from 'react-redux'; + +export const CopyLinkButton = () => { + const dispatch = useDispatch(); + + return ( +
{ + dispatch(copyAnimationLink()); + }} + > + {/* link icon: https://esri.github.io/calcite-ui-icons/#link */} + + + + +
+ ); +}; diff --git a/src/components/AnimationDownloadPanel/DownloadPanel.tsx b/src/components/AnimationDownloadPanel/DownloadPanel.tsx index e6bed3a..334a819 100644 --- a/src/components/AnimationDownloadPanel/DownloadPanel.tsx +++ b/src/components/AnimationDownloadPanel/DownloadPanel.tsx @@ -33,6 +33,8 @@ import { selectShouldShowDownloadPanel, showDownloadAnimationPanelToggled, } from '@store/AnimationMode/reducer'; +import { CopyLinkButton } from './CopyLinkButton'; +import { CopiedLinkMessage } from './CopiedLinkMessage'; // /** // * This object contains the data for each animation frame. @@ -141,7 +143,11 @@ export const AnimationDownloadPanel: FC = ({
{/* Download Button that opens the Download Animation Panel */} {shouldShowDownloadPanel === false && ( - + <> + + + + )} {downloadJobStatus !== null && ( diff --git a/src/constants/UI.ts b/src/constants/UI.ts index aef3ef6..f82c4be 100644 --- a/src/constants/UI.ts +++ b/src/constants/UI.ts @@ -19,3 +19,8 @@ export const GUTTER_WIDTH = 50; export const MOBILE_HEADER_HEIGHT = 45; export const DEFAULT_BACKGROUND_COLOR = '#121212'; + +/** + * milliseconds to wait before turn off the copy link message + */ +export const COPIED_LINK_MESSAGE_TIME_TO_STAY_OPEN_IN_MILLISECONDS = 3000; diff --git a/src/store/AnimationMode/reducer.ts b/src/store/AnimationMode/reducer.ts index 647b37b..e105a5d 100644 --- a/src/store/AnimationMode/reducer.ts +++ b/src/store/AnimationMode/reducer.ts @@ -52,6 +52,10 @@ export type AnimationModeState = { * release number of wayback item that is being displayed as current animation frame */ releaseNumberOfActiveAnimationFrame: number; + /** + * if true, the link of the current animiation has been copied to the clipboard + */ + animationLinkIsCopied: boolean; }; /** @@ -73,6 +77,7 @@ export const initialAnimationModeState = { rNum2Exclude: [], animationSpeed: DEFAULT_ANIMATION_SPEED_IN_MILLISECONDS, releaseNumberOfActiveAnimationFrame: null, + animationLinkIsCopied: false, } as AnimationModeState; const slice = createSlice({ @@ -91,6 +96,12 @@ const slice = createSlice({ ) => { state.showDownloadAnimationPanel = action.payload; }, + animationLinkIsCopiedChanged: ( + state, + action: PayloadAction + ) => { + state.animationLinkIsCopied = action.payload; + }, rNum2ExcludeToggled: ( state: AnimationModeState, action: PayloadAction @@ -134,6 +145,7 @@ export const { rNum2ExcludeReset, animationSpeedChanged, releaseNumberOfActiveAnimationFrameChanged, + animationLinkIsCopiedChanged, } = slice.actions; export const toggleAnimationMode = @@ -176,4 +188,9 @@ export const selectReleaseNumberOfActiveAnimationFrame = createSelector( (releaseNumberOfActiveAnimationFrame) => releaseNumberOfActiveAnimationFrame ); +export const selectAnimationLinkIsCopied = createSelector( + (state: RootState) => state.AnimationMode.animationLinkIsCopied, + (animationLinkIsCopied) => animationLinkIsCopied +); + export default reducer; diff --git a/src/store/AnimationMode/thunks.ts b/src/store/AnimationMode/thunks.ts new file mode 100644 index 0000000..a2f90bf --- /dev/null +++ b/src/store/AnimationMode/thunks.ts @@ -0,0 +1,32 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { delay } from '@utils/snippets/delay'; +import { RootState, StoreDispatch, StoreGetState } from '../configureStore'; +import { animationLinkIsCopiedChanged } from './reducer'; +import { COPIED_LINK_MESSAGE_TIME_TO_STAY_OPEN_IN_MILLISECONDS } from '@constants/UI'; + +export const copyAnimationLink = + () => async (dispatch: StoreDispatch, getState: StoreGetState) => { + await navigator.clipboard.writeText(window.location.href); + + // set to true to display the success message + dispatch(animationLinkIsCopiedChanged(true)); + + // the message should be turned off after 3 seconds + await delay(COPIED_LINK_MESSAGE_TIME_TO_STAY_OPEN_IN_MILLISECONDS); + + dispatch(animationLinkIsCopiedChanged(false)); + }; diff --git a/src/style/index.css b/src/style/index.css index f70a75b..c723c83 100644 --- a/src/style/index.css +++ b/src/style/index.css @@ -51,5 +51,5 @@ input, textarea { } svg.with-drop-shadow { - filter: drop-shadow(0px 0px 4px #000000); + filter: drop-shadow(0px 0px 8px #000000); } \ No newline at end of file diff --git a/src/utils/snippets/delay.ts b/src/utils/snippets/delay.ts new file mode 100644 index 0000000..a2b3a45 --- /dev/null +++ b/src/utils/snippets/delay.ts @@ -0,0 +1,20 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const delay = (milliseconds: number): Promise => { + return new Promise((resolve) => { + setTimeout(resolve, milliseconds); + }); +}; From ac9d75677b9ff023d0ebc3c5a81efff4dc02dd14 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Fri, 15 Mar 2024 15:54:19 -0700 Subject: [PATCH 36/56] fix: icon of CopyLink button in Gutter --- src/components/Gutter/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Gutter/index.tsx b/src/components/Gutter/index.tsx index b48533d..fae5743 100644 --- a/src/components/Gutter/index.tsx +++ b/src/components/Gutter/index.tsx @@ -94,7 +94,7 @@ export const Gutter: FC = ({ }, 3000); }} > - +
Date: Sat, 16 Mar 2024 05:04:50 -0700 Subject: [PATCH 37/56] fix: remove style class names from calcite-web.js --- public/index.html | 4 ++-- .../AnimationControls/AnimationControls.tsx | 15 ++++++------ .../DonwloadAnimationButton.tsx | 22 +++++++++++++---- .../AnimationControls/FramesSeletor.tsx | 19 ++++++++------- .../AnimationControls/PlayPauseBtn.tsx | 2 +- .../AnimationControls/SpeedSelector.tsx | 4 ++-- src/components/Gutter/index.tsx | 2 +- src/components/ListView/Card.tsx | 2 +- src/components/MobileHeader/index.tsx | 2 +- src/components/PopUp/index.tsx | 3 ++- src/components/PopUp/style.css | 6 ++--- .../ReferenceLayerToggle.tsx | 5 +--- .../SwipeWidgetLayerSelector.tsx | 24 ++++++++++--------- src/components/Title4ActiveItem/index.tsx | 2 +- src/style/index.css | 6 ++++- tailwind.config.js | 1 + 16 files changed, 69 insertions(+), 50 deletions(-) diff --git a/public/index.html b/public/index.html index 0ac42e3..a7534f7 100644 --- a/public/index.html +++ b/public/index.html @@ -11,8 +11,8 @@ - - + +
diff --git a/src/components/AnimationControls/AnimationControls.tsx b/src/components/AnimationControls/AnimationControls.tsx index 119f444..462a1ba 100644 --- a/src/components/AnimationControls/AnimationControls.tsx +++ b/src/components/AnimationControls/AnimationControls.tsx @@ -100,7 +100,7 @@ const AnimationControls = () => { ) { return (
-

+

Loading versions with local changes.

@@ -111,8 +111,8 @@ const AnimationControls = () => { <> -
- Animation Speed +
+ Animation Speed
{ return ( <>
{getContent()}
diff --git a/src/components/AnimationControls/DonwloadAnimationButton.tsx b/src/components/AnimationControls/DonwloadAnimationButton.tsx index 894ae57..853a7dd 100644 --- a/src/components/AnimationControls/DonwloadAnimationButton.tsx +++ b/src/components/AnimationControls/DonwloadAnimationButton.tsx @@ -33,13 +33,25 @@ export const DonwloadAnimationButton = () => { dispatch(showDownloadAnimationPanelToggled(true)); }, []); - const classNames = classnames('btn btn-fill', { - 'btn-disabled': animationStatus === 'loading', - }); + // const classNames = classnames('btn btn-fill', { + // 'btn-disabled': animationStatus === 'loading', + // }); return ( -
- Download Animation +
+ + Download Animation +
); }; diff --git a/src/components/AnimationControls/FramesSeletor.tsx b/src/components/AnimationControls/FramesSeletor.tsx index 5828b35..06e3470 100644 --- a/src/components/AnimationControls/FramesSeletor.tsx +++ b/src/components/AnimationControls/FramesSeletor.tsx @@ -106,11 +106,11 @@ const FramesSeletor: React.FC = ({ }} >
{ evt.stopPropagation(); toggleFrame(releaseNum); @@ -127,10 +127,11 @@ const FramesSeletor: React.FC = ({ return (
{/*
diff --git a/src/components/AnimationControls/PlayPauseBtn.tsx b/src/components/AnimationControls/PlayPauseBtn.tsx index 4f78f51..0328369 100644 --- a/src/components/AnimationControls/PlayPauseBtn.tsx +++ b/src/components/AnimationControls/PlayPauseBtn.tsx @@ -59,7 +59,7 @@ const PlayPauseBtn: React.FC = ({ }; return (
= ({ defaultVal, onChange }: Props) => { }} className="calcite-theme-dark" > - - + -
= ({ defaultVal, onChange }: Props) => { >
- + + +
); }; diff --git a/src/components/Gutter/index.tsx b/src/components/Gutter/index.tsx index fae5743..aeef179 100644 --- a/src/components/Gutter/index.tsx +++ b/src/components/Gutter/index.tsx @@ -50,7 +50,7 @@ export const Gutter: FC = ({ > {/* gradient effect on right side of gutter */} diff --git a/src/components/PopUp/style.css b/src/components/PopUp/style.css index 7a9340e..c268333 100644 --- a/src/components/PopUp/style.css +++ b/src/components/PopUp/style.css @@ -30,9 +30,9 @@ .popup-container .content-wrap .close-btn { position: absolute; - top: 3px; - right: 10px; - width: 10px; + top: 5px; + right: .25rem; + /* width: 10px; */ font-size: 0.75rem; cursor: pointer; } diff --git a/src/components/ReferenceLayerToggle/ReferenceLayerToggle.tsx b/src/components/ReferenceLayerToggle/ReferenceLayerToggle.tsx index 45a094c..f332094 100644 --- a/src/components/ReferenceLayerToggle/ReferenceLayerToggle.tsx +++ b/src/components/ReferenceLayerToggle/ReferenceLayerToggle.tsx @@ -53,10 +53,7 @@ class ReferenceLayerToggle extends React.PureComponent { return (
-
+
{icon}
reference label overlay
diff --git a/src/components/SwipeWidgetLayerSelector/SwipeWidgetLayerSelector.tsx b/src/components/SwipeWidgetLayerSelector/SwipeWidgetLayerSelector.tsx index 74d9a04..bbf7cdd 100644 --- a/src/components/SwipeWidgetLayerSelector/SwipeWidgetLayerSelector.tsx +++ b/src/components/SwipeWidgetLayerSelector/SwipeWidgetLayerSelector.tsx @@ -70,7 +70,7 @@ const SwipeWidgetLayerSelector: React.FC = ({ return (
- + Versions with{' '} local changes @@ -86,14 +86,14 @@ const SwipeWidgetLayerSelector: React.FC = ({ } return ( -
-

+
+

{targetLayerType === 'leading' ? 'Left' : 'Right'} Selection

{selectedItem.releaseDateLabel}
- + Click map for imagery details
@@ -108,15 +108,17 @@ const SwipeWidgetLayerSelector: React.FC = ({ return (
- + {/* */} +
); }; diff --git a/src/components/Title4ActiveItem/index.tsx b/src/components/Title4ActiveItem/index.tsx index 2bae28d..3dafa4b 100644 --- a/src/components/Title4ActiveItem/index.tsx +++ b/src/components/Title4ActiveItem/index.tsx @@ -43,7 +43,7 @@ class Title4ActiveItem extends React.PureComponent { return (
-
+
Selected release
{releaseDate} diff --git a/src/style/index.css b/src/style/index.css index c723c83..48b29f7 100644 --- a/src/style/index.css +++ b/src/style/index.css @@ -2,7 +2,7 @@ @tailwind components; @tailwind utilities; -@import './calcite-web/css/calcite-web.min.css'; +/* @import './calcite-web/css/calcite-web.min.css'; */ @import './variables.css'; @import './customized-modal.css'; @import './customized-tooltip.css'; @@ -52,4 +52,8 @@ input, textarea { svg.with-drop-shadow { filter: drop-shadow(0px 0px 8px #000000); +} + +.disabled { + @apply pointer-events-none opacity-50; } \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index 06c3477..53113ba 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -17,6 +17,7 @@ module.exports = { 'theme': { 'blue': { DEFAULT: '#2267AE', + 'brand': '#0079c1', 'light': '#56a5d8', 'dark': '#1A3D60' } From b527910121673cedfcb210251f83f2fdd7ecd4ae Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Sat, 16 Mar 2024 05:30:59 -0700 Subject: [PATCH 38/56] refactor: create Modal component to replace the modal from calcite-web.js update AppThisApp to use this Modal component --- src/components/Modal/Modal.tsx | 68 ++++++++++ .../ModalAboutApp/AboutThisAppContainer.tsx | 11 +- .../ModalAboutApp/AboutThisAppContent.tsx | 96 ++++++++++++++ src/components/ModalAboutApp/config.ts | 18 --- src/components/ModalAboutApp/index.tsx | 125 ------------------ 5 files changed, 173 insertions(+), 145 deletions(-) create mode 100644 src/components/Modal/Modal.tsx create mode 100644 src/components/ModalAboutApp/AboutThisAppContent.tsx delete mode 100644 src/components/ModalAboutApp/config.ts delete mode 100644 src/components/ModalAboutApp/index.tsx diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx new file mode 100644 index 0000000..6cc0423 --- /dev/null +++ b/src/components/Modal/Modal.tsx @@ -0,0 +1,68 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC } from 'react'; +import classnames from 'classnames'; + +type Props = { + isOpen: boolean; + width: 's' | 'm' | 'l'; + onClose: () => void; + children?: React.ReactNode; +}; + +export const Modal: FC = ({ + isOpen, + width = 'm', + children, + onClose, +}) => { + if (!isOpen) { + return null; + } + return ( +
+
+
+ +
+ +
+ {children} +
+
+
+ ); +}; diff --git a/src/components/ModalAboutApp/AboutThisAppContainer.tsx b/src/components/ModalAboutApp/AboutThisAppContainer.tsx index d91fdaa..10e8362 100644 --- a/src/components/ModalAboutApp/AboutThisAppContainer.tsx +++ b/src/components/ModalAboutApp/AboutThisAppContainer.tsx @@ -22,7 +22,8 @@ import { isAboutThisAppModalOpenToggled, } from '@store/UI/reducer'; -import AboutThisApp from './index'; +import { AboutThiAppContent } from './AboutThisAppContent'; +import { Modal } from '@components/Modal/Modal'; const AboutThisAppContainer = () => { const isOpen = useSelector(isAboutThisAppModalOpenSelector); @@ -33,7 +34,13 @@ const AboutThisAppContainer = () => { dispatch(isAboutThisAppModalOpenToggled()); }; - return isOpen ? : null; + return ( + + + + ); + + // return isOpen ? : null; }; export default AboutThisAppContainer; diff --git a/src/components/ModalAboutApp/AboutThisAppContent.tsx b/src/components/ModalAboutApp/AboutThisAppContent.tsx new file mode 100644 index 0000000..de15db0 --- /dev/null +++ b/src/components/ModalAboutApp/AboutThisAppContent.tsx @@ -0,0 +1,96 @@ +/* Copyright 2024 Esri + * + * Licensed under the Apache License Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useContext } from 'react'; +import { AppContext } from '@contexts/AppContextProvider'; + +// interface IProps { +// onClose: () => void; +// } +// interface IState {} + +export const AboutThiAppContent: React.FC = () => { + const { onPremises } = useContext(AppContext); + + const githubRepoInfo = ( + <> + The source code for this app is available on{' '} + + Github + + . + + ); + + return ( + <> +

World Imagery Wayback

+
WHAT
+

+ Wayback is a digital archive, providing users with access to the + different versions of{' '} + + World Imagery + {' '} + created over time. Each layer in the archive represents a + snapshot of the entire World Imagery map, as it existed on the + date it was published. Wayback currently provides access to all + published versions of World Imagery, dating back to February 20, + 2014. There is an ArcGIS Online item for every version which can + be accessed directly from this app or within the{' '} + + Wayback Imagery group + + . {!onPremises ? githubRepoInfo : null} +

+ +
WHY
+

+ As World Imagery is updated with more current imagery, new + versions of the map are published. When and where updates occur, + the previous imagery is replaced and is no longer visible. For + many use cases, the new imagery is more desirable and typically + preferred. Other times, however, the previous imagery may + support use cases that the new imagery does not. In these cases, + a user may need to access a previous version of World Imagery. +

+ +
HOW
+

+ Available versions of the World Imagery map are presented within + a timeline and as layers in a list. Versions that resulted in + local changes are highlighted in bold white, and the layer + currently selected is highlighted in blue. Point and click on + the map for additional imagery details within the selected + layer. One or more layers can be added to a queue and pushed to + a new ArcGIS Online web map. +

+ + ); +}; + +// export default About; diff --git a/src/components/ModalAboutApp/config.ts b/src/components/ModalAboutApp/config.ts deleted file mode 100644 index 5e57314..0000000 --- a/src/components/ModalAboutApp/config.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export default { - 'modal-id': 'about', -}; diff --git a/src/components/ModalAboutApp/index.tsx b/src/components/ModalAboutApp/index.tsx deleted file mode 100644 index bcdbb15..0000000 --- a/src/components/ModalAboutApp/index.tsx +++ /dev/null @@ -1,125 +0,0 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useContext } from 'react'; -import { AppContext } from '@contexts/AppContextProvider'; - -interface IProps { - onClose: () => void; -} -// interface IState {} - -const About: React.FC = ({ onClose }: IProps) => { - const { onPremises } = useContext(AppContext); - - const githubRepoInfo = ( - <> - The source code for this app is available on{' '} - - Github - - . - - ); - - return ( -
-
- - - - - - -

- World Imagery Wayback -

-
WHAT
-

- Wayback is a digital archive, providing users with access to - the different versions of{' '} - - World Imagery - {' '} - created over time. Each layer in the archive represents a - snapshot of the entire World Imagery map, as it existed on - the date it was published. Wayback currently provides access - to all published versions of World Imagery, dating back to - February 20, 2014. There is an ArcGIS Online item for every - version which can be accessed directly from this app or - within the{' '} - - Wayback Imagery group - - . {!onPremises ? githubRepoInfo : null} -

- -
WHY
-

- As World Imagery is updated with more current imagery, new - versions of the map are published. When and where updates - occur, the previous imagery is replaced and is no longer - visible. For many use cases, the new imagery is more - desirable and typically preferred. Other times, however, the - previous imagery may support use cases that the new imagery - does not. In these cases, a user may need to access a - previous version of World Imagery. -

- -
HOW
-

- Available versions of the World Imagery map are presented - within a timeline and as layers in a list. Versions that - resulted in local changes are highlighted in bold white, and - the layer currently selected is highlighted in blue. Point - and click on the map for additional imagery details within - the selected layer. One or more layers can be added to a - queue and pushed to a new ArcGIS Online web map. -

-
-
- ); -}; - -export default About; From 79dd7e4d60ecfde24cdf523d781660c5e6c10e9b Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Sat, 16 Mar 2024 06:21:03 -0700 Subject: [PATCH 39/56] fix: update SettingDialog to use Modal component and stop using styles from calcite-web.js --- src/components/Modal/Modal.tsx | 2 +- .../SettingDialog/SettingDialogContainer.tsx | 36 ++- .../{index.tsx => SettingDialogContent.tsx} | 233 +++++++++--------- src/components/SettingDialog/Switch.tsx | 43 ++++ src/components/SettingDialog/config.ts | 18 -- src/components/SettingDialog/style.css | 3 - src/types/calcite-components.d.ts | 1 + 7 files changed, 180 insertions(+), 156 deletions(-) rename src/components/SettingDialog/{index.tsx => SettingDialogContent.tsx} (50%) create mode 100644 src/components/SettingDialog/Switch.tsx delete mode 100644 src/components/SettingDialog/config.ts delete mode 100644 src/components/SettingDialog/style.css diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 6cc0423..9559985 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -59,7 +59,7 @@ export const Modal: FC = ({ />
-
+
{children}
diff --git a/src/components/SettingDialog/SettingDialogContainer.tsx b/src/components/SettingDialog/SettingDialogContainer.tsx index 815c9e9..9b30a22 100644 --- a/src/components/SettingDialog/SettingDialogContainer.tsx +++ b/src/components/SettingDialog/SettingDialogContainer.tsx @@ -17,8 +17,6 @@ import React, { useContext } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import SetttingDialog from './index'; - import { isSettingModalOpenSelector, isSettingModalOpenToggled, @@ -29,6 +27,8 @@ import { mapExtentSelector } from '@store/Map/reducer'; // import { AppContext } from '@contexts/AppContextProvider'; import { isAnonymouns, signIn, signOut } from '@utils/Esri-OAuth'; +import SettingDialogContent from './SettingDialogContent'; +import { Modal } from '@components/Modal/Modal'; const SettingDialogContainer = () => { const dispatch = useDispatch(); @@ -55,17 +55,27 @@ const SettingDialogContainer = () => { // dispatch(shouldOnlyShowItemsWithLocalChangeToggled(val)); // }; - return isOpen ? ( - - ) : null; + // return isOpen ? ( + // + // ) : null; + + return ( + + + + ); }; export default SettingDialogContainer; diff --git a/src/components/SettingDialog/index.tsx b/src/components/SettingDialog/SettingDialogContent.tsx similarity index 50% rename from src/components/SettingDialog/index.tsx rename to src/components/SettingDialog/SettingDialogContent.tsx index b4ea67e..078a2c9 100644 --- a/src/components/SettingDialog/index.tsx +++ b/src/components/SettingDialog/SettingDialogContent.tsx @@ -13,7 +13,6 @@ * limitations under the License. */ -import './style.css'; import React from 'react'; import classnames from 'classnames'; import { @@ -24,6 +23,7 @@ import { // getShouldShowUpdatesWithLocalChanges, } from '@utils/LocalStorage'; import { IExtentGeomety } from '@typings/index'; +import { Switch } from './Switch'; // import config from './config'; type SaveBtnLabelValue = 'Save' | 'Saved'; @@ -31,13 +31,11 @@ type SaveBtnLabelValue = 'Save' | 'Saved'; interface IProps { mapExtent?: IExtentGeomety; signedInAlready?: boolean; - toggleSignInBtnOnClick: (shouldSignIn: boolean) => void; // shouldShowLocalChangesByDefaultOnClick: ( // shouldShowLocalChangesByDefault: boolean // ) => void; - - onClose: () => void; + // onClose: () => void; } interface IState { @@ -52,7 +50,7 @@ type StateKeys = keyof IState; const CustomUrlFromLocalStorage = getCustomPortalUrl(); -class SettingDialog extends React.PureComponent { +class SettingDialogContent extends React.PureComponent { constructor(props: IProps) { super(props); @@ -98,7 +96,7 @@ class SettingDialog extends React.PureComponent { // shouldShowLocalChangesByDefault, } = this.state; - const { onClose, mapExtent } = this.props; + const { mapExtent } = this.props; if (shouldSaveAsDefaultExtent) { // const mapExt = getMapExtent(); @@ -132,7 +130,7 @@ class SettingDialog extends React.PureComponent { // this.close(); - onClose(); + // onClose(); } toggleSaveBtnLabel(isSaved = false) { @@ -172,7 +170,7 @@ class SettingDialog extends React.PureComponent { // } render() { - const { signedInAlready, toggleSignInBtnOnClick, onClose } = this.props; + const { signedInAlready, toggleSignInBtnOnClick } = this.props; const { portalUrl, shouldUseCustomPortalUrl, @@ -185,136 +183,129 @@ class SettingDialog extends React.PureComponent { // shouldShowLocalChangesByDefault !== // getShouldShowUpdatesWithLocalChanges(); - const saveBtnClasses = classnames('btn', { - 'btn-disabled': !portalUrl && !shouldSaveAsDefaultExtent, - }); + // const saveBtnClasses = classnames('btn', { + // 'btn-disabled': !portalUrl && !shouldSaveAsDefaultExtent, + // }); const signOutBtn = ( - Sign Out - + ); return ( -
-
- - - - - - -

Settings

- -
- -
+ <> +

Settings

+ +
+ {/* */} + + { + // console.log('on change') + this.toggleBooleanStateVal( + 'shouldSaveAsDefaultExtent' + ); + }} + /> +
- {/*
- -
*/} - -
-
-
- - {signedInAlready ? signOutBtn : null} - +
+ + {signedInAlready ? signOutBtn : null} + - +
+ {saveBtnLable} - +
+ {/* + {saveBtnLable} + */}
-
+ ); } } -export default SettingDialog; +export default SettingDialogContent; diff --git a/src/components/SettingDialog/Switch.tsx b/src/components/SettingDialog/Switch.tsx new file mode 100644 index 0000000..a4ac697 --- /dev/null +++ b/src/components/SettingDialog/Switch.tsx @@ -0,0 +1,43 @@ +import React, { FC, useEffect, useRef } from 'react'; + +type Props = { + /** + * emits when user click on the Switch button + * @returns + */ + onChange: (checked: boolean) => void; + /** + * label text to be placed next to the switch button + */ + label: string; + /** + * if true, the switch button should be checked + */ + checked: boolean; +}; + +export const Switch: FC = ({ label, checked, onChange }) => { + const switchRef = useRef(); + + const props = {}; + + if (checked) { + props['checked'] = true; + } + + useEffect(() => { + switchRef.current.addEventListener( + 'calciteSwitchChange', + (evt: any) => { + onChange(evt.target?.checked); + } + ); + }, []); + + return ( +
+ + {label} +
+ ); +}; diff --git a/src/components/SettingDialog/config.ts b/src/components/SettingDialog/config.ts deleted file mode 100644 index 82a6555..0000000 --- a/src/components/SettingDialog/config.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export default { - 'modal-id': 'setting', -}; diff --git a/src/components/SettingDialog/style.css b/src/components/SettingDialog/style.css deleted file mode 100644 index 07f9a31..0000000 --- a/src/components/SettingDialog/style.css +++ /dev/null @@ -1,3 +0,0 @@ -.setting-dialog .btn-transparent { - color: #fff; -} diff --git a/src/types/calcite-components.d.ts b/src/types/calcite-components.d.ts index 68f4182..fab936c 100644 --- a/src/types/calcite-components.d.ts +++ b/src/types/calcite-components.d.ts @@ -24,5 +24,6 @@ declare namespace React.JSX { 'calcite-icon': any; 'calcite-button': any; 'calcite-slider': any; + 'calcite-switch': any; } } From 8f6ffec7ac3ee63a36baa2db5fdc1e8505709c94 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Sat, 16 Mar 2024 06:33:17 -0700 Subject: [PATCH 40/56] chore: remove share dialog as it's no longer needed --- .../ShareDialog/ShareDialogContainer.tsx | 41 ----- src/components/ShareDialog/config.ts | 22 --- src/components/ShareDialog/index.tsx | 163 ------------------ src/components/ShareDialog/style.css | 5 - src/components/index.ts | 2 +- 5 files changed, 1 insertion(+), 232 deletions(-) delete mode 100644 src/components/ShareDialog/ShareDialogContainer.tsx delete mode 100644 src/components/ShareDialog/config.ts delete mode 100644 src/components/ShareDialog/index.tsx delete mode 100644 src/components/ShareDialog/style.css diff --git a/src/components/ShareDialog/ShareDialogContainer.tsx b/src/components/ShareDialog/ShareDialogContainer.tsx deleted file mode 100644 index 374c451..0000000 --- a/src/components/ShareDialog/ShareDialogContainer.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; - -import { useSelector, useDispatch } from 'react-redux'; - -import { - isShareModalOpenSelector, - isShareModalOpenToggled, -} from '@store/UI/reducer'; - -import ShareDialog from './index'; - -const ShareDialogContainer = () => { - const isOpen = useSelector(isShareModalOpenSelector); - - const dispatch = useDispatch(); - - const onCloseHandler = () => { - dispatch(isShareModalOpenToggled()); - }; - - return isOpen ? ( - - ) : null; -}; - -export default ShareDialogContainer; diff --git a/src/components/ShareDialog/config.ts b/src/components/ShareDialog/config.ts deleted file mode 100644 index 1629a4c..0000000 --- a/src/components/ShareDialog/config.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export default { - 'modal-id': 'share', - 'github-repo-url': 'https://github.com/vannizhang/wayback', - title: 'World Imagery Wayback', - description: - 'Wayback is a digital archive, providing users with access to the different versions of World Imagery created over time.', -}; diff --git a/src/components/ShareDialog/index.tsx b/src/components/ShareDialog/index.tsx deleted file mode 100644 index 6418680..0000000 --- a/src/components/ShareDialog/index.tsx +++ /dev/null @@ -1,163 +0,0 @@ -/* Copyright 2024 Esri - * - * Licensed under the Apache License Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import './style.css'; -import React from 'react'; -import config from './config'; - -interface IProps { - currentUrl: string; - onClose: () => void; -} - -interface IState { - copyBtnLabel: string; -} - -class ShareDialog extends React.PureComponent { - private textInputRef = React.createRef(); - - constructor(props: IProps) { - super(props); - - this.state = { - copyBtnLabel: 'Copy', - }; - - this.copyToClipboard = this.copyToClipboard.bind(this); - } - - copyToClipboard() { - const textInput = this.textInputRef.current; - textInput.select(); - document.execCommand('copy'); - - this.updateCopyBtnLabel('Copied'); - } - - updateCopyBtnLabel(label = 'Copy') { - this.setState({ - copyBtnLabel: label, - }); - - if (label === 'Copied') { - setTimeout(() => { - this.updateCopyBtnLabel('Copy'); - }, 1500); - } - } - - componentDidMount() { - // modal(); - } - - render() { - const { currentUrl, onClose } = this.props; - - const { copyBtnLabel } = this.state; - - const share2TwitterLink = `https://arcgis.com/home/socialnetwork.html?t=${config.title}&n=tw&u=${currentUrl}`; - const share2facebookLink = `https://arcgis.com/home/socialnetwork.html?t=${config.title}&n=fb&u=${currentUrl}`; - const share2LinkedInLink = `https://www.linkedin.com/shareArticle?url=${currentUrl}&title=${config.title}&summary=${config.description}?mini=true&source=livingatlas.arcgis.com`; - - return ( -
-
-
- - - - - - -

- Share World Imagery Wayback -

-
- -
-
- - - - -
-
- -
- - - - {/* */} -
-
-
- ); - } -} - -export default ShareDialog; diff --git a/src/components/ShareDialog/style.css b/src/components/ShareDialog/style.css deleted file mode 100644 index 2c54606..0000000 --- a/src/components/ShareDialog/style.css +++ /dev/null @@ -1,5 +0,0 @@ -.social-media-icons [class*="icon-social-"] { - width: 35px; - height: 35px; - background-size: cover; -} \ No newline at end of file diff --git a/src/components/index.ts b/src/components/index.ts index a30bf4c..f006f53 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -29,7 +29,7 @@ export { default as ReferenceLayer } from './ReferenceLayer/ReferenceLayerContai export { default as ReferenceLayerToggle } from './ReferenceLayerToggle/ReferenceLayerToggleContainer'; export { default as Sidebar } from './Sidebar/SidebarContainer'; export { default as SearchWidget } from './SearchWidget/SearchWidget'; -export { default as ShareDialog } from './ShareDialog/ShareDialogContainer'; +// export { default as ShareDialog } from './ShareDialog/ShareDialogContainer'; export { default as SwipeWidget } from './SwipeWidget/SwipeWidgetContainer'; export { default as SaveAsWebMapDialog } from './SaveAsWebmapDialog/SaveAsWebmapDialogContainer'; export { default as SwipeWidgetToggleBtn } from './SwipeWidgetToggleBtn/SwipeWidgetToggleBtnContainer'; From cfad192f2feb1f4b9cea1ecd06da60b3e605ccc8 Mon Sep 17 00:00:00 2001 From: Jinnan Zhang Date: Sat, 16 Mar 2024 07:00:07 -0700 Subject: [PATCH 41/56] fix: update SaveWebMapDialog use Modal component stop using calcite-web.js styles --- src/components/Modal/Modal.tsx | 4 +- .../SaveAsWebmapDialogContainer.tsx | 49 +++++++--- src/components/SaveAsWebmapDialog/config.ts | 2 +- src/components/SaveAsWebmapDialog/index.tsx | 93 ++++++++----------- 4 files changed, 77 insertions(+), 71 deletions(-) diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 9559985..3aa149f 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -18,7 +18,7 @@ import classnames from 'classnames'; type Props = { isOpen: boolean; - width: 's' | 'm' | 'l'; + width?: 's' | 'm' | 'l'; onClose: () => void; children?: React.ReactNode; }; @@ -59,7 +59,7 @@ export const Modal: FC = ({ />
-
+
{children}
diff --git a/src/components/SaveAsWebmapDialog/SaveAsWebmapDialogContainer.tsx b/src/components/SaveAsWebmapDialog/SaveAsWebmapDialogContainer.tsx index c8f5bbb..498604f 100644 --- a/src/components/SaveAsWebmapDialog/SaveAsWebmapDialogContainer.tsx +++ b/src/components/SaveAsWebmapDialog/SaveAsWebmapDialogContainer.tsx @@ -40,6 +40,7 @@ import { isAnonymouns, signInUsingDifferentAccount, } from '@utils/Esri-OAuth'; +import { Modal } from '@components/Modal/Modal'; const SaveAsWebmapDialogContainer = () => { const dispatch = useDispatch(); @@ -64,21 +65,39 @@ const SaveAsWebmapDialogContainer = () => { // console.log(isOpen); // }, [isOpen]); - return isOpen ? ( - { - signInUsingDifferentAccount(); - }} - /> - ) : null; + return ( + + { + signInUsingDifferentAccount(); + }} + /> + + ); + + // return isOpen ? ( + // { + // signInUsingDifferentAccount(); + // }} + // /> + // ) : null; }; export default SaveAsWebmapDialogContainer; diff --git a/src/components/SaveAsWebmapDialog/config.ts b/src/components/SaveAsWebmapDialog/config.ts index ed6d427..47a121a 100644 --- a/src/components/SaveAsWebmapDialog/config.ts +++ b/src/components/SaveAsWebmapDialog/config.ts @@ -14,7 +14,7 @@ */ export default { - 'modal-id': 'save-as-webmap-dialog', + // 'modal-id': 'save-as-webmap-dialog', title: 'Custom Wayback Imagery Web Map', tags: 'wayback', description: diff --git a/src/components/SaveAsWebmapDialog/index.tsx b/src/components/SaveAsWebmapDialog/index.tsx index 50e1df0..b7e69eb 100644 --- a/src/components/SaveAsWebmapDialog/index.tsx +++ b/src/components/SaveAsWebmapDialog/index.tsx @@ -39,7 +39,7 @@ interface IProps { portalBaseURL: string; mapExtent: IExtentGeomety; - onClose: (val: boolean) => void; + // onClose: (val: boolean) => void; signInButtonOnClick: () => void; } @@ -195,41 +195,49 @@ class SaveAsWebmapDialog extends React.PureComponent { } = this.state; const creatingIndicator = isCreatingWebmap ? ( - + Creating Web Map... ) : null; - const creatWebMapBtnClasses = classnames('btn upload-webmap-btn', { - 'btn-disabled': isRequiredFieldMissing, + const creatWebMapBtnClasses = classnames({ + disabled: isRequiredFieldMissing, }); const creatWebMapBtn = !isCreatingWebmap ? (
- Create Wayback Map + Create Wayback Map
) : null; return (
-
Wayback Map Settings:
-