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