Skip to content

Commit a2f9d69

Browse files
authored
Fix #3373. Support for disable playback buttons (#3377)
1 parent 4f293e0 commit a2f9d69

8 files changed

Lines changed: 283 additions & 99 deletions

File tree

web/client/actions/playback.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const SELECT_PLAYBACK_RANGE = "PLAYBACK:SELECT_PLAYBACK_RANGE";
1111
const CHANGE_SETTING = "PLAYBACK:SETTINGS_CHANGE";
1212
const TOGGLE_ANIMATION_MODE = "PLAYBACK:TOGGLE_ANIMATION_MODE";
1313
const ANIMATION_STEP_MOVE = "PLAYBACK:ANIMATION_STEP_MOVE";
14+
const UPDATE_METADATA = "PLAYBACK:UPDATE_METADATA";
1415

1516
const STATUS = {
1617
PLAY: "PLAY",
@@ -64,6 +65,13 @@ const animationStepMove = (direction) => ({
6465
direction
6566
});
6667

68+
const updateMetadata = ({next, previous, forTime}) => ({
69+
type: UPDATE_METADATA,
70+
forTime,
71+
next,
72+
previous
73+
});
74+
6775
module.exports = {
6876
play,
6977
stop,
@@ -76,6 +84,7 @@ module.exports = {
7684
changeSetting,
7785
toggleAnimationMode,
7886
animationStepMove,
87+
updateMetadata,
7988
PLAY,
8089
PAUSE,
8190
STOP,
@@ -87,5 +96,6 @@ module.exports = {
8796
SELECT_PLAYBACK_RANGE,
8897
CHANGE_SETTING,
8998
TOGGLE_ANIMATION_MODE,
90-
ANIMATION_STEP_MOVE
99+
ANIMATION_STEP_MOVE,
100+
UPDATE_METADATA
91101
};

web/client/epics/playback.js

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const { get } = require('lodash');
1010
const {
1111
PLAY, PAUSE, STOP, STATUS, SET_FRAMES, SET_CURRENT_FRAME, TOGGLE_ANIMATION_MODE, ANIMATION_STEP_MOVE,
1212
stop, setFrames, appendFrames, setCurrentFrame,
13-
framesLoading
13+
framesLoading, updateMetadata
1414
} = require('../actions/playback');
1515
const {
1616
moveTime, SET_CURRENT_TIME, MOVE_TIME, SET_OFFSET_TIME
@@ -28,7 +28,7 @@ const { currentTimeSelector, layersWithTimeDataSelector, layerTimeSequenceSelect
2828

2929
const { LOCATION_CHANGE } = require('react-router-redux');
3030

31-
const { currentFrameSelector, currentFrameValueSelector, lastFrameSelector, playbackRangeSelector, playbackSettingsSelector, frameDurationSelector, statusSelector } = require('../selectors/playback');
31+
const { currentFrameSelector, currentFrameValueSelector, lastFrameSelector, playbackRangeSelector, playbackSettingsSelector, frameDurationSelector, statusSelector, playbackMetadataSelector } = require('../selectors/playback');
3232
const { selectedLayerName, selectedLayerUrl, selectedLayerData, selectedLayerTimeDimensionConfiguration, rangeSelector, selectedLayerSelector } = require('../selectors/timeline');
3333

3434
const pausable = require('../observables/pausable');
@@ -112,6 +112,8 @@ const filterAnimationValues = (values, getState, {fromValue, limit = BUFFER_SIZE
112112
* - If configured as fixed steps, it returns the list of next animation frame calculating them
113113
* - If there is a selected layer and there is the Multidim extension, then use it (in favour of static values configured)
114114
* - If there are values in the dimension configuration, and the Multidim extension is not present, use them to animate
115+
* @param {function} getState returns the application state
116+
* @param {object} options the options that normally match the getDomainValues options
115117
*/
116118
const getAnimationFrames = (getState, options) => {
117119
if (selectedLayerName(getState())) {
@@ -144,6 +146,8 @@ const setupAnimation = (getState = () => ({})) => animationEventsStream$ => {
144146
};
145147
/**
146148
* Check if a time is in out of the defined range. If range start or end are not defined, returns false.
149+
* @param {string|Date} time the time to check
150+
* @param {Object} interval the interval where the time should stay `{start: ISODate|Date, end: ISODate|Date}
147151
*/
148152
const isOutOfRange = (time, { start, end } = {}) =>
149153
start && end && ( moment(time).isBefore(start) || moment(time).isAfter(end));
@@ -238,13 +242,46 @@ module.exports = {
238242
playbackMoveStep: (action$, { getState = () => { } } = {}) =>
239243
action$
240244
.ofType(ANIMATION_STEP_MOVE)
241-
.filter(() => statusSelector(getState()) !== STATUS.PLAY) // if is playing, the animation manages this event
242-
.switchMap(({ direction = 1 }) =>
243-
getAnimationFrames(getState, {limit: 1, sort: direction > 0 ? "asc" : "desc", fromValue: currentTimeSelector(getState()) })
244-
.map(([t] = []) => t)
245-
.filter(t => !!t)
246-
.map(t => moveTime(t))
247-
),
245+
.filter(() => statusSelector(getState()) !== STATUS.PLAY /* && statusSelector(getState()) !== STATUS.PAUSE*/) // if is playing, the animation manages this event
246+
.switchMap(({ direction = 1 }) => {
247+
const md = playbackMetadataSelector(getState()) || {};
248+
const currentTime = currentTimeSelector(getState());
249+
// check if the next/prev value is present in the state (by `playbackCacheNextPreviousTimes`)
250+
if (currentTime && md.forTime === currentTime) {
251+
return Rx.Observable.of(direction > 0 ? md.next : md.previous);
252+
}
253+
// if not downloaded yet, download it
254+
return getAnimationFrames(getState, { limit: 1, sort: direction > 0 ? "asc" : "desc", fromValue: currentTimeSelector(getState()) })
255+
.map(([t] = []) => t);
256+
}).filter(t => !!t)
257+
.map(t => moveTime(t)),
258+
/**
259+
* Pre-loads next and previous values for the current time, when change.
260+
* This is useful to enable/disable playback buttons in guide-layer mode. The state updated by this
261+
* epic is also used as a cache to load next/previous button (only when the animation is not active)
262+
*/
263+
playbackCacheNextPreviousTimes: (action$, { getState = () => { } } = {}) =>
264+
action$
265+
.ofType(SET_CURRENT_TIME, MOVE_TIME)
266+
.filter(() => statusSelector(getState()) !== STATUS.PLAY && statusSelector(getState()) !== STATUS.PAUSE)
267+
.filter(() => selectedLayerSelector(getState()))
268+
.filter( t => !!t )
269+
.switchMap(({time}) =>
270+
Rx.Observable.forkJoin( // TODO: find out a way to optimize and do only one request
271+
getDomainValues(...domainArgs(getState, { sort: "asc", limit: 1, fromValue: time }))
272+
.map(res => res.DomainValues.Domain.split(","))
273+
.map(([tt]) => tt).catch(err => err && Rx.Observable.of(null)),
274+
getDomainValues(...domainArgs(getState, { sort: "desc", limit: 1, fromValue: time }))
275+
.map(res => res.DomainValues.Domain.split(","))
276+
.map(([tt]) => tt).catch(err => err && Rx.Observable.of(null))
277+
).map(([next, previous]) =>
278+
updateMetadata({
279+
forTime: time,
280+
next,
281+
previous
282+
})
283+
)
284+
),
248285
/**
249286
* During animation, on every current time change event, if the current time is out of the current range window, the timeline will shift to
250287
* current start-end values

web/client/epics/timeline.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ const TIME_DIMENSION = "time";
2020
const MAX_ITEMS_PER_LAYER = 20;
2121
const MAX_HISTOGRAM = 20;
2222

23-
23+
/**
24+
* Gets the getDomain args for retrieve **single** value surrounding current time for the selected layer
25+
* @param {object} state application state
26+
* @param {object} paginationOptions
27+
*/
2428
const domainArgs = (state, paginationOptions = {}) => {
2529

2630
const layerName = selectedLayerName(state);
@@ -40,6 +44,7 @@ const snapTime = (state, group, time) => {
4044
if (selectedLayerName(state)) {
4145
// do parallel request and return and observable that emit the correct value/ time as it is by default
4246
return Rx.Observable.forkJoin(
47+
// TODO: find out a way to optimize and do only one request
4348
getDomainValues(...domainArgs(state, { sort: "asc", fromValue: time }))
4449
.map(res => res.DomainValues.Domain.split(","))
4550
.map(([tt])=> tt).catch(err => err && Rx.Observable.of(null)),
@@ -72,7 +77,7 @@ const toISOString = date => isString(date) ? date : date.toISOString();
7277
*/
7378
const loadRangeData = (id, timeData, getState) => {
7479
/**
75-
* when there is no timeline state rangeSelector(getState()) returns undefiend, so instead we use the timeData[id] range
80+
* when there is no timeline state rangeSelector(getState()) returns undefined, so instead we use the timeData[id] range
7681
*/
7782
const dataRange = timeData.domain.split('--');
7883

web/client/plugins/playback/Playback.jsx

Lines changed: 27 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -8,94 +8,18 @@
88

99
const React = require('react');
1010
const { connect } = require('react-redux');
11-
const {createSelector} = require('reselect');
12-
const moment = require('moment');
13-
const { compose, withState, withProps, withHandlers} = require('recompose');
14-
const { playbackSettingsSelector, playbackRangeSelector} = require('../../selectors/playback');
15-
const { selectedLayerSelector, rangeSelector, selectedLayerDataRangeSelector} = require('../../selectors/timeline');
16-
const { selectPlaybackRange, changeSetting, toggleAnimationMode, animationStepMove } = require('../../actions/playback');
17-
const Message = require('../../components/I18N/Message');
18-
const { onRangeChanged } = require('../../actions/timeline');
19-
20-
21-
const Toolbar = require('../../components/misc/toolbar/Toolbar');
11+
const { createSelector } = require('reselect');
12+
const { compose, withState, withProps, withHandlers } = require('recompose');
13+
const {selectedLayerSelector} = require('../../selectors/timeline');
2214

23-
const PlaybackSettings = compose(
24-
connect(createSelector(
25-
playbackSettingsSelector,
26-
selectedLayerSelector,
27-
playbackRangeSelector,
28-
(settings, selectedLayer, playbackRange) => ({
29-
fixedStep: !selectedLayer,
30-
playbackRange,
31-
...settings
32-
})
33-
), {
34-
setPlaybackRange: selectPlaybackRange,
35-
onSettingChange: changeSetting,
36-
toggleAnimationMode
37-
}
15+
const { statusSelector, hasPrevNextAnimationSteps, playbackMetadataSelector } = require('../../selectors/playback');
16+
const { animationStepMove, STATUS } = require('../../actions/playback');
3817

39-
),
40-
// playback buttons
41-
compose(
42-
connect(createSelector(
43-
rangeSelector,
44-
selectedLayerDataRangeSelector,
45-
(viewRange, layerRange) => ({
46-
layerRange,
47-
viewRange
48-
})
49-
), {
50-
moveTo: onRangeChanged
51-
}),
52-
withHandlers({
53-
toggleAnimationRange: ({ fixedStep, layerRange, viewRange = {}, setPlaybackRange = () => { } }) => (enabled) => {
54-
let currentPlaybackRange = fixedStep ? viewRange : layerRange;
55-
// when view range is collapsed, nothing may be initialized yet, so by default 1 day before and after today
56-
currentPlaybackRange = {
57-
startPlaybackTime: moment(currentPlaybackRange && currentPlaybackRange.start || new Date()).subtract(1, 'days').toISOString(),
58-
endPlaybackTime: moment(currentPlaybackRange && currentPlaybackRange.end || new Date()).add(1, 'days').toISOString()
59-
};
60-
setPlaybackRange(enabled ? currentPlaybackRange : undefined);
61-
},
62-
setPlaybackToCurrentViewRange: ({ viewRange = {}, setPlaybackRange = () => { } }) => () => {
63-
if (viewRange.start && viewRange.end) {
64-
setPlaybackRange({
65-
startPlaybackTime: moment(viewRange.start).toISOString(),
66-
endPlaybackTime: moment(viewRange.end).toISOString()
67-
});
68-
}
69-
},
70-
setPlaybackToCurrentLayerDataRange: ({ setPlaybackRange = () => { }, layerRange }) => () => layerRange && setPlaybackRange({
71-
startPlaybackTime: layerRange.start,
72-
endPlaybackTime: layerRange.end
73-
})
74-
}),
75-
withProps(({ playbackRange, fixedStep, moveTo = () => { }, setPlaybackToCurrentViewRange = () => { }, setPlaybackToCurrentLayerDataRange = () => {} }) => {
76-
return {
77-
playbackButtons: [{
78-
glyph: "search",
79-
tooltipId: "playback.settings.range.zoomToCurrentPlayackRange",
80-
onClick: () => moveTo({start: playbackRange.startPlaybackTime, end: playbackRange.endPlaybackTime})
81-
}, {
82-
glyph: "resize-horizontal",
83-
tooltipId: "playback.settings.range.setToCurrentViewRange",
84-
onClick: () => setPlaybackToCurrentViewRange()
85-
}, {
86-
glyph: "1-layer",
87-
visible: !fixedStep,
88-
tooltipId: "playback.settings.range.fitToSelectedLayerRange",
89-
onClick: () => setPlaybackToCurrentLayerDataRange()
90-
}]
91-
};
92-
})
93-
)
9418

95-
)(
96-
require("../../components/playback/PlaybackSettings")
97-
);
19+
const Message = require('../../components/I18N/Message');
20+
const Toolbar = require('../../components/misc/toolbar/Toolbar');
9821

22+
const PlaybackSettings = require('./Settings');
9923

10024
/**
10125
* Support for expand/collapse timeline
@@ -111,11 +35,25 @@ const collapsible = compose(
11135
}))
11236
);
11337

38+
const playbackButtonsSelector = createSelector(
39+
statusSelector,
40+
selectedLayerSelector,
41+
playbackMetadataSelector,
42+
hasPrevNextAnimationSteps,
43+
(status, layer, metadata = {}, animationState) =>
44+
!layer
45+
? { hasNext: true, hasPrevious: true } // fixed step
46+
: status === STATUS.PLAY || status === STATUS.PAUSE
47+
? animationState // animation mode with guide layer
48+
: { hasNext: !!metadata.next, hasPrevious: !!metadata.previous} // normal mode with guide layer
49+
);
50+
51+
11452
/**
11553
* Implements playback buttons functionalities
11654
*/
11755
const playbackButtons = compose(
118-
connect(() => ({}), {
56+
connect(playbackButtonsSelector, {
11957
stepMove: animationStepMove
12058
}),
12159
withHandlers({
@@ -137,6 +75,8 @@ module.exports = playbackEnhancer(({
13775
backward = () => {},
13876
pause = () => {},
13977
stop = () => {},
78+
hasPrevious,
79+
hasNext,
14080
showSettings,
14181
onShowSettings = () => {}
14282
}) =>
@@ -151,6 +91,7 @@ module.exports = playbackEnhancer(({
15191
{
15292
glyph: "step-backward",
15393
onClick: backward,
94+
disabled: !hasPrevious,
15495
tooltip: <Message msgId={"playback.backwardStep"} />
15596
}, {
15697
glyph: status === statusMap.PLAY ? "pause" : "play",
@@ -168,6 +109,7 @@ module.exports = playbackEnhancer(({
168109
}, {
169110
glyph: "step-forward",
170111
onClick: forward,
112+
disabled: !hasNext,
171113
tooltip: <Message msgId={"playback.forwardStep"} />
172114
}, {
173115
glyph: "wrench",

0 commit comments

Comments
 (0)