import { OntimeEvent } from 'ontime-types'; import { dayInMs } from 'ontime-utils'; /** * handle events that span over midnight */ export const normaliseEndTime = (start: number, end: number) => (end < start ? end + dayInMs : end); /** * @description Sorts an array of objects by given property * @param {array} arr - array to be sorted * @param {string} property - property to compare * @returns {array} copy of array sorted in ascending order */ export const sortArrayByProperty = (arr: T[], property: string): T[] => { return [...arr].sort((a, b) => { return a[property] - b[property]; }); }; /** * Finds loading information given a current rundown and time * @param {OntimeEvent[]} rundown - List of playable events * @param {number} timeNow - time now in ms * @returns {{}} */ export const getRollTimers = (rundown: OntimeEvent[], timeNow: number) => { let nowIndex: number | null = null; // index of event now let nowId: string | null = null; // id of event now let publicIndex: number | null = null; // index of public event now let nextIndex: number | null = null; // index of next event let publicNextIndex: number | null = null; // index of next public event let timeToNext: number | null = null; // counter: time for next event let publicTimeToNext: number | null = null; // counter: time for next public event const orderedEvents = sortArrayByProperty(rundown, 'timeStart'); const lastEvent = orderedEvents[orderedEvents.length - 1]; const lastNormalEnd = normaliseEndTime(lastEvent.timeStart, lastEvent.timeEnd); let nextEvent: OntimeEvent | null = null; let nextPublicEvent: OntimeEvent | null = null; let currentEvent: OntimeEvent | null = null; let currentPublicEvent: OntimeEvent | null = null; if (timeNow > lastNormalEnd) { // we are past last end // preload first and find next const firstEvent = orderedEvents[0]; nextIndex = 0; nextEvent = firstEvent; timeToNext = firstEvent.timeStart + dayInMs - timeNow; if (firstEvent.isPublic) { nextPublicEvent = firstEvent; publicNextIndex = 0; } else { // look for next public // dev note: we feel that this is more efficient than filtering // since the next event will likely be close to the one playing for (const event of orderedEvents) { if (event.isPublic) { nextPublicEvent = event; // we need the index before this was sorted publicNextIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id); break; } } } } else { // flags: select first event if several overlapping let nowFound = false; // keep track of the end times when looking for public let publicTime = -1; for (const event of orderedEvents) { // When does the event end (handle midnight) const normalEnd = normaliseEndTime(event.timeStart, event.timeEnd); const hasNotEnded = normalEnd > timeNow; const isFromDayBefore = normalEnd > dayInMs && timeNow < event.timeEnd; const hasStarted = isFromDayBefore || timeNow >= event.timeStart; if (normalEnd <= timeNow) { // event ran already if (event.isPublic && normalEnd > publicTime) { // public event might not be the one running publicTime = normalEnd; currentPublicEvent = event; publicIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id); } } else if (hasNotEnded && hasStarted && !nowFound) { // event is running currentEvent = event; nowIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id); nowId = event.id; nowFound = true; // it could also be public if (event.isPublic) { publicTime = normalEnd; currentPublicEvent = event; publicIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id); } } else if (normalEnd > timeNow) { // event will run // we already know whats next and next-public if (nextIndex !== null && publicNextIndex !== null) { continue; } // look for next events // check how far the start is from now const timeToEventStart = event.timeStart - timeNow; // we don't have a next or this one starts sooner than current next if (nextIndex === null || timeToEventStart < timeToNext) { timeToNext = timeToEventStart; nextEvent = event; nextIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id); } if (event.isPublic) { // if we don't have a public next or this one start sooner than assigned next if (publicNextIndex === null || timeToEventStart < publicTimeToNext) { publicTimeToNext = timeToEventStart; nextPublicEvent = event; publicNextIndex = rundown.findIndex((rundownEvent) => rundownEvent.id === event.id); } } } } } return { nowIndex, nowId, publicIndex, nextIndex, publicNextIndex, timeToNext, nextEvent, nextPublicEvent, currentEvent, currentPublicEvent, }; }; type CurrentTimers = { selectedEventId: string | null; current: number | null; _finishAt: number | null; clock: number | null; secondaryTimer: number | null; secondaryTarget: number | null; }; /** * @description Implements update functions for roll mode * @param {CurrentTimers} currentTimers * @returns {object} object with selection variables */ export const updateRoll = (currentTimers: CurrentTimers) => { const { selectedEventId, current, _finishAt, clock, secondaryTimer, secondaryTarget } = currentTimers; // timers let updatedTimer = current; let updatedSecondaryTimer = secondaryTimer; // whether rollLoad should be called: force reload of events let doRollLoad = false; // whether finished event should trigger let isPrimaryFinished = false; if (selectedEventId && current !== null) { // if we have something selected and a timer, we are running updatedTimer = _finishAt - clock; if (updatedTimer > dayInMs) { updatedTimer -= dayInMs; } if (updatedTimer < 0) { isPrimaryFinished = true; // we need a new event doRollLoad = true; } } else if (secondaryTimer >= 0) { // if secondaryTimer is running we are in waiting to roll updatedSecondaryTimer = secondaryTarget - clock; if (updatedSecondaryTimer <= 0) { // we need a new event doRollLoad = true; } } return { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished: isPrimaryFinished }; };