xref: /webtrees/resources/views/modules/timeline-chart/chart.phtml (revision 05babb969dd66fd95325613c7360d51ed3aa19fc)
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