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