Skip to content

Commit aeb28d5

Browse files
authored
Fix #3642 Switch between widgets and timeline (#3698)
1 parent e713958 commit aeb28d5

25 files changed

Lines changed: 925 additions & 149 deletions

web/client/actions/__tests__/timeline-test.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ const {
1717
LOADING,
1818
timeDataLoading,
1919
ENABLE_OFFSET,
20-
enableOffset
20+
enableOffset,
21+
SET_COLLAPSED,
22+
setCollapsed
2123
} = require('../timeline');
2224

2325
describe('timeline actions', () => {
@@ -47,4 +49,10 @@ describe('timeline actions', () => {
4749
expect(retval.type).toBe(ENABLE_OFFSET);
4850
expect(retval.enabled).toBe(true);
4951
});
52+
it('setCollapsed', () => {
53+
const retval = setCollapsed(true);
54+
expect(retval).toExist();
55+
expect(retval.type).toBe(SET_COLLAPSED);
56+
expect(retval.collapsed).toBe(true);
57+
});
5058
});

web/client/actions/timeline.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ const ENABLE_OFFSET = "TIMELINE:ENABLE_OFFSET";
6363
*/
6464
const enableOffset = enabled => ({ type: ENABLE_OFFSET, enabled});
6565

66+
const SET_COLLAPSED = "TIMELINE:SET_COLLAPSED";
67+
const setCollapsed = collapsed => ({ type: SET_COLLAPSED, collapsed});
6668
/**
6769
* Actions for timeline
6870
* @module actions.timeline
@@ -79,5 +81,7 @@ module.exports = {
7981
SELECT_LAYER,
8082
selectLayer,
8183
ENABLE_OFFSET,
82-
enableOffset
84+
enableOffset,
85+
SET_COLLAPSED,
86+
setCollapsed
8387
};
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
/*
2+
* Copyright 2019, GeoSolutions Sas.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
const expect = require('expect');
9+
const { testEpic, TEST_TIMEOUT, addTimeoutEpic } = require('./epicTestUtils');
10+
const { configureMap } = require('../../actions/config');
11+
12+
const { updateLayerDimensionData } = require('../../actions/dimension');
13+
const { changeLayerProperties } = require('../../actions/layers');
14+
const { SHOW_NOTIFICATION } = require('../../actions/notifications');
15+
const { rangeDataLoaded, setCollapsed, SET_COLLAPSED } = require('../../actions/timeline');
16+
const { deleteWidget, insertWidget, toggleCollapseAll, updateWidgetProperty, TOGGLE_COLLAPSE_ALL } = require('../../actions/widgets');
17+
18+
const timeline = require('../../reducers/timeline');
19+
const dimension = require('../../reducers/dimension');
20+
const widgets = require('../../reducers/widgets');
21+
// TEST DATA
22+
const T1 = '2016-01-01T00:00:00.000Z';
23+
const T2 = '2016-02-01T00:00:00.000Z';
24+
const TEST_LAYER_ID = "TEST_LAYER";
25+
const SAMPLE_RANGE = {
26+
start: T1,
27+
end: T2
28+
};
29+
30+
const TIME_DIMENSION_DATA = {
31+
source: {
32+
type: "multidim-extension",
33+
url: "FAKE"
34+
},
35+
name: "time",
36+
dimension: `${T1}--${T2}`
37+
};
38+
39+
// sample timeline state with histogram
40+
const TIMELINE_STATE_VALUES = timeline(undefined, rangeDataLoaded(
41+
TEST_LAYER_ID,
42+
SAMPLE_RANGE,
43+
// sample with daily histogram of values 1,2,3,..., 31
44+
{
45+
values: Array(31).fill().map((x, i) => i),
46+
domain: `${T1}/${T2}/1D`
47+
},
48+
{ values: [T1] }
49+
));
50+
51+
// sample dimension state for TEST_LAYER
52+
const DIMENSION_STATE = dimension(undefined, updateLayerDimensionData(TEST_LAYER_ID, "time", TIME_DIMENSION_DATA));
53+
54+
const LAYERS_NO_TIME_STATE = {
55+
flat: [{
56+
id: TEST_LAYER_ID
57+
}]
58+
};
59+
60+
const LAYERS_WITH_TIME = {
61+
flat: [{
62+
id: TEST_LAYER_ID,
63+
dimensions: [TIME_DIMENSION_DATA]
64+
}]
65+
};
66+
67+
// SAMPLE STATE WITH TIMELINE SHOWING
68+
const SAMPLE_TIMELINE_STATE = {
69+
layers: LAYERS_WITH_TIME,
70+
dimension: DIMENSION_STATE,
71+
timeline: TIMELINE_STATE_VALUES
72+
};
73+
// SAMPLE STATE WITH TIMELINE COLLAPSED
74+
const SAMPLE_COLLAPSED_TIMELINE_STATE = {
75+
...SAMPLE_TIMELINE_STATE,
76+
timeline: timeline(SAMPLE_TIMELINE_STATE, setCollapsed(true))
77+
};
78+
// SAMPLE STATE WITH NO TIMELINE (NO TIME DATA)
79+
const NO_TIMELINE_STATE = {
80+
layers: LAYERS_NO_TIME_STATE,
81+
dimension: DIMENSION_STATE,
82+
timeline: TIMELINE_STATE_VALUES
83+
};
84+
const WIDGETS_STATE = {
85+
widgets: widgets(undefined, insertWidget({id: "TEST"}))
86+
};
87+
const NO_WIDGETS_STATE = {
88+
widgets: widgets(undefined, { type: "DUMMY" })
89+
};
90+
91+
// EPICS TO TEST
92+
const {
93+
collapseTimelineOnWidgetsEvents,
94+
collapseWidgetsOnTimelineEvents,
95+
expandTimelineIfCollapsedOnTrayUnmount
96+
} = require('../widgetsTray');
97+
98+
99+
describe('widgetsTray epics', () => {
100+
describe('collapseTimelineOnWidgetsEvents', () => {
101+
it('collapse timeline on widget add', done => {
102+
testEpic(collapseTimelineOnWidgetsEvents, 2, [insertWidget({ id: "TEST" })], ([a0, a1]) => {
103+
expect(a0.type).toBe(SHOW_NOTIFICATION);
104+
expect(a1.type).toBe(SET_COLLAPSED);
105+
expect(a1.collapsed).toBe(true);
106+
done();
107+
}, {
108+
...SAMPLE_TIMELINE_STATE,
109+
...WIDGETS_STATE
110+
});
111+
});
112+
it('collapse timeline on expand', done => {
113+
testEpic(collapseTimelineOnWidgetsEvents, 2, [toggleCollapseAll()], ([a0, a1]) => {
114+
expect(a0.type).toBe(SHOW_NOTIFICATION);
115+
expect(a1.type).toBe(SET_COLLAPSED);
116+
expect(a1.collapsed).toBe(true);
117+
done();
118+
}, {
119+
...SAMPLE_TIMELINE_STATE,
120+
...WIDGETS_STATE
121+
});
122+
});
123+
it('notification triggered once', done => {
124+
testEpic(addTimeoutEpic(collapseTimelineOnWidgetsEvents, 10), 4, [toggleCollapseAll(), toggleCollapseAll()], ([a0, a1, a2, a3]) => {
125+
expect(a0.type).toBe(SHOW_NOTIFICATION);
126+
expect(a1.type).toBe(SET_COLLAPSED);
127+
expect(a1.collapsed).toBe(true);
128+
expect(a2.type).toBe(SET_COLLAPSED);
129+
expect(a2.collapsed).toBe(true);
130+
expect(a3.type).toBe(TEST_TIMEOUT);
131+
done();
132+
}, {
133+
...SAMPLE_TIMELINE_STATE,
134+
...WIDGETS_STATE
135+
});
136+
});
137+
it('timeline not collapsed if widgets are not on map', done => {
138+
testEpic(addTimeoutEpic(collapseTimelineOnWidgetsEvents, 10), 1, [toggleCollapseAll()], ([a0]) => {
139+
expect(a0.type).toBe(TEST_TIMEOUT);
140+
done();
141+
}, {
142+
...SAMPLE_TIMELINE_STATE,
143+
...NO_WIDGETS_STATE
144+
});
145+
});
146+
it('timeline if not collapsed if are all static (pinned)', done => {
147+
testEpic(addTimeoutEpic(collapseTimelineOnWidgetsEvents, 10), 1, [updateWidgetProperty("TEST", "dataGrid.static", true)], ([a0]) => {
148+
expect(a0.type).toBe(TEST_TIMEOUT);
149+
done();
150+
}, {
151+
...SAMPLE_TIMELINE_STATE,
152+
widgets: widgets(WIDGETS_STATE.widgets, updateWidgetProperty("TEST", "dataGrid.static", true))
153+
});
154+
});
155+
});
156+
describe('collapseWidgetsOnTimelineEvents', () => {
157+
it('collapse widgets on timeline expand', done => {
158+
testEpic(collapseWidgetsOnTimelineEvents, 2, [setCollapsed(false)], ([a0, a1]) => {
159+
expect(a0.type).toBe(SHOW_NOTIFICATION);
160+
expect(a1.type).toBe(TOGGLE_COLLAPSE_ALL);
161+
done();
162+
}, {
163+
...SAMPLE_TIMELINE_STATE,
164+
...WIDGETS_STATE
165+
});
166+
});
167+
it('collapse widgets on timeline layer dimension set', done => { // AKA new layer with time dimension
168+
testEpic(collapseWidgetsOnTimelineEvents, 2, [changeLayerProperties("TEST", {dimensions: [{name: "time"}]})], ([a0, a1]) => {
169+
expect(a0.type).toBe(SHOW_NOTIFICATION);
170+
expect(a1.type).toBe(TOGGLE_COLLAPSE_ALL);
171+
done();
172+
}, {
173+
...SAMPLE_TIMELINE_STATE,
174+
...WIDGETS_STATE
175+
});
176+
});
177+
it('notification triggered once', done => {
178+
testEpic(addTimeoutEpic(collapseWidgetsOnTimelineEvents, 10), 4, [setCollapsed(false), setCollapsed(false)], ([a0, a1, a2, a3]) => {
179+
expect(a0.type).toBe(SHOW_NOTIFICATION);
180+
expect(a1.type).toBe(TOGGLE_COLLAPSE_ALL);
181+
expect(a2.type).toBe(TOGGLE_COLLAPSE_ALL);
182+
expect(a3.type).toBe(TEST_TIMEOUT);
183+
done();
184+
}, {
185+
...SAMPLE_TIMELINE_STATE,
186+
...WIDGETS_STATE
187+
});
188+
});
189+
190+
it('check widgets not collapsed if timeline is not present', done => {
191+
testEpic(addTimeoutEpic(collapseWidgetsOnTimelineEvents, 10), 1, [setCollapsed(false)], ([a0]) => {
192+
expect(a0.type).toBe(TEST_TIMEOUT);
193+
done();
194+
}, {
195+
...NO_TIMELINE_STATE,
196+
...WIDGETS_STATE
197+
});
198+
});
199+
it('check not trigger collapse if only pinned widgets', done => {
200+
testEpic(addTimeoutEpic(collapseWidgetsOnTimelineEvents, 10), 1, [setCollapsed(false)], ([a0]) => {
201+
expect(a0.type).toBe(TEST_TIMEOUT);
202+
done();
203+
}, {
204+
...SAMPLE_TIMELINE_STATE,
205+
widgets: widgets(WIDGETS_STATE.widgets, updateWidgetProperty("TEST", "dataGrid.static", true))
206+
});
207+
});
208+
});
209+
describe('expandTimelineIfCollapsedOnTrayUnmount', () => {
210+
/*
211+
* when widgets are not present or all static, the WidgetsTray is not visible anymore.
212+
* So the timeline have to be expanded or it will not be visible anymore
213+
*/
214+
it('timeline expanded if are widgets become are static', done => {
215+
testEpic(expandTimelineIfCollapsedOnTrayUnmount, 1, [updateWidgetProperty("TEST", "dataGrid.static", true)], ([a0]) => {
216+
expect(a0.type).toBe(SET_COLLAPSED);
217+
expect(a0.collapsed).toBe(false);
218+
done();
219+
}, {
220+
...SAMPLE_COLLAPSED_TIMELINE_STATE,
221+
widgets: widgets(WIDGETS_STATE.widgets, updateWidgetProperty("TEST", "dataGrid.static", true))
222+
});
223+
});
224+
it('timeline expanded if no widgets anymore', done => {
225+
testEpic(expandTimelineIfCollapsedOnTrayUnmount, 1, [deleteWidget("TEST")], ([a0]) => {
226+
expect(a0.type).toBe(SET_COLLAPSED);
227+
expect(a0.collapsed).toBe(false);
228+
done();
229+
}, {
230+
...SAMPLE_COLLAPSED_TIMELINE_STATE,
231+
NO_WIDGETS_STATE
232+
});
233+
});
234+
it('timeline expanded on map config loaded, if collapsed but it should be present and no widgets are on map', done => {
235+
testEpic(expandTimelineIfCollapsedOnTrayUnmount, 1, [configureMap("TEST")], ([a0]) => {
236+
expect(a0.type).toBe(SET_COLLAPSED);
237+
expect(a0.collapsed).toBe(false);
238+
done();
239+
}, {
240+
...SAMPLE_COLLAPSED_TIMELINE_STATE,
241+
NO_WIDGETS_STATE
242+
});
243+
});
244+
it('no effect if timeline is not collapsed', done => {
245+
testEpic(addTimeoutEpic(expandTimelineIfCollapsedOnTrayUnmount, 10), 1, [configureMap("TEST")], ([a0]) => {
246+
expect(a0.type).toBe(TEST_TIMEOUT);
247+
done();
248+
}, {
249+
...SAMPLE_TIMELINE_STATE, // collapsed: false
250+
NO_WIDGETS_STATE
251+
});
252+
});
253+
254+
});
255+
});

