bat-8.x-1.x-dev/modules/bat_roomify/src/Calendar/AbstractCalendar.php
modules/bat_roomify/src/Calendar/AbstractCalendar.php
<?php
/**
* @file
* Class AbstractCalendar
*/
namespace Drupal\bat_roomify\Calendar;
use Drupal\bat_roomify\Event\Event;
use Drupal\bat_roomify\Unit\Unit;
use Drupal\bat_roomify\Calendar\CalendarInterface;
use Drupal\bat_roomify\Calendar\CalendarResponse;
use Drupal\bat_roomify\Event\EventItemizer;
/**
* Handles querying and updating state stores
*/
abstract class AbstractCalendar implements CalendarInterface {
/**
* The units we are dealing with. If no unit ids set the calendar will return
* results for date range and all units within that range.
*
* @var array
*/
protected $units;
/**
* The class that will access the actual event store where event data is held.
*
* @var \Drupal\bat_roomify\Store\StoreInterface
*/
protected $store;
/**
* The default value for events. In the event store this is represented by 0 which is then
* replaced by the default value provided in the constructor.
*
* @var
*/
protected $default_value;
/**
* Stores itemized events allowing us to perform searches over them without having to pull
* them out of storage (i.e. reducing DB calls)
*
* @var array
*/
protected $itemized_events;
/**
* {@inheritdoc}
*/
public function addEvents($events, $granularity) {
$added = TRUE;
foreach ($events as $event) {
// Events save themselves so here we cycle through each and return true if all events
// were saved
$check = $event->saveEvent($this->store, $granularity);
if ($check == FALSE) {
$added = FALSE;
break;
}
}
return $added;
}
/**
* Given a start and end time will retrieve events from the defined store.
*
* If unit_ids where defined it will filter for those unit ids.
*
* @param \DateTime $start_date
* @param \DateTime $end_date
* @param $reset - if set to TRUE we will always refer to the Store to retrieve events
*
* @return array
*/
public function getEvents(\DateTime $start_date, \DateTime $end_date, $reset = TRUE) {
if ($reset || empty($this->itemized_events)) {
// We first get events in the itemized format
$this->itemized_events = $this->getEventsItemized($start_date, $end_date);
}
// We then normalize those events to create Events that get added to an array
return $this->getEventsNormalized($start_date, $end_date, $this->itemized_events);
}
/**
* Given a start and end time this will return the states units find themselves in for that range.
*
* @param \DateTime $start_date
* @param \DateTime $end_date
* @param $reset - if set to TRUE we will refer to the Store to retrieve events
*
* @return array
* An array of states keyed by unit
*/
public function getStates(\DateTime $start_date, \DateTime $end_date, $reset = TRUE) {
$events = $this->getEvents($start_date, $end_date, $reset);
$states = [];
foreach ($events as $unit => $unit_events) {
foreach ($unit_events as $event) {
$states[$unit][$event->getValue()] = $event->getValue();
}
}
return $states;
}
/**
* Given a date range and a set of valid states it will return all units within the
* set of valid states.
* If intersect is set to TRUE a unit will report as matched as long as within the time
* period requested it finds itself at least once within a valid state.
* Alternatively units will match ONLY if they find themselves withing the valid states and
* no other state.
*
* @param \DateTime $start_date
* @param \DateTime $end_date
* @param $valid_states
* @param $constraints
* @param $intersect - performs an intersect rather than a diff on valid states
* @param $reset - if set to true we refer to the Store to retrieve events
*
* @return CalendarResponse
*/
public function getMatchingUnits(\DateTime $start_date, \DateTime $end_date, $valid_states, $constraints = [], $intersect = FALSE, $reset = TRUE) {
$data = [];
$d['units'] = [];
$d['response'] = new CalendarResponse($start_date, $end_date, $valid_states);
$d['keyed_units'] = $this->keyUnitsById();
$d['states'] = $this->getStates($start_date, $end_date, $reset);
foreach ($d['states'] as $unit => $unit_states) {
// Create an array with just the states
$current_states = array_keys($unit_states);
// Compare the current states with the set of valid states
if ($intersect) {
$remaining_states = array_intersect($current_states, $valid_states);
}
else {
$remaining_states = array_diff($current_states, $valid_states);
}
if ((count($remaining_states) == 0 && !$intersect) || (count($remaining_states) > 0 && $intersect)) {
// Unit is in a state that is within the set of valid states so add to result set
$units[$unit] = $unit;
$d['response']->addMatch($d['keyed_units'][$unit], CalendarResponse::VALID_STATE);
}
else {
$d['response']->addMiss($d['keyed_units'][$unit], CalendarResponse::INVALID_STATE);
}
$unit_constraints = $d['keyed_units'][$unit]->getConstraints();
$d['response']->applyConstraints($unit_constraints);
}
$d['response']->applyConstraints($constraints);
return $d['response'];
}
/**
* Provides an itemized array of events keyed by the unit_id and divided by day,
* hour and minute.
*
* @param \DateTime $start_date
* @param \DateTime $end_date
* @param String $granularity
*
* @return array
*/
public function getEventsItemized(\DateTime $start_date, \DateTime $end_date, $granularity = Event::BAT_HOURLY) {
// The final events we will return
$events = [];
$keyed_units = $this->keyUnitsById();
$db_events = $this->store->getEventData($start_date, $end_date, array_keys($keyed_units));
// Create a mock itemized event for the period in question - since event data is either
// in the database or the default value we first create a mock event and then fill it in
// accordingly
$mock_event = new Event($start_date, $end_date, new Unit(0,0), $this->default_value);
$itemized = $mock_event->itemize(new EventItemizer($mock_event, $granularity));
// Cycle through each unit retrieved and provide it with a fully configured itemized mock event
foreach ($db_events as $unit => $event) {
// Add the mock event
$events[$unit] = $itemized;
$events[$unit][Event::BAT_DAY] = $this->itemizeDays($db_events, $itemized, $unit, $keyed_units);
// Handle hours
if (isset($itemized[Event::BAT_HOUR]) || isset($db_events[$unit][Event::BAT_HOUR])) {
$events[$unit][Event::BAT_HOUR] = $this->itemizeHours($db_events, $itemized, $unit, $keyed_units);
} else {
// No hours - set an empty array
$events[$unit][Event::BAT_HOUR] = [];
}
// Handle minutes
if (isset($itemized[Event::BAT_MINUTE]) || isset($db_events[$unit][Event::BAT_MINUTE])) {
$events[$unit][Event::BAT_MINUTE] = $this->itemizeMinutes($db_events, $itemized, $unit, $keyed_units);
} else {
// No minutes - set an empty array
$events[$unit][Event::BAT_MINUTE] = [];
}
}
foreach ($keyed_units as $id => $unit) {
// OLD Comment :If we don't have any db events add mock events (itemized)
// mar25 question to comment: Why?
if ((isset($events[$id]) && count($events[$id]) == 0) || !isset($events[$id])) {
$empty_event = new Event($start_date, $end_date, $unit, $unit->getDefaultValue());
// Mar25: do we need empty events from mock??
//$events[$id] = $empty_event->itemize(new EventItemizer($empty_event, $granularity));
}
}
return $events;
}
/**
* Helper function that cycles through db results and setups the BAT_DAY itemized array
*
* @param $db_events
* @param $itemized
* @param $unit
* @param $keyed_units
*
* @return array
*/
private function itemizeDays($db_events, $itemized, $unit, $keyed_units) {
$result = [];
foreach ($itemized[Event::BAT_DAY] as $year => $months) {
foreach ($months as $month => $days) {
// Check if month is defined in DB otherwise set to default value
if (isset($db_events[$unit][Event::BAT_DAY][$year][$month])) {
foreach ($days as $day => $value) {
$result[$year][$month][$day] = ((int)$db_events[$unit][Event::BAT_DAY][$year][$month][$day] == 0 ? $keyed_units[$unit]->getDefaultValue() : (int)$db_events[$unit][Event::BAT_DAY][$year][$month][$day]);
}
}
else {
foreach ($days as $day => $value) {
$result[$year][$month][$day] = $keyed_units[$unit]->getDefaultValue();
}
}
}
}
return $result;
}
/**
* Helper function that cycles through db results and setups the BAT_HOUR itemized array
* @param $db_events
* @param $itemized
* @param $unit
* @param $keyed_units
*
* @return array
*/
private function itemizeHours($db_events, $itemized, $unit, $keyed_units) {
$result = [];
if (isset($itemized[Event::BAT_HOUR])) {
foreach ($itemized[Event::BAT_HOUR] as $year => $months) {
foreach ($months as $month => $days) {
foreach ($days as $day => $hours) {
foreach ($hours as $hour => $value) {
if (isset($db_events[$unit][Event::BAT_HOUR][$year][$month][$day][$hour])) {
$result[$year][$month][$day][$hour] = ((int) $db_events[$unit][Event::BAT_HOUR][$year][$month][$day][$hour] == 0 ? $keyed_units[$unit]->getDefaultValue() : (int) $db_events[$unit][Event::BAT_HOUR][$year][$month][$day][$hour]);
}
else {
// If nothing from db - then revert to the defaults
$result[$year][$month][$day][$hour] = (int) $keyed_units[$unit]->getDefaultValue();
}
}
}
}
}
}
// Now fill in hour data coming from the database which the mock event did *not* cater for in the data structure
if (isset($db_events[$unit][Event::BAT_HOUR])) {
foreach ($db_events[$unit][Event::BAT_HOUR] as $year => $months) {
foreach ($months as $month => $days) {
foreach ($days as $day => $hours) {
foreach ($hours as $hour => $value) {
$result[$year][$month][$day][$hour] = ((int) $value == 0 ? $keyed_units[$unit]->getDefaultValue() : (int) $value);
}
ksort($result[$year][$month][$day], SORT_NATURAL);
}
}
}
}
return $result;
}
/**
* Helper function that cycles through db results and setups the BAT_MINUTE itemized array
*
* @param $db_events
* @param $itemized
* @param $unit
* @param $keyed_units
*
* @return array
*/
private function itemizeMinutes($db_events, $itemized, $unit, $keyed_units) {
$result = [];
if (isset($itemized[Event::BAT_MINUTE])) {
foreach ($itemized[Event::BAT_MINUTE] as $year => $months) {
foreach ($months as $month => $days) {
foreach ($days as $day => $hours) {
foreach ($hours as $hour => $minutes) {
foreach ($minutes as $minute => $value) {
if (isset($db_events[$unit][Event::BAT_MINUTE][$year][$month][$day][$hour][$minute])) {
$result[$year][$month][$day][$hour][$minute] = ((int) $db_events[$unit][Event::BAT_MINUTE][$year][$month][$day][$hour][$minute] == 0 ? $keyed_units[$unit]->getDefaultValue() : (int) $db_events[$unit][Event::BAT_MINUTE][$year][$month][$day][$hour][$minute]);
}
else {
// If nothing from db - then revert to the defaults
$result[$year][$month][$day][$hour][$minute] = (int) $keyed_units[$unit]->getDefaultValue();
}
}
}
}
}
}
}
// Now fill in minute data coming from the database which the mock event did *not* cater for
if (isset($db_events[$unit][Event::BAT_MINUTE])) {
foreach ($db_events[$unit][Event::BAT_MINUTE] as $year => $months) {
foreach ($months as $month => $days) {
foreach ($days as $day => $hours) {
foreach ($hours as $hour => $minutes) {
foreach ($minutes as $minute => $value) {
$result[$year][$month][$day][$hour][$minute] = ((int) $value == 0 ? $keyed_units[$unit]->getDefaultValue() : (int) $value);
}
ksort($result[$year][$month][$day][$hour], SORT_NATURAL);
}
}
}
}
}
return $result;
}
/**
* Given an itemized set of event data it will return an array of Events
*
* @param \DateTime $start_date
* @param \DateTime $end_date
* @param $events
*
* @return array
*/
public function getEventsNormalized(\DateTime $start_date, \DateTime $end_date, $events) {
// Daylight Saving Time
$timezone = new \DateTimeZone(date_default_timezone_get());
$transitions = $timezone->getTransitions($start_date->getTimestamp(), $end_date->getTimestamp());
$dst_transitions = [];
unset($transitions[0]);
foreach ($transitions as $transition) {
if ($transition['isdst']) {
$dst_transitions[] = $transition['ts'] - 60;
}
}
$is_daylight_saving_time = (empty($dst_transitions)) ? FALSE : TRUE;
$normalized_events = [];
$events_copy = $events;
foreach ($events_copy as $unit_id => $data) {
// Make sure years are sorted
ksort($data[Event::BAT_DAY]);
if (isset($data[Event::BAT_HOUR])) ksort($data[Event::BAT_HOUR]);
if (isset($data[Event::BAT_MINUTE])) ksort($data[Event::BAT_MINUTE]);
// Set up variables to keep track of stuff
$current_value = NULL;
$start_event = new \DateTime();
$end_event = new \DateTime();
foreach ($data[Event::BAT_DAY] as $year => $months) {
// Make sure months are in right order
ksort($months);
foreach ($months as $month => $days) {
foreach ($days as $day => $value) {
if ($value == -1) {
// Retrieve hour data
$hour_data = $events[$unit_id][Event::BAT_HOUR][$year][$month][$day];
ksort($hour_data, SORT_NATURAL);
foreach ($hour_data as $hour => $hour_value) {
if ($hour_value == -1) {
// We are going to need minute values
$minute_data = $events[$unit_id][Event::BAT_MINUTE][$year][$month][$day][$hour];
ksort($minute_data, SORT_NATURAL);
foreach ($minute_data as $minute => $minute_value) {
if ($current_value === $minute_value) {
// We are still in minutes and going through so add a minute
$end_event->add(new \DateInterval('PT1M'));
}
elseif (($current_value != $minute_value) && ($current_value !== NULL)) {
// Value just switched - let us wrap up with current event and start a new one
$normalized_events[$unit_id][] = new Event($start_event, $end_event, $this->getUnit($unit_id), $current_value);
$start_event = clone($end_event->add(new \DateInterval('PT1M')));
$end_event = new \DateTime($year . '-' . $month . '-' . substr($day, 1) . ' ' . substr($hour, 1) . ':' . substr($minute,1));
$current_value = $minute_value;
}
if ($current_value === NULL) {
// We are down to minutes and haven't created and event yet - do one now
$start_event = new \DateTime($year . '-' . $month . '-' . substr($day, 1) . ' ' . substr($hour, 1) . ':' . substr($minute,1));
$end_event = clone($start_event);
}
$current_value = $minute_value;
}
}
elseif ($current_value === $hour_value) {
// We are in hours and can add something
$end_event->add(new \DateInterval('PT1H'));
}
elseif (($current_value != $hour_value) && ($current_value !== NULL)) {
$skip_finalize_event = FALSE;
if ($is_daylight_saving_time) {
if (in_array($end_event->getTimestamp(), $dst_transitions)) {
$skip_finalize_event = TRUE;
}
}
if ($skip_finalize_event === FALSE) {
// Value just switched - let us wrap up with current event and start a new one
$normalized_events[$unit_id][] = new Event($start_event, $end_event, $this->getUnit($unit_id), $current_value);
// Start event becomes the end event with a minute added
$start_event = clone($end_event->add(new \DateInterval('PT1M')));
// End event comes the current point in time
$end_event = new \DateTime($year . '-' . $month . '-' . substr($day, 1) . ' ' . substr($hour, 1) . ':59');
$current_value = $hour_value;
}
}
if ($current_value === NULL) {
// Got into hours and still haven't created an event so
$start_event = new \DateTime($year . '-' . $month . '-' . substr($day, 1) . ' ' . substr($hour, 1) . ':00');
// We will be occupying at least this hour so might as well mark it
$end_event = new \DateTime($year . '-' . $month . '-' . substr($day, 1) . ' ' . substr($hour, 1) . ':59');
$current_value = $hour_value;
}
}
}
elseif ($current_value === $value) {
// We are adding a whole day so the end event gets moved to the end of the day we are adding
$end_event = new \DateTime($year . '-' . $month . '-' . substr($day, 1) . ' ' . '23:59');
}
elseif (($current_value !== $value) && ($current_value !== NULL)) {
// Value just switched - let us wrap up with current event and start a new one
$normalized_events[$unit_id][] = new Event($start_event, $end_event, $this->getUnit($unit_id), $current_value);
// Start event becomes the end event with a minute added
$start_event = clone($end_event->add(new \DateInterval('PT1M')));
// End event becomes the current day which we have not account for yet
$end_event = new \DateTime($year . '-' . $month . '-' . substr($day, 1) . ' ' . '23:59');
$current_value = $value;
}
if ($current_value === NULL) {
// We have not created an event yet so let's do it now
$start_event = new \DateTime($year . '-' . $month . '-' . substr($day, 1) . ' ' . '00:00');
$end_event = new \DateTime($year . '-' . $month . '-' . substr($day, 1) . ' ' . '23:59');
$current_value = $value;
}
}
}
}
// Add the last event in for which there is nothing in the loop to catch it
$normalized_events[$unit_id][] = new Event($start_event, $end_event, $this->getUnit($unit_id), $current_value);
}
// Given the database structure we may get events that are not with the date ranges we were looking for
// We get rid of them here so that the user has a clean result.
foreach ($normalized_events as $unit_id => $events) {
foreach ($events as $key => $event) {
if ($event->overlaps($start_date, $end_date)) {
// Adjust start or end dates of events so everything is within range
if ($event->startsEarlier($start_date)) {
$event->setStartDate($start_date);
}
if ($event->endsLater($end_date)) {
$event->setEndDate($end_date);
}
}
else {
// Event completely not in range so unset it
unset($normalized_events[$unit_id][$key]);
}
}
}
return $normalized_events;
}
/**
* A simple utility function that given an array of datum=>value will group results based on
* those that have the same value. Useful for grouping events based on state.
*
* @param $data
* @param $length
*/
public function groupData($data, $length) {
$flipped = [];
$e = 0;
$j = 0;
$old_value = NULL;
foreach ($data as $datum => $value) {
$j++;
if ($j <= $length) {
// If the value has changed and we are not just starting
if (($value != $old_value)) {
$e++;
$flipped[$e][$value][$datum] = $datum;
$old_value = $value;
}
else {
$flipped[$e][$value][$datum] = $datum;
}
}
}
}
/**
* Return an array of unit ids from the set of units
* supplied to the Calendar.
*
* @return array
*/
protected function getUnitIds() {
$unit_ids = [];
foreach ($this->units as $unit) {
$unit_ids[] = $unit->getUnitId();
}
return $unit_ids;
}
/**
* Return an array of units keyed by unit id
*
* @return array
*/
protected function keyUnitsById() {
$keyed_units = [];
foreach ($this->units as $unit) {
$keyed_units[$unit->getUnitId()] = $unit;
}
return $keyed_units;
}
/**
* Returns the unit object.
*
* @param $unit_id
* @return Unit
*/
protected function getUnit($unit_id) {
$keyed = $this->keyUnitsById();
return $keyed[$unit_id];
}
}
