Skip to content

Commit

Permalink
geosolutions-it#10002 Fix timeline snap issue (geosolutions-it#10003)
Browse files Browse the repository at this point in the history
  • Loading branch information
offtherailz authored Mar 1, 2024
1 parent d3d5fbc commit 352402a
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 26 deletions.
124 changes: 123 additions & 1 deletion web/client/epics/__tests__/timeline-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@


import expect from 'expect';
import MockAdapter from 'axios-mock-adapter';

import moment from 'moment';
import { testEpic, addTimeoutEpic, TEST_TIMEOUT } from './epicTestUtils';
import axios from '../../libs/ajax';


import {
setTimelineCurrentTime,
Expand Down Expand Up @@ -55,8 +58,9 @@ import { changeLayerProperties, removeNode } from '../../actions/layers';
import { SET_CURRENT_TIME, SET_OFFSET_TIME, updateLayerDimensionData } from '../../actions/dimension';
import { configureMap } from "../../actions/config";


describe('timeline Epics', () => {


it('setTimelineCurrentTime without selected layer', done => {
const t = new Date().toISOString();
testEpic(setTimelineCurrentTime, 1, selectTime(t), ([action]) => {
Expand Down Expand Up @@ -916,6 +920,14 @@ describe('timeline Epics', () => {
});
});
describe('Timeline GuideLayer', () => {
let mockAxios;
beforeEach(() => {
mockAxios = new MockAdapter(axios);

});
afterEach(() => {
mockAxios.restore();
});
const doAssertion = (NUM_ACTIONS, actions, layerId) => {
expect(actions.length).toBe(NUM_ACTIONS);
actions.map(action=>{
Expand Down Expand Up @@ -979,6 +991,116 @@ describe('timeline Epics', () => {
done();
}, {...STATE_TIMELINE, timeline: { settings: {autoSelect: true, showHiddenLayers: true}}});
});

// function to test time snap
function createDomainResponse(times) {
return `<?xml version="1.0" encoding="UTF-8"?><DomainValues xmlns="http://demo.geo-solutions.it/share/wmts-multidim/wmts_multi_dimensional.xsd" xmlns:ows="http://www.opengis.net/ows/1.1">
<ows:Identifier>time</ows:Identifier>
<Limit>2</Limit>
<Sort>asc</Sort>
<Domain>${times.join(',')}</Domain>
<Size>2</Size>
</DomainValues>`;
}
// utility function to test snapTimeGuideLayer
function testSnap({
currentTime,
prevResponseTimes,
nextResponseTimes,
expectedTime
}, done ) {

let ascCount = 0;
let descCount = 0;
mockAxios.onGet().reply(({ params }) => {
// expect 2 requests,1 asc,1 desc, with from value properly buffered to include the current time.
const {sort, fromValue } = params;
if (sort === 'asc') {
expect(fromValue < currentTime).toBeTruthy(); // check buffer is applied
ascCount++;
return [200, createDomainResponse(prevResponseTimes)];
} else if (sort === 'desc') {
expect(fromValue > currentTime).toBeTruthy(); // check buffer is applied
descCount++;
return [200, createDomainResponse(nextResponseTimes)];
}
done('unexpected request');
return [404, ''];

});
testEpic(snapTimeGuideLayer, NUM_ACTIONS, [selectLayer('TEST_LAYER')], ([action]) => {
expect(action.type).toBe(SET_CURRENT_TIME);
expect(action.time).toBe(expectedTime);
expect(ascCount).toBe(1);
expect(descCount).toBe(1);
done();
}, {
...STATE_TIMELINE,
timeline: {
selectedLayer: "TEST_LAYER",
range: {
start: "2000-01-01T00:00:00.000Z",
end: "2023-12-31T00:00:00.000Z"
},
settings: {autoSelect: true, showHiddenLayers: true}

},
dimension: {
currentTime: currentTime,
data: {
time: {
TEST_LAYER: {
name: "time",
domain: "2000-01-01T00:00:00.000Z--2027-12-31T00:00:00.000Z"
}
}
}
}
});
}
// this verifies that, when switch layer, if the current time is a valid value to snap, the current time remains the same (by triggering an update with same value)
it('snapTimeGuideLayer with selected layer, currentTime = time to snap', done => {
const currentTime = "2021-09-08T22:00:00.000Z";
testSnap({
currentTime,
prevResponseTimes: [currentTime],
nextResponseTimes: [currentTime],
expectedTime: currentTime
}, done);
});
it('snapTimeGuideLayer with selected layer, snapping to nearest value after', done => {
const currentTime = "2021-09-09T00:00:00.000Z";
const previousTime = "2021-09-08T22:00:00.000Z";
const nextTime = "2021-09-09T01:00:00.000Z"; // this should be selected because it is nearest to the current time
const expectedTime = nextTime;
try {
testSnap({
currentTime,
prevResponseTimes: [previousTime],
nextResponseTimes: [nextTime],
expectedTime
}, done);
} catch (e) {
done(e);
}
});
it('snapTimeGuideLayer with selected layer, snapping to nearest value before', done => {
const currentTime = "2021-09-08T00:00:00.000Z";
const previousTime = "2021-09-08T23:00:00.000Z"; // this should be selected because it is nearest to the current time
const nextTime = "2021-09-08T23:02:00.000Z";
const expectedTime = previousTime;
try {
testSnap({
currentTime,
prevResponseTimes: [previousTime],
nextResponseTimes: [nextTime],
expectedTime
}, done);
} catch (e) {
done(e);
}
});

it('snapTimeGuideLayer when autoselect disabled', done => {
testEpic(addTimeoutEpic(snapTimeGuideLayer, 500), NUM_ACTIONS, [selectLayer('TEST_LAYER')], ([action]) => {
expect(action.type).toBe(TEST_TIMEOUT); // Don't trigger a snap time when snap to guide layer settings is turned off
Expand Down
13 changes: 6 additions & 7 deletions web/client/epics/playback.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ import {
import { getDatesInRange } from '../utils/TimeUtils';
import pausable from '../observables/pausable';
import { wrapStartStop } from '../observables/epics';
import { getTimeDomainsObservable } from '../observables/multidim';
import { getNearestTimesObservable } from '../observables/multidim';
import { getDomainValues } from '../api/MultiDim';
import Rx from 'rxjs';
import { MAP_CONFIG_LOADED } from '../actions/config';
Expand All @@ -98,15 +98,14 @@ const domainArgs = (getState, paginationOptions = {}) => {
const layerUrl = selectedLayerUrl(getState());
const { startPlaybackTime, endPlaybackTime } = playbackRangeSelector(getState()) || {};
const shouldFilter = statusSelector(getState()) === STATUS.PLAY || statusSelector(getState()) === STATUS.PAUSE;
const bboxOptions = multidimOptionsSelectorCreator(id)(getState());
const fromEnd = snapTypeSelector(getState()) === 'end';
return [layerUrl, layerName, "time", {
limit: BUFFER_SIZE, // default, can be overridden by pagination options
time: startPlaybackTime && endPlaybackTime && shouldFilter ? toAbsoluteInterval(startPlaybackTime, endPlaybackTime) : undefined,
fromEnd,
...paginationOptions
},
multidimOptionsSelectorCreator(id)(getState())
];
}, bboxOptions ];
};

/**
Expand Down Expand Up @@ -194,7 +193,7 @@ const getAnimationFrames = (getState, options) => {
return domainValues.map(res => {
const domainsArray = res.DomainValues.Domain.split(",");
// if there is a selected layer check for time intervals (start/end)
// and filter-out domain dates falling outisde the start/end playBack time
// and filter-out domain dates falling outside the start/end playBack time
const selectedLayer = selectedLayerSelector(getState());
const x = selectedLayer ? getTimeIntervalDomains(getState, domainsArray) : domainsArray;
return x;
Expand Down Expand Up @@ -361,7 +360,7 @@ export const playbackCacheNextPreviousTimes = (action$, { getState = () => { } }
// get current time in case of SELECT_LAYER or INIT_SELECT_LAYER
const time = actionTime || currentTimeSelector(getState());
const snapType = snapTypeSelector(getState());
return getTimeDomainsObservable(domainArgs, false, getState, snapType, time).map(([next, previous]) => {
return getNearestTimesObservable(domainArgs, false, getState, snapType, time).map(([next, previous]) => {
return updateMetadata({
forTime: time,
next,
Expand All @@ -381,7 +380,7 @@ export const setIsIntervalData = (action$, { getState = () => { } } = {}) =>
.switchMap(({time: actionTime}) => {
const time = actionTime || currentTimeSelector(getState());
const snapType = snapTypeSelector(getState());
return getTimeDomainsObservable(domainArgs, true, getState, snapType, time)
return getNearestTimesObservable(domainArgs, true, getState, snapType, time)
.map(([next, previous]) => {
const isTimeIntervalData = next.indexOf('/') !== -1 || previous.indexOf('/') !== -1;
return setIntervalData(isTimeIntervalData);
Expand Down
6 changes: 3 additions & 3 deletions web/client/epics/timeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ import {

import { getNearestDate, roundRangeResolution, isTimeDomainInterval, getStartEndDomainValues } from '../utils/TimeUtils';
import { getHistogram, describeDomains } from '../api/MultiDim';
import { getTimeDomainsObservable } from '../observables/multidim';
import { getNearestTimesObservable } from '../observables/multidim';
import { MAP_CONFIG_LOADED } from '../actions/config';

const TIME_DIMENSION = "time";
Expand Down Expand Up @@ -98,7 +98,7 @@ const snapTime = (getState, group, time) => {
const state = getState();
if (selectedLayerName(state)) {
const snapType = snapTypeSelector(state);
return getTimeDomainsObservable(domainArgs, false, getState, snapType, time).map(values => {
return getNearestTimesObservable(domainArgs, true, getState, snapType, time).map(values => {
return getNearestDate(values.filter(v => !!v), time, snapType) || time;
});
}
Expand Down Expand Up @@ -474,7 +474,7 @@ export const setRangeOnInit = (action$, { getState = () => { } } = {}) =>
};
const { domain } = layerDimensionDataSelectorCreator(layerId, "time")(state) || {};
const currentTime = currentTimeSelector(state);
const getTimeDomain = (time) => getTimeDomainsObservable(domainArgs, false, getState, snapType, time);
const getTimeDomain = (time) => getNearestTimesObservable(domainArgs, false, getState, snapType, time);
const updateRangeObs = (time) => getTimeDomain().switchMap((values) => updateRangeOnInit(rangeState, values, time));

if (!isEmpty(domain) && !isEmpty(currentTime)) {
Expand Down
29 changes: 24 additions & 5 deletions web/client/observables/__tests__/multidim-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import {playbackRangeSelector, statusSelector} from "../../selectors/playback";
import {STATUS} from "../../actions/playback";
import DOMAIN_INTERVAL_VALUES_RESPONSE from 'raw-loader!../../test-resources/wmts/DomainIntervalValues.xml';
import {getTimeDomainsObservable} from "../multidim";
import {getNearestTimesObservable} from "../multidim";
import {currentTimeSelector} from "../../selectors/dimension";
import expect from "expect";

Expand Down Expand Up @@ -78,13 +78,32 @@ describe('multidim Observables', () => {
};


it('test getTimeDomainsObservable', (done) => {
mock.onGet('MOCK_DOMAIN_VALUES').reply(200, DOMAIN_INTERVAL_VALUES_RESPONSE);
const time = currentTimeSelector(() => ANIMATION_MOCK_STATE);
it('test getNearestTimesObservable', (done) => {
const time = currentTimeSelector(ANIMATION_MOCK_STATE);
let ascCount = 0;
let descCount = 0;
mock.onGet('MOCK_DOMAIN_VALUES').reply(({params}) => {
expect(params).toExist();
expect(params.limit).toBe(1);
const {sort, fromValue } = params;
if (sort === 'asc') {
expect(fromValue < time).toBeTruthy(); // check buffer is applied
ascCount++;
return [200, DOMAIN_INTERVAL_VALUES_RESPONSE];
} else if (sort === 'desc') {
expect(fromValue > time).toBeTruthy(); // check buffer is applied
descCount++;
return [200, DOMAIN_INTERVAL_VALUES_RESPONSE];
}
return [200, DOMAIN_INTERVAL_VALUES_RESPONSE];
});

const snapType = snapTypeSelector(() => ANIMATION_MOCK_STATE);
getTimeDomainsObservable(domainArgs, true, () => ANIMATION_MOCK_STATE, snapType, time).subscribe(args => {
getNearestTimesObservable(domainArgs, true, () => ANIMATION_MOCK_STATE, snapType, time).subscribe(args => {
expect(args[0]).toBe('2021-09-08T22:00:00.000Z/2021-10-21T22:00:00.000Z');
expect(args[1]).toBe('2021-09-08T22:00:00.000Z/2021-10-21T22:00:00.000Z');
expect(ascCount).toBe(1); // asc should be called only once
expect(descCount).toBe(1); // desc should be called only once
done();
});
});
Expand Down
16 changes: 8 additions & 8 deletions web/client/observables/multidim.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,21 @@ import { getDomainValues } from '../api/MultiDim';
import { getBufferedTime } from '../utils/TimeUtils';

/**
* Creates an observable that gets the nearest time domains for the selected layer in timeline redux state.
* **note**: this function should be moved in utils and refactored to not take domainArgs and state,but only the arguments to send and override.
* @param {Function} domainArgs function that returns an object with the settings and params for the getDomainValues request
* @param {Boolean} useBuffer if set to true applies or removes 1 millisecond buffer to current time
* @param {Function} getState function that returns the current state
* @param {String} snapType in case of time intervals whether snapping is set to "start" or "end"
* @param {String} time the submitted time to claculate the domains for
* @returns {[String, String]} [previous, next] next and previous time domains of the submitted reference time
* @param {String} time the submitted time to calculate the domains for
* @returns {Observable<[String, String]>} an observable that emits an array with the nearest time values in domain.
*/
export const getTimeDomainsObservable = (domainArgs, useBuffer, getState, snapType, time) => (
// TODO: find out a way to optimize and do only one request
export const getNearestTimesObservable = (domainArgs, useBuffer, getState, snapType, time) =>
Rx.Observable.forkJoin(
getDomainValues(...domainArgs(getState, { sort: "asc", ...(time && {fromValue: useBuffer ? getBufferedTime(time, 0.0001, 'remove') : time}), ...(snapType === 'end' ? {fromEnd: true} : {}) }))
getDomainValues(...domainArgs(getState, { sort: "asc", limit: 1, ...(time && {fromValue: useBuffer ? getBufferedTime(time, 0.001, 'remove') : time}), ...(snapType === 'end' ? {fromEnd: true} : {}) }))
.map(res => res.DomainValues.Domain.split(","))
.map(([tt]) => tt).catch(err => err && Rx.Observable.of(null)),
getDomainValues(...domainArgs(getState, { sort: "desc", ...(time && {fromValue: useBuffer ? getBufferedTime(time, 0.0001, 'add') : time}), ...(snapType === 'end' ? {fromEnd: true} : {}) }))
getDomainValues(...domainArgs(getState, { sort: "desc", limit: 1, ...(time && {fromValue: useBuffer ? getBufferedTime(time, 0.001, 'add') : time}), ...(snapType === 'end' ? {fromEnd: true} : {}) }))
.map(res => res.DomainValues.Domain.split(","))
.map(([tt]) => tt).catch(err => err && Rx.Observable.of(null))
)
);
);
4 changes: 2 additions & 2 deletions web/client/selectors/timeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ export const loadingSelector = state => get(state, "timeline.loading");
export const selectedLayerSelector = state => get(state, "timeline.selectedLayer");

export const selectedLayerData = state => getLayerFromId(state, selectedLayerSelector(state));
export const selectedLayerName = state => selectedLayerData(state) && selectedLayerData(state).name;
export const selectedLayerTimeDimensionConfiguration = state => selectedLayerData(state) && selectedLayerData(state).dimensions && head(selectedLayerData(state).dimensions.filter((x) => x.name === "time"));
export const selectedLayerName = state => selectedLayerData(state)?.name;
export const selectedLayerTimeDimensionConfiguration = state => selectedLayerData(state)?.dimensions && head(selectedLayerData(state).dimensions.filter((x) => x.name === "time"));
export const selectedLayerUrl = state => get(selectedLayerTimeDimensionConfiguration(state), "source.url");

export const currentTimeRangeSelector = createSelector(
Expand Down

0 comments on commit 352402a

Please sign in to comment.