web/client/epics/playback.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const { LOCATION_CHANGE } = require('react-router-redux');
3232

3333
const { currentFrameSelector, currentFrameValueSelector, lastFrameSelector, playbackRangeSelector, playbackSettingsSelector, frameDurationSelector, statusSelector, playbackMetadataSelector } = require('../selectors/playback');
3434

35-
const { selectedLayerName, selectedLayerUrl, selectedLayerData, selectedLayerTimeDimensionConfiguration, rangeSelector, selectedLayerSelector } = require('../selectors/timeline');
35+
const { selectedLayerName, selectedLayerUrl, selectedLayerData, selectedLayerTimeDimensionConfiguration, rangeSelector, selectedLayerSelector, timelineLayersSelector } = require('../selectors/timeline');
3636

3737
const pausable = require('../observables/pausable');
3838
const { wrapStartStop } = require('../observables/epics');
@@ -249,7 +249,7 @@ module.exports = {
249249
// need to select first
250250
: Rx.Observable.of(
251251
selectLayer(
252-
get(layersWithTimeDataSelector(getState()), "[0].id")
252+
get(timelineLayersSelector(getState()), "[0].id")
253253
)
254254
)
255255
),

web/client/epics/timeline.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ const {REMOVE_NODE} = require('../actions/layers');
1010
const {error} = require('../actions/notifications');
1111

