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