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