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