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