1212
const {getLayerFromId} = require('../selectors/layers');
13-
const { rangeSelector, selectedLayerName, selectedLayerUrl, isAutoSelectEnabled, selectedLayerSelector } = require('../selectors/timeline');
14-
const { layerTimeSequenceSelectorCreator, timeDataSelector, offsetTimeSelector, currentTimeSelector, layersWithTimeDataSelector } = require('../selectors/dimension');
13+
const { rangeSelector, selectedLayerName, selectedLayerUrl, isAutoSelectEnabled, selectedLayerSelector, timelineLayersSelector } = require('../selectors/timeline');
14+
const { layerTimeSequenceSelectorCreator, timeDataSelector, offsetTimeSelector, currentTimeSelector } = require('../selectors/dimension');
1515

1616
const { getNearestDate, roundRangeResolution, isTimeDomainInterval } = require('../utils/TimeUtils');
1717
const { getHistogram, describeDomains, getDomainValues } = require('../api/MultiDim');
@@ -128,8 +128,12 @@ const loadRangeData = (id, timeData, getState) => {
128128
}
129129

130130
const domainValues = domain && domain.indexOf('--') < 0 && domain.split(',');
131-
// const total = values.reduce((a, b) => a + b, 0);
132131

132+
/*
133+
* shape of range: {start: "T_START", end: "T_END"}
134+
* shape of histogram {values: [1, 2, 3], domain: "T_START/T_END/RESOLUTION" }
135+
* shape of domain: {values: ["T1", "T2", ....]}, present only if not in the form "T1--T2"
136+
*/
133137
return Rx.Observable.of({
134138
range,
135139
histogram: histogram && histogram.Domain
@@ -188,18 +192,18 @@ module.exports = {
188192
setupTimelineExistingSettings: (action$, { getState = () => { } } = {}) => action$.ofType(REMOVE_NODE, UPDATE_LAYER_DIMENSION_DATA)
189193
.exhaustMap(() =>
190194
isAutoSelectEnabled(getState())
191-
&& get(layersWithTimeDataSelector(getState()), "[0].id")
195+
&& get(timelineLayersSelector(getState()), "[0].id")
192196
&& !selectedLayerSelector(getState())
193-
? Rx.Observable.of(selectLayer(get(layersWithTimeDataSelector(getState()), "[0].id")))
197+
? Rx.Observable.of(selectLayer(get(timelineLayersSelector(getState()), "[0].id")))
194198
.concat(
195199
Rx.Observable.of(1).switchMap( () =>
196-
snapTime(getState(), get(layersWithTimeDataSelector(getState()), "[0].id"), currentTimeSelector(getState) || new Date().toISOString())
200+
snapTime(getState(), get(timelineLayersSelector(getState()), "[0].id"), currentTimeSelector(getState) || new Date().toISOString())
197201
.filter( v => v)
198202
.map(time => setCurrentTime(time)))
199203
)
200204
: Rx.Observable.empty()
201205
),
202-
/**
206+
/**
203207
* When offset is initiated this epic sets both initial current time and offset if any does not exist
204208
* The policy is:
205209
* - if current time is not defined, it will be placed to the center of the current timeline's viewport. If the viewport is undefined it is set to "now"

0 commit comments

Comments
 (0)