xref: /webtrees/resources/views/modules/timeline-chart/chart.phtml (revision b3087c253cf6a932d19775d2730fe656c3008d7f)
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<int,Fact>       $indifacts
20 * @var array<int,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 position 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