I'm creating some kind of calendar/agenda which shows the events for a specific day. Each event is displayed as an HTML element in a vertical hours grid. There could be multiple ("colliding") events at the same time, and in those cases the elements should be placed next to each other, horizontally, and have equal widths. E.g. four colliding events get the column value 4, in this way the width of 25%.
The tricky part is these colliding events. I thought I solved it, but some elements get the wrong number of columns.
There might be a better way to calculate the column count and placement - I'm open to suggestions.
Sample image for current (wrong) result:
Relevant code:
<?php
class Calendar {
const ROW_HEIGHT = 24;
public $events = array();
public $blocks = array();
public function calculate_blocks() {
foreach($this->events as $event) {
// Calculate the correct height and vertical placement
$top = $this->time_to_pixels($event->_event_start_time);
$bottom = $this->time_to_pixels($event->_event_end_time);
$height = $bottom - $top;
// Abort if there's no height
if(!$height) continue;
$this->blocks[] = array(
'id' => $event->ID,
'columns' => 1,
'placement' => 0, // Column order, 0 = first
'css' => array(
'top' => $top,
'bottom' => $bottom, // bottom = top + height
'height' => $height
)
);
}
$done = array();
// Compare all the blocks with each other
foreach($this->blocks as &$block) {
foreach($this->blocks as &$sub) {
// Only compare two blocks once, and never compare a block with itself
if($block['id'] == $sub['id'] || (isset($done[$block['id']]) && in_array($sub['id'], $done[$block['id']])) || (isset($done[$sub['id']]) && in_array($block['id'], $done[$sub['id']]))) continue;
$done[$block['id']][] = $sub['id'];
// If the blocks are colliding
if(($sub['css']['top'] >= $block['css']['top'] && $sub['css']['top'] < $block['css']['bottom'])
|| ($sub['css']['bottom'] >= $block['css']['top'] && $sub['css']['bottom'] < $block['css']['bottom'])
|| ($sub['css']['top'] <= $block['css']['top'] && $sub['css']['bottom'] >= $block['css']['bottom'])) {
// Increase both blocks' columns and sub-block's placement
$sub['columns'] = ++$block['columns'];
$sub['placement']++;
}
}
}
}
private function time_to_int($time) {
// H:i:s (24-hour format)
$hms = explode(':', $time);
return ($hms[0] + ($hms[1] / 60) + ($hms[2] / 3600));
}
private function time_to_pixels($time) {
$block = $this->time_to_int($time);
return (int)round($block * self::ROW_HEIGHT * 2);
}
}
?>
Try this:
public function calculate_blocks()
{
$n = count($events);
$collumns = array();
$placements = array();
// Set initial values.
for ($i = 0; $i < $n; $i++)
{
$collumns[$i] = 1;
$placements[$i] = 0;
}
// Loop over all events.
for ($i = 0; $i < $n; $i++)
{
$top1 = $this->time_to_pixels($events[$i]->_event_start_time);
$bottom1 = $this->time_to_pixels($events[$i]->_event_end_time);
// Check for collisions with events with higher indices.
for ($j = $i + 1; $j < $n; $j++)
{
$top2 = $this->time_to_pixels($events[$k]->_event_start_time);
$bottom2 = $this->time_to_pixels($events[$k]->_event_end_time);
$collides = $top1 < $bottom2 && $top2 < $bottom1;
// If there is a collision, increase the collumn count for both events and move the j'th event one place to the right.
if ($collides)
{
$collumns[$i]++;
$collumns[$j]++;
$placements[$j]++;
}
}
$this->blocks[] = array(
'id' => $events[$i]->ID,
'columns' => $collumns[$i],
'placement' => $placements[$i],
'css' => array(
'top' => $top1,
'bottom' => $bottom1,
'height' => $bottom1 - $top1;
)
);
}
}
I can't actually test it, but I think it should leave you with a correct blocks array.
Edit 1: Doesn't seem to yield the required result, see comments below.
Edit 2: I think this is the exact same problem: Visualization of calendar events. Algorithm to layout events with maximum width. Someone solved it with C#, but it should be relatively easy to port that answer to PHP to solve your problem.