Fixed program schedule generation algorithm

This commit is contained in:
2017-08-01 02:03:12 +02:00
parent b3a3e4e5da
commit 3ad71819a4
3 changed files with 161 additions and 183 deletions

View File

@@ -18,7 +18,7 @@ class ProgramController extends Controller
((`schedule`.`enddate` IS NULL) OR (`schedule`.`enddate` >= :startdate)) AND ((`schedule`.`enddate` IS NULL) OR (`schedule`.`enddate` >= :startdate)) AND
`schedule`.`active` = 1 AND `schedule`.`active` = 1 AND
`schedule`.`final` = 1 `schedule`.`final` = 1
ORDER BY `schedule`.`priority` DESC, MOD(`startday` + 6, 7) + 1, `schedule`.`starttime` ASC ORDER BY `schedule`.`priority` DESC, `schedule`.`startday` ASC, `schedule`.`starttime` ASC
QUERY; QUERY;
/** /**
@@ -30,6 +30,153 @@ QUERY;
{ {
// //
} }
public function addToSchedule(&$schedule, $start, $end, $program) {
if($start == $end) {
return;
}
$last = count($schedule) ? $schedule[count($schedule) - 1] : null;
if($last && ($last['program'] == $program) && ($last['end'] == $start))
{
// Merge with the previous entry
$last['end'] = $end;
}
else
{
$schedule[] = ['start' => clone $start, 'end' => clone $end, 'program' => new \Model\Program($program)];
}
}
public function createSchedule(\DateTimeImmutable $start, \DateTimeImmutable $end, $DEBUG = false) {
$startDay = $start->format('N');
$endDay = $end->format('N');
$schedule = [];
$START = 1; $END = -1;
$WEEK = new \DateInterval('P7D');
$WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" /* == 0 and 7 */];
$scheduleItems = app('db')->select(self::$SCHEDULE_SQL,
['enddate' => $end->format('Y-m-d'), 'startdate' => $start->format('Y-m-d')]);
$scheduleChanges = new \SplPriorityQueue();
$active = [];
foreach($scheduleItems as $item) {
// Find the start time and end time of the last occurrence on or before the start time
$itemStart = new \DateTime("last " . $WEEKDAYS[$item->startday] . " " . $start->format('Y-m-d') . " " . $item->starttime);
$itemEnd = new \DateTime("last " . $WEEKDAYS[$item->endday] . " " . $start->format('Y-m-d') . " " . $item->endtime);
if($itemEnd <= $itemStart) {
$itemEnd->add($WEEK);
}
// If the first occurrence is completely before the period of interest, look at the next week
while($itemEnd < $start) {
$itemStart->add($WEEK);
$itemEnd->add($WEEK);
}
// Create a list in which all start and end times are separate entries
$scheduleChanges->insert( [$START, $itemStart, $item], [-$itemStart->getTimestamp(), -$START] );
$scheduleChanges->insert( [$END, $itemEnd, $item], [-$itemEnd->getTimestamp(), -$END] );
// Also keep track of all priorities seen
$active[$item->priority] = null;
}
if($DEBUG) {
print "<h2>Schedule changes - initial state</h2><ul>";
foreach(clone $scheduleChanges as $c) {
print "<li>{$c[1]->format('d-m-Y H:i')}: " . ($c[0] == $START ? 'Start' : 'Einde') . " {$c[2]->name} {$c[2]->suffix}</li>";
}
print "</ul><hr/>";
}
// Go through all programs and keep going until we have one that starts after the end date.
$activeProgram = null;
$activeProgramStart = null;
while(true) {
$item = $scheduleChanges->extract();
if($DEBUG) print "<b>{$item[1]->format('d-m-Y H:i')}: {$item[2]->name} " . ($item[0] == $START ? "begint" : "eindigt") . "</b><br/>";
// If we passed the end time, we are (almost) done
if($item[1] > $end) {
if($DEBUG) { print "Reached end at {$item[1]->format('d-m-Y H:i')} ({$item[2]->name}).<br/>"; }
break;
}
$priority = $item[2]->priority;
if($item[0] == $START) {
if($active[$priority] != null) {
print( "<b>Error:</b>" . $item[2]->name . ' starts at ' . $item[1]->format('d-m-Y H:i') . ' but ' . $active[$priority]->name . ' is still active then.' );
print "<pre>"; var_dump($active); print "</pre>";
}
// When a program starts, it becomes the active program in its layer
$active[$priority] = $item[2];
// In addition, if it has higher priority than the currently active program, it replaces that.
if($activeProgram == null || $priority < $activeProgram->priority) {
if(($activeProgram != null) && ($item[1] > $start)) {
if($DEBUG) print "{$activeProgramStart->format('d-m-Y H:i')} -- {$item[1]->format('d-m-Y H:i')}: {$activeProgram->name} (reason: {$item[2]->name} starts).<br/>";
$this->addToSchedule($schedule, $activeProgramStart, $item[1], $activeProgram);
}
$activeProgramStart = ($item[1] < $start) ? $start : clone $item[1];
$activeProgram = $item[2];
}
} else if($item[0] == $END) {
if($active[$priority] == null) {
print( "<b>Error:</b>" . $item[2]->name . ' ends at ' . $item[1]->format('d-m-Y H:i') . ' but no program is active at that priority and timestamp.');
print "<pre>"; var_dump($active); print "</pre>";
}
// When the program ends, its layer becomes inactive.
$active[$priority] = null;
// In addition, when the ending program was the active program, we need to schedule it and find the new currently active program
if($activeProgram == $item[2]) {
if($DEBUG) print "{$activeProgramStart->format('d-m-Y H:i')} -- {$item[1]->format('d-m-Y H:i')}: {$activeProgram->name} (reason: {$item[2]->name} ends).<br/>";
$this->addToSchedule($schedule, $activeProgramStart, $item[1], $activeProgram);
$activeProgram = null;
foreach($active as $newPriority => $newActive) {
if(($newActive != null) && ($activeProgram == null || $newPriority < $activeProgram->priority)) {
$activeProgram = $newActive;
}
}
if($activeProgram != null) {
$activeProgramStart = clone $item[1];
}
if($DEBUG) print " New active program is " . ($activeProgram ? $activeProgram->name : "---") . "<br/>";
}
} else {
return response()->abort(500, "Invalid item type: expected START ($START) or END ($END), but got {$item[0]}.");
}
// The item will recur in a week
$item[1]->add($WEEK);
$scheduleChanges->insert($item, [-$item[1]->getTimestamp(), -$item[0]]);
}
if(($activeProgram != null) && ($activeProgramStart != $end)) {
if($DEBUG) print "{$activeProgramStart->format('d-m-Y H:i')} -- {$end->format('d-m-Y H:i')}: {$activeProgram->name} (reason: schedule finished).<br/>";
$this->addToSchedule($schedule, $activeProgramStart, $end, $activeProgram);
}
return ['startdate' => $start, 'enddate' => $end, 'schedule' => $schedule];
}
public function testSchedule($from, $to)
{
$start = new \DateTimeImmutable(urldecode($from));
$end = new \DateTimeImmutable(urldecode($to));
$this->createSchedule($start, $end, true);
}
/** /**
* Details over een specifiek programma, inclusief schema en podcasts * Details over een specifiek programma, inclusief schema en podcasts
@@ -51,88 +198,14 @@ QUERY;
AND `user_emailaddresses`.`preference` = 0 AND `user_emailaddresses`.`preference` = 0
AND `programs_users`.`active` = 1 AND `programs_users`.`active` = 1
ORDER BY `name`", ['program' => $program->id]); ORDER BY `name`", ['program' => $program->id]);
foreach($hosts as $host) { foreach($hosts as $host) {
$program->hosts[] = new \Model\ProgramHost($host); $program->hosts[] = new \Model\ProgramHost($host);
} }
$schedule = app('db')->select(<<<QUERY
SELECT `schedule`.`startdate`, `schedule`.`startday`, `schedule`.`starttime`, `schedule`.`enddate`, `schedule`.`endday`, `schedule`.`endtime`, `schedule`.`shortnamesuffix` AS `suffix`, `schedule`.`priority`
FROM `programs_schedule` AS `schedule`
WHERE (schedule.`program` = :program) AND (`schedule`.`active` = 1) AND (`schedule`.`final` = 1)
ORDER BY `schedule`.`priority` DESC, MOD(`startday` + 6, 7) + 1, `schedule`.`starttime` ASC
QUERY
, ['program' => $program->id] );
$recentSchedule = $this->recent($schedule);
$program->schedule = $recentSchedule;
return response()->json($program); return response()->json($program);
} }
private function recent($schedule) {
$now = new \DateTimeImmutable();
$ONE_DAY = \DateInterval::createFromDateString('1 day');
$ONE_WEEK = \DateInterval::createFromDateString('1 week');
$recent = [];
$next = [];
// Vind voor elke regel in de uitzendmomenten de eerstvolgende en twee meest recente.
$isCurrent = false;
foreach($schedule as $broadcast) {
$broadcast->startdate = ($broadcast->startdate == null) ? null : new \DateTimeImmutable($broadcast->startdate);
$broadcast->enddate = ($broadcast->enddate == null) ? null : new \DateTimeImmutable($broadcast->enddate);
$startTijd = new \DateTimeImmutable($broadcast->starttime);
$endDate = ($broadcast->enddate == null) ? $now : $broadcast->enddate;
$endDate = $endDate->setTime($startTijd->format('H'), $startTijd->format('i'), $startTijd->format('s'));
$endDateWeekday = $endDate->format('N');
// Zoek de laatste uitzending voor de einddatum (of voor het huidige tijdstip), op de juiste weekdag
while($endDateWeekday != $broadcast->startday) {
$endDate = $endDate->sub($ONE_DAY);
if(--$endDateWeekday < 0) { $endDateWeekday += 7; }
}
$eindTijd = new \DateTimeImmutable($broadcast->endtime);
$endOfBroadcast = $endDate->setTime($eindTijd->format('H'), $eindTijd->format('i'), $eindTijd->format('s'));
if($endOfBroadcast < $endDate) { $endOfBroadcast = $endOfBroadcast->add($ONE_DAY); }
if($isInProgress = (($endDate <= $now) && ($endOfBroadcast > $now))) {
$isCurrent = true;
}
// Als de uitzending niet al afgelopen is, telt hij als volgende.
// Dit gebeurt als de uitzending later vandaag is of nu loopt.
if($endOfBroadcast >= $now) {
$endDate = $endDate->sub($ONE_WEEK);
$endOfBroadcast = $endOfBroadcast->sub($ONE_WEEK);
}
if((($broadcast->startdate == null) || ($broadcast->startdate <= $endDate))
&& (($broadcast->enddate == null) || ($broadcast->enddate >= $endDate->setTime(0,0,0)))) {
$recent[] = ['time' => $endDate, 'suffix' => $broadcast->suffix, 'isSpecial' => ($broadcast->priority < 2)];
}
$previous = $endDate->sub($ONE_WEEK);
if((($broadcast->startdate == null) || ($broadcast->startdate <= $previous))
&& (($broadcast->enddate == null) || ($broadcast->enddate >= $previous->setTime(0,0,0)))) {
$recent[] = ['time' => $previous, 'suffix' => $broadcast->suffix, 'isSpecial' => ($broadcast->priority < 2)];
}
$following = $endDate->add($ONE_WEEK);
if(($broadcast->enddate == null) || ($broadcast->enddate->setTime(23,59,59) > $following)) {
if($following < $now) {
$isCurrent = true;
}
$next[] = ['time' => $following, 'suffix' => $broadcast->suffix, 'isSpecial' => ($broadcast->priority < 2)];
}
}
sort($next);
rsort($recent);
return ['recent' => array_reverse(array_splice($recent, 0, 2)), 'next' => count($next) ? $next[0] : null, 'iscurrent' => $isCurrent];
}
/** /**
* Programmas nu en straks (24 uur vooruit) * Programmas nu en straks (24 uur vooruit)
*/ */
@@ -153,109 +226,4 @@ QUERY
return response()->json($this->createSchedule($start, $einde)); return response()->json($this->createSchedule($start, $einde));
} }
private function createSchedule($start, $einde) {
if(!defined('PROGRAMMA_START')) {
define('PROGRAMMA_START', 1);
define('PROGRAMMA_EINDE', -1);
define('PW_TIJDSTIP', 0);
define('PW_TYPE', 1);
define('PW_PROGRAMMA', 2);
}
$oneWeek = \DateInterval::createFromDateString('1 week');
$programmas = app('db')->select(self::$SCHEDULE_SQL,
['enddate' => $einde->format('Y-m-d'), 'startdate' => $start->format('Y-m-d')]);
$programmaInfo = [];
// Maak een lijstje van alle start en eindtijden en welk programma dan begint/eindigt
$startEinde = [];
$startMaandag = $start->modify('Monday this week');
foreach($programmas as $programma) {
if($programma->suffix) $programma->name .= ' ' . $programma->suffix;
$programmaInfo[$programma->scheduleid] = new \Model\Program($programma);
$programmaStart = $startMaandag->add(\DateInterval::createFromDateString(($programma->startday - 1) % 7 . ' days'));
$programmaEinde = $startMaandag->add(\DateInterval::createFromDateString(($programma->endday - 1) % 7 . ' days'));
$startTijd = new \DateTime($programma->starttime);
$eindTijd = new \DateTime($programma->endtime);
$programmaStart = $programmaStart->setTime($startTijd->format('H'), $startTijd->format('i'), $startTijd->format('s'));
$programmaEinde = $programmaEinde->setTime($eindTijd->format('H'), $eindTijd->format('i'), $eindTijd->format('s'));
if($programmaEinde <= $programmaStart) {
$programmaEinde = $programmaEinde->add($oneWeek);
}
if($programmaEinde < $start) {
$programmaStart = $programmaStart->add($oneWeek);
$programmaEinde = $programmaEinde->add($oneWeek);
}
while($programmaStart < $einde) {
if((($programma->startdate == null) || ($programmaStart >= new \DateTime($programma->startdate)))
&& (($programma->enddate == null) || ($programmaStart <= (new \DateTimeImmutable($programma->enddate))->setTime(23,59,59)))
&& (($programmaEinde >= $start) && ($programmaStart <= $einde)))
{
$startEinde[] = [$programmaStart, PROGRAMMA_START, $programma->scheduleid];
$startEinde[] = [$programmaEinde, PROGRAMMA_EINDE, $programma->scheduleid];
}
$programmaStart = $programmaStart->add($oneWeek);
$programmaEinde = $programmaEinde->add($oneWeek);
}
}
array_multisort($startEinde);
$actieveProgrammas = [null, null, null, null];
$schema = [];
$fouten = [];
$index = -1;
foreach($startEinde as $programmaWissel) {
$scheduleId = $programmaWissel[PW_PROGRAMMA];
$programma = $programmaInfo[$scheduleId];
$tijdstip = $programmaWissel[PW_TIJDSTIP];
if($programmaWissel[PW_TIJDSTIP] < $start) { $tijdstip = $start; }
if($programmaWissel[PW_TIJDSTIP] > $einde) { $tijdstip = $einde; }
if($tijdstip == $einde) { break; }
if($programmaWissel[PW_TYPE] == PROGRAMMA_START) {
if($actieveProgrammas[$programma->priority] != null) {
$fouten[] = [$tijdstip, "Begin van {$programma->name} maar {$actieveProgrammas[$programma->priority]->name} is nog actief."];
}
$actieveProgrammas[$programma->priority] = $scheduleId;
for($prio = $programma->priority; $prio >= 0; --$prio) {
if($actieveProgrammas[$prio] != null) {
if(($index == -1) || ($schema[$index]['starttime'] != $tijdstip)) { $index++; }
$schema[$index] = ['starttime' => $tijdstip, 'endtime' => null, 'program' => $programmaInfo[$actieveProgrammas[$prio]]];
break;
}
}
} else /*if($programmaWissel[PW_TYPE] == PROGRAMMA_EINDE)*/ {
$actieveProgrammas[$programma->priority] = null;
if(($index == -1) || ($schema[$index]['program'] != null)) { $index++; }
$schema[$index] = ['starttime' => $tijdstip, 'program' => null];
for($prio = $programma->priority; $prio < count($actieveProgrammas); ++$prio) {
if($actieveProgrammas[$prio] != null) {
$schema[$index] = ['starttime' => $tijdstip, 'endtime' => null, 'program' => $programmaInfo[$actieveProgrammas[$prio]]];
break;
}
}
}
}
for($i = 1; $i <= count($schema); $i++) {
$eindTijd = ($i == count($schema)) ? $einde : $schema[$i]['starttime'];
$schema[$i - 1]['endtime'] = $eindTijd;
//$programma = $schema[$i-1]['program'];
//print $schema[$i-1]['starttime']->format('D d-m-Y, H:i:s') . " - " . $eindTijd->format('D d-m-Y, H:i:s') . ": " . ($programma != null ? $programma->name : "NULL") . "\n";
}
return ['startdate' => $start, 'enddate' => $einde, 'schedule' => $schema, 'errors' => $fouten];
}
} }

