summaryrefslogtreecommitdiffstats
path: root/modules/monitoring/library/Monitoring/Timeline
diff options
context:
space:
mode:
Diffstat (limited to 'modules/monitoring/library/Monitoring/Timeline')
-rw-r--r--modules/monitoring/library/Monitoring/Timeline/TimeEntry.php233
-rw-r--r--modules/monitoring/library/Monitoring/Timeline/TimeLine.php491
-rw-r--r--modules/monitoring/library/Monitoring/Timeline/TimeRange.php258
3 files changed, 982 insertions, 0 deletions
diff --git a/modules/monitoring/library/Monitoring/Timeline/TimeEntry.php b/modules/monitoring/library/Monitoring/Timeline/TimeEntry.php
new file mode 100644
index 0000000..ee313b3
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Timeline/TimeEntry.php
@@ -0,0 +1,233 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Timeline;
+
+use DateTime;
+use Icinga\Web\Url;
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * An event group that is part of a timeline
+ */
+class TimeEntry
+{
+ /**
+ * The name of this group
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * The amount of events that are part of this group
+ *
+ * @var int
+ */
+ protected $value;
+
+ /**
+ * The date and time of this group
+ *
+ * @var DateTime
+ */
+ protected $dateTime;
+
+ /**
+ * The url to this group's detail view
+ *
+ * @var Url
+ */
+ protected $detailUrl;
+
+ /**
+ * The weight of this group
+ *
+ * @var float
+ */
+ protected $weight = 1.0;
+
+ /**
+ * The label of this group
+ *
+ * @var string
+ */
+ protected $label;
+
+ /**
+ * The CSS class of the entry
+ *
+ * @var string
+ */
+ protected $class;
+
+ /**
+ * Return a new TimeEntry object with the given attributes being set
+ *
+ * @param array $attributes The attributes to set
+ * @return TimeEntry The resulting TimeEntry object
+ * @throws ProgrammingError If one of the given attributes cannot be set
+ */
+ public static function fromArray(array $attributes)
+ {
+ $entry = new TimeEntry();
+
+ foreach ($attributes as $name => $value) {
+ $methodName = 'set' . ucfirst($name);
+ if (method_exists($entry, $methodName)) {
+ $entry->{$methodName}($value);
+ } else {
+ throw new ProgrammingError(
+ 'Method "%s" does not exist on object of type "%s"',
+ $methodName,
+ __CLASS__
+ );
+ }
+ }
+
+ return $entry;
+ }
+
+ /**
+ * Set this group's name
+ *
+ * @param string $name The name to set
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+ }
+
+ /**
+ * Return the name of this group
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Set this group's amount of events
+ *
+ * @param int $value The value to set
+ */
+ public function setValue($value)
+ {
+ $this->value = intval($value);
+ }
+
+ /**
+ * Return the amount of events in this group
+ *
+ * @return int
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Set this group's date and time
+ *
+ * @param DateTime $dateTime The date and time to set
+ */
+ public function setDateTime(DateTime $dateTime)
+ {
+ $this->dateTime = $dateTime;
+ }
+
+ /**
+ * Return the date and time of this group
+ *
+ * @return DateTime
+ */
+ public function getDateTime()
+ {
+ return $this->dateTime;
+ }
+
+ /**
+ * Set the url to this group's detail view
+ *
+ * @param Url $detailUrl The url to set
+ */
+ public function setDetailUrl(Url $detailUrl)
+ {
+ $this->detailUrl = $detailUrl;
+ }
+
+ /**
+ * Return the url to this group's detail view
+ *
+ * @return Url
+ */
+ public function getDetailUrl()
+ {
+ return $this->detailUrl;
+ }
+
+ /**
+ * Set this group's weight
+ *
+ * @param float $weight The weight for this group
+ */
+ public function setWeight($weight)
+ {
+ $this->weight = floatval($weight);
+ }
+
+ /**
+ * Return the weight of this group
+ *
+ * @return float
+ */
+ public function getWeight()
+ {
+ return $this->weight;
+ }
+
+ /**
+ * Set this group's label
+ *
+ * @param string $label The label to set
+ */
+ public function setLabel($label)
+ {
+ $this->label = $label;
+ }
+
+ /**
+ * Return the label of this group
+ *
+ * @return string
+ */
+ public function getLabel()
+ {
+ return $this->label;
+ }
+
+ /**
+ * Get the CSS class
+ *
+ * @return string
+ */
+ public function getClass()
+ {
+ return $this->class;
+ }
+
+ /**
+ * Set the CSS class
+ *
+ * @param string $class
+ *
+ * @return $this
+ */
+ public function setClass($class)
+ {
+ $this->class = $class;
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Timeline/TimeLine.php b/modules/monitoring/library/Monitoring/Timeline/TimeLine.php
new file mode 100644
index 0000000..128b64b
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Timeline/TimeLine.php
@@ -0,0 +1,491 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Timeline;
+
+use DateTime;
+use Exception;
+use ArrayIterator;
+use Icinga\Exception\IcingaException;
+use IteratorAggregate;
+use Icinga\Data\Filter\Filter;
+use Icinga\Web\Hook;
+use Icinga\Web\Session\SessionNamespace;
+use Icinga\Module\Monitoring\DataView\DataView;
+use Traversable;
+
+/**
+ * Represents a set of events in a specific range of time
+ */
+class TimeLine implements IteratorAggregate
+{
+ /**
+ * The resultset returned by the dataview
+ *
+ * @var array
+ */
+ private $resultset;
+
+ /**
+ * The groups this timeline uses for display purposes
+ *
+ * @var array
+ */
+ private $displayGroups;
+
+ /**
+ * The session to use
+ *
+ * @var SessionNamespace
+ */
+ protected $session;
+
+ /**
+ * The base that is used to calculate each circle's diameter
+ *
+ * @var float
+ */
+ protected $calculationBase;
+
+ /**
+ * The dataview to fetch entries from
+ *
+ * @var DataView
+ */
+ protected $dataview;
+
+ /**
+ * The names by which to group entries
+ *
+ * @var array
+ */
+ protected $identifiers;
+
+ /**
+ * The range of time for which to display entries
+ *
+ * @var TimeRange
+ */
+ protected $displayRange;
+
+ /**
+ * The range of time for which to calculate forecasts
+ *
+ * @var TimeRange
+ */
+ protected $forecastRange;
+
+ /**
+ * The maximum diameter each circle can have
+ *
+ * @var float
+ */
+ protected $circleDiameter = 100.0;
+
+ /**
+ * The minimum diameter each circle can have
+ *
+ * @var float
+ */
+ protected $minCircleDiameter = 1.0;
+
+ /**
+ * The unit of a circle's diameter
+ *
+ * @var string
+ */
+ protected $diameterUnit = 'px';
+
+ /**
+ * Return a iterator for this timeline
+ *
+ * @return ArrayIterator
+ */
+ public function getIterator(): Traversable
+ {
+ return new ArrayIterator($this->toArray());
+ }
+
+ /**
+ * Create a new timeline
+ *
+ * The given dataview must provide the following columns:
+ * - name A string identifying an entry (Corresponds to the keys of "$identifiers")
+ * - time A unix timestamp that defines where to place an entry on the timeline
+ *
+ * @param DataView $dataview The dataview to fetch entries from
+ * @param array $identifiers The names by which to group entries
+ */
+ public function __construct(DataView $dataview, array $identifiers)
+ {
+ $this->dataview = $dataview;
+ $this->identifiers = $identifiers;
+ }
+
+ /**
+ * Set the session to use
+ *
+ * @param SessionNamespace $session The session to use
+ */
+ public function setSession(SessionNamespace $session)
+ {
+ $this->session = $session;
+ }
+
+ /**
+ * Set the range of time for which to display elements
+ *
+ * @param TimeRange $range The range of time for which to display elements
+ */
+ public function setDisplayRange(TimeRange $range)
+ {
+ $this->displayRange = $range;
+ }
+
+ /**
+ * Set the range of time for which to calculate forecasts
+ *
+ * @param TimeRange $range The range of time for which to calculate forecasts
+ */
+ public function setForecastRange(TimeRange $range)
+ {
+ $this->forecastRange = $range;
+ }
+
+ /**
+ * Set the maximum diameter each circle can have
+ *
+ * @param string $width The diameter to set, suffixed with its unit
+ *
+ * @throws Exception If the given diameter is invalid
+ */
+ public function setMaximumCircleWidth($width)
+ {
+ $matches = array();
+ if (preg_match('#([\d|\.]+)([a-z]+|%)#', $width, $matches)) {
+ $this->circleDiameter = floatval($matches[1]);
+ $this->diameterUnit = $matches[2];
+ } else {
+ throw new IcingaException(
+ 'Width "%s" is not a valid width',
+ $width
+ );
+ }
+ }
+
+ /**
+ * Set the minimum diameter each circle can have
+ *
+ * @param string $width The diameter to set, suffixed with its unit
+ *
+ * @throws Exception If the given diameter is invalid or its unit differs from the maximum
+ */
+ public function setMinimumCircleWidth($width)
+ {
+ $matches = array();
+ if (preg_match('#([\d|\.]+)([a-z]+|%)#', $width, $matches)) {
+ if ($matches[2] === $this->diameterUnit) {
+ $this->minCircleDiameter = floatval($matches[1]);
+ } else {
+ throw new IcingaException(
+ 'Unit needs to be in "%s"',
+ $this->diameterUnit
+ );
+ }
+ } else {
+ throw new IcingaException(
+ 'Width "%s" is not a valid width',
+ $width
+ );
+ }
+ }
+
+ /**
+ * Return all known group types (identifiers) with their respective labels and classess as array
+ *
+ * @return array
+ */
+ public function getGroupInfo()
+ {
+ $groupInfo = array();
+ foreach ($this->identifiers as $name => $attributes) {
+ if (isset($attributes['groupBy'])) {
+ $name = $attributes['groupBy'];
+ }
+
+ $groupInfo[$name]['class'] = $attributes['class'];
+ $groupInfo[$name]['label'] = $attributes['label'];
+ }
+
+ return $groupInfo;
+ }
+
+ /**
+ * Return the circle's diameter for the given event group
+ *
+ * @param TimeEntry $group The group for which to return a circle width
+ * @param int $precision Amount of decimal places to preserve
+ *
+ * @return string
+ */
+ public function calculateCircleWidth(TimeEntry $group, $precision = 0)
+ {
+ $base = $this->getCalculationBase(true);
+ $factor = log($group->getValue() * $group->getWeight(), $base) / 100;
+ $width = $this->circleDiameter * $factor;
+ return sprintf(
+ '%.' . $precision . 'F%s',
+ $width > $this->minCircleDiameter ? $width : $this->minCircleDiameter,
+ $this->diameterUnit
+ );
+ }
+
+ /**
+ * Return an extrapolated circle width for the given event group
+ *
+ * @param TimeEntry $group The event group for which to return an extrapolated circle width
+ * @param int $precision Amount of decimal places to preserve
+ *
+ * @return string
+ */
+ public function getExtrapolatedCircleWidth(TimeEntry $group, $precision = 0)
+ {
+ $eventCount = 0;
+ foreach ($this->displayGroups as $groups) {
+ if (array_key_exists($group->getName(), $groups)) {
+ $eventCount += $groups[$group->getName()]->getValue();
+ }
+ }
+
+ $extrapolatedCount = (int) $eventCount / count($this->displayGroups);
+ if ($extrapolatedCount < $group->getValue()) {
+ return $this->calculateCircleWidth($group, $precision);
+ }
+
+ return $this->calculateCircleWidth(
+ TimeEntry::fromArray(
+ array(
+ 'value' => $extrapolatedCount,
+ 'weight' => $group->getWeight()
+ )
+ ),
+ $precision
+ );
+ }
+
+ /**
+ * Return the base that should be used to calculate circle widths
+ *
+ * @param bool $create Whether to generate a new base if none is known yet
+ *
+ * @return float|null
+ */
+ public function getCalculationBase($create)
+ {
+ if ($this->calculationBase === null) {
+ $calculationBase = $this->session !== null ? $this->session->get('calculationBase') : null;
+
+ if ($create) {
+ $new = $this->generateCalculationBase();
+ if ($new > $calculationBase) {
+ $this->calculationBase = $new;
+
+ if ($this->session !== null) {
+ $this->session->calculationBase = $new;
+ }
+ } else {
+ $this->calculationBase = $calculationBase;
+ }
+ } else {
+ return $calculationBase;
+ }
+ }
+
+ return $this->calculationBase;
+ }
+
+ /**
+ * Generate a new base to calculate circle widths with
+ *
+ * @return float
+ */
+ protected function generateCalculationBase()
+ {
+ $allEntries = $this->groupEntries(
+ array_merge(
+ $this->fetchEntries(),
+ $this->fetchForecasts()
+ ),
+ new TimeRange(
+ $this->displayRange->getStart(),
+ $this->forecastRange->getEnd(),
+ $this->displayRange->getInterval()
+ )
+ );
+
+ $highestValue = 0;
+ foreach ($allEntries as $groups) {
+ foreach ($groups as $group) {
+ if ($group->getValue() * $group->getWeight() > $highestValue) {
+ $highestValue = $group->getValue() * $group->getWeight();
+ }
+ }
+ }
+
+ return pow($highestValue, 1 / 100); // 100 == 100%
+ }
+
+ /**
+ * Fetch all entries and forecasts by using the dataview associated with this timeline
+ *
+ * @return array The dataview's result
+ */
+ private function fetchResults()
+ {
+ $hookResults = array();
+ foreach (Hook::all('timeline') as $timelineProvider) {
+ $hookResults = array_merge(
+ $hookResults,
+ $timelineProvider->fetchEntries($this->displayRange),
+ $timelineProvider->fetchForecasts($this->forecastRange)
+ );
+
+ foreach ($timelineProvider->getIdentifiers() as $identifier => $attributes) {
+ if (!array_key_exists($identifier, $this->identifiers)) {
+ $this->identifiers[$identifier] = $attributes;
+ }
+ }
+ }
+
+ $query = $this->dataview;
+ $filter = Filter::matchAll(
+ Filter::where('type', array_keys($this->identifiers)),
+ Filter::expression('timestamp', '<=', $this->displayRange->getStart()->getTimestamp()),
+ Filter::expression('timestamp', '>', $this->displayRange->getEnd()->getTimestamp())
+ );
+ $query->applyFilter($filter);
+ return array_merge($query->getQuery()->fetchAll(), $hookResults);
+ }
+
+ /**
+ * Fetch all entries
+ *
+ * @return array The entries to display on the timeline
+ */
+ protected function fetchEntries()
+ {
+ if ($this->resultset === null) {
+ $this->resultset = $this->fetchResults();
+ }
+
+ $range = $this->displayRange;
+ return array_filter(
+ $this->resultset,
+ function ($e) use ($range) {
+ return $range->validateTime($e->time);
+ }
+ );
+ }
+
+ /**
+ * Fetch all forecasts
+ *
+ * @return array The entries to calculate forecasts with
+ */
+ protected function fetchForecasts()
+ {
+ if ($this->resultset === null) {
+ $this->resultset = $this->fetchResults();
+ }
+
+ $range = $this->forecastRange;
+ return array_filter(
+ $this->resultset,
+ function ($e) use ($range) {
+ return $range->validateTime($e->time);
+ }
+ );
+ }
+
+ /**
+ * Return the given entries grouped together
+ *
+ * @param array $entries The entries to group
+ * @param TimeRange $timeRange The range of time to group by
+ *
+ * @return array displayGroups The grouped entries
+ */
+ protected function groupEntries(array $entries, TimeRange $timeRange)
+ {
+ $counts = array();
+ foreach ($entries as $entry) {
+ $entryTime = new DateTime();
+ $entryTime->setTimestamp($entry->time);
+ $timestamp = $timeRange->findTimeframe($entryTime, true);
+
+ if ($timestamp !== null) {
+ if (array_key_exists($entry->name, $counts)) {
+ if (array_key_exists($timestamp, $counts[$entry->name])) {
+ $counts[$entry->name][$timestamp] += 1;
+ } else {
+ $counts[$entry->name][$timestamp] = 1;
+ }
+ } else {
+ $counts[$entry->name][$timestamp] = 1;
+ }
+ }
+ }
+
+ $groups = array();
+ foreach ($counts as $name => $data) {
+ foreach ($data as $timestamp => $count) {
+ $dateTime = new DateTime();
+ $dateTime->setTimestamp($timestamp);
+
+ $groupName = $name;
+ if (isset($this->identifiers[$name]['groupBy'])) {
+ $groupName = $this->identifiers[$name]['groupBy'];
+ }
+
+ if (isset($groups[$timestamp][$groupName])) {
+ $groups[$timestamp][$groupName]->setValue(
+ $groups[$timestamp][$groupName]->getValue() + $count
+ );
+ } else {
+ $groups[$timestamp][$groupName] = TimeEntry::fromArray(
+ array(
+ 'name' => $groupName,
+ 'value' => $count,
+ 'dateTime' => $dateTime,
+ 'class' => $this->identifiers[$name]['class'],
+ 'detailUrl' => $this->identifiers[$name]['detailUrl'],
+ 'label' => $this->identifiers[$name]['label']
+ )
+ );
+ }
+ }
+ }
+
+ return $groups;
+ }
+
+ /**
+ * Return the contents of this timeline as array
+ *
+ * @return array
+ */
+ protected function toArray()
+ {
+ $this->displayGroups = $this->groupEntries($this->fetchEntries(), $this->displayRange);
+
+ $array = array();
+ foreach ($this->displayRange as $timestamp => $timeframe) {
+ $array[] = array(
+ $timeframe,
+ array_key_exists($timestamp, $this->displayGroups) ? $this->displayGroups[$timestamp] : array()
+ );
+ }
+
+ return $array;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Timeline/TimeRange.php b/modules/monitoring/library/Monitoring/Timeline/TimeRange.php
new file mode 100644
index 0000000..08c7a2c
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Timeline/TimeRange.php
@@ -0,0 +1,258 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Timeline;
+
+use StdClass;
+use Iterator;
+use DateTime;
+use DateInterval;
+use Icinga\Util\Format;
+
+/**
+ * A range of time split into a specific interval
+ *
+ * @see Iterator
+ */
+class TimeRange implements Iterator
+{
+ /**
+ * The start of this time range
+ *
+ * @var DateTime
+ */
+ protected $start;
+
+ /**
+ * The end of this time range
+ *
+ * @var DateTime
+ */
+ protected $end;
+
+ /**
+ * The interval by which this time range is split
+ *
+ * @var DateInterval
+ */
+ protected $interval;
+
+ /**
+ * The current date in the iteration
+ *
+ * @var DateTime
+ */
+ protected $current;
+
+ /**
+ * Whether the date iteration is negative
+ *
+ * @var bool
+ */
+ protected $negative;
+
+ /**
+ * Initialize a new time range
+ *
+ * @param DateTime $start When the time range should start
+ * @param DateTime $end When the time range should end
+ * @param DateInterval $interval The interval of the time range
+ */
+ public function __construct(DateTime $start, DateTime $end, DateInterval $interval)
+ {
+ $this->interval = $interval;
+ $this->start = $start;
+ $this->end = $end;
+ $this->negative = $this->start > $this->end;
+ }
+
+ /**
+ * Return when this range of time starts
+ *
+ * @return DateTime
+ */
+ public function getStart()
+ {
+ return $this->start;
+ }
+
+ /**
+ * Return when this range of time ends
+ *
+ * @return DateTime
+ */
+ public function getEnd()
+ {
+ return $this->end;
+ }
+
+ /**
+ * Return the interval by which this time range is split
+ *
+ * @return DateInterval
+ */
+ public function getInterval()
+ {
+ return $this->interval;
+ }
+
+ /**
+ * Return the appropriate timeframe for the given date and time or null if none could be found
+ *
+ * @param DateTime $dateTime The date and time for which to search the timeframe
+ * @param bool $asTimestamp Whether the start of the timeframe should be returned as timestamp
+ * @return StdClass|int An object with a ´start´ and ´end´ property or a timestamp
+ */
+ public function findTimeframe(DateTime $dateTime, $asTimestamp = false)
+ {
+ foreach ($this as $timeframeIdentifier => $timeframe) {
+ if ($this->negative) {
+ if ($dateTime <= $timeframe->start && $dateTime >= $timeframe->end) {
+ return $asTimestamp ? $timeframeIdentifier : $timeframe;
+ }
+ } elseif ($dateTime >= $timeframe->start && $dateTime <= $timeframe->end) {
+ return $asTimestamp ? $timeframeIdentifier : $timeframe;
+ }
+ }
+ }
+
+ /**
+ * Return whether the given time is within this range of time
+ *
+ * @param string|int|DateTime $time The timestamp or date and time to check
+ */
+ public function validateTime($time)
+ {
+ if ($time instanceof DateTime) {
+ $dateTime = $time;
+ } elseif (is_string($time)) {
+ $dateTime = DateTime::createFromFormat('d/m/Y g:i A', $time);
+ } else {
+ $dateTime = new DateTime();
+ $dateTime->setTimestamp($time);
+ }
+
+ return ($this->negative && ($dateTime <= $this->start && $dateTime >= $this->end)) ||
+ (!$this->negative && ($dateTime >= $this->start && $dateTime <= $this->end));
+ }
+
+ /**
+ * Return the appropriate timeframe for the given timeframe start
+ *
+ * @param int|DateTime $time The timestamp or date and time for which to return the timeframe
+ * @return StdClass An object with a ´start´ and ´end´ property
+ */
+ public function getTimeframe($time)
+ {
+ if ($time instanceof DateTime) {
+ $startTime = clone $time;
+ } else {
+ $startTime = new DateTime();
+ $startTime->setTimestamp($time);
+ }
+
+ return $this->buildTimeframe($startTime, $this->applyInterval(clone $startTime, 1));
+ }
+
+ /**
+ * Apply the current interval to the given date and time
+ *
+ * @param DateTime $dateTime The date and time to apply the interval to
+ * @param int $adjustBy By how much seconds the resulting date and time should be adjusted
+ *
+ * @return DateTime
+ */
+ protected function applyInterval(DateTime $dateTime, $adjustBy)
+ {
+ if (!$this->interval->y && !$this->interval->m) {
+ if ($this->negative) {
+ return $dateTime->sub($this->interval)->add(new DateInterval('PT' . $adjustBy . 'S'));
+ } else {
+ return $dateTime->add($this->interval)->sub(new DateInterval('PT' . $adjustBy . 'S'));
+ }
+ } elseif ($this->interval->m) {
+ for ($i = 0; $i < $this->interval->m; $i++) {
+ if ($this->negative) {
+ $dateTime->sub(new DateInterval('PT' . Format::secondsByMonth($dateTime) . 'S'));
+ } else {
+ $dateTime->add(new DateInterval('PT' . Format::secondsByMonth($dateTime) . 'S'));
+ }
+ }
+ } elseif ($this->interval->y) {
+ for ($i = 0; $i < $this->interval->y; $i++) {
+ if ($this->negative) {
+ $dateTime->sub(new DateInterval('PT' . Format::secondsByYear($dateTime) . 'S'));
+ } else {
+ $dateTime->add(new DateInterval('PT' . Format::secondsByYear($dateTime) . 'S'));
+ }
+ }
+ }
+ $adjustment = new DateInterval('PT' . $adjustBy . 'S');
+ return $this->negative ? $dateTime->add($adjustment) : $dateTime->sub($adjustment);
+ }
+
+ /**
+ * Return an object representation of the given timeframe
+ *
+ * @param DateTime $start The start of the timeframe
+ * @param DateTime $end The end of the timeframe
+ * @return StdClass
+ */
+ protected function buildTimeframe(DateTime $start, DateTime $end)
+ {
+ $timeframe = new StdClass();
+ $timeframe->start = $start;
+ $timeframe->end = $end;
+ return $timeframe;
+ }
+
+ /**
+ * Reset the iterator to its initial state
+ */
+ public function rewind(): void
+ {
+ $this->current = clone $this->start;
+ }
+
+ /**
+ * Return whether the current iteration step is valid
+ *
+ * @return bool
+ */
+ public function valid(): bool
+ {
+ if ($this->negative) {
+ return $this->current > $this->end;
+ } else {
+ return $this->current < $this->end;
+ }
+ }
+
+ /**
+ * Return the current value in the iteration
+ *
+ * @return StdClass
+ */
+ public function current(): object
+ {
+ return $this->getTimeframe($this->current);
+ }
+
+ /**
+ * Return a unique identifier for the current value in the iteration
+ *
+ * @return int
+ */
+ public function key(): int
+ {
+ return $this->current->getTimestamp();
+ }
+
+ /**
+ * Advance the iterator position by one
+ */
+ public function next(): void
+ {
+ $this->applyInterval($this->current, 0);
+ }
+}