mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-16 12:53:32 +00:00
More rundown metadata (#1429)
* add dayOffset to OntimeEvent * refactor: isNewLatest * calculate totalDays * refactor: getTimeFromPrevious * add gap as dataset in OntimeEvent * use in ui * format overlap is just a simple text formatting, big test is not needed * add new fields where needed * update apply delay * show nex day eaven if there is no gap * create test * use buildin day offset in timeline * remove todo * consistent naming * make a calculateDayOffset util for rundownCache * refactor: checkIsNextDay to use dayOffset * spelling Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> * remove unneeded test * remove todo * update test db * use data-testid * spelling Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com> * update comments * remove null option --------- Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
9ba40c35a1
commit
e9a7cd0400
@@ -150,11 +150,11 @@ describe('apply()', () => {
|
||||
makeOntimeDelay(100),
|
||||
makeOntimeEvent({ id: '1', timeStart: 0, timeEnd: 100, duration: 100 }),
|
||||
// gap 50
|
||||
makeOntimeEvent({ id: '2', timeStart: 150, timeEnd: 200, duration: 50 }),
|
||||
makeOntimeEvent({ id: '2', timeStart: 150, timeEnd: 200, duration: 50, gap: 50 }),
|
||||
// gap 0
|
||||
makeOntimeEvent({ id: '3', timeStart: 200, timeEnd: 250, duration: 50, gap: 0 }),
|
||||
// gap 50
|
||||
makeOntimeEvent({ id: '3', timeStart: 200, timeEnd: 250, duration: 50 }),
|
||||
// gap 50
|
||||
makeOntimeEvent({ id: '4', timeStart: 300, timeEnd: 350, duration: 50 }),
|
||||
makeOntimeEvent({ id: '4', timeStart: 300, timeEnd: 350, duration: 50, gap: 50 }),
|
||||
// linked
|
||||
makeOntimeEvent({ id: '5', timeStart: 350, timeEnd: 400, duration: 50, linkStart: '4' }),
|
||||
];
|
||||
@@ -165,7 +165,7 @@ describe('apply()', () => {
|
||||
// gap 50 (100 - 50)
|
||||
{ id: '2', timeStart: 150 + 50, timeEnd: 200 + 50, duration: 50, revision: 2 },
|
||||
// gap 50 (50 - 50)
|
||||
{ id: '3', timeStart: 200 + 50, timeEnd: 250 + 50, duration: 50, revision: 2 },
|
||||
{ id: '3', timeStart: 200 + 50, timeEnd: 250 + 50, duration: 50, revision: 2, gap: 0 },
|
||||
// gap (delay is 0)
|
||||
{ id: '4', timeStart: 300, timeEnd: 350, duration: 50, revision: 1 },
|
||||
// linked
|
||||
@@ -178,6 +178,8 @@ describe('apply()', () => {
|
||||
makeOntimeDelay(2 * MILLIS_PER_HOUR),
|
||||
makeOntimeEvent({
|
||||
id: '1',
|
||||
gap: 0,
|
||||
dayOffset: 0,
|
||||
timeStart: 46800000, // 13:00:00
|
||||
timeEnd: 50400000, // 14:00:00
|
||||
duration: MILLIS_PER_HOUR,
|
||||
@@ -185,6 +187,8 @@ describe('apply()', () => {
|
||||
// gap 1h
|
||||
makeOntimeEvent({
|
||||
id: '2',
|
||||
gap: 1 * MILLIS_PER_HOUR,
|
||||
dayOffset: 0,
|
||||
timeStart: 54000000, // 15:00:00
|
||||
timeEnd: 57600000, // 16:00:00
|
||||
duration: MILLIS_PER_HOUR,
|
||||
|
||||
@@ -577,6 +577,8 @@ describe('calculateRuntimeDelays', () => {
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
delay: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '659e1',
|
||||
@@ -605,6 +607,8 @@ describe('calculateRuntimeDelays', () => {
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
delay: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '1c48f',
|
||||
@@ -633,6 +637,8 @@ describe('calculateRuntimeDelays', () => {
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
delay: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: 'd48c2',
|
||||
@@ -661,6 +667,8 @@ describe('calculateRuntimeDelays', () => {
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
delay: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '2f185',
|
||||
@@ -701,6 +709,8 @@ describe('getDelayAt()', () => {
|
||||
timeDanger: 60000,
|
||||
id: '659e1',
|
||||
delay: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
cue: '1',
|
||||
custom: {},
|
||||
},
|
||||
@@ -725,6 +735,8 @@ describe('getDelayAt()', () => {
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '1c48f',
|
||||
@@ -753,6 +765,8 @@ describe('getDelayAt()', () => {
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: 'd48c2',
|
||||
@@ -781,6 +795,8 @@ describe('getDelayAt()', () => {
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '2f185',
|
||||
@@ -835,6 +851,8 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '659e1',
|
||||
@@ -863,6 +881,8 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '1c48f',
|
||||
@@ -891,6 +911,8 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: 'd48c2',
|
||||
@@ -919,6 +941,8 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
||||
colour: '',
|
||||
type: SupportedEvent.Event,
|
||||
revision: 0,
|
||||
dayOffset: 0,
|
||||
gap: 0,
|
||||
timeWarning: 120000,
|
||||
timeDanger: 60000,
|
||||
id: '2f185',
|
||||
|
||||
@@ -9,12 +9,14 @@ import {
|
||||
} from 'ontime-types';
|
||||
import {
|
||||
addToCustomAssignment,
|
||||
calculateDayOffset,
|
||||
getLink,
|
||||
handleCustomField,
|
||||
handleLink,
|
||||
hasChanges,
|
||||
isDataStale,
|
||||
} from '../rundownCacheUtils.js';
|
||||
import { MILLIS_PER_HOUR } from 'ontime-utils';
|
||||
|
||||
describe('getLink()', () => {
|
||||
it('should return null if there is no link', () => {
|
||||
@@ -247,3 +249,61 @@ describe('hasChanges()', () => {
|
||||
expect(hasChanges(existing, newEvent)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateDayOffset', () => {
|
||||
it('returns 0 if there is no previous event', () => {
|
||||
expect(calculateDayOffset({ timeStart: 0 })).toBe(0);
|
||||
});
|
||||
|
||||
it('returns 0 if the previous event duration is 0', () => {
|
||||
expect(calculateDayOffset({ timeStart: 0 }, { timeStart: 0, duration: 0 })).toBe(0);
|
||||
});
|
||||
|
||||
it('returns 0 if event starts after previous', () => {
|
||||
expect(calculateDayOffset({ timeStart: 11 }, { timeStart: 10, duration: 2 })).toBe(0);
|
||||
});
|
||||
|
||||
it('returns 1 if event starts before previous', () => {
|
||||
expect(calculateDayOffset({ timeStart: 9 }, { timeStart: 10, duration: 2 })).toBe(1);
|
||||
});
|
||||
|
||||
it('returns 1 if event starts at the same time as one before', () => {
|
||||
expect(calculateDayOffset({ timeStart: 10 }, { timeStart: 10, duration: 2 })).toBe(1);
|
||||
});
|
||||
|
||||
it('should account for an event that crossed midnight and there is a overlap', () => {
|
||||
expect(
|
||||
calculateDayOffset(
|
||||
{ timeStart: MILLIS_PER_HOUR }, // starts at 01:00:00
|
||||
{ timeStart: 20 * MILLIS_PER_HOUR, duration: 6 * MILLIS_PER_HOUR }, // ends at 02:00:00
|
||||
),
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('should account for an event that crossed midnight and there is a gap', () => {
|
||||
expect(
|
||||
calculateDayOffset(
|
||||
{ timeStart: 2 * MILLIS_PER_HOUR }, // starts at 02:00:00
|
||||
{ timeStart: 23 * MILLIS_PER_HOUR, duration: 2 * MILLIS_PER_HOUR }, // ends at 01:00:00
|
||||
),
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('should account for an event that crossed midnight with no overlaps or gaps', () => {
|
||||
expect(
|
||||
calculateDayOffset(
|
||||
{ timeStart: 2 * MILLIS_PER_HOUR }, // starts at 02:00:00
|
||||
{ timeStart: 20 * MILLIS_PER_HOUR, duration: 6 * MILLIS_PER_HOUR }, // ends at 02:00:00
|
||||
),
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('should account for an event that finishes exactly at midnight', () => {
|
||||
expect(
|
||||
calculateDayOffset(
|
||||
{ timeStart: 2 * MILLIS_PER_HOUR }, // starts at 02:00:00
|
||||
{ timeStart: 23 * MILLIS_PER_HOUR, duration: 6 * MILLIS_PER_HOUR }, // ends at 24:00:00
|
||||
),
|
||||
).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OntimeRundown, isOntimeDelay, isOntimeBlock, isOntimeEvent, OntimeEvent } from 'ontime-types';
|
||||
import { getTimeFromPrevious, deleteAtIndex } from 'ontime-utils';
|
||||
import { deleteAtIndex } from 'ontime-utils';
|
||||
|
||||
/**
|
||||
* Calculates all delays in a given rundown
|
||||
@@ -133,21 +133,14 @@ export function apply(eventId: string, rundown: OntimeRundown): OntimeRundown {
|
||||
|
||||
// if the event is not linked, we try and maintain gaps
|
||||
if (lastEntry !== null) {
|
||||
const timeFromPrevious: number = getTimeFromPrevious(
|
||||
currentEntry.timeStart,
|
||||
lastEntry.timeStart,
|
||||
lastEntry.timeEnd,
|
||||
lastEntry.duration,
|
||||
);
|
||||
|
||||
// when applying negative delays, we need to unlink the event
|
||||
// if the previous event was fully consumed by the delay
|
||||
if (currentEntry.linkStart && delayValue < 0 && lastEntry.timeStart + delayValue < 0) {
|
||||
shouldUnlink = true;
|
||||
}
|
||||
|
||||
if (timeFromPrevious > 0) {
|
||||
delayValue = Math.max(delayValue - timeFromPrevious, 0);
|
||||
if (currentEntry.gap > 0) {
|
||||
delayValue = Math.max(delayValue - currentEntry.gap, 0);
|
||||
}
|
||||
|
||||
if (delayValue === 0) {
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||
import { createPatch } from '../../utils/parser.js';
|
||||
import { apply } from './delayUtils.js';
|
||||
import { handleCustomField, handleLink, hasChanges, isDataStale } from './rundownCacheUtils.js';
|
||||
import { calculateDayOffset, handleCustomField, handleLink, hasChanges, isDataStale } from './rundownCacheUtils.js';
|
||||
|
||||
type EventID = string;
|
||||
type NormalisedRundown = Record<EventID, OntimeRundownEntry>;
|
||||
@@ -55,6 +55,7 @@ function setIsStale() {
|
||||
|
||||
let totalDelay = 0;
|
||||
let totalDuration = 0;
|
||||
let totalDays = 0;
|
||||
let firstStart: MaybeNumber = null;
|
||||
let lastEnd: MaybeNumber = null;
|
||||
|
||||
@@ -107,6 +108,7 @@ export function generate(
|
||||
firstStart = null;
|
||||
lastEnd = null;
|
||||
totalDuration = 0;
|
||||
totalDays = 0;
|
||||
totalDelay = 0;
|
||||
|
||||
let lastEntry: PlayableEvent | null = null;
|
||||
@@ -117,6 +119,7 @@ export function generate(
|
||||
|
||||
if (isOntimeEvent(currentEntry)) {
|
||||
currentEntry.delay = 0;
|
||||
currentEntry.gap = 0;
|
||||
|
||||
// 1. handle links - mutates updatedEvent
|
||||
handleLink(i, initialRundown, currentEntry, links);
|
||||
@@ -124,6 +127,9 @@ export function generate(
|
||||
// 2. handle custom fields - mutates updatedEvent
|
||||
handleCustomField(customFields, customFieldChangelog, currentEntry, assignedCustomFields);
|
||||
|
||||
totalDays += calculateDayOffset(currentEntry, lastEntry);
|
||||
currentEntry.dayOffset = totalDays;
|
||||
|
||||
// update rundown metadata, it only concerns playable events
|
||||
if (isPlayableEvent(currentEntry)) {
|
||||
// fist start is always the first event
|
||||
@@ -131,36 +137,31 @@ export function generate(
|
||||
firstStart = currentEntry.timeStart;
|
||||
}
|
||||
|
||||
const timeFromPrevious: number = getTimeFromPrevious(
|
||||
currentEntry.timeStart,
|
||||
lastEntry?.timeStart,
|
||||
lastEntry?.timeEnd,
|
||||
lastEntry?.duration,
|
||||
);
|
||||
currentEntry.gap = getTimeFromPrevious(currentEntry, lastEntry);
|
||||
|
||||
if (timeFromPrevious === 0) {
|
||||
if (currentEntry.gap === 0) {
|
||||
// event starts on previous finish, we add its duration
|
||||
totalDuration += currentEntry.duration;
|
||||
} else if (timeFromPrevious > 0) {
|
||||
} else if (currentEntry.gap > 0) {
|
||||
// event has a gap, we add the gap and the duration
|
||||
totalDuration += timeFromPrevious + currentEntry.duration;
|
||||
} else if (timeFromPrevious < 0) {
|
||||
totalDuration += currentEntry.gap + currentEntry.duration;
|
||||
} else if (currentEntry.gap < 0) {
|
||||
// there is an overlap, we remove the overlap from the duration
|
||||
// ensuring that the sum is not negative (ie: fully overlapped events)
|
||||
// NOTE: we add the gap since it is a negative number
|
||||
totalDuration += Math.max(currentEntry.duration + timeFromPrevious, 0);
|
||||
totalDuration += Math.max(currentEntry.duration + currentEntry.gap, 0);
|
||||
}
|
||||
|
||||
// remove eventual gaps from the accumulated delay
|
||||
// we only affect positive delays (time forwards)
|
||||
if (totalDelay > 0 && timeFromPrevious > 0) {
|
||||
totalDelay = Math.max(totalDelay - timeFromPrevious, 0);
|
||||
if (totalDelay > 0 && currentEntry.gap > 0) {
|
||||
totalDelay = Math.max(totalDelay - currentEntry.gap, 0);
|
||||
}
|
||||
// current event delay is the current accumulated delay
|
||||
currentEntry.delay = totalDelay;
|
||||
|
||||
// lastEntry is the event with the latest end time
|
||||
if (isNewLatest(currentEntry.timeStart, currentEntry.timeEnd, lastEntry?.timeStart, lastEntry?.timeEnd)) {
|
||||
if (isNewLatest(currentEntry, lastEntry)) {
|
||||
lastEntry = currentEntry;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
OntimeRundownEntry,
|
||||
OntimeBaseEvent,
|
||||
} from 'ontime-types';
|
||||
import { getLinkedTimes } from 'ontime-utils';
|
||||
import { dayInMs, getLinkedTimes } from 'ontime-utils';
|
||||
|
||||
/**
|
||||
* Get linked event
|
||||
@@ -147,3 +147,36 @@ export function hasChanges<T extends OntimeBaseEvent>(existingEvent: T, newEvent
|
||||
(key) => !Object.hasOwn(existingEvent, key) || existingEvent[key as keyof T] !== newEvent[key as keyof T],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility for calculating if the current events should have a day offset
|
||||
* @param current the current event under test
|
||||
* @param previous the previous event
|
||||
* @returns 0 or 1 for easy accumulation with the total days
|
||||
*/
|
||||
export function calculateDayOffset(
|
||||
current: Pick<OntimeEvent, 'timeStart'>,
|
||||
previous?: Pick<OntimeEvent, 'timeStart' | 'duration'>,
|
||||
) {
|
||||
// if there is no previous there can't be a day offset
|
||||
if (!previous) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// if the previous events duration is zero it will push the current event to next day
|
||||
if (previous.duration === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// if the previous event crossed midnight then the current event is in the next day
|
||||
if (previous.timeStart + previous.duration >= dayInMs) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// if the current events starts at the same time or before the previous event then it is the next day
|
||||
if (current.timeStart <= previous.timeStart) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ describe('cellRequestFromEvent()', () => {
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
delay: 0,
|
||||
gap: 0,
|
||||
dayOffset: 0,
|
||||
revision: 0,
|
||||
id: '1358',
|
||||
timeWarning: 0,
|
||||
@@ -84,6 +86,8 @@ describe('cellRequestFromEvent()', () => {
|
||||
colour: 'red',
|
||||
revision: 0,
|
||||
delay: 0,
|
||||
gap: 0,
|
||||
dayOffset: 0,
|
||||
id: '1358',
|
||||
timeWarning: 0,
|
||||
timeDanger: 0,
|
||||
@@ -132,6 +136,8 @@ describe('cellRequestFromEvent()', () => {
|
||||
colour: 'red',
|
||||
revision: 0,
|
||||
delay: 0,
|
||||
gap: 0,
|
||||
dayOffset: 0,
|
||||
id: '1358',
|
||||
timeWarning: 0,
|
||||
timeDanger: 0,
|
||||
@@ -178,6 +184,8 @@ describe('cellRequestFromEvent()', () => {
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
delay: 0,
|
||||
gap: 0,
|
||||
dayOffset: 0,
|
||||
revision: 0,
|
||||
id: '1358',
|
||||
timeWarning: 0,
|
||||
@@ -212,6 +220,8 @@ describe('cellRequestFromEvent()', () => {
|
||||
colour: 'red',
|
||||
revision: 0,
|
||||
delay: 0,
|
||||
gap: 0,
|
||||
dayOffset: 0,
|
||||
id: '1358',
|
||||
timeWarning: 0,
|
||||
timeDanger: 0,
|
||||
@@ -246,6 +256,8 @@ describe('cellRequestFromEvent()', () => {
|
||||
colour: 'red',
|
||||
revision: 0,
|
||||
delay: 0,
|
||||
gap: 0,
|
||||
dayOffset: 0,
|
||||
id: '1358',
|
||||
timeWarning: 0,
|
||||
timeDanger: 0,
|
||||
|
||||
Reference in New Issue
Block a user