View File

@@ -32,7 +32,9 @@ $app->get('podcast/stream/{id:\d+}/{title}', 'PodcastController@stream' );
$app->get('programma/schema/nustraks', 'ProgramController@comingup' ); $app->get('programma/schema/nustraks', 'ProgramController@comingup' );
$app->get('programma/schema/week[/{shiftWeeks:-?\d+}]', 'ProgramController@schedule' ); $app->get('programma/schema/week[/{shiftWeeks:-?\d+}]', 'ProgramController@schedule' );
$app->get('/programma/details/{id:\d+}', 'ProgramController@details' ); $app->get('programma/details/{id:\d+}', 'ProgramController@details' );
$app->get('programma/schema/test/{from}/{to}', 'ProgramController@testSchedule' );
// live/onair // live/onair
// live/stream // live/stream

View File

@@ -9,12 +9,16 @@ class Program extends Model {
public $email; public $email;
public $priority; public $priority;
// TODO: Implementeren
public $hosts; public $hosts;
public $schedule; public $schedule;
public function __construct($data) { public function __construct($data) {
parent::__construct($data); parent::__construct($data);
if(isset($data->suffix) && $data->suffix) {
$this->name .= ' ' . $data->suffix;
}
if($this->schedule && $this->schedule->next && $this->schedule->next->time) { if($this->schedule && $this->schedule->next && $this->schedule->next->time) {
parent::ConvertToDateTime($this->schedule->next->time); parent::ConvertToDateTime($this->schedule->next->time);
} }
@@ -31,4 +35,8 @@ class Program extends Model {
$this->url = "/{$this->id}/" . parent::url_slug($this->name); $this->url = "/{$this->id}/" . parent::url_slug($this->name);
} }
public function isSpecial() {
return ($this->priority < 2);
}
} }