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