'j', // shown in header
'period' => 'P1D', // smallest shown interval
'next' => '+1 day', // one more interval
'short' => 'q', // interval label
'long' => 'Y-m-d', // interval long label
'comp' => 'Y-m-d', // sortable interval
];
/** @inheritdoc */
public function __construct($id, $mode, \Doku_Renderer $renderer, SearchConfig $searchConfig)
{
parent::__construct($id, $mode, $renderer, $searchConfig);
if ($this->mode !== 'xhtml') {
return;
}
$conf = $searchConfig->getConf();
$this->skipWeekends = $conf['skipweekends'] ?? false;
$this->initColumnRefs();
}
/**
* Figure out which columns will be used for dates and color
*
* The first date column is the start, the second is the end
*
* @todo suport Lookups pointing to dates and colors
* @todo handle multi columns
*/
protected function initColumnRefs()
{
$ref = 0;
foreach ($this->columns as $column) {
if (
$column->getType() instanceof Date ||
$column->getType() instanceof \dokuwiki\plugin\struct\types\DateTime
) {
if ($this->colrefStart == -1) {
$this->colrefStart = $ref;
} else {
$this->colrefEnd = $ref;
}
} elseif ($column->getType() instanceof Color) {
$this->colrefColor = $ref;
} elseif ($this->labelRef == -1) {
$this->labelRef = $ref;
} elseif ($this->titleRef == -1) {
$this->titleRef = $ref;
}
$ref++;
}
if ($this->colrefStart === -1 || $this->colrefEnd === -1) {
throw new StructException('Not enough Date columns selected');
}
if ($this->labelRef === -1) {
throw new StructException('No label column found');
}
if ($this->titleRef === -1) {
$this->titleRef = $this->labelRef;
}
}
/**
* Figure out the minimum and maximum dates and number of days inbetween
*
* @throws StructException when the range is not at least two days
*/
protected function initMinMax()
{
$min = PHP_INT_MAX;
$max = 0;
/** @var Value[] $row */
foreach ($this->searchConfig->getResult()->getRows() as $row) {
$start = $row[$this->colrefStart]->getCompareValue();
$start = explode(' ', $start); // cut off time
$start = array_shift($start);
if ($start && $start < $min) $min = $start;
if ($start && $start > $max) $max = $start;
$end = $row[$this->colrefEnd]->getCompareValue();
$end = explode(' ', $end); // cut off time
$end = array_shift($end);
if ($end && $end < $min) $min = $end;
if ($end && $end > $max) $max = $end;
}
$daynum = (new \DateTime($min))->diff(new \DateTime($max))->days;
if ($daynum <= 1) {
throw new StructException('Not enough variation in dates to create a range');
}
// define the resolution
if ($daynum < 14) {
$this->interval = [
'header' => 'j', // days
'period' => 'P1D',
'next' => '+1 day',
'short' => '\\\\q',
'long' => 'Y-m-d',
'comp' => 'Y-m-d'
];
} elseif ($daynum < 52) {
$this->interval = [
'header' => '\wW', // week numbers
'period' => 'P1D',
'next' => '+1 day',
'short' => '\\\\q',
'long' => 'Y-m-d',
'comp' => 'Y-m-d',
];
} elseif ($daynum < 360) {
$this->interval = [
'header' => 'F', // months
'period' => 'P1D',
'next' => '+1 day',
'short' => '\\\\q',
'long' => 'Y-m-d',
'comp' => 'Y-m-d',
];
} elseif ($daynum < 600) {
$this->interval = [
'header' => 'M \'y', // months and year
'period' => 'P1W', // weeks
'next' => '+1 week',
'short' => '\wW',
'long' => '\wW o',
'comp' => 'o-W',
];
$this->skipWeekends = false;
} else {
$this->interval = [
'header' => '\\\\Q \'y', // quarter and year
'period' => 'P1M', // months
'next' => '+1 month',
'short' => 'M',
'long' => 'F Y',
'comp' => 'Y-m',
];
$this->skipWeekends = false;
}
$this->minDate = $min;
$this->maxDate = $max;
$this->days = $this->listDays($min, $max);
$this->daynum = $daynum;
}
/** @inheritdoc */
public function render($showNotFound = false)
{
if ($this->mode !== 'xhtml') {
return;
}
if ($this->searchConfig->getCount()) {
$this->initMinMax();
$this->renderer->doc .= '
';
$this->renderer->doc .= '
';
$this->renderer->doc .= '';
$this->renderHeaders();
$this->renderer->doc .= '';
$this->renderer->doc .= '';
foreach ($this->searchConfig->getResult()->getRows() as $row) {
$this->renderRow($row);
}
$this->renderer->doc .= '';
$this->renderer->doc .= '';
$this->renderDayRow();
$this->renderer->doc .= '';
$this->renderer->doc .= '
';
$this->renderer->doc .= '
';
} elseif ($showNotFound) {
global $lang;
$this->renderer->cdata($lang['nothingfound']);
}
}
/**
* Get the color to use in this row
*
* @param Value[] $row
* @return string
*/
protected function getColorStyle($row)
{
if ($this->colrefColor === -1) return '';
$color = $row[$this->colrefColor]->getValue();
$conf = $row[$this->colrefColor]->getColumn()->getType()->getConfig();
if ($color == $conf['default']) return '';
return 'style="background-color:' . $color . '"';
}
/**
* Render the headers
*
* Automatically decides on the scale
*/
protected function renderHeaders()
{
$headers = $this->makeHeaders($this->minDate, $this->maxDate);
$this->renderer->doc .= '';
$this->renderer->doc .= ' | ';
foreach ($headers as $name => $days) {
$this->renderer->doc .= '' . $name . ' | ';
}
$this->renderer->doc .= '
';
$this->renderDayRow();
}
/**
* Render a row for the days and the today pointer
*/
protected function renderDayRow()
{
$today = new \DateTime();
$this->renderer->doc .= '';
$this->renderer->doc .= ' | ';
foreach ($this->days as $day) {
if ($day->format($this->interval['long']) == $today->format($this->interval['long'])) {
$class = 'today';
} else {
$class = '';
}
$text = $this->intervalFormat($day, 'short');
$title = $this->intervalFormat($day, 'long');
$this->renderer->doc .= '' . $text . ' | ';
}
$this->renderer->doc .= '
';
}
/**
* Render one row in the diagram
*
* @param Value[] $row
*/
protected function renderRow($row)
{
$start = $row[$this->colrefStart]->getCompareValue();
$end = $row[$this->colrefEnd]->getCompareValue();
if ($start && $end) {
$r1 = $this->listDays($this->minDate, $start);
$r2 = $this->listDays($start, $end);
$r3 = $this->listDays($end, $this->maxDate);
while ($r1 && ($this->intervalFormat(end($r1), 'comp') >= $this->intervalFormat($r2[0], 'comp'))) {
array_pop($r1);
}
while ($r3 && ($this->intervalFormat($r3[0], 'comp') <= $this->intervalFormat(end($r2), 'comp'))) {
array_shift($r3);
}
} else {
$r1 = $this->days;
$r2 = 0;
$r3 = 0;
}
// header
$this->renderer->doc .= '';
$this->renderer->doc .= '';
$row[$this->labelRef]->render($this->renderer, $this->mode);
$this->renderer->doc .= ' | ';
// period before the task
foreach ($r1 as $day) {
$this->renderer->doc .= ' | ';
}
// the task itself
if ($r2) {
$style = $this->getColorStyle($row);
$this->renderer->doc .= '';
$row[$this->titleRef]->render($this->renderer, $this->mode);
$this->renderer->doc .= '';
foreach ($row as $value) {
$this->renderer->doc .= '- ';
$value->render($this->renderer, $this->mode);
$this->renderer->doc .= '
- ';
}
$this->renderer->doc .= '
';
$this->renderer->doc .= ' | ';
}
// period after the task
foreach ($r3 as $day) {
$this->renderer->doc .= ' | ';
}
$this->renderer->doc .= '
';
}
/**
* Returns the interval units in the given period
*
* @fixme currently it's still called days, but may actually use weeks or months
* @link based on http://stackoverflow.com/a/31046319/172068
* @param string $start as YYYY-MM-DD
* @param string $end as YYYY-MM-DD
* @return \DateTime[]
*/
protected function listDays($start, $end)
{
if ($start > $end) [$start, $end] = [$end, $start];
$days = [];
$period = new \DatePeriod(
new \DateTime($start),
new \DateInterval($this->interval['period']),
(new \DateTime($end))->modify($this->interval['next']) // Include End Date (flag is only available in PHP8)
);
/** @var \DateTime $date */
foreach ($period as $date) {
if ($this->skipWeekends && (int)$date->format('N') >= 6) {
continue;
} else {
$days[] = $date;
}
}
return $days;
}
/**
* Returns the headers
*
* @param string $start as YYYY-MM-DD
* @param string $end as YYYY-MM-DD
* @return array
*/
protected function makeHeaders($start, $end)
{
if ($start > $end) [$start, $end] = [$end, $start];
$headers = [];
$period = new \DatePeriod(
new \DateTime($start),
new \DateInterval($this->interval['period']),
(new \DateTime($end))->modify($this->interval['next']) // Include End Date (flag is only available in PHP8)
);
/** @var \DateTime $date */
foreach ($period as $date) {
if ($this->skipWeekends && (int)$date->format('N') >= 6) {
continue;
} else {
$ident = $this->intervalFormat($date, 'header');
if (!isset($headers[$ident])) {
$headers[$ident] = 1;
} else {
$headers[$ident]++;
}
}
}
return $headers;
}
/**
* Wrapper around DateTime->format() to implement our own placeholders
*
* @param \DateTime $date
* @param string $formatname
* @return string
*/
protected function intervalFormat(\DateTime $date, $formatname)
{
$format = $this->interval[$formatname];
$label = $date->format($format);
return str_replace(
[
'\Q', // quarter of the year
'\q', // first letter of the day
],
[
'Q' . ceil($date->format('n') / 3),
substr($date->format('l'), 0, 1),
],
$label
);
}
}