1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2019 webtrees development team 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * You should have received a copy of the GNU General Public License 14 * along with this program. If not, see <http://www.gnu.org/licenses/>. 15 */ 16namespace Fisharebest\Webtrees\Controller; 17 18use Fisharebest\Webtrees\Date; 19use Fisharebest\Webtrees\Date\GregorianDate; 20use Fisharebest\Webtrees\Fact; 21use Fisharebest\Webtrees\Family; 22use Fisharebest\Webtrees\Filter; 23use Fisharebest\Webtrees\Functions\FunctionsDate; 24use Fisharebest\Webtrees\Functions\FunctionsPrint; 25use Fisharebest\Webtrees\I18N; 26use Fisharebest\Webtrees\Individual; 27use Fisharebest\Webtrees\Theme; 28 29/** 30 * Controller for the timeline chart 31 */ 32class TimelineController extends PageController 33{ 34 /** @var int Height of the age box */ 35 public $bheight = 30; 36 37 /** @var Fact[] The facts to display on the chart */ 38 public $indifacts = array(); // array to store the fact records in for sorting and displaying 39 40 /** @var int[] Numeric birth years of each individual */ 41 public $birthyears = array(); 42 43 /** @var int[] Numeric birth months of each individual */ 44 public $birthmonths = array(); 45 46 /** @var int[] Numeric birth days of each individual */ 47 public $birthdays = array(); 48 49 /** @var int Lowest year to display */ 50 public $baseyear = 0; 51 52 /** @var int Highest year to display */ 53 public $topyear = 0; 54 55 /** @var Individual[] List of individuals to display */ 56 public $people = array(); 57 58 /** @var string URL-encoded list of XREFs */ 59 public $pidlinks = ''; 60 61 /** @var int Vertical scale */ 62 public $scale = 2; 63 64 /** @var string[] GEDCOM elements that may have DATE data, but should not be displayed */ 65 private $nonfacts = array('BAPL', 'ENDL', 'SLGC', 'SLGS', '_TODO', 'CHAN'); 66 67 /** 68 * Startup activity 69 */ 70 public function __construct() 71 { 72 global $WT_TREE; 73 74 parent::__construct(); 75 76 $this->setPageTitle(I18N::translate('Timeline')); 77 78 $this->baseyear = (int) date('Y'); 79 80 $pids = Filter::getArray('pids', WT_REGEX_XREF); 81 $remove = Filter::get('remove', WT_REGEX_XREF); 82 83 foreach (array_unique($pids) as $pid) { 84 if ($pid !== $remove) { 85 $person = Individual::getInstance($pid, $WT_TREE); 86 if ($person && $person->canShow()) { 87 $this->people[] = $person; 88 } 89 } 90 } 91 $this->pidlinks = ''; 92 93 foreach ($this->people as $indi) { 94 // setup string of valid pids for links 95 $this->pidlinks .= 'pids%5B%5D=' . $indi->getXref() . '&'; 96 $bdate = $indi->getBirthDate(); 97 if ($bdate->isOK()) { 98 $date = new GregorianDate($bdate->minimumJulianDay()); 99 $this->birthyears [$indi->getXref()] = $date->y; 100 $this->birthmonths[$indi->getXref()] = max(1, $date->m); 101 $this->birthdays [$indi->getXref()] = max(1, $date->d); 102 } 103 // find all the fact information 104 $facts = $indi->getFacts(); 105 foreach ($indi->getSpouseFamilies() as $family) { 106 foreach ($family->getFacts() as $fact) { 107 $facts[] = $fact; 108 } 109 } 110 foreach ($facts as $event) { 111 // get the fact type 112 $fact = $event->getTag(); 113 if (!in_array($fact, $this->nonfacts)) { 114 // check for a date 115 $date = $event->getDate(); 116 if ($date->isOK()) { 117 $date = new GregorianDate($date->minimumJulianDay()); 118 $this->baseyear = min($this->baseyear, $date->y); 119 $this->topyear = max($this->topyear, $date->y); 120 121 if (!$indi->isDead()) { 122 $this->topyear = max($this->topyear, (int) date('Y')); 123 } 124 125 // do not add the same fact twice (prevents marriages from being added multiple times) 126 if (!in_array($event, $this->indifacts, true)) { 127 $this->indifacts[] = $event; 128 } 129 } 130 } 131 } 132 } 133 $scale = Filter::getInteger('scale', 0, 200); 134 if ($scale === 0) { 135 $this->scale = (int) (($this->topyear - $this->baseyear) / 20 * count($this->indifacts) / 4); 136 if ($this->scale < 6) { 137 $this->scale = 6; 138 } 139 } else { 140 $this->scale = $scale; 141 } 142 if ($this->scale < 2) { 143 $this->scale = 2; 144 } 145 $this->baseyear -= 5; 146 $this->topyear += 5; 147 } 148 149 /** 150 * Print a fact for an individual. 151 * 152 * @param Fact $event 153 */ 154 public function printTimeFact(Fact $event) 155 { 156 global $basexoffset, $baseyoffset, $factcount, $placements; 157 158 $desc = $event->getValue(); 159 // check if this is a family fact 160 $gdate = $event->getDate(); 161 $date = $gdate->minimumDate(); 162 $date = $date->convertToCalendar('gregorian'); 163 $year = $date->y; 164 $month = max(1, $date->m); 165 $day = max(1, $date->d); 166 $xoffset = $basexoffset + 22; 167 $yoffset = $baseyoffset + (($year - $this->baseyear) * $this->scale) - ($this->scale); 168 $yoffset = $yoffset + (($month / 12) * $this->scale); 169 $yoffset = $yoffset + (($day / 30) * ($this->scale / 12)); 170 $yoffset = (int) ($yoffset); 171 $place = (int) ($yoffset / $this->bheight); 172 $i = 1; 173 $j = 0; 174 $tyoffset = 0; 175 while (isset($placements[$place])) { 176 if ($i === $j) { 177 $tyoffset = $this->bheight * $i; 178 $i++; 179 } else { 180 $tyoffset = -1 * $this->bheight * $j; 181 $j++; 182 } 183 $place = (int) (($yoffset + $tyoffset) / ($this->bheight)); 184 } 185 $yoffset += $tyoffset; 186 $xoffset += abs($tyoffset); 187 $placements[$place] = $yoffset; 188 189 echo "<div id=\"fact$factcount\" style=\"position:absolute; " . (I18N::direction() === 'ltr' ? 'left: ' . ($xoffset) : 'right: ' . ($xoffset)) . 'px; top:' . ($yoffset) . "px; font-size: 8pt; height: " . ($this->bheight) . "px;\" onmousedown=\"factMouseDown(this, '" . $factcount . "', " . ($yoffset - $tyoffset) . ");\">"; 190 echo '<table cellspacing="0" cellpadding="0" border="0" style="cursor: hand;"><tr><td>'; 191 echo '<img src="' . Theme::theme()->parameter('image-hline') . '" name="boxline' . $factcount . '" id="boxline' . $factcount . '" height="3" width="10" style="padding-'; 192 if (I18N::direction() === 'ltr') { 193 echo 'left: 3px;">'; 194 } else { 195 echo 'right: 3px;">'; 196 } 197 198 $col = array_search($event->getParent(), $this->people); 199 if ($col === false) { 200 // Marriage event - use the color of the husband 201 $col = array_search($event->getParent()->getHusband(), $this->people); 202 } 203 if ($col === false) { 204 // Marriage event - use the color of the wife 205 $col = array_search($event->getParent()->getWife(), $this->people); 206 } 207 $col = $col % 6; 208 echo '</td><td class="person' . $col . '">'; 209 if (count($this->people) > 6) { 210 // We only have six colours, so show naes if more than this number 211 echo $event->getParent()->getFullName() . ' — '; 212 } 213 $record = $event->getParent(); 214 echo $event->getLabel(); 215 echo ' — '; 216 if ($record instanceof Individual) { 217 echo FunctionsPrint::formatFactDate($event, $record, false, false); 218 } elseif ($record instanceof Family) { 219 echo $gdate->display(); 220 if ($record->getHusband() && $record->getHusband()->getBirthDate()->isOK()) { 221 $ageh = FunctionsDate::getAgeAtEvent(Date::getAgeGedcom($record->getHusband()->getBirthDate(), $gdate)); 222 } else { 223 $ageh = null; 224 } 225 if ($record->getWife() && $record->getWife()->getBirthDate()->isOK()) { 226 $agew = FunctionsDate::getAgeAtEvent(Date::getAgeGedcom($record->getWife()->getBirthDate(), $gdate)); 227 } else { 228 $agew = null; 229 } 230 if ($ageh && $agew) { 231 echo '<span class="age"> ', I18N::translate('Husband’s age'), ' ', $ageh, ' ', I18N::translate('Wife’s age'), ' ', $agew, '</span>'; 232 } elseif ($ageh) { 233 echo '<span class="age"> ', I18N::translate('Age'), ' ', $ageh, '</span>'; 234 } elseif ($agew) { 235 echo '<span class="age"> ', I18N::translate('Age'), ' ', $ageh, '</span>'; 236 } 237 } 238 echo ' ' . Filter::escapeHtml($desc); 239 if (!$event->getPlace()->isEmpty()) { 240 echo ' — ' . $event->getPlace()->getShortName(); 241 } 242 // Print spouses names for family events 243 if ($event->getParent() instanceof Family) { 244 echo ' — <a href="', $event->getParent()->getHtmlUrl(), '">', $event->getParent()->getFullName(), '</a>'; 245 } 246 echo '</td></tr></table>'; 247 echo '</div>'; 248 if (I18N::direction() === 'ltr') { 249 $img = 'image-dline2'; 250 $ypos = '0%'; 251 } else { 252 $img = 'image-dline'; 253 $ypos = '100%'; 254 } 255 $dyoffset = ($yoffset - $tyoffset) + $this->bheight / 3; 256 if ($tyoffset < 0) { 257 $dyoffset = $yoffset + $this->bheight / 3; 258 if (I18N::direction() === 'ltr') { 259 $img = 'image-dline'; 260 $ypos = '100%'; 261 } else { 262 $img = 'image-dline2'; 263 $ypos = '0%'; 264 } 265 } 266 // Print the diagonal line 267 echo '<div id="dbox' . $factcount . '" style="position:absolute; ' . (I18N::direction() === 'ltr' ? 'left: ' . ($basexoffset + 25) : 'right: ' . ($basexoffset + 25)) . 'px; top:' . ($dyoffset) . 'px; font-size: 8pt; height: ' . abs($tyoffset) . 'px; width: ' . abs($tyoffset) . 'px;'; 268 echo ' background-image: url(\'' . Theme::theme()->parameter($img) . '\');'; 269 echo ' background-position: 0% ' . $ypos . ';">'; 270 echo '</div>'; 271 } 272 273 /** 274 * Get significant information from this page, to allow other pages such as 275 * charts and reports to initialise with the same records 276 * 277 * @return Individual 278 */ 279 public function getSignificantIndividual() 280 { 281 global $WT_TREE; 282 283 if ($this->people) { 284 return $this->people[0]; 285 } else { 286 return parent::getSignificantIndividual(); 287 } 288 } 289} 290