1<?php 2 3declare(strict_types=1); 4 5use Fisharebest\Webtrees\Age; 6use Fisharebest\Webtrees\Fact; 7use Fisharebest\Webtrees\Family; 8use Fisharebest\Webtrees\I18N; 9use Fisharebest\Webtrees\Individual; 10 11/** 12 * @var int $baseyear 13 * @var int $bheight 14 * @var array<int> $birthdays 15 * @var array<int> $birthmonths 16 * @var array<int> $birthyears 17 * @var int $scale 18 * @var int $topyear 19 * @var array<Fact> $indifacts 20 * @var array<Individual> $individuals 21 */ 22 23?> 24 25<script> 26 let bottomy = <?= json_encode(($topyear - $baseyear) * $scale - 5, JSON_THROW_ON_ERROR) ?>; 27 let topy = 0; 28 let baseyear = <?= $baseyear - 25 / $scale ?>; 29 let birthyears = []; 30 let birthmonths = []; 31 let birthdays = []; 32 <?php foreach ($individuals as $c => $indi) : ?> 33 birthyears [<?= json_encode($c, JSON_THROW_ON_ERROR) ?>] = <?= json_encode($birthyears[$indi->xref()] ?? null, JSON_THROW_ON_ERROR) ?>; 34 birthmonths[<?= json_encode($c, JSON_THROW_ON_ERROR) ?>] = <?= json_encode($birthmonths[$indi->xref()] ?? null, JSON_THROW_ON_ERROR) ?>; 35 birthdays [<?= json_encode($c, JSON_THROW_ON_ERROR) ?>] = <?= json_encode($birthdays[$indi->xref()] ?? null, JSON_THROW_ON_ERROR) ?>; 36 <?php endforeach ?> 37 38 let bheight = <?= json_encode($bheight, JSON_THROW_ON_ERROR) ?>; 39 let scale = <?= json_encode($scale, JSON_THROW_ON_ERROR) ?>; 40 41 timeline_chart_div = document.getElementById("timeline_chart"); 42 timeline_chart_div.style.height = '<?= json_encode(0 + ($topyear - $baseyear) * $scale * 1.1, JSON_THROW_ON_ERROR) ?>px'; 43 44 /** 45 * Find the position of an event, relative to an element. 46 * 47 * @param event 48 * @param element 49 */ 50 function clickPosition(event, element) { 51 let xpos = event.pageX; 52 let ypos = event.pageY; 53 54 if (element.offsetParent) { 55 do { 56 xpos -= element.offsetLeft; 57 ypos -= element.offsetTop; 58 } while (element = element.offsetParent); 59 } 60 61 return {x: xpos, y: ypos} 62 } 63 64 let ob = null; 65 let Y = 0; 66 let X = 0; 67 let oldx = 0; 68 let oldlinew = 0; 69 let personnum = 0; 70 let type = 0; 71 let boxmean = 0; 72 73 function ageCursorMouseDown(divbox, num) { 74 ob = divbox; 75 personnum = num; 76 type = 0; 77 X = ob.offsetLeft; 78 Y = ob.offsetTop; 79 } 80 81 function factMouseDown(divbox, num, mean) { 82 ob = divbox; 83 personnum = num; 84 boxmean = mean; 85 type = 1; 86 oldx = ob.offsetLeft; 87 oldlinew = 0; 88 } 89 90 document.onmousemove = function (e) { 91 let textDirection = document.documentElement.dir; 92 93 if (ob === null) { 94 return true; 95 } 96 let newx = 0; 97 let newy = 0; 98 if (type === 0) { 99 // age boxes 100 newPosition = clickPosition(e, document.getElementById("timeline_chart")); 101 newx = newPosition.x; 102 newy = newPosition.y; 103 104 if (oldx === 0) { 105 oldx = newx; 106 } 107 if (newy < topy - bheight / 2) { 108 newy = topy - bheight / 2; 109 } 110 if (newy > bottomy) { 111 newy = bottomy - 1; 112 } 113 ob.style.top = newy + "px"; 114 let tyear = (newy + bheight - 4 - topy + scale) / scale + baseyear; 115 let year = Math.floor(tyear); 116 let month = Math.floor(tyear * 12 - year * 12); 117 let day = Math.floor(tyear * 365 - year * 365 - month * 30); 118 let mstamp = year * 365 + month * 30 + day; 119 let bdstamp = birthyears[personnum] * 365 + birthmonths[personnum] * 30 + birthdays[personnum]; 120 let daydiff = mstamp - bdstamp; 121 let ba = 1; 122 if (daydiff < 0) { 123 ba = -1; 124 daydiff = (bdstamp - mstamp); 125 } 126 let yage = Math.floor(daydiff / 365); 127 let mage = Math.floor((daydiff - yage * 365) / 30); 128 let dage = Math.floor(daydiff - yage * 365 - mage * 30); 129 if (dage < 0) { 130 mage = mage - 1; 131 } 132 if (dage < -30) { 133 dage = 30 + dage; 134 } 135 if (mage < 0) { 136 yage = yage - 1; 137 } 138 if (mage < -11) { 139 mage = 12 + mage; 140 } 141 let yearform = document.getElementById('yearform' + personnum); 142 let ageform = document.getElementById('ageform' + personnum); 143 yearform.innerHTML = year + " " + month + " <?= mb_substr(I18N::translate('Month:'), 0, 1) ?> " + day + " <?= mb_substr(I18N::translate('Day:'), 0, 1) ?>"; 144 if (ba * yage > 1 || ba * yage < -1 || ba * yage === 0) { 145 ageform.innerHTML = (ba * yage) + " <?= mb_substr(I18N::translate('years'), 0, 1) ?> " + (ba * mage) + " <?= mb_substr(I18N::translate('Month:'), 0, 1) ?> " + (ba * dage) + " <?= mb_substr(I18N::translate('Day:'), 0, 1) ?>"; 146 } else { 147 ageform.innerHTML = (ba * yage) + " <?= mb_substr(I18N::translate('Year:'), 0, 1) ?> " + (ba * mage) + " <?= mb_substr(I18N::translate('Month:'), 0, 1) ?> " + (ba * dage) + " <?= mb_substr(I18N::translate('Day:'), 0, 1) ?>"; 148 } 149 let line = document.getElementById('ageline' + personnum); 150 let temp = newx - oldx; 151 152 if (textDirection === 'rtl') { 153 temp = temp * -1; 154 } 155 line.style.width = (line.width + temp) + "px"; 156 oldx = newx; 157 return false; 158 } else { 159 // fact boxes 160 let linewidth; 161 newPosition = clickPosition(e, document.getElementById("timeline_chart")); 162 newx = newPosition.x; 163 newy = newPosition.y; 164 if (oldx === 0) { 165 oldx = newx; 166 } 167 linewidth = e.pageX; 168 169 // get diagnal line box 170 let dbox = document.getElementById('dbox' + personnum); 171 let etopy; 172 let ebottomy; 173 // set up limits 174 if (boxmean - 175 < topy) { 175 etopy = topy; 176 } else { 177 etopy = boxmean - 175; 178 } 179 if (boxmean + 175 > bottomy) { 180 ebottomy = bottomy; 181 } else { 182 ebottomy = boxmean + 175; 183 } 184 // check if in the bounds of the limits 185 if (newy < etopy) { 186 newy = etopy; 187 } 188 if (newy > ebottomy) { 189 newy = ebottomy; 190 } 191 // calculate the change in Y position 192 let dy = newy - ob.offsetTop; 193 // check if we are above the starting point and switch the background image 194 195 if (newy < boxmean) { 196 if (textDirection === 'rtl') { 197 dbox.style.backgroundImage = "url('<?= asset('css/images/dline2.png') ?>')"; 198 dbox.style.backgroundPosition = "0% 0%"; 199 } else { 200 dbox.style.backgroundImage = "url('<?= asset('css/images/dline.png') ?>')"; 201 dbox.style.backgroundPosition = "0% 100%"; 202 } 203 dy = -dy; 204 dbox.style.top = (newy + bheight / 3) + "px"; 205 } else { 206 if (textDirection === 'rtl') { 207 dbox.style.backgroundImage = "url('<?= asset('css/images/dline.png') ?>')"; 208 dbox.style.backgroundPosition = "0% 100%"; 209 } else { 210 dbox.style.backgroundImage = "url('<?= asset('css/images/dline2.png') ?>')"; 211 dbox.style.backgroundPosition = "0% 0%"; 212 } 213 214 dbox.style.top = (boxmean + bheight / 3) + "px"; 215 } 216 // the new X posistion moves the same as the y position 217 if (textDirection === 'rtl') { 218 newx = dbox.offsetRight + Math.abs(newy - boxmean); 219 } else { 220 newx = dbox.offsetLeft + Math.abs(newy - boxmean); 221 } 222 // set the X position of the box 223 if (textDirection === 'rtl') { 224 ob.style.right = newx + "px"; 225 } else { 226 ob.style.left = newx + "px"; 227 } 228 // set new top positions 229 ob.style.top = newy + "px"; 230 // get the width for the diagnal box 231 let newwidth = (ob.offsetLeft - dbox.offsetLeft); 232 // set the width 233 dbox.style.width = newwidth + "px"; 234 if (textDirection === 'rtl') { 235 dbox.style.right = (dbox.offsetRight - newwidth) + 'px'; 236 } 237 dbox.style.height = newwidth + "px"; 238 // change the line width to the change in the mouse X position 239 line = document.getElementById('boxline' + personnum); 240 if (oldlinew !== 0) { 241 line.width = line.width + (linewidth - oldlinew); 242 } 243 oldlinew = linewidth; 244 oldx = newx; 245 return false; 246 } 247 }; 248 249 document.onmouseup = function () { 250 ob = null; 251 oldx = 0; 252 } 253</script> 254 255<div id="timeline_chart"> 256 <!-- print the timeline line image --> 257 <div id="line" style="position:absolute; <?= I18N::direction() === 'ltr' ? 'left:22px;' : 'right:22px;' ?> top:0;"> 258 <img src="<?= e(asset('css/images/vline.png')) ?>" width="3" 259 height="<?= 0 + ($topyear - $baseyear) * $scale ?>"> 260 </div> 261 262 <!-- print divs for the grid --> 263 <div id="scale<?= e($baseyear) ?>" 264 style="position:absolute; <?= I18N::direction() === 'ltr' ? 'left' : 'right' ?>:0; top:-5px; font-size: 7pt; text-align: <?= I18N::direction() === 'ltr' ? 'left' : 'right' ?>;"> 265 <?= $baseyear ?> 266 </div> 267 <?php 268 // at a scale of 25 or higher, show every year 269 $mod = intdiv(25, $scale); 270 if ($mod < 1) { 271 $mod = 1; 272 } 273 for ($i = $baseyear + 1; $i < $topyear; $i++) { 274 if ($i % $mod === 0) { 275 echo '<div id="scale' . $i . '" style="position:absolute; ' . (I18N::direction() === 'ltr' ? 'left:0;' : 'right:0;') . ' top:' . (($i - $baseyear) * $scale - $scale / 2) . 'px; font-size: 7pt; text-align:' . (I18N::direction() === 'ltr' ? 'left' : 'right') . ';">'; 276 echo $i; 277 echo '</div>'; 278 } 279 } 280 echo ''; 281 ?> 282 <div id="scale<?= e($topyear) ?>" 283 style="position:absolute; <?= I18N::direction() === 'ltr' ? 'left' : 'right' ?>:0; top:<?= ($topyear - $baseyear) * $scale ?>px; font-size: 7pt; text-align:<?= I18N::direction() === 'ltr' ? 'left' : 'right' ?>;"> 284 <?= e($topyear) ?> 285 </div> 286 287 <?php foreach ($indifacts as $factcount => $event) : ?> 288 <?php 289 $desc = $event->value(); 290 $gdate = $event->date(); 291 $date = $gdate->minimumDate(); 292 $date = $date->convertToCalendar('gregorian'); 293 $year = $date->year(); 294 $month = max(1, $date->month()); 295 $day = max(1, $date->day()); 296 $xoffset = 0 + 22; 297 $yoffset = 0 + ($year - $baseyear) * $scale - $scale; 298 $yoffset = $yoffset + $month / 12 * $scale; 299 $yoffset = $yoffset + $day / 30 * ($scale / 12); 300 $yoffset = (int) $yoffset; 301 $place = (int) ($yoffset / $bheight); 302 $i = 1; 303 $j = 0; 304 $tyoffset = 0; 305 while (isset($placements[$place])) { 306 if ($i === $j) { 307 $tyoffset = $bheight * $i; 308 $i++; 309 } else { 310 $tyoffset = -1 * $bheight * $j; 311 $j++; 312 } 313 $place = (int) (($yoffset + $tyoffset) / $bheight); 314 } 315 $yoffset += $tyoffset; 316 $xoffset += abs($tyoffset); 317 $placements[$place] = $yoffset; 318 319 echo "<div id=\"fact$factcount\" style=\"position:absolute; " . (I18N::direction() === 'ltr' ? 'left: ' . $xoffset : 'right: ' . $xoffset) . 'px; top:' . $yoffset . 'px; font-size: 8pt; height: ' . $bheight . "px;\" onmousedown=\"factMouseDown(this, '" . $factcount . "', " . ($yoffset - $tyoffset) . ');">'; 320 echo '<table cellspacing="0" cellpadding="0" border="0" style="cursor: grab;"><tr><td>'; 321 echo '<img src="' . e(asset('css/images/hline.png')) . '" id="boxline' . $factcount . '" height="3" width="10" style="padding-'; 322 if (I18N::direction() === 'ltr') { 323 echo 'left: 3px;">'; 324 } else { 325 echo 'right: 3px;">'; 326 } 327 328 $col = array_search($event->record(), $individuals, true); 329 if ($col === false) { 330 // Marriage event - use the color of the husband 331 $col = array_search($event->record()->husband(), $individuals, true); 332 } 333 if ($col === false) { 334 // Marriage event - use the color of the wife 335 $col = array_search($event->record()->wife(), $individuals, true); 336 } 337 $col %= 6; 338 echo '</td><td class="person' . $col . '">'; 339 if (count($individuals) > 6) { 340 // We only have six colours, so show naes if more than this number 341 echo $event->record()->fullName() . ' — '; 342 } 343 $record = $event->record(); 344 echo $event->label(); 345 echo ' — '; 346 if ($record instanceof Individual) { 347 echo view('fact-date', ['cal_link' => 'false', 'fact' => $event, 'record' => $record, 'time' => false]); 348 } elseif ($record instanceof Family) { 349 echo $gdate->display(); 350 351 foreach ($record->spouses() as $spouse) { 352 if ($spouse->getBirthDate()->isOK()) { 353 $age = (string) new Age($spouse->getBirthDate(), $gdate); 354 if ($spouse->sex() === 'F') { 355 echo '<span class="age"> ', I18N::translate('Wife’s age'), ' ', $age, '</span>'; 356 } else { 357 echo '<span class="age"> ', I18N::translate('Husband’s age'), ' ', $age, '</span>'; 358 } 359 } 360 } 361 } 362 echo ' ' . e($desc); 363 if ($event->place()->gedcomName() !== '') { 364 echo ' — ' . $event->place()->shortName(); 365 } 366 // Print spouses names for family events 367 if ($event->record() instanceof Family) { 368 echo ' — <a href="', e($event->record()->url()), '">', $event->record()->fullName(), '</a>'; 369 } 370 echo '</td></tr></table>'; 371 echo '</div>'; 372 if (I18N::direction() === 'ltr') { 373 $img = asset('css/images/dline2.png'); 374 $ypos = '0%'; 375 } else { 376 $img = asset('css/images/dline.png'); 377 $ypos = '100%'; 378 } 379 $dyoffset = $yoffset - $tyoffset + $bheight / 3; 380 if ($tyoffset < 0) { 381 $dyoffset = $yoffset + $bheight / 3; 382 if (I18N::direction() === 'ltr') { 383 $img = asset('css/images/dline.png'); 384 $ypos = '100%'; 385 } else { 386 $img = asset('css/images/dline2.png'); 387 $ypos = '0%'; 388 } 389 } 390 ?> 391 392 <!-- diagonal line --> 393 <div id="dbox<?= $factcount ?>" style="position:absolute; <?= I18N::direction() === 'ltr' ? 'left: ' . (0 + 25) : 'right: ' . (0 + 25) ?>px; top:<?= $dyoffset ?>px; font-size: 8pt; height: <?= abs($tyoffset) ?>px; width: <?= abs($tyoffset) ?>px; background-image: url('<?= e($img) ?>'); background-position: 0 <?= $ypos ?>;"> 394 </div> 395 <?php endforeach ?> 396 397 <!-- age cursors --> 398 <?php foreach ($individuals as $p => $indi) : ?> 399 <?php $ageyoffset = 0 + $bheight * $p; ?> 400 <div id="agebox<?= $p ?>" style="cursor:move; position:absolute; <?= I18N::direction() === 'ltr' ? 'left:20px;' : 'right:20px;' ?> top:<?= $ageyoffset ?>px; height:<?= $bheight ?>px; display:none;" onmousedown="ageCursorMouseDown(this, <?= $p ?>);"> 401 <table cellspacing="0" cellpadding="0"> 402 <tr> 403 <td> 404 <img src="<?= e(asset('css/images/hline.png')) ?>" id="ageline<?= $p ?>" width="25" height="3"> 405 </td> 406 <td> 407 <?php if (!empty($birthyears[$indi->xref()])) : ?> 408 <?php $tyear = round(($ageyoffset + $bheight / 2) / $scale) + $baseyear; ?> 409 <table class="person<?= $p % 6 ?>" style="cursor: grab;"> 410 <tr> 411 <td> 412 <?= I18N::translate('Year:') ?> 413 <span id="yearform<?= $p ?>" class="field"> 414 <?= $tyear ?> 415 </span> 416 </td> 417 <td> 418 (<?= I18N::translate('Age') ?> 419 <span id="ageform<?= $p ?>" class="field"><?= $tyear - $birthyears[$indi->xref()] ?></span>) 420 </td> 421 </tr> 422 </table> 423 <?php endif ?> 424 </td> 425 </tr> 426 </table> 427 <br> 428 <br> 429 <br> 430 </div> 431 <br> 432 <br> 433 <br> 434 <br> 435 <?php endforeach ?> 436</div> 437