mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-10 08:39:34 +00:00
refactor: calculate rundown duration
This commit is contained in:
committed by
Carlos Valente
parent
d71df886f2
commit
66774321a5
@@ -187,6 +187,15 @@ export const useRuntimePlaybackOverview = () => {
|
|||||||
return useRuntimeStore(featureSelector);
|
return useRuntimeStore(featureSelector);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useTimelineOverview = () => {
|
||||||
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
|
plannedStart: state.runtime.plannedStart,
|
||||||
|
plannedEnd: state.runtime.plannedEnd,
|
||||||
|
});
|
||||||
|
|
||||||
|
return useRuntimeStore(featureSelector);
|
||||||
|
};
|
||||||
|
|
||||||
export const useTimelineStatus = () => {
|
export const useTimelineStatus = () => {
|
||||||
const featureSelector = (state: RuntimeStore) => ({
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
clock: state.clock,
|
clock: state.clock,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ type WithDataProps = {
|
|||||||
publicSelectedId: string | null;
|
publicSelectedId: string | null;
|
||||||
runtime: Runtime;
|
runtime: Runtime;
|
||||||
selectedId: string | null;
|
selectedId: string | null;
|
||||||
settings: Settings | undefined;
|
settings: Settings | undefined; // TODO: what is the case for this being undefined?
|
||||||
time: ViewExtendedTimer;
|
time: ViewExtendedTimer;
|
||||||
viewSettings: ViewSettings;
|
viewSettings: ViewSettings;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useViewportSize } from '@mantine/hooks';
|
import { useViewportSize } from '@mantine/hooks';
|
||||||
import { isOntimeEvent, MaybeNumber, OntimeEvent } from 'ontime-types';
|
import { isOntimeEvent, MaybeNumber, OntimeEvent } from 'ontime-types';
|
||||||
import { dayInMs, getFirstEvent, getLastEvent, MILLIS_PER_HOUR } from 'ontime-utils';
|
import { dayInMs, getLastEvent, MILLIS_PER_HOUR } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { useTimelineOverview } from '../../../common/hooks/useSocket';
|
||||||
|
|
||||||
import TimelineMarkers from './timeline-markers/TimelineMarkers';
|
import TimelineMarkers from './timeline-markers/TimelineMarkers';
|
||||||
import ProgressBar from './timeline-progress-bar/TimelineProgressBar';
|
import ProgressBar from './timeline-progress-bar/TimelineProgressBar';
|
||||||
@@ -10,26 +12,6 @@ import { ProgressStatus, TimelineEntry } from './TimelineEntry';
|
|||||||
|
|
||||||
import style from './Timeline.module.scss';
|
import style from './Timeline.module.scss';
|
||||||
|
|
||||||
function useTimeline(rundown: OntimeEvent[]) {
|
|
||||||
const { firstEvent } = getFirstEvent(rundown);
|
|
||||||
const { lastEvent } = getLastEvent(rundown);
|
|
||||||
const firstStart = firstEvent?.timeStart ?? 0;
|
|
||||||
const lastEnd = lastEvent?.timeEnd ?? 0;
|
|
||||||
const normalisedLastEnd = lastEnd < firstStart ? lastEnd + dayInMs : lastEnd;
|
|
||||||
|
|
||||||
// we make sure the end accounts for delays
|
|
||||||
const accumulatedDelay = lastEvent?.delay ?? 0;
|
|
||||||
// timeline is padded to nearest hours (floor and ceil)
|
|
||||||
const startHour = getStartHour(firstStart);
|
|
||||||
const endHour = getEndHour(normalisedLastEnd + accumulatedDelay);
|
|
||||||
|
|
||||||
return {
|
|
||||||
rundown: rundown,
|
|
||||||
startHour,
|
|
||||||
endHour,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TimelineProps {
|
interface TimelineProps {
|
||||||
selectedEventId: string | null;
|
selectedEventId: string | null;
|
||||||
rundown: OntimeEvent[];
|
rundown: OntimeEvent[];
|
||||||
@@ -38,15 +20,17 @@ interface TimelineProps {
|
|||||||
export default memo(Timeline);
|
export default memo(Timeline);
|
||||||
|
|
||||||
function Timeline(props: TimelineProps) {
|
function Timeline(props: TimelineProps) {
|
||||||
const { selectedEventId, rundown: baseRundown } = props;
|
const { selectedEventId, rundown } = props;
|
||||||
const { width: screenWidth } = useViewportSize();
|
const { width: screenWidth } = useViewportSize();
|
||||||
const timelineData = useTimeline(baseRundown);
|
const { plannedStart, plannedEnd } = useTimelineOverview();
|
||||||
|
|
||||||
if (timelineData === null) {
|
if (plannedStart === null || plannedEnd === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { rundown, startHour, endHour } = timelineData;
|
const { lastEvent } = getLastEvent(rundown);
|
||||||
|
const startHour = getStartHour(plannedStart);
|
||||||
|
const endHour = getEndHour(plannedEnd + (lastEvent?.delay ?? 0));
|
||||||
|
|
||||||
let hasTimelinePassedMidnight = false;
|
let hasTimelinePassedMidnight = false;
|
||||||
let previousEventStartTime: MaybeNumber = null;
|
let previousEventStartTime: MaybeNumber = null;
|
||||||
@@ -95,7 +79,7 @@ function Timeline(props: TimelineProps) {
|
|||||||
duration={event.duration}
|
duration={event.duration}
|
||||||
left={elementLeftPosition}
|
left={elementLeftPosition}
|
||||||
status={eventStatus}
|
status={eventStatus}
|
||||||
start={normalisedStart} // solve issues related to crossing midnight
|
start={normalisedStart} // dataset solves issues related to crossing midnight
|
||||||
title={event.title}
|
title={event.title}
|
||||||
width={elementWidth}
|
width={elementWidth}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export default function TimelinePage(props: TimelinePageProps) {
|
|||||||
const { getLocalizedString } = useTranslation();
|
const { getLocalizedString } = useTranslation();
|
||||||
const clock = formatTime(time.clock);
|
const clock = formatTime(time.clock);
|
||||||
|
|
||||||
|
// holds copy of the rundown with only relevant events
|
||||||
const scopedRundown = useMemo(() => {
|
const scopedRundown = useMemo(() => {
|
||||||
return getScopedRundown(backstageEvents, selectedId);
|
return getScopedRundown(backstageEvents, selectedId);
|
||||||
}, [backstageEvents, selectedId]);
|
}, [backstageEvents, selectedId]);
|
||||||
|
|||||||
@@ -70,13 +70,13 @@ describe('generate()', () => {
|
|||||||
|
|
||||||
it('accounts for gaps in rundown when calculating delays', () => {
|
it('accounts for gaps in rundown when calculating delays', () => {
|
||||||
const testRundown: OntimeRundown = [
|
const testRundown: OntimeRundown = [
|
||||||
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 200 } as OntimeEvent,
|
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 200, duration: 100 } as OntimeEvent,
|
||||||
{ type: SupportedEvent.Delay, id: 'delay', duration: 200 } as OntimeDelay,
|
{ type: SupportedEvent.Delay, id: 'delay', duration: 200 } as OntimeDelay,
|
||||||
{ type: SupportedEvent.Event, id: '2', timeStart: 200, timeEnd: 300 } as OntimeEvent,
|
{ type: SupportedEvent.Event, id: '2', timeStart: 200, timeEnd: 300, duration: 100 } as OntimeEvent,
|
||||||
{ type: SupportedEvent.Block, id: 'block', title: 'break' } as OntimeBlock,
|
{ type: SupportedEvent.Block, id: 'block', title: 'break' } as OntimeBlock,
|
||||||
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500 } as OntimeEvent,
|
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500, duration: 100 } as OntimeEvent,
|
||||||
{ type: SupportedEvent.Block, id: 'another-block', title: 'another-break' } as OntimeBlock,
|
{ type: SupportedEvent.Block, id: 'another-block', title: 'another-break' } as OntimeBlock,
|
||||||
{ type: SupportedEvent.Event, id: '4', timeStart: 600, timeEnd: 700 } as OntimeEvent,
|
{ type: SupportedEvent.Event, id: '4', timeStart: 600, timeEnd: 700, duration: 100 } as OntimeEvent,
|
||||||
];
|
];
|
||||||
|
|
||||||
const initResult = generate(testRundown);
|
const initResult = generate(testRundown);
|
||||||
@@ -91,13 +91,13 @@ describe('generate()', () => {
|
|||||||
|
|
||||||
it('handles negative delays', () => {
|
it('handles negative delays', () => {
|
||||||
const testRundown: OntimeRundown = [
|
const testRundown: OntimeRundown = [
|
||||||
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 200 } as OntimeEvent,
|
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 200, duration: 100 } as OntimeEvent,
|
||||||
{ type: SupportedEvent.Delay, id: 'delay', duration: -200 } as OntimeDelay,
|
{ type: SupportedEvent.Delay, id: 'delay', duration: -200 } as OntimeDelay,
|
||||||
{ type: SupportedEvent.Event, id: '2', timeStart: 200, timeEnd: 300 } as OntimeEvent,
|
{ type: SupportedEvent.Event, id: '2', timeStart: 200, timeEnd: 300, duration: 100 } as OntimeEvent,
|
||||||
{ type: SupportedEvent.Block, id: 'block', title: 'break' } as OntimeBlock,
|
{ type: SupportedEvent.Block, id: 'block', title: 'break' } as OntimeBlock,
|
||||||
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500 } as OntimeEvent,
|
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500, duration: 100 } as OntimeEvent,
|
||||||
{ type: SupportedEvent.Block, id: 'another-block', title: 'another-break' } as OntimeBlock,
|
{ type: SupportedEvent.Block, id: 'another-block', title: 'another-break' } as OntimeBlock,
|
||||||
{ type: SupportedEvent.Event, id: '4', timeStart: 600, timeEnd: 700 } as OntimeEvent,
|
{ type: SupportedEvent.Event, id: '4', timeStart: 600, timeEnd: 700, duration: 100 } as OntimeEvent,
|
||||||
];
|
];
|
||||||
|
|
||||||
const initResult = generate(testRundown);
|
const initResult = generate(testRundown);
|
||||||
@@ -172,10 +172,17 @@ describe('generate()', () => {
|
|||||||
|
|
||||||
it('calculates total duration', () => {
|
it('calculates total duration', () => {
|
||||||
const testRundown: OntimeRundown = [
|
const testRundown: OntimeRundown = [
|
||||||
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 200 } as OntimeEvent,
|
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 200, duration: 100 } as OntimeEvent,
|
||||||
{ type: SupportedEvent.Event, id: '2', timeStart: 200, timeEnd: 300 } as OntimeEvent,
|
{ type: SupportedEvent.Event, id: '2', timeStart: 200, timeEnd: 300, duration: 100 } as OntimeEvent,
|
||||||
{ type: SupportedEvent.Event, id: 'skipped', skip: true, timeStart: 300, timeEnd: 400 } as OntimeEvent,
|
{
|
||||||
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500 } as OntimeEvent,
|
type: SupportedEvent.Event,
|
||||||
|
id: 'skipped',
|
||||||
|
skip: true,
|
||||||
|
timeStart: 300,
|
||||||
|
timeEnd: 400,
|
||||||
|
duration: 100,
|
||||||
|
} as OntimeEvent,
|
||||||
|
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500, duration: 100 } as OntimeEvent,
|
||||||
];
|
];
|
||||||
|
|
||||||
const initResult = generate(testRundown);
|
const initResult = generate(testRundown);
|
||||||
@@ -186,9 +193,16 @@ describe('generate()', () => {
|
|||||||
it('calculates total duration with 0 duration events without causing a next day', () => {
|
it('calculates total duration with 0 duration events without causing a next day', () => {
|
||||||
const testRundown: OntimeRundown = [
|
const testRundown: OntimeRundown = [
|
||||||
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 100, duration: 0 } as OntimeEvent,
|
{ type: SupportedEvent.Event, id: '1', timeStart: 100, timeEnd: 100, duration: 0 } as OntimeEvent,
|
||||||
{ type: SupportedEvent.Event, id: '2', timeStart: 100, timeEnd: 300 } as OntimeEvent,
|
{ type: SupportedEvent.Event, id: '2', timeStart: 100, timeEnd: 300, duration: 200 } as OntimeEvent,
|
||||||
{ type: SupportedEvent.Event, id: 'skipped', skip: true, timeStart: 300, timeEnd: 400 } as OntimeEvent,
|
{
|
||||||
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500 } as OntimeEvent,
|
type: SupportedEvent.Event,
|
||||||
|
id: 'skipped',
|
||||||
|
skip: true,
|
||||||
|
timeStart: 300,
|
||||||
|
timeEnd: 400,
|
||||||
|
duration: 0,
|
||||||
|
} as OntimeEvent,
|
||||||
|
{ type: SupportedEvent.Event, id: '3', timeStart: 400, timeEnd: 500, duration: 100 } as OntimeEvent,
|
||||||
];
|
];
|
||||||
|
|
||||||
const initResult = generate(testRundown);
|
const initResult = generate(testRundown);
|
||||||
@@ -201,27 +215,28 @@ describe('generate()', () => {
|
|||||||
{
|
{
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
id: '1',
|
id: '1',
|
||||||
timeStart: new Date(0).setHours(9),
|
timeStart: 9 * MILLIS_PER_HOUR,
|
||||||
timeEnd: new Date(0).setHours(23),
|
timeEnd: 23 * MILLIS_PER_HOUR,
|
||||||
|
duration: (23 - 9) * MILLIS_PER_HOUR,
|
||||||
} as OntimeEvent,
|
} as OntimeEvent,
|
||||||
{
|
{
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
id: '2',
|
id: '2',
|
||||||
timeStart: new Date(0).setHours(9),
|
timeStart: 9 * MILLIS_PER_HOUR,
|
||||||
timeEnd: new Date(0).setHours(23),
|
timeEnd: 23 * MILLIS_PER_HOUR,
|
||||||
|
duration: (23 - 9) * MILLIS_PER_HOUR,
|
||||||
} as OntimeEvent,
|
} as OntimeEvent,
|
||||||
{
|
{
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
id: '3',
|
id: '3',
|
||||||
timeStart: new Date(0).setHours(9),
|
timeStart: 9 * MILLIS_PER_HOUR,
|
||||||
timeEnd: new Date(0).setHours(23),
|
timeEnd: 23 * MILLIS_PER_HOUR,
|
||||||
|
duration: (23 - 9) * MILLIS_PER_HOUR,
|
||||||
} as OntimeEvent,
|
} as OntimeEvent,
|
||||||
];
|
];
|
||||||
|
|
||||||
const initResult = generate(testRundown);
|
const initResult = generate(testRundown);
|
||||||
const expectedDuration = (23 - 9 + 48) * MILLIS_PER_HOUR;
|
expect(initResult.totalDuration).toBe((23 - 9 + 48) * MILLIS_PER_HOUR);
|
||||||
expect(millisToString(initResult.totalDuration)).toBe('62:00:00');
|
|
||||||
expect(initResult.totalDuration).toBe(expectedDuration);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculates total duration across days', () => {
|
it('calculates total duration across days', () => {
|
||||||
@@ -229,20 +244,21 @@ describe('generate()', () => {
|
|||||||
{
|
{
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
id: '1',
|
id: '1',
|
||||||
timeStart: new Date(0).setHours(12),
|
timeStart: 12 * MILLIS_PER_HOUR,
|
||||||
timeEnd: new Date(0).setHours(22),
|
timeEnd: 22 * MILLIS_PER_HOUR,
|
||||||
|
duration: 10 * MILLIS_PER_HOUR,
|
||||||
} as OntimeEvent,
|
} as OntimeEvent,
|
||||||
{
|
{
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
id: '2',
|
id: '2',
|
||||||
timeStart: new Date(0).setHours(22),
|
timeStart: 22 * MILLIS_PER_HOUR,
|
||||||
timeEnd: new Date(0).setHours(8),
|
timeEnd: 8 * MILLIS_PER_HOUR,
|
||||||
|
duration: (24 - 22 + 8) * MILLIS_PER_HOUR,
|
||||||
} as OntimeEvent,
|
} as OntimeEvent,
|
||||||
];
|
];
|
||||||
|
|
||||||
const initResult = generate(testRundown);
|
const initResult = generate(testRundown);
|
||||||
const expectedDuration = 8 * MILLIS_PER_HOUR + (dayInMs - 12 * MILLIS_PER_HOUR);
|
const expectedDuration = 8 * MILLIS_PER_HOUR + (dayInMs - 12 * MILLIS_PER_HOUR);
|
||||||
expect(millisToString(initResult.totalDuration)).toBe('20:00:00');
|
|
||||||
expect(initResult.totalDuration).toBe(expectedDuration);
|
expect(initResult.totalDuration).toBe(expectedDuration);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ import {
|
|||||||
CustomFields,
|
CustomFields,
|
||||||
isOntimeDelay,
|
isOntimeDelay,
|
||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
|
isPlayableEvent,
|
||||||
MaybeNumber,
|
MaybeNumber,
|
||||||
OntimeEvent,
|
OntimeEvent,
|
||||||
OntimeRundown,
|
OntimeRundown,
|
||||||
OntimeRundownEntry,
|
OntimeRundownEntry,
|
||||||
|
PlayableEvent,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { generateId, insertAtIndex, reorderArray, swapEventData, checkIsNextDay } from 'ontime-utils';
|
import { generateId, insertAtIndex, reorderArray, swapEventData, getTimeFromPrevious } from 'ontime-utils';
|
||||||
|
|
||||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
import { createPatch } from '../../utils/parser.js';
|
import { createPatch } from '../../utils/parser.js';
|
||||||
import { getTotalDuration } from '../timerUtils.js';
|
|
||||||
import { apply } from './delayUtils.js';
|
import { apply } from './delayUtils.js';
|
||||||
import { handleCustomField, handleLink, hasChanges, isDataStale } from './rundownCacheUtils.js';
|
import { handleCustomField, handleLink, hasChanges, isDataStale } from './rundownCacheUtils.js';
|
||||||
|
|
||||||
@@ -83,73 +83,72 @@ export function generate(
|
|||||||
totalDuration = 0;
|
totalDuration = 0;
|
||||||
totalDelay = 0;
|
totalDelay = 0;
|
||||||
|
|
||||||
let accumulatedDelay = 0;
|
let previousEntry: PlayableEvent | null = null;
|
||||||
let daySpan = 0;
|
let lastEntry: PlayableEvent | null = null;
|
||||||
let previousStart: MaybeNumber = null;
|
|
||||||
let previousEnd: MaybeNumber = null;
|
|
||||||
let previousDuration: MaybeNumber = null;
|
|
||||||
|
|
||||||
for (let i = 0; i < initialRundown.length; i++) {
|
for (let i = 0; i < initialRundown.length; i++) {
|
||||||
const currentEvent = initialRundown[i];
|
// TODO: filter properties that should not be persisted (eg: delay)
|
||||||
const updatedEvent = { ...currentEvent };
|
// we assign a reference to the current entry, this will be mutated in place
|
||||||
|
const currentEntry = initialRundown[i];
|
||||||
|
|
||||||
if (isOntimeEvent(updatedEvent)) {
|
if (isOntimeEvent(currentEntry)) {
|
||||||
// 1. handle links
|
// 1. handle links - mutates updatedEvent
|
||||||
handleLink(i, initialRundown, updatedEvent, links);
|
handleLink(i, initialRundown, currentEntry, links);
|
||||||
|
|
||||||
// 2. handle custom fields
|
// 2. handle custom fields - mutates updatedEvent
|
||||||
handleCustomField(customFields, customFieldChangelog, updatedEvent, assignedCustomFields);
|
handleCustomField(customFields, customFieldChangelog, currentEntry, assignedCustomFields);
|
||||||
|
|
||||||
// update the persisted event
|
// update rundown metadata, it only concerns playable events
|
||||||
initialRundown[i] = updatedEvent;
|
if (isPlayableEvent(currentEntry)) {
|
||||||
|
|
||||||
// we need to generate the skip event, but dont want to use its times
|
|
||||||
if (!updatedEvent.skip) {
|
|
||||||
// update rundown duration
|
|
||||||
if (firstStart === null) {
|
if (firstStart === null) {
|
||||||
firstStart = updatedEvent.timeStart;
|
firstStart = currentEntry.timeStart;
|
||||||
}
|
}
|
||||||
lastEnd = updatedEvent.timeEnd;
|
// TODO: carry on last event
|
||||||
|
lastEnd = currentEntry.timeEnd;
|
||||||
|
|
||||||
// check if we go over midnight, account for eventual gaps
|
const timeFromPrevious: number = getTimeFromPrevious(
|
||||||
const gapOverMidnight =
|
currentEntry.timeStart,
|
||||||
previousStart !== null && checkIsNextDay(previousStart, updatedEvent.timeStart, previousDuration);
|
currentEntry.timeEnd,
|
||||||
const durationOverMidnight = updatedEvent.timeStart > updatedEvent.timeEnd;
|
previousEntry?.timeStart,
|
||||||
if (gapOverMidnight || durationOverMidnight) {
|
previousEntry?.timeEnd,
|
||||||
daySpan++;
|
previousEntry?.duration,
|
||||||
|
);
|
||||||
|
totalDuration += timeFromPrevious + currentEntry.duration;
|
||||||
|
|
||||||
|
// remove eventual gaps from the accumulated delay
|
||||||
|
// we only affect positive delays (time forwards)
|
||||||
|
if (totalDelay > 0 && previousEntry) {
|
||||||
|
const gap = Math.max(currentEntry.timeStart - previousEntry.timeEnd, 0);
|
||||||
|
totalDelay = Math.max(totalDelay - gap, 0);
|
||||||
}
|
}
|
||||||
|
// current event delay is the current accumulated delay
|
||||||
|
currentEntry.delay = totalDelay;
|
||||||
|
// keep copy of event
|
||||||
|
previousEntry = currentEntry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculate delays
|
// calculate delays
|
||||||
// !!! this must happen after handling the links
|
// !!! this must happen after handling the links
|
||||||
if (isOntimeDelay(updatedEvent)) {
|
if (isOntimeDelay(currentEntry)) {
|
||||||
accumulatedDelay += updatedEvent.duration;
|
totalDelay += currentEntry.duration;
|
||||||
} else if (isOntimeEvent(updatedEvent) && !updatedEvent.skip) {
|
|
||||||
const eventStart = updatedEvent.timeStart;
|
|
||||||
|
|
||||||
// we only affect positive delays (time forwards)
|
|
||||||
if (accumulatedDelay > 0 && previousEnd) {
|
|
||||||
const gap = Math.max(eventStart - previousEnd, 0);
|
|
||||||
accumulatedDelay = Math.max(accumulatedDelay - gap, 0);
|
|
||||||
}
|
|
||||||
updatedEvent.delay = accumulatedDelay;
|
|
||||||
previousStart = updatedEvent.timeStart;
|
|
||||||
previousEnd = updatedEvent.timeEnd;
|
|
||||||
previousDuration = updatedEvent.duration;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
order.push(updatedEvent.id);
|
// eslint-disable-next-line no-unused-labels -- dev code path
|
||||||
rundown[updatedEvent.id] = { ...updatedEvent };
|
DEV: {
|
||||||
|
if (totalDuration < 0) {
|
||||||
|
throw new Error('rundownCache.generate: invalid data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add id to order
|
||||||
|
order.push(currentEntry.id);
|
||||||
|
// add entry to rundown
|
||||||
|
rundown[currentEntry.id] = currentEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
isStale = false;
|
isStale = false;
|
||||||
customFieldChangelog.clear();
|
customFieldChangelog.clear();
|
||||||
totalDelay = accumulatedDelay;
|
|
||||||
if (lastEnd !== null && firstStart !== null) {
|
|
||||||
totalDuration = getTotalDuration(firstStart, lastEnd, daySpan);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { rundown, order, links, totalDelay, totalDuration, assignedCustomProperties: assignedCustomFields };
|
return { rundown, order, links, totalDelay, totalDuration, assignedCustomProperties: assignedCustomFields };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ const initialRuntime: Runtime = {
|
|||||||
numEvents: 0, // change initiated by user
|
numEvents: 0, // change initiated by user
|
||||||
offset: 0, // changes at runtime
|
offset: 0, // changes at runtime
|
||||||
plannedStart: 0, // only changes if event changes
|
plannedStart: 0, // only changes if event changes
|
||||||
plannedEnd: 0, // only changes if event changes
|
plannedEnd: 0, // only changes if event changes, overflows over dayInMs
|
||||||
actualStart: null, // set once we start the timer
|
actualStart: null, // set once we start the timer
|
||||||
expectedEnd: null, // changes with runtime, based on offset
|
expectedEnd: null, // changes with runtime, based on offset, overflows over dayInMs
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const initialTimer: TimerState = {
|
const initialTimer: TimerState = {
|
||||||
@@ -60,7 +60,7 @@ export type RuntimeState = {
|
|||||||
timer: TimerState;
|
timer: TimerState;
|
||||||
// private properties of the timer calculations
|
// private properties of the timer calculations
|
||||||
_timer: {
|
_timer: {
|
||||||
forceFinish: MaybeNumber; // wether we should declare an event as finished, will contain the finish time
|
forceFinish: MaybeNumber; // whether we should declare an event as finished, will contain the finish time
|
||||||
totalDelay: number; // this value comes from rundown service
|
totalDelay: number; // this value comes from rundown service
|
||||||
pausedAt: MaybeNumber;
|
pausedAt: MaybeNumber;
|
||||||
secondaryTarget: MaybeNumber;
|
secondaryTarget: MaybeNumber;
|
||||||
@@ -149,6 +149,7 @@ type RundownData = {
|
|||||||
* @param playableRundown
|
* @param playableRundown
|
||||||
*/
|
*/
|
||||||
export function updateRundownData(rundownData: RundownData) {
|
export function updateRundownData(rundownData: RundownData) {
|
||||||
|
// we keep this in private state since there is no UI use case for it
|
||||||
runtimeState._timer.totalDelay = rundownData.totalDelay;
|
runtimeState._timer.totalDelay = rundownData.totalDelay;
|
||||||
|
|
||||||
runtimeState.runtime.numEvents = rundownData.numEvents;
|
runtimeState.runtime.numEvents = rundownData.numEvents;
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ export { validateEndAction, validateTimerType } from './src/validate-events/vali
|
|||||||
// 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';
|
||||||
|
export { checkOverlap } from './src/date-utils/checkOverlap.js';
|
||||||
|
export { getTimeFromPrevious } from './src/date-utils/getTimeFromPrevious.js';
|
||||||
|
|
||||||
// feature business logic - spreadsheet import
|
// feature business logic - spreadsheet import
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -8,6 +8,6 @@
|
|||||||
* 09:00 - 10:00
|
* 09:00 - 10:00
|
||||||
* 09:30 - 10:30
|
* 09:30 - 10:30
|
||||||
*/
|
*/
|
||||||
export function checkIsNextDay(previousStart: number, timeStart: number, previousDuration?: number): boolean {
|
export function checkIsNextDay(previousStart: number, timeStart: number, previousDuration: number): boolean {
|
||||||
return previousDuration === 0 ? false : timeStart <= previousStart;
|
return previousDuration === 0 ? false : timeStart <= previousStart;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { checkOverlap } from './checkOverlap';
|
||||||
|
|
||||||
|
describe('checkOverlap', () => {
|
||||||
|
it('should return true if events fully overlap', () => {
|
||||||
|
expect(checkOverlap(1000, 2000, 1000, 2000)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if one of the events is inside the other', () => {
|
||||||
|
expect(checkOverlap(1000, 2000, 1000, 1000)).toBe(true);
|
||||||
|
expect(checkOverlap(1000, 2000, 1500, 1750)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if events don't overlap", () => {
|
||||||
|
expect(checkOverlap(1000, 2000, 2000, 3000)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Check if two Ontime events overlap
|
||||||
|
* @link https://stackoverflow.com/questions/3269434/whats-the-most-efficient-way-to-test-if-two-ranges-overlap
|
||||||
|
* We use the deconstructed times to facilitate implementation in UI
|
||||||
|
*/
|
||||||
|
export function checkOverlap(
|
||||||
|
previousStart: number,
|
||||||
|
previousEnd: number,
|
||||||
|
currentStart: number,
|
||||||
|
currentEnd: number,
|
||||||
|
): boolean {
|
||||||
|
// deal with simple case where the event is later
|
||||||
|
if (currentStart >= previousEnd) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// at this point we know there may be an overlap
|
||||||
|
return Math.max(previousStart, currentStart) - Math.min(previousEnd, currentEnd) <= 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { getTimeFromPrevious } from './getTimeFromPrevious';
|
||||||
|
|
||||||
|
describe('getTimeFromPrevious', () => {
|
||||||
|
it('returns the time elapsed (gap or overlap) from the previous', () => {
|
||||||
|
const previousStart = 69600000; // 19:20
|
||||||
|
const previousEnd = 71700000; // 19:55
|
||||||
|
const previousDuration = 2100000; // 35 minutes
|
||||||
|
const currentStart = 75600000; // 21:00
|
||||||
|
const currentEnd = 81000000; // 22:30
|
||||||
|
const expected = 75600000 - 71700000; // current staart - previousEnd
|
||||||
|
|
||||||
|
expect(getTimeFromPrevious(currentStart, currentEnd, previousStart, previousEnd, previousDuration)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { checkIsNextDay } from './checkIsNextDay';
|
||||||
|
import { checkOverlap } from './checkOverlap';
|
||||||
|
import { dayInMs } from './conversionUtils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility returns the time elapsed (gap or overlap) from the previous
|
||||||
|
* It uses deconstructed parameters to simplify implementation in UI
|
||||||
|
*/
|
||||||
|
export function getTimeFromPrevious(
|
||||||
|
currentStart: number,
|
||||||
|
currentEnd: number,
|
||||||
|
previousStart?: number,
|
||||||
|
previousEnd?: number,
|
||||||
|
previousDuration?: number,
|
||||||
|
): number {
|
||||||
|
// there is no previous event
|
||||||
|
if (previousStart == null || previousEnd == null || previousDuration == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// event is linked to previous
|
||||||
|
if (currentStart === previousEnd) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// event is the day after
|
||||||
|
if (checkIsNextDay(previousStart, currentStart, previousDuration)) {
|
||||||
|
// duration is difference between normalised start and previous end
|
||||||
|
return currentStart + dayInMs - previousEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// event has a gap from previous
|
||||||
|
if (currentStart > previousEnd) {
|
||||||
|
return currentStart - previousEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// event overlaps with previous
|
||||||
|
if (checkOverlap(previousStart, previousEnd, currentStart, currentEnd)) {
|
||||||
|
// duration is the amount of time the current event has over the previous
|
||||||
|
// this value must be capped at 0
|
||||||
|
return Math.max(currentEnd - previousEnd, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we need to make sure we return a number, but there are no business cases for this
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user