mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-01 20:39:18 +00:00
block end calculation (#1683)
This commit is contained in:
committed by
Carlos Valente
parent
e811a82be2
commit
e9dda817e5
@@ -174,6 +174,7 @@ export const useRuntimePlaybackOverview = createSelector((state: RuntimeStore) =
|
|||||||
offset: state.runtime.offsetMode === OffsetMode.Absolute ? state.runtime.offset : state.runtime.relativeOffset,
|
offset: state.runtime.offsetMode === OffsetMode.Absolute ? state.runtime.offset : state.runtime.relativeOffset,
|
||||||
|
|
||||||
blockStartedAt: state.blockNow?.startedAt ?? null,
|
blockStartedAt: state.blockNow?.startedAt ?? null,
|
||||||
|
blockExpectedEnd: state.blockNow?.expectedEnd ?? null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const useTimelineStatus = createSelector((state: RuntimeStore) => ({
|
export const useTimelineStatus = createSelector((state: RuntimeStore) => ({
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import { OffsetMode } from 'ontime-types';
|
import { formatTime, nowInMillis } from '../time';
|
||||||
import { dayInMs } from 'ontime-utils';
|
|
||||||
|
|
||||||
import { calculateTimeUntilStart, formatTime, nowInMillis } from '../time';
|
|
||||||
|
|
||||||
describe('nowInMillis()', () => {
|
describe('nowInMillis()', () => {
|
||||||
it('should return the current time in milliseconds', () => {
|
it('should return the current time in milliseconds', () => {
|
||||||
@@ -41,257 +38,3 @@ describe('formatTime()', () => {
|
|||||||
expect(time).toStrictEqual('-01:00');
|
expect(time).toStrictEqual('-01:00');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('calculateTimeUntilStart()', () => {
|
|
||||||
describe('Absolute offset mode', () => {
|
|
||||||
test('ontime', () => {
|
|
||||||
const test = {
|
|
||||||
timeStart: 100,
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
totalGap: 0,
|
|
||||||
clock: 90,
|
|
||||||
offset: 0,
|
|
||||||
offsetMode: OffsetMode.Absolute,
|
|
||||||
actualStart: null,
|
|
||||||
plannedStart: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(10);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('running behind', () => {
|
|
||||||
const test = {
|
|
||||||
timeStart: 100,
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
totalGap: 0,
|
|
||||||
clock: 90,
|
|
||||||
offset: -20,
|
|
||||||
offsetMode: OffsetMode.Absolute,
|
|
||||||
actualStart: null,
|
|
||||||
plannedStart: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(30);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(30);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('running ahead', () => {
|
|
||||||
const test = {
|
|
||||||
timeStart: 100,
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
totalGap: 0,
|
|
||||||
clock: 80,
|
|
||||||
offset: 10,
|
|
||||||
offsetMode: OffsetMode.Absolute,
|
|
||||||
actualStart: null,
|
|
||||||
plannedStart: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20); // <-- when running ahead the unlinked timer stays put
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('running behind with enough gaps', () => {
|
|
||||||
const test = {
|
|
||||||
timeStart: 100,
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
totalGap: 20,
|
|
||||||
clock: 50,
|
|
||||||
offset: -20,
|
|
||||||
offsetMode: OffsetMode.Absolute,
|
|
||||||
actualStart: null,
|
|
||||||
plannedStart: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(50); // <-- when gap is enough to compensate for the running behind
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(70); // This should not be possible
|
|
||||||
});
|
|
||||||
|
|
||||||
test('running behind with too little gaps', () => {
|
|
||||||
const test = {
|
|
||||||
timeStart: 100,
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
totalGap: 10,
|
|
||||||
clock: 50,
|
|
||||||
offset: -20,
|
|
||||||
offsetMode: OffsetMode.Absolute,
|
|
||||||
actualStart: 0,
|
|
||||||
plannedStart: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(60); // <-- when gap is not enough to compensate for the running behind it absorbs at much as possible
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(70); // This should not be possible
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Relative offset mode', () => {
|
|
||||||
test('basic function', () => {
|
|
||||||
const test = {
|
|
||||||
timeStart: 0,
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
totalGap: 0,
|
|
||||||
clock: 100,
|
|
||||||
actualStart: 100,
|
|
||||||
plannedStart: 0,
|
|
||||||
offset: 0,
|
|
||||||
offsetMode: OffsetMode.Relative,
|
|
||||||
};
|
|
||||||
|
|
||||||
const timeStartEvent2 = 10;
|
|
||||||
const timeStartEvent3 = 20;
|
|
||||||
|
|
||||||
//event 1 is the currently running event
|
|
||||||
|
|
||||||
//event 2
|
|
||||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: true })).toBe(10);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: false })).toBe(10);
|
|
||||||
|
|
||||||
//event 3
|
|
||||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: true })).toBe(20);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: false })).toBe(20);
|
|
||||||
|
|
||||||
// When clock advances by 5ms, time until start should decrease by 5ms
|
|
||||||
test.clock = 105;
|
|
||||||
|
|
||||||
//event 2
|
|
||||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: true })).toBe(5);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: false })).toBe(5);
|
|
||||||
|
|
||||||
//event 3
|
|
||||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: true })).toBe(15);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: false })).toBe(15);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('gaps', () => {
|
|
||||||
const test = {
|
|
||||||
timeStart: 20,
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
totalGap: 10,
|
|
||||||
clock: 100,
|
|
||||||
actualStart: 100,
|
|
||||||
plannedStart: 0,
|
|
||||||
offset: 0,
|
|
||||||
offsetMode: OffsetMode.Relative,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(20);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20);
|
|
||||||
|
|
||||||
// When clock advances by 5ms, time until start should decrease by 5ms
|
|
||||||
test.clock = 105;
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(15);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(15);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('added/remove time', () => {
|
|
||||||
const test = {
|
|
||||||
timeStart: 20,
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
totalGap: 0,
|
|
||||||
clock: 100,
|
|
||||||
actualStart: 100,
|
|
||||||
plannedStart: 0,
|
|
||||||
offset: 0,
|
|
||||||
offsetMode: OffsetMode.Relative,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(20);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20);
|
|
||||||
|
|
||||||
test.offset = 5; // remove 5 with addtime - we are ahead of time
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(15);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20); // unlocked evets will stay on schedule
|
|
||||||
|
|
||||||
test.offset = -5; // add 5 with addtime - we are behind
|
|
||||||
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(25);
|
|
||||||
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(25);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('next day', () => {
|
|
||||||
const test = {
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
clock: 100,
|
|
||||||
actualStart: 100,
|
|
||||||
plannedStart: 0,
|
|
||||||
offset: 0,
|
|
||||||
offsetMode: OffsetMode.Relative,
|
|
||||||
};
|
|
||||||
|
|
||||||
// this event will start the current day
|
|
||||||
expect(
|
|
||||||
calculateTimeUntilStart({
|
|
||||||
...test,
|
|
||||||
timeStart: 10,
|
|
||||||
dayOffset: 0,
|
|
||||||
totalGap: 0,
|
|
||||||
isLinkedToLoaded: false,
|
|
||||||
}),
|
|
||||||
).toBe(10);
|
|
||||||
|
|
||||||
// this event will start the next day
|
|
||||||
// in absolute mode this would start in dayInMs - 100 since the gap would compensate
|
|
||||||
// but in relative mode with and actual start that is 100 offset it starts in dayInMs
|
|
||||||
expect(
|
|
||||||
calculateTimeUntilStart({
|
|
||||||
...test,
|
|
||||||
timeStart: 0,
|
|
||||||
dayOffset: 1,
|
|
||||||
totalGap: dayInMs - 20,
|
|
||||||
isLinkedToLoaded: false,
|
|
||||||
}),
|
|
||||||
).toBe(dayInMs);
|
|
||||||
|
|
||||||
// advancing 100ms
|
|
||||||
test.clock = 200;
|
|
||||||
|
|
||||||
expect(
|
|
||||||
calculateTimeUntilStart({
|
|
||||||
...test,
|
|
||||||
timeStart: 0,
|
|
||||||
dayOffset: 1,
|
|
||||||
totalGap: dayInMs - 20,
|
|
||||||
isLinkedToLoaded: false,
|
|
||||||
}),
|
|
||||||
).toBe(dayInMs - 100);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('overlap with negative total gap', () => {
|
|
||||||
const test = {
|
|
||||||
dayOffset: 0,
|
|
||||||
delay: 0,
|
|
||||||
currentDay: 0,
|
|
||||||
clock: 100,
|
|
||||||
actualStart: 100,
|
|
||||||
plannedStart: 0,
|
|
||||||
offset: 0,
|
|
||||||
offsetMode: OffsetMode.Relative,
|
|
||||||
isLinkedToLoaded: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// the overlap will be pushed out to the expected available time
|
|
||||||
expect(calculateTimeUntilStart({ ...test, timeStart: 5, totalGap: -5 })).toBe(10);
|
|
||||||
|
|
||||||
test.clock = 105;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { MaybeNumber, OffsetMode, OntimeEvent, Settings, TimeFormat } from 'ontime-types';
|
import { MaybeNumber, OntimeEvent, Settings, TimeFormat } from 'ontime-types';
|
||||||
import { dayInMs, formatFromMillis, MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils';
|
import {
|
||||||
|
calculateTimeUntilStart,
|
||||||
|
formatFromMillis,
|
||||||
|
MILLIS_PER_HOUR,
|
||||||
|
MILLIS_PER_MINUTE,
|
||||||
|
MILLIS_PER_SECOND,
|
||||||
|
} from 'ontime-utils';
|
||||||
|
|
||||||
import { FORMAT_12, FORMAT_24 } from '../../viewerConfig';
|
import { FORMAT_12, FORMAT_24 } from '../../viewerConfig';
|
||||||
import { APP_SETTINGS } from '../api/constants';
|
import { APP_SETTINGS } from '../api/constants';
|
||||||
@@ -143,72 +149,3 @@ export function useTimeUntilStart(
|
|||||||
const { offset, clock, currentDay, offsetMode, actualStart, plannedStart } = useTimeUntilData();
|
const { offset, clock, currentDay, offsetMode, actualStart, plannedStart } = useTimeUntilData();
|
||||||
return calculateTimeUntilStart({ ...data, currentDay, clock, offset, offsetMode, actualStart, plannedStart });
|
return calculateTimeUntilStart({ ...data, currentDay, clock, offset, offsetMode, actualStart, plannedStart });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param currentDay the day offset of the urrently running event
|
|
||||||
* @param totalGap accumulated gap from the current event
|
|
||||||
* @param isLinkedToLoaded is this event part of a chain linking back to the current loaded event
|
|
||||||
* @param clock
|
|
||||||
* @param offset
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function calculateTimeUntilStart(
|
|
||||||
data: Pick<OntimeEvent, 'timeStart' | 'dayOffset' | 'delay'> & {
|
|
||||||
currentDay: number;
|
|
||||||
totalGap: number;
|
|
||||||
isLinkedToLoaded: boolean;
|
|
||||||
clock: number;
|
|
||||||
offset: number;
|
|
||||||
offsetMode: OffsetMode;
|
|
||||||
actualStart: MaybeNumber;
|
|
||||||
plannedStart: MaybeNumber;
|
|
||||||
},
|
|
||||||
): number {
|
|
||||||
const {
|
|
||||||
timeStart,
|
|
||||||
dayOffset,
|
|
||||||
currentDay,
|
|
||||||
totalGap,
|
|
||||||
isLinkedToLoaded,
|
|
||||||
clock,
|
|
||||||
offset,
|
|
||||||
delay,
|
|
||||||
offsetMode,
|
|
||||||
actualStart,
|
|
||||||
plannedStart,
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
//How many days from the currently running event to this one
|
|
||||||
const relativeDayOffset = dayOffset - currentDay;
|
|
||||||
|
|
||||||
const delayedStart = Math.max(0, timeStart + delay);
|
|
||||||
|
|
||||||
//The normalised start time of this event relative to the currently running event
|
|
||||||
const normalisedTimeStart = delayedStart + relativeDayOffset * dayInMs;
|
|
||||||
|
|
||||||
let relativeStartOffset = 0;
|
|
||||||
|
|
||||||
if (offsetMode === OffsetMode.Relative) {
|
|
||||||
relativeStartOffset = (actualStart ?? 0) - (plannedStart ?? 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const scheduledTimeUntil = normalisedTimeStart - clock + relativeStartOffset;
|
|
||||||
|
|
||||||
const offsetTimeUntil = scheduledTimeUntil - offset;
|
|
||||||
|
|
||||||
if (isLinkedToLoaded) {
|
|
||||||
//if we are directly linked back to the loaded event we just follow the offset
|
|
||||||
return offsetTimeUntil;
|
|
||||||
}
|
|
||||||
|
|
||||||
const gapsCanCompensateForOffset = totalGap + offset >= 0;
|
|
||||||
if (gapsCanCompensateForOffset) {
|
|
||||||
// if we are ahead of schedule or the gap can compensate for the amount we are behind then expect to start at the scheduled time
|
|
||||||
return scheduledTimeUntil;
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise consume as much of the offset as possible with the gap
|
|
||||||
const offsetTimeUntilBufferedByGaps = offsetTimeUntil - totalGap;
|
|
||||||
return offsetTimeUntilBufferedByGaps;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { PropsWithChildren, ReactNode } from 'react';
|
import { PropsWithChildren, ReactNode } from 'react';
|
||||||
import { ErrorBoundary } from '@sentry/react';
|
import { ErrorBoundary } from '@sentry/react';
|
||||||
import { isOntimeBlock } from 'ontime-types';
|
import { isOntimeBlock, TimerType } from 'ontime-types';
|
||||||
import { isPlaybackActive, millisToString } from 'ontime-utils';
|
import { isPlaybackActive, millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
|
import Tooltip from '../../../common/components/tooltip/Tooltip';
|
||||||
import {
|
import {
|
||||||
useClock,
|
useClock,
|
||||||
useCurrentBlockId,
|
useCurrentBlockId,
|
||||||
@@ -12,7 +13,7 @@ import {
|
|||||||
} from '../../../common/hooks/useSocket';
|
} from '../../../common/hooks/useSocket';
|
||||||
import useProjectData from '../../../common/hooks-query/useProjectData';
|
import useProjectData from '../../../common/hooks-query/useProjectData';
|
||||||
import { useEntry } from '../../../common/hooks-query/useRundown';
|
import { useEntry } from '../../../common/hooks-query/useRundown';
|
||||||
import { cx, enDash, timerPlaceholder, timerPlaceholderMin } from '../../../common/utils/styleUtils';
|
import { cx, enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
|
||||||
import { formatedTime, getOffsetText } from '../overviewUtils';
|
import { formatedTime, getOffsetText } from '../overviewUtils';
|
||||||
|
|
||||||
import { TimeColumn, TimeRow } from './TimeLayout';
|
import { TimeColumn, TimeRow } from './TimeLayout';
|
||||||
@@ -51,46 +52,61 @@ export function TitlesOverview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CurrentBlockOverview() {
|
export function CurrentBlockOverview() {
|
||||||
const { blockStartedAt: blockStartAt, clock } = useRuntimePlaybackOverview();
|
const { blockStartedAt, clock, blockExpectedEnd } = useRuntimePlaybackOverview();
|
||||||
const { currentBlockId } = useCurrentBlockId();
|
const { currentBlockId } = useCurrentBlockId();
|
||||||
const entry = useEntry(currentBlockId);
|
const entry = useEntry(currentBlockId);
|
||||||
|
|
||||||
const timeInBlock = formatedTime(blockStartAt ? clock - blockStartAt : null, 2);
|
const timeInBlock = formatedTime(blockStartedAt ? clock - blockStartedAt : null, 3, TimerType.CountUp);
|
||||||
|
const blockExpectedEndString = formatedTime(blockExpectedEnd, 3, TimerType.CountUp);
|
||||||
|
|
||||||
/**
|
const remainingBlockDuration = (() => {
|
||||||
* The time to the end of the block
|
if (blockStartedAt === null || !entry) return timerPlaceholder;
|
||||||
* as scheduled
|
if (!isOntimeBlock(entry)) return timerPlaceholder;
|
||||||
* TODO(v4): this needs to be calculated according to offset mode
|
return formatedTime(blockStartedAt + entry.duration - clock, 3, TimerType.CountDown);
|
||||||
*/
|
|
||||||
const blockEnd = (() => {
|
|
||||||
if (!entry) return timerPlaceholderMin;
|
|
||||||
if (!isOntimeBlock(entry)) return timerPlaceholderMin;
|
|
||||||
if (entry.timeEnd === null) return timerPlaceholderMin;
|
|
||||||
return formatedTime(entry.timeEnd - clock, 2);
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/**
|
const timeUntilBlockEnd = (() => {
|
||||||
* The time to the end of the block
|
if (blockExpectedEnd === null) return timerPlaceholder;
|
||||||
* as projected accounting for delays and offset
|
return formatedTime(blockExpectedEnd - clock, 3, TimerType.CountDown);
|
||||||
* TODO(v4): this needs to be calculated according to offset mode
|
})() ;
|
||||||
*/
|
|
||||||
const projectedBlockEnd = (() => {
|
|
||||||
if (blockStartAt === null || !entry) return timerPlaceholderMin;
|
|
||||||
if (!isOntimeBlock(entry)) return timerPlaceholderMin;
|
|
||||||
return formatedTime(blockStartAt + entry.duration - clock, 2);
|
|
||||||
})();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TimeColumn label='Elapsed in group' value={timeInBlock} className={style.clock} muted={blockStartAt === null} />
|
|
||||||
<div>
|
<div>
|
||||||
<TimeRow label='Group end' value={blockEnd} className={style.end} muted={blockStartAt === null} />
|
<Tooltip text='How long the group has been active'>
|
||||||
<TimeRow
|
<TimeRow
|
||||||
label='Projected group end'
|
label='Elapsed in group'
|
||||||
value={projectedBlockEnd}
|
value={timeInBlock}
|
||||||
className={style.end}
|
className={style.clock}
|
||||||
muted={blockStartAt === null}
|
muted={blockStartedAt === null}
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip text='Remaining time until the planed group duration is up'>
|
||||||
|
<TimeRow
|
||||||
|
label='Remaining group duration'
|
||||||
|
value={remainingBlockDuration}
|
||||||
|
className={style.clock}
|
||||||
|
muted={blockStartedAt === null}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Tooltip text='Expected time until the group can end, if everything ends on time from now on'>
|
||||||
|
<TimeRow
|
||||||
|
label='Expected time until group end'
|
||||||
|
value={timeUntilBlockEnd}
|
||||||
|
className={style.end}
|
||||||
|
muted={blockStartedAt === null}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip text='Expected time the group will end, if everything ends on time from now on'>
|
||||||
|
<TimeRow
|
||||||
|
label='Expected group end'
|
||||||
|
value={blockExpectedEndString}
|
||||||
|
className={style.end}
|
||||||
|
muted={blockStartedAt === null}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.label {
|
.label {
|
||||||
color: $label-gray;
|
color: $label-gray;
|
||||||
font-size: calc(1rem - 2px);
|
font-size: calc(1rem - 2px);
|
||||||
width: 10em; // a number large enough to force right alignment
|
width: 15em; // a number large enough to force right alignment
|
||||||
}
|
}
|
||||||
|
|
||||||
.clock {
|
.clock {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MaybeNumber } from 'ontime-types';
|
import { MaybeNumber, TimerType } from 'ontime-types';
|
||||||
import { dayInMs, millisToString } from 'ontime-utils';
|
import { dayInMs, millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
import { enDash, timerPlaceholder, timerPlaceholderMin } from '../../common/utils/styleUtils';
|
import { enDash, timerPlaceholder, timerPlaceholderMin } from '../../common/utils/styleUtils';
|
||||||
@@ -6,8 +6,12 @@ import { enDash, timerPlaceholder, timerPlaceholderMin } from '../../common/util
|
|||||||
/**
|
/**
|
||||||
* Encapsulates the logic for formatting time in overview
|
* Encapsulates the logic for formatting time in overview
|
||||||
*/
|
*/
|
||||||
export function formatedTime(time: MaybeNumber, segments: number = 3): string {
|
export function formatedTime(
|
||||||
return millisToString(time, { fallback: segments === 3 ? timerPlaceholder : timerPlaceholderMin });
|
time: MaybeNumber,
|
||||||
|
segments: number = 3,
|
||||||
|
direction?: TimerType.CountDown | TimerType.CountUp,
|
||||||
|
): string {
|
||||||
|
return millisToString(time, { fallback: segments === 3 ? timerPlaceholder : timerPlaceholderMin, direction });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -771,11 +771,12 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
|||||||
!deepEqual(RuntimeService.previousState?.runtime, state.runtime);
|
!deepEqual(RuntimeService.previousState?.runtime, state.runtime);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the currentBlock object has no ticking values so we only need to check for equality
|
* the currentBlock object has the potential to tick on expected end
|
||||||
|
* TODO: the value shows up one tick to late
|
||||||
*/
|
*/
|
||||||
const shouldBlockUpdate =
|
const shouldBlockUpdate =
|
||||||
!deepEqual(RuntimeService?.previousState.blockNow, state.blockNow) ||
|
!deepEqual(RuntimeService?.previousState.blockNow, state.blockNow) ||
|
||||||
!deepEqual(RuntimeService?.previousState.blockNext, state.blockNext);
|
RuntimeService?.previousState.blockNext !== state.blockNext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Many other values are calculated based on the clock
|
* Many other values are calculated based on the clock
|
||||||
@@ -806,7 +807,7 @@ function broadcastResult(_target: any, _propertyKey: string, descriptor: Propert
|
|||||||
batch.add('blockNow', state.blockNow);
|
batch.add('blockNow', state.blockNow);
|
||||||
batch.add('blockNext', state.blockNext);
|
batch.add('blockNext', state.blockNext);
|
||||||
RuntimeService.previousState.blockNow = structuredClone(state.blockNow);
|
RuntimeService.previousState.blockNow = structuredClone(state.blockNow);
|
||||||
RuntimeService.previousState.blockNext = structuredClone(state.blockNext);
|
RuntimeService.previousState.blockNext = state.blockNext;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasImmediateChanges) {
|
if (hasImmediateChanges) {
|
||||||
|
|||||||
@@ -1,12 +1,65 @@
|
|||||||
import { MaybeNumber, TimerPhase } from 'ontime-types';
|
import { isOntimeEvent, MaybeNumber, OntimeBlock, OntimeEvent, Rundown, TimerPhase } from 'ontime-types';
|
||||||
import { dayInMs, isPlaybackActive } from 'ontime-utils';
|
import { calculateTimeUntilStart, dayInMs, isPlaybackActive } from 'ontime-utils';
|
||||||
import type { RuntimeState } from '../stores/runtimeState.js';
|
import type { RuntimeState } from '../stores/runtimeState.js';
|
||||||
|
import { shouldCrashDev } from '../utils/development.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* handle events that span over midnight
|
* handle events that span over midnight
|
||||||
*/
|
*/
|
||||||
export const normaliseEndTime = (start: number, end: number) => (end < start ? end + dayInMs : end);
|
export const normaliseEndTime = (start: number, end: number) => (end < start ? end + dayInMs : end);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the expected time of the group to end.
|
||||||
|
* Should only be called if a block is running
|
||||||
|
* TODO: take a look at how it handles relative offset mode
|
||||||
|
*/
|
||||||
|
export function getExpectedBlockFinish(state: RuntimeState, rundown: Rundown): MaybeNumber {
|
||||||
|
const { blockNow, eventNow, timer, clock } = state;
|
||||||
|
|
||||||
|
if (blockNow === null) return null;
|
||||||
|
// if the group doesn't have a start time there is no end time either
|
||||||
|
if (blockNow.startedAt === null) return null;
|
||||||
|
if (eventNow === null) return null;
|
||||||
|
if (timer.current === null) return null;
|
||||||
|
|
||||||
|
const { entries } = rundown;
|
||||||
|
const orderInBlock = (entries[blockNow.id] as OntimeBlock).entries;
|
||||||
|
|
||||||
|
const indexInBlock = orderInBlock.findIndex((id) => eventNow.id === id);
|
||||||
|
shouldCrashDev(indexInBlock < 0, 'Running event is not in current block');
|
||||||
|
|
||||||
|
if (indexInBlock === orderInBlock.length - 1) return timer.expectedFinish;
|
||||||
|
|
||||||
|
let totalGap = 0;
|
||||||
|
let isLinkedToLoaded = true;
|
||||||
|
|
||||||
|
for (let i = indexInBlock + 1; i < orderInBlock.length; i++) {
|
||||||
|
const entry = entries[orderInBlock[i]];
|
||||||
|
if (isOntimeEvent(entry)) {
|
||||||
|
totalGap += entry.gap;
|
||||||
|
isLinkedToLoaded = isLinkedToLoaded && entry.linkStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const lastEntry = entries[orderInBlock.at(-1)!] as OntimeEvent;
|
||||||
|
const { offsetMode, offset, plannedStart, actualStart } = state.runtime;
|
||||||
|
|
||||||
|
const timeUntilLastEvent = calculateTimeUntilStart({
|
||||||
|
timeStart: lastEntry.timeStart,
|
||||||
|
dayOffset: lastEntry.dayOffset,
|
||||||
|
delay: lastEntry.delay,
|
||||||
|
currentDay: eventNow.dayOffset,
|
||||||
|
totalGap,
|
||||||
|
isLinkedToLoaded,
|
||||||
|
clock,
|
||||||
|
offsetMode,
|
||||||
|
offset,
|
||||||
|
plannedStart,
|
||||||
|
actualStart,
|
||||||
|
});
|
||||||
|
|
||||||
|
return clock + timeUntilLastEvent + lastEntry.duration;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates expected finish time of a running timer
|
* Calculates expected finish time of a running timer
|
||||||
* @param {RuntimeState} state runtime state
|
* @param {RuntimeState} state runtime state
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { timeNow } from '../utils/time.js';
|
|||||||
import type { RestorePoint } from '../services/RestoreService.js';
|
import type { RestorePoint } from '../services/RestoreService.js';
|
||||||
import {
|
import {
|
||||||
getCurrent,
|
getCurrent,
|
||||||
|
getExpectedBlockFinish,
|
||||||
getExpectedEnd,
|
getExpectedEnd,
|
||||||
getExpectedFinish,
|
getExpectedFinish,
|
||||||
getRuntimeOffset,
|
getRuntimeOffset,
|
||||||
@@ -27,11 +28,12 @@ import { loadRoll, normaliseRollStart } from '../services/rollUtils.js';
|
|||||||
import { timerConfig } from '../setup/config.js';
|
import { timerConfig } from '../setup/config.js';
|
||||||
import { RundownMetadata } from '../api-data/rundown/rundown.types.js';
|
import { RundownMetadata } from '../api-data/rundown/rundown.types.js';
|
||||||
import { getPlayableIndexFromTimedIndex } from '../api-data/rundown/rundown.utils.js';
|
import { getPlayableIndexFromTimedIndex } from '../api-data/rundown/rundown.utils.js';
|
||||||
|
import { getCurrentRundown } from '../api-data/rundown/rundown.dao.js';
|
||||||
|
|
||||||
export type RuntimeState = {
|
export type RuntimeState = {
|
||||||
clock: number; // realtime clock
|
clock: number; // realtime clock
|
||||||
blockNow: BlockState | null;
|
blockNow: BlockState | null;
|
||||||
blockNext: BlockState | null;
|
blockNext: MaybeString;
|
||||||
eventNow: PlayableEvent | null;
|
eventNow: PlayableEvent | null;
|
||||||
eventNext: PlayableEvent | null;
|
eventNext: PlayableEvent | null;
|
||||||
runtime: Runtime;
|
runtime: Runtime;
|
||||||
@@ -90,6 +92,8 @@ export function clearEventData() {
|
|||||||
runtimeState.runtime.expectedEnd = null;
|
runtimeState.runtime.expectedEnd = null;
|
||||||
runtimeState.runtime.selectedEventIndex = null;
|
runtimeState.runtime.selectedEventIndex = null;
|
||||||
|
|
||||||
|
if (runtimeState.blockNow) runtimeState.blockNow.expectedEnd = null;
|
||||||
|
|
||||||
runtimeState.timer.playback = Playback.Stop;
|
runtimeState.timer.playback = Playback.Stop;
|
||||||
runtimeState.clock = timeNow();
|
runtimeState.clock = timeNow();
|
||||||
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
|
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
|
||||||
@@ -509,6 +513,11 @@ export function update(): UpdateResult {
|
|||||||
runtimeState.timer.expectedFinish = getExpectedFinish(runtimeState);
|
runtimeState.timer.expectedFinish = getExpectedFinish(runtimeState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (runtimeState.blockNow) {
|
||||||
|
const expectedBlockFinish = getExpectedBlockFinish(runtimeState, getCurrentRundown());
|
||||||
|
runtimeState.blockNow.expectedEnd = expectedBlockFinish;
|
||||||
|
}
|
||||||
|
|
||||||
return { hasTimerFinished: finishedNow, hasSecondaryTimerFinished: false };
|
return { hasTimerFinished: finishedNow, hasSecondaryTimerFinished: false };
|
||||||
|
|
||||||
function updateIfIdle() {
|
function updateIfIdle() {
|
||||||
@@ -690,7 +699,7 @@ export function loadBlock(rundown: Rundown, state = runtimeState) {
|
|||||||
let foundEventNow = false;
|
let foundEventNow = false;
|
||||||
for (const id of rundown.order) {
|
for (const id of rundown.order) {
|
||||||
if (foundEventNow && isOntimeBlock(rundown.entries[id])) {
|
if (foundEventNow && isOntimeBlock(rundown.entries[id])) {
|
||||||
state.blockNext = { id, startedAt: null }; // the id is set here, the start time is set in other placed that handel starting events
|
state.blockNext = id; // the id is set here, the start time is set in other placed that handel starting events
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (id === state.eventNow.id) {
|
if (id === state.eventNow.id) {
|
||||||
@@ -707,7 +716,7 @@ export function loadBlock(rundown: Rundown, state = runtimeState) {
|
|||||||
|
|
||||||
//we went into a new block - and it is different from the one we might have come from
|
//we went into a new block - and it is different from the one we might have come from
|
||||||
if ((state.blockNow != null && state.blockNow.id != currentBlockId) || state.blockNow == null) {
|
if ((state.blockNow != null && state.blockNow.id != currentBlockId) || state.blockNow == null) {
|
||||||
state.blockNow = { id: currentBlockId, startedAt: null }; // the id is set here, the start time is set in other placed that handel starting events
|
state.blockNow = { id: currentBlockId, startedAt: null, expectedEnd: null }; // the id is set here, the start time is set in other placed that handel starting events
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ test('CRUD operations on the rundown', async ({ page }) => {
|
|||||||
await expect(page.getByTestId('rundown-block')).toHaveCount(0);
|
await expect(page.getByTestId('rundown-block')).toHaveCount(0);
|
||||||
|
|
||||||
// create groups using the quick add buttons
|
// create groups using the quick add buttons
|
||||||
await page.getByRole('button', { name: 'Group' }).nth(1).click();
|
await page.getByTestId('rundown').getByRole('button', { name: 'Group' }).nth(1).click();
|
||||||
await page.getByRole('button', { name: 'Delay' }).nth(1).click();
|
await page.getByRole('button', { name: 'Delay' }).nth(1).click();
|
||||||
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
await page.getByRole('button', { name: 'Event', exact: true }).nth(1).click();
|
||||||
await expect(page.getByTestId('rundown-event')).toHaveCount(2);
|
await expect(page.getByTestId('rundown-event')).toHaveCount(2);
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ import type { EntryId } from '../core/OntimeEntry.js';
|
|||||||
export type BlockState = {
|
export type BlockState = {
|
||||||
id: EntryId;
|
id: EntryId;
|
||||||
startedAt: MaybeNumber;
|
startedAt: MaybeNumber;
|
||||||
|
expectedEnd: MaybeNumber;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { MaybeString } from '../../utils/utils.type.js';
|
||||||
import type { OntimeEvent } from '../core/OntimeEntry.js';
|
import type { OntimeEvent } from '../core/OntimeEntry.js';
|
||||||
import type { SimpleTimerState } from './AuxTimer.type.js';
|
import type { SimpleTimerState } from './AuxTimer.type.js';
|
||||||
import type { BlockState } from './CurrentBlockState.type.js';
|
import type { BlockState } from './CurrentBlockState.type.js';
|
||||||
@@ -18,9 +19,9 @@ export type RuntimeStore = {
|
|||||||
runtime: Runtime;
|
runtime: Runtime;
|
||||||
eventNow: OntimeEvent | null;
|
eventNow: OntimeEvent | null;
|
||||||
eventNext: OntimeEvent | null;
|
eventNext: OntimeEvent | null;
|
||||||
|
|
||||||
blockNow: BlockState | null;
|
blockNow: BlockState | null;
|
||||||
blockNext: BlockState | null;
|
blockNext: MaybeString;
|
||||||
|
|
||||||
// extra timers
|
// extra timers
|
||||||
auxtimer1: SimpleTimerState;
|
auxtimer1: SimpleTimerState;
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ export { validateEndAction, validateTimerType } from './src/validate-events/vali
|
|||||||
|
|
||||||
// feature business logic
|
// feature business logic
|
||||||
|
|
||||||
|
export { calculateTimeUntilStart } from './src/date-utils/calculateTimeUntilStart.js';
|
||||||
|
|
||||||
// feature business logic - rundown
|
// feature business logic - rundown
|
||||||
export { checkIsNow } from './src/date-utils/checkIsNow.js';
|
export { checkIsNow } from './src/date-utils/checkIsNow.js';
|
||||||
export { checkIsNextDay } from './src/date-utils/checkIsNextDay.js';
|
export { checkIsNextDay } from './src/date-utils/checkIsNextDay.js';
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
import { OffsetMode } from 'ontime-types';
|
||||||
|
|
||||||
|
import { calculateTimeUntilStart } from './calculateTimeUntilStart';
|
||||||
|
import { dayInMs } from './conversionUtils';
|
||||||
|
|
||||||
|
describe('calculateTimeUntilStart()', () => {
|
||||||
|
describe('Absolute offset mode', () => {
|
||||||
|
test('ontime', () => {
|
||||||
|
const test = {
|
||||||
|
timeStart: 100,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 0,
|
||||||
|
clock: 90,
|
||||||
|
offset: 0,
|
||||||
|
offsetMode: OffsetMode.Absolute,
|
||||||
|
actualStart: null,
|
||||||
|
plannedStart: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(10);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('running behind', () => {
|
||||||
|
const test = {
|
||||||
|
timeStart: 100,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 0,
|
||||||
|
clock: 90,
|
||||||
|
offset: -20,
|
||||||
|
offsetMode: OffsetMode.Absolute,
|
||||||
|
actualStart: null,
|
||||||
|
plannedStart: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(30);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('running ahead', () => {
|
||||||
|
const test = {
|
||||||
|
timeStart: 100,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 0,
|
||||||
|
clock: 80,
|
||||||
|
offset: 10,
|
||||||
|
offsetMode: OffsetMode.Absolute,
|
||||||
|
actualStart: null,
|
||||||
|
plannedStart: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20); // <-- when running ahead the unlinked timer stays put
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('running behind with enough gaps', () => {
|
||||||
|
const test = {
|
||||||
|
timeStart: 100,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 20,
|
||||||
|
clock: 50,
|
||||||
|
offset: -20,
|
||||||
|
offsetMode: OffsetMode.Absolute,
|
||||||
|
actualStart: null,
|
||||||
|
plannedStart: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(50); // <-- when gap is enough to compensate for the running behind
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(70); // This should not be possible
|
||||||
|
});
|
||||||
|
|
||||||
|
test('running behind with too little gaps', () => {
|
||||||
|
const test = {
|
||||||
|
timeStart: 100,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 10,
|
||||||
|
clock: 50,
|
||||||
|
offset: -20,
|
||||||
|
offsetMode: OffsetMode.Absolute,
|
||||||
|
actualStart: 0,
|
||||||
|
plannedStart: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(60); // <-- when gap is not enough to compensate for the running behind it absorbs at much as possible
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(70); // This should not be possible
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Relative offset mode', () => {
|
||||||
|
test('basic function', () => {
|
||||||
|
const test = {
|
||||||
|
timeStart: 0,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 0,
|
||||||
|
clock: 100,
|
||||||
|
actualStart: 100,
|
||||||
|
plannedStart: 0,
|
||||||
|
offset: 0,
|
||||||
|
offsetMode: OffsetMode.Relative,
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeStartEvent2 = 10;
|
||||||
|
const timeStartEvent3 = 20;
|
||||||
|
|
||||||
|
//event 1 is the currently running event
|
||||||
|
|
||||||
|
//event 2
|
||||||
|
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: true })).toBe(10);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: false })).toBe(10);
|
||||||
|
|
||||||
|
//event 3
|
||||||
|
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: true })).toBe(20);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: false })).toBe(20);
|
||||||
|
|
||||||
|
// When clock advances by 5ms, time until start should decrease by 5ms
|
||||||
|
test.clock = 105;
|
||||||
|
|
||||||
|
//event 2
|
||||||
|
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: true })).toBe(5);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent2, isLinkedToLoaded: false })).toBe(5);
|
||||||
|
|
||||||
|
//event 3
|
||||||
|
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: true })).toBe(15);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, timeStart: timeStartEvent3, isLinkedToLoaded: false })).toBe(15);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gaps', () => {
|
||||||
|
const test = {
|
||||||
|
timeStart: 20,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 10,
|
||||||
|
clock: 100,
|
||||||
|
actualStart: 100,
|
||||||
|
plannedStart: 0,
|
||||||
|
offset: 0,
|
||||||
|
offsetMode: OffsetMode.Relative,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(20);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20);
|
||||||
|
|
||||||
|
// When clock advances by 5ms, time until start should decrease by 5ms
|
||||||
|
test.clock = 105;
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(15);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(15);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('added/remove time', () => {
|
||||||
|
const test = {
|
||||||
|
timeStart: 20,
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
currentDay: 0,
|
||||||
|
totalGap: 0,
|
||||||
|
clock: 100,
|
||||||
|
actualStart: 100,
|
||||||
|
plannedStart: 0,
|
||||||
|
offset: 0,
|
||||||
|
offsetMode: OffsetMode.Relative,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(20);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20);
|
||||||
|
|
||||||
|
test.offset = 5; // remove 5 with addtime - we are ahead of time
|
||||||
|
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(15);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(20); // unlocked evets will stay on schedule
|
||||||
|
|
||||||
|
test.offset = -5; // add 5 with addtime - we are behind
|
||||||
|
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: true })).toBe(25);
|
||||||
|
expect(calculateTimeUntilStart({ ...test, isLinkedToLoaded: false })).toBe(25);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('next day', () => {
|
||||||
|
const test = {
|
||||||
|
delay: 0,
|
||||||
|
currentDay: 0,
|
||||||
|
clock: 100,
|
||||||
|
actualStart: 100,
|
||||||
|
plannedStart: 0,
|
||||||
|
offset: 0,
|
||||||
|
offsetMode: OffsetMode.Relative,
|
||||||
|
};
|
||||||
|
|
||||||
|
// this event will start the current day
|
||||||
|
expect(
|
||||||
|
calculateTimeUntilStart({
|
||||||
|
...test,
|
||||||
|
timeStart: 10,
|
||||||
|
dayOffset: 0,
|
||||||
|
totalGap: 0,
|
||||||
|
isLinkedToLoaded: false,
|
||||||
|
}),
|
||||||
|
).toBe(10);
|
||||||
|
|
||||||
|
// this event will start the next day
|
||||||
|
// in absolute mode this would start in dayInMs - 100 since the gap would compensate
|
||||||
|
// but in relative mode with and actual start that is 100 offset it starts in dayInMs
|
||||||
|
expect(
|
||||||
|
calculateTimeUntilStart({
|
||||||
|
...test,
|
||||||
|
timeStart: 0,
|
||||||
|
dayOffset: 1,
|
||||||
|
totalGap: dayInMs - 20,
|
||||||
|
isLinkedToLoaded: false,
|
||||||
|
}),
|
||||||
|
).toBe(dayInMs);
|
||||||
|
|
||||||
|
// advancing 100ms
|
||||||
|
test.clock = 200;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
calculateTimeUntilStart({
|
||||||
|
...test,
|
||||||
|
timeStart: 0,
|
||||||
|
dayOffset: 1,
|
||||||
|
totalGap: dayInMs - 20,
|
||||||
|
isLinkedToLoaded: false,
|
||||||
|
}),
|
||||||
|
).toBe(dayInMs - 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('overlap with negative total gap', () => {
|
||||||
|
const test = {
|
||||||
|
dayOffset: 0,
|
||||||
|
delay: 0,
|
||||||
|
currentDay: 0,
|
||||||
|
clock: 100,
|
||||||
|
actualStart: 100,
|
||||||
|
plannedStart: 0,
|
||||||
|
offset: 0,
|
||||||
|
offsetMode: OffsetMode.Relative,
|
||||||
|
isLinkedToLoaded: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// the overlap will be pushed out to the expected available time
|
||||||
|
expect(calculateTimeUntilStart({ ...test, timeStart: 5, totalGap: -5 })).toBe(10);
|
||||||
|
|
||||||
|
test.clock = 105;
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import type { MaybeNumber, OntimeEvent } from 'ontime-types';
|
||||||
|
import { OffsetMode } from 'ontime-types';
|
||||||
|
|
||||||
|
import { dayInMs } from './conversionUtils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param currentDay the day offset of the currently running event
|
||||||
|
* @param totalGap accumulated gap from the current event
|
||||||
|
* @param isLinkedToLoaded is this event part of a chain linking back to the current loaded event
|
||||||
|
* @param clock
|
||||||
|
* @param offset
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function calculateTimeUntilStart(
|
||||||
|
data: Pick<OntimeEvent, 'timeStart' | 'dayOffset' | 'delay'> & {
|
||||||
|
currentDay: number;
|
||||||
|
totalGap: number;
|
||||||
|
isLinkedToLoaded: boolean;
|
||||||
|
clock: number;
|
||||||
|
offset: number;
|
||||||
|
offsetMode: OffsetMode;
|
||||||
|
actualStart: MaybeNumber;
|
||||||
|
plannedStart: MaybeNumber;
|
||||||
|
},
|
||||||
|
): number {
|
||||||
|
const {
|
||||||
|
timeStart,
|
||||||
|
dayOffset,
|
||||||
|
currentDay,
|
||||||
|
totalGap,
|
||||||
|
isLinkedToLoaded,
|
||||||
|
clock,
|
||||||
|
offset,
|
||||||
|
delay,
|
||||||
|
offsetMode,
|
||||||
|
actualStart,
|
||||||
|
plannedStart,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
//How many days from the currently running event to this one
|
||||||
|
const relativeDayOffset = dayOffset - currentDay;
|
||||||
|
|
||||||
|
const delayedStart = Math.max(0, timeStart + delay);
|
||||||
|
|
||||||
|
//The normalised start time of this event relative to the currently running event
|
||||||
|
const normalisedTimeStart = delayedStart + relativeDayOffset * dayInMs;
|
||||||
|
|
||||||
|
let relativeStartOffset = 0;
|
||||||
|
|
||||||
|
if (offsetMode === OffsetMode.Relative) {
|
||||||
|
relativeStartOffset = (actualStart ?? 0) - (plannedStart ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduledTimeUntil = normalisedTimeStart - clock + relativeStartOffset;
|
||||||
|
|
||||||
|
const offsetTimeUntil = scheduledTimeUntil - offset;
|
||||||
|
|
||||||
|
if (isLinkedToLoaded) {
|
||||||
|
//if we are directly linked back to the loaded event we just follow the offset
|
||||||
|
return offsetTimeUntil;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gapsCanCompensateForOffset = totalGap + offset >= 0;
|
||||||
|
if (gapsCanCompensateForOffset) {
|
||||||
|
// if we are ahead of schedule or the gap can compensate for the amount we are behind then expect to start at the scheduled time
|
||||||
|
return scheduledTimeUntil;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise consume as much of the offset as possible with the gap
|
||||||
|
const offsetTimeUntilBufferedByGaps = offsetTimeUntil - totalGap;
|
||||||
|
return offsetTimeUntilBufferedByGaps;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user