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