Skip to content

Commit

Permalink
feat(globe): wait for layer to load before advancing timestep (#1268)
Browse files Browse the repository at this point in the history
* feat(globe): store layer loading state in app-state
* feat(globe): only advance time when layers have rendered
* fix: add defaults for layerLoadingState
  • Loading branch information
usefulthink committed Aug 17, 2023
1 parent 30af60a commit 6807f57
Show file tree
Hide file tree
Showing 10 changed files with 141 additions and 12 deletions.
16 changes: 16 additions & 0 deletions src/scripts/actions/update-layer-loading-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {LayerLoadingState} from '@ubilabs/esa-webgl-globe';

export const UPDATE_LAYER_LOADING_STATE = 'UPDATE_LAYER_LOADING_STATE';

export interface UpdateLayerLoadingStateAction {
type: typeof UPDATE_LAYER_LOADING_STATE;
layerId: string;
loadingState: LayerLoadingState;
}

export default function updateLayerLoadingStateAction(
layerId: string,
loadingState: LayerLoadingState
): UpdateLayerLoadingStateAction {
return {type: UPDATE_LAYER_LOADING_STATE, layerId, loadingState};
}
44 changes: 40 additions & 4 deletions src/scripts/components/layers/time-playback/time-playback.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import {FunctionComponent} from 'react';
import {useSelector, useDispatch} from 'react-redux';
import {FunctionComponent, useEffect, useState} from 'react';
import {useDispatch, useSelector} from 'react-redux';

import {timeSelector} from '../../../selectors/globe/time';
import {layerLoadingStateSelector} from '../../../selectors/globe/layer-loading-state';

import setGlobeTime from '../../../actions/set-globe-time';
import {useInterval} from '../../../hooks/use-interval';
import {LayerLoadingState} from '@ubilabs/esa-webgl-globe';

const PLAYBACK_STEP = 1000 * 60 * 60 * 24 * 30; // one month
const PLAYBACK_SPEED = 1000; // increase one step per x milliseconds
Expand All @@ -13,27 +16,60 @@ interface Props {
maxTime: number;
speed?: number;
step?: number;
mainLayerId?: string | null;
compareLayerId?: string | null;
}

const TimePlayback: FunctionComponent<Props> = ({
minTime,
maxTime,
speed = PLAYBACK_SPEED,
step = PLAYBACK_STEP
step = PLAYBACK_STEP,
mainLayerId,
compareLayerId
}) => {
const dispatch = useDispatch();
const time = useSelector(timeSelector);

const [nextTime, setNextTime] = useState(time);

useInterval(() => {
let newTime = time + step;

if (newTime > maxTime) {
newTime = minTime;
}

dispatch(setGlobeTime(newTime));
// don't immediately set the new time, since we might have to wait
// for the layer to complete loading the previous timestamp.
setNextTime(newTime);
}, speed);

// before the globe-time is updated, make sure the layers have actually
// rendered the current time.

// get the state for both possible layers. When no layer is specified, we
// use 'idle' as to not interfere with the loading logic
const layerLoadingState = useSelector(layerLoadingStateSelector);
const mainLayerState = mainLayerId
? layerLoadingState[mainLayerId]
: ('idle' as LayerLoadingState);
const compareLayerState = compareLayerId
? layerLoadingState[compareLayerId]
: ('idle' as LayerLoadingState);

useEffect(() => {
if (
nextTime === time ||
mainLayerState === 'loading' ||
compareLayerState === 'loading'
) {
return;
}

dispatch(setGlobeTime(nextTime));
}, [dispatch, time, nextTime, mainLayerState, compareLayerState]);

return null;
};

Expand Down
2 changes: 2 additions & 0 deletions src/scripts/components/layers/time-slider/time-slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ const TimeSlider: FunctionComponent<Props> = ({
minTime={combined.min}
maxTime={combined.max}
step={playbackStep}
mainLayerId={mainId}
compareLayerId={compareId}
/>
)}
<div className={styles.container}>
Expand Down
10 changes: 9 additions & 1 deletion src/scripts/components/main/data-viewer/data-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import React, {
useLayoutEffect
} from 'react';
import {useSelector, useDispatch} from 'react-redux';
import {CameraView} from '@ubilabs/esa-webgl-globe';
import {CameraView, LayerLoadingState} from '@ubilabs/esa-webgl-globe';

import {layerListItemSelector} from '../../../selectors/layers/list-item';
import {globeViewSelector} from '../../../selectors/globe/view';
Expand All @@ -18,6 +18,7 @@ import {selectedLayerIdsSelector} from '../../../selectors/layers/selected-ids';
import {globeSpinningSelector} from '../../../selectors/globe/spinning';
import setGlobeViewAction from '../../../actions/set-globe-view';
import setGlobeSpinningAction from '../../../actions/set-globe-spinning';
import updateLayerLoadingStateAction from '../../../actions/update-layer-loading-state';
import {State} from '../../../reducers';
import Globe from '../globe/globe';
import Gallery from '../gallery/gallery';
Expand Down Expand Up @@ -90,6 +91,12 @@ const DataViewer: FunctionComponent<Props> = ({
[dispatch]
);

const onLayerLoadingStateChangeHandler = useCallback(
(layerId: string, loadingState: LayerLoadingState) =>
dispatch(updateLayerLoadingStateAction(layerId, loadingState)),
[dispatch]
);

const mainImageLayer = useImageLayerData(mainLayerDetails, time);
const compareImageLayer = useImageLayerData(compareLayerDetails, time);

Expand Down Expand Up @@ -147,6 +154,7 @@ const DataViewer: FunctionComponent<Props> = ({
onChange={onChangeHandler}
onMoveStart={onMoveStartHandler}
onMoveEnd={onMoveEndHandler}
onLayerLoadingStateChange={onLayerLoadingStateChangeHandler}
/>
);
};
Expand Down
46 changes: 41 additions & 5 deletions src/scripts/components/main/globe/globe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import cx from 'classnames';

import {
CameraView,
LayerLoadingState,
LayerProps,
MarkerProps,
RenderMode,
Expand Down Expand Up @@ -53,6 +54,10 @@ interface Props {
onChange: (view: CameraView) => void;
onMoveStart: () => void;
onMoveEnd: (view: CameraView) => void;
onLayerLoadingStateChange: (
layerId: string,
state: LayerLoadingState
) => void;
}

const EMPTY_FUNCTION = () => {};
Expand All @@ -76,7 +81,9 @@ const Globe: FunctionComponent<Props> = props => {

useProjectionSwitch(globe, projectionState.projection);
useMultiGlobeSynchronization(globe, props);
// eslint-disable-next-line no-warning-comments

useLayerLoadingStateUpdater(globe, props.onLayerLoadingStateChange);

// fixme: add auto-rotate functionality

return (
Expand Down Expand Up @@ -186,8 +193,11 @@ function useInitialBasemapTilesLoaded(globe: WebGlGlobe | null) {
const handleLoadingStateChange = useCallback(
(ev: LayerLoadingStateChangedEvent) => {
const {layer, state} = ev.detail;
if (layer.id === 'basemap' && state === 'ready') {
setInitialTilesLoaded(true);

if (layer.id === 'basemap') {
if (state === 'ready' || state === 'idle') {
setInitialTilesLoaded(true);
}
}
},
[]
Expand All @@ -214,6 +224,9 @@ function useInitialBasemapTilesLoaded(globe: WebGlGlobe | null) {
return initalTilesLoaded;
}

/**
* Switch the projection mode used by the globe.
*/
function useProjectionSwitch(
globe: WebGlGlobe | null,
projection: GlobeProjection
Expand Down Expand Up @@ -258,8 +271,6 @@ function useMultiGlobeSynchronization(globe: WebGlGlobe | null, props: Props) {

/**
* Call the onChange callback from the props from an active globe.
* @param globe
* @param props
*/
function useCameraChangeEvents(globe: WebGlGlobe | null, props: Props) {
const {active, onMoveStart, onChange, onMoveEnd} = props;
Expand Down Expand Up @@ -307,6 +318,31 @@ function useCameraChangeEvents(globe: WebGlGlobe | null, props: Props) {
}, [globe, active, handleViewChanged]);
}

/**
* Dispatch layerLoadingStates from the globe to the parent component.
*/
function useLayerLoadingStateUpdater(
globe: WebGlGlobe | null,
callback: (layerId: string, state: LayerLoadingState) => void
) {
const handler = useCallback(
(ev: LayerLoadingStateChangedEvent) => {
callback(ev.detail.layer.id, ev.detail.state);
},
[callback]
);

useEffect(() => {
if (!globe) {
return EMPTY_FUNCTION;
}

globe.addEventListener('layerLoadingStateChanged', handler);

return () => globe.removeEventListener('layerLoadingStateChanged', handler);
}, [globe, handler]);
}

// ----
// utility functions
// ----
Expand Down
3 changes: 2 additions & 1 deletion src/scripts/config/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ const globeState: GlobeState = {
altitude: 23840000,
zoom: 0
},
spinning: true
spinning: true,
layerLoadingState: {}
};

export const uiEmbedElements: UiEmbedElement[] = [
Expand Down
1 change: 1 addition & 0 deletions src/scripts/libs/globe-url-parameter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export function parseUrl(): UrlHashState | null {
projection,
morphTime: 2
},
layerLoadingState: {},
time,
spinning
},
Expand Down
4 changes: 3 additions & 1 deletion src/scripts/reducers/globe/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import projectionReducer from './projection';
import viewReducer from './view';
import timeReducer from './time';
import spinningReducer from './spinning';
import layerLoadingStateReducer from './layer-loading-state';

const globeReducer = combineReducers({
view: viewReducer,
projectionState: projectionReducer,
time: timeReducer,
spinning: spinningReducer
spinning: spinningReducer,
layerLoadingState: layerLoadingStateReducer
});

export default globeReducer;
Expand Down
21 changes: 21 additions & 0 deletions src/scripts/reducers/globe/layer-loading-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {LayerLoadingState} from '@ubilabs/esa-webgl-globe';
import {
UPDATE_LAYER_LOADING_STATE,
UpdateLayerLoadingStateAction
} from '../../actions/update-layer-loading-state';

export type LoadingStateByLayer = {[layerId: string]: LayerLoadingState};

function layerLoadingStateReducer(
state: LoadingStateByLayer = {},
action: UpdateLayerLoadingStateAction
): LoadingStateByLayer {
switch (action.type) {
case UPDATE_LAYER_LOADING_STATE:
return {...state, [action.layerId]: action.loadingState};
default:
return state;
}
}

export default layerLoadingStateReducer;
6 changes: 6 additions & 0 deletions src/scripts/selectors/globe/layer-loading-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {State} from '../../reducers/index';
import {LoadingStateByLayer} from '../../reducers/globe/layer-loading-state';

export function layerLoadingStateSelector(state: State): LoadingStateByLayer {
return state.globe.layerLoadingState;
}

0 comments on commit 6807f57

Please sign in to comment.