LifespanController::fillTimeline()   F
last analyzed

Complexity

Conditions 21
Paths 1442

Size

Total Lines 120
Code Lines 79

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 21
eloc 79
nc 1442
nop 0
dl 0
loc 120
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2019 webtrees development team
5
 * This program is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
 * GNU General Public License for more details.
13
 * You should have received a copy of the GNU General Public License
14
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15
 */
16
namespace Fisharebest\Webtrees\Controller;
17
18
use Fisharebest\Webtrees\ColorGenerator;
19
use Fisharebest\Webtrees\Database;
20
use Fisharebest\Webtrees\Date;
21
use Fisharebest\Webtrees\Date\FrenchDate;
22
use Fisharebest\Webtrees\Date\GregorianDate;
23
use Fisharebest\Webtrees\Date\HijriDate;
24
use Fisharebest\Webtrees\Date\JalaliDate;
25
use Fisharebest\Webtrees\Date\JewishDate;
26
use Fisharebest\Webtrees\Date\JulianDate;
27
use Fisharebest\Webtrees\Fact;
28
use Fisharebest\Webtrees\Family;
29
use Fisharebest\Webtrees\Filter;
30
use Fisharebest\Webtrees\Functions\Functions;
31
use Fisharebest\Webtrees\GedcomTag;
32
use Fisharebest\Webtrees\I18N;
33
use Fisharebest\Webtrees\Individual;
34
use Fisharebest\Webtrees\Place;
35
use Fisharebest\Webtrees\Session;
36
37
/**
38
 * Controller for the timeline chart
39
 */
40
class LifespanController extends PageController
41
{
42
    // Base color parameters
43
    const RANGE           = 120; // degrees
44
    const SATURATION      = 100; // percent
45
    const LIGHTNESS       = 30; // percent
46
    const ALPHA           = 0.25;
47
    const CHART_TOP       = 10; // pixels
48
    const BAR_SPACING     = 22; // pixels
49
    const YEAR_SPAN       = 10; // Number of years per scale section
50
    const PIXELS_PER_YEAR = 7; // Number of pixels to shift per year
51
    const SESSION_DATA    = 'lifespan_data';
52
53
    /** @var string|null Chart parameter */
54
    public $place     = null;
55
56
    /** @var int|null Chart parameter */
57
    public $beginYear = null;
58
59
    /** @var int|null Chart parameter */
60
    public $endYear   = null;
61
62
    /** @var string Chart parameter */
63
    public $subtitle  = '&nbsp;';
64
65
    /** @var Individual[] A list of individuals to display. */
66
    private $people = array();
67
68
    /** @var string The default calendar to use. */
69
    private $defaultCalendar;
70
71
    /** @var string Which calendar to use. */
72
    private $calendar;
73
74
    /** @var string Which calendar escape to use. */
75
    private $calendarEscape;
76
77
    /** @var int The earliest year to show. */
78
    private $timelineMinYear;
79
80
    /** @var int That latest year to show. */
81
    private $timelineMaxYear;
82
83
    /** @var int The current year. */
84
    private $currentYear;
85
86
    /** @var string[] A list of colors to use. */
87
    private $colors = array();
88
89
    /** @todo This attribute is public to support the PHP5.3 closure workaround. */
90
    /** @var Place|null A place to serarh. */
91
    public $place_obj = null;
92
93
    /** @todo This attribute is public to support the PHP5.3 closure workaround. */
94
    /** @var Date|null Start of the date range. */
95
    public $startDate = null;
96
97
    /** @todo This attribute is public to support the PHP5.3 closure workaround. */
98
    /** @var Date|null End of the date range. */
99
    public $endDate = null;
100
101
    /** @var bool Only match dates in the chosen calendar. */
102
    private $strictDate;
103
104
    /** @todo This attribute is public to support the PHP5.3 closure workaround. */
105
    /** @var string[] List of facts/events to include. */
106
    public $facts;
107
108
    /** @var string[] Facts and events to exclude from the chart */
109
    private $nonfacts = array(
110
        'FAMS', 'FAMC', 'MAY', 'BLOB', 'OBJE', 'SEX', 'NAME', 'SOUR', 'NOTE', 'BAPL', 'ENDL',
111
        'SLGC', 'SLGS', '_TODO', '_WT_OBJE_SORT', 'CHAN', 'HUSB', 'WIFE', 'CHIL', 'OCCU', 'ASSO',
112
    );
113
114
    /**
115
     * Startup activity
116
     */
117
    public function __construct()
118
    {
119
        global $WT_TREE;
120
121
        parent::__construct();
122
        $this->setPageTitle(I18N::translate('Lifespans'));
123
124
        $this->facts           = explode('|', WT_EVENTS_BIRT . '|' . WT_EVENTS_DEAT . '|' . WT_EVENTS_MARR . '|' . WT_EVENTS_DIV);
125
        $tmp                   = explode('\\', get_class(I18N::defaultCalendar()));
126
        $cal                   = strtolower(array_pop($tmp));
127
        $this->defaultCalendar = str_replace('calendar', '', $cal);
128
        $filterPids            = false;
129
130
        // Request parameters
131
        $clear            = Filter::getBool('clear');
132
        $newpid           = Filter::get('newpid', WT_REGEX_XREF);
133
        $addfam           = Filter::getBool('addFamily');
134
        $this->place      = Filter::get('place');
135
        $this->beginYear  = Filter::getInteger('beginYear', 0, PHP_INT_MAX, null);
136
        $this->endYear    = Filter::getInteger('endYear', 0, PHP_INT_MAX, null);
137
        $this->calendar   = Filter::get('calendar', null, $this->defaultCalendar);
138
        $this->strictDate = Filter::getBool('strictDate');
139
140
        // Set up base color parameters
141
        $this->colors['M'] = new ColorGenerator(240, self::SATURATION, self::LIGHTNESS, self::ALPHA, self::RANGE * -1);
0 ignored issues
show
Bug introduced by
self::ALPHA of type double is incompatible with the type integer expected by parameter $alpha of Fisharebest\Webtrees\ColorGenerator::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

141
        $this->colors['M'] = new ColorGenerator(240, self::SATURATION, self::LIGHTNESS, /** @scrutinizer ignore-type */ self::ALPHA, self::RANGE * -1);
Loading history...
142
        $this->colors['F'] = new ColorGenerator(000, self::SATURATION, self::LIGHTNESS, self::ALPHA, self::RANGE);
143
144
        // Build a list of people based on the input parameters
145
        if ($clear) {
146
            // Empty list & reset form
147
            $xrefs           = array();
148
            $this->place     = null;
149
            $this->beginYear = null;
150
            $this->endYear   = null;
151
            $this->calendar  = $this->defaultCalendar;
152
        } elseif ($this->place) {
153
            // Get all individual & family records found for a place
154
            $this->place_obj = new Place($this->place, $WT_TREE);
155
            $xrefs           = Database::prepare(
156
                "SELECT DISTINCT `i_id` FROM `##placelinks`" .
157
                " JOIN `##individuals` ON `pl_gid`=`i_id` AND `pl_file`=`i_file`" .
158
                " WHERE `i_file`=:tree_id" .
159
                " AND `pl_p_id`=:place_id" .
160
                " UNION" .
161
                " SELECT DISTINCT `f_id` FROM `##placelinks`" .
162
                " JOIN `##families` ON `pl_gid`=`f_id` AND `pl_file`=`f_file`" .
163
                " WHERE `f_file`=:tree_id" .
164
                " AND `pl_p_id`=:place_id"
165
            )->execute(array(
166
                'tree_id'  => $WT_TREE->getTreeId(),
167
                'place_id' => $this->place_obj->getPlaceId(),
168
            ))->fetchOneColumn();
169
        } else {
170
            // Modify an existing list of records
171
            $xrefs = Session::get(self::SESSION_DATA, array());
172
            if ($newpid) {
173
                $xrefs = array_merge($xrefs, $this->addFamily(Individual::getInstance($newpid, $WT_TREE), $addfam));
0 ignored issues
show
Bug introduced by
It seems like Fisharebest\Webtrees\Ind...ance($newpid, $WT_TREE) can also be of type Fisharebest\Webtrees\Family and Fisharebest\Webtrees\Media and Fisharebest\Webtrees\Note and Fisharebest\Webtrees\Repository and Fisharebest\Webtrees\Source and null; however, parameter $person of Fisharebest\Webtrees\Con...Controller::addFamily() does only seem to accept Fisharebest\Webtrees\Individual, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

173
                $xrefs = array_merge($xrefs, $this->addFamily(/** @scrutinizer ignore-type */ Individual::getInstance($newpid, $WT_TREE), $addfam));
Loading history...
174
                $xrefs = array_unique($xrefs);
175
            } elseif (!$xrefs) {
176
                $xrefs = $this->addFamily($this->getSignificantIndividual(), false);
177
            }
178
        }
179
180
        $tmp               = $this->getCalendarDate(unixtojd());
181
        $this->currentYear = $tmp->today()->y;
182
183
        $tmp = strtoupper(strtr($this->calendar, array(
0 ignored issues
show
Bug introduced by
It seems like $this->calendar can also be of type null; however, parameter $str of strtr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

183
        $tmp = strtoupper(strtr(/** @scrutinizer ignore-type */ $this->calendar, array(
Loading history...
184
            'jewish' => 'hebrew',
185
            'french' => 'french r',
186
        )));
187
        $this->calendarEscape = sprintf('@#D%s@', $tmp);
188
189
        if ($xrefs) {
190
            // ensure date ranges are valid in preparation for filtering list
191
            if ($this->beginYear || $this->endYear) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->beginYear of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Bug Best Practice introduced by
The expression $this->endYear of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
192
                $filterPids = true;
193
                if (!$this->beginYear) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->beginYear of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
194
                    $tmp             = new Date($this->calendarEscape . ' 1');
195
                    $this->beginYear = $tmp->minimumDate()->y;
196
                }
197
                if (!$this->endYear) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->endYear of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
198
                    $this->endYear = $this->currentYear;
199
                }
200
                $this->startDate = new Date($this->calendarEscape . $this->beginYear);
201
                $this->endDate   = new Date($this->calendarEscape . $this->endYear);
202
            }
203
204
            // Test each xref to see if the search criteria are met
205
            foreach ($xrefs as $key => $xref) {
206
                $valid  = false;
207
                $person = Individual::getInstance($xref, $WT_TREE);
208
                if ($person !== null && $person->canShow()) {
209
                    foreach ($person->getFacts() as $fact) {
210
                        if ($this->checkFact($fact)) {
211
                            $this->people[] = $person;
212
                            $valid          = true;
213
                            break;
214
                        }
215
                    }
216
                } else {
217
                    $family = Family::getInstance($xref, $WT_TREE);
218
                    if ($family !== null && $family->canShow()) {
219
                        foreach ($family->getFacts() as $fact) {
220
                            if ($this->checkFact($fact)) {
221
                                $valid          = true;
222
                                $this->people[] = $family->getHusband();
0 ignored issues
show
Bug introduced by
The method getHusband() does not exist on Fisharebest\Webtrees\Source. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

222
                                /** @scrutinizer ignore-call */ 
223
                                $this->people[] = $family->getHusband();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getHusband() does not exist on Fisharebest\Webtrees\Individual. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

222
                                /** @scrutinizer ignore-call */ 
223
                                $this->people[] = $family->getHusband();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getHusband() does not exist on Fisharebest\Webtrees\Repository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

222
                                /** @scrutinizer ignore-call */ 
223
                                $this->people[] = $family->getHusband();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getHusband() does not exist on Fisharebest\Webtrees\GedcomRecord. It seems like you code against a sub-type of Fisharebest\Webtrees\GedcomRecord such as Fisharebest\Webtrees\Family. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

222
                                /** @scrutinizer ignore-call */ 
223
                                $this->people[] = $family->getHusband();
Loading history...
Bug introduced by
The method getHusband() does not exist on Fisharebest\Webtrees\Media. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

222
                                /** @scrutinizer ignore-call */ 
223
                                $this->people[] = $family->getHusband();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getHusband() does not exist on Fisharebest\Webtrees\Note. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

222
                                /** @scrutinizer ignore-call */ 
223
                                $this->people[] = $family->getHusband();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
223
                                $this->people[] = $family->getWife();
0 ignored issues
show
Bug introduced by
The method getWife() does not exist on Fisharebest\Webtrees\Individual. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

223
                                /** @scrutinizer ignore-call */ 
224
                                $this->people[] = $family->getWife();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getWife() does not exist on Fisharebest\Webtrees\Source. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

223
                                /** @scrutinizer ignore-call */ 
224
                                $this->people[] = $family->getWife();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getWife() does not exist on Fisharebest\Webtrees\Repository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

223
                                /** @scrutinizer ignore-call */ 
224
                                $this->people[] = $family->getWife();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getWife() does not exist on Fisharebest\Webtrees\Media. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

223
                                /** @scrutinizer ignore-call */ 
224
                                $this->people[] = $family->getWife();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getWife() does not exist on Fisharebest\Webtrees\Note. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

223
                                /** @scrutinizer ignore-call */ 
224
                                $this->people[] = $family->getWife();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getWife() does not exist on Fisharebest\Webtrees\GedcomRecord. It seems like you code against a sub-type of Fisharebest\Webtrees\GedcomRecord such as Fisharebest\Webtrees\Family. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

223
                                /** @scrutinizer ignore-call */ 
224
                                $this->people[] = $family->getWife();
Loading history...
224
                            }
225
                        }
226
                    }
227
                }
228
                if (!$valid) {
229
                    unset($xrefs[$key]); // no point in storing a xref if we can't use it
230
                }
231
            }
232
            Session::put(self::SESSION_DATA, $xrefs);
233
        } else {
234
            Session::forget(self::SESSION_DATA);
235
        }
236
237
        $this->people = array_filter(array_unique($this->people));
238
        $count        = count($this->people);
239
        if ($count) {
240
            // Build the subtitle
241
            if ($this->place && $filterPids) {
242
                $this->subtitle = I18N::plural(
243
                    '%s individual with events in %s between %s and %s',
244
                    '%s individuals with events in %s between %s and %s',
245
                    $count, I18N::number($count),
246
                    $this->place, $this->startDate->display(false, '%Y'), $this->endDate->display(false, '%Y')
0 ignored issues
show
Bug introduced by
The method display() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

246
                    $this->place, $this->startDate->/** @scrutinizer ignore-call */ display(false, '%Y'), $this->endDate->display(false, '%Y')

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
247
                );
248
            } elseif ($this->place) {
249
                $this->subtitle = I18N::plural(
250
                    '%s individual with events in %s',
251
                    '%s individuals with events in %s',
252
                    $count, I18N::number($count),
253
                    $this->place
254
                );
255
            } elseif ($filterPids) {
256
                $this->subtitle = I18N::plural(
257
                    '%s individual with events between %s and %s',
258
                    '%s individuals with events between %s and %s',
259
                    $count, I18N::number($count),
260
                    $this->startDate->display(false, '%Y'), $this->endDate->display(false, '%Y')
261
                );
262
            } else {
263
                $this->subtitle = I18N::plural(
264
                    '%s individual',
265
                    '%s individuals',
266
                    $count, I18N::number($count));
267
            }
268
269
            // Sort the array in order of birth year
270
            usort($this->people, function (Individual $a, Individual $b) {
271
                return Date::compare($a->getEstimatedBirthDate(), $b->getEstimatedBirthDate());
272
            });
273
274
            //Find the mimimum birth year and maximum death year from the individuals in the array.
275
            $bdate   = $this->getCalendarDate($this->people[0]->getEstimatedBirthDate()->minimumJulianDay());
276
            $minyear = $bdate->y;
277
278
            $that    = $this; // PHP5.3 cannot access $this inside a closure
279
            $maxyear = array_reduce($this->people, function ($carry, Individual $item) use ($that) {
280
                $date = $that->getCalendarDate($item->getEstimatedDeathDate()->maximumJulianDay());
281
282
                return max($carry, $date->y);
283
            }, 0);
284
        } elseif ($filterPids) {
285
            $minyear = $this->endYear;
286
            $maxyear = $this->endYear;
287
        } else {
288
            $minyear = $this->currentYear;
289
            $maxyear = $this->currentYear;
290
        }
291
292
        $maxyear = min($maxyear, $this->currentYear); // Limit maximum year to current year as we can't forecast the future
293
        $minyear = min($minyear, $maxyear - $WT_TREE->getPreference('MAX_ALIVE_AGE')); // Set default minimum chart length
294
295
        $this->timelineMinYear = (int) floor($minyear / 10) * 10; // round down to start of the decade
296
        $this->timelineMaxYear = (int) ceil($maxyear / 10) * 10; // round up to start of next decade
297
    }
298
299
    /**
300
     * Add a person (and optionally their immediate family members) to the pids array
301
     *
302
     * @param Individual $person
303
     * @param bool $add_family
304
     *
305
     * @return array
306
     */
307
    private function addFamily(Individual $person, $add_family)
308
    {
309
        $xrefs   = array();
310
        $xrefs[] = $person->getXref();
311
        if ($add_family) {
312
            foreach ($person->getSpouseFamilies() as $family) {
313
                $spouse = $family->getSpouse($person);
314
                if ($spouse) {
315
                    $xrefs[] = $spouse->getXref();
316
                    foreach ($family->getChildren() as $child) {
317
                        $xrefs[] = $child->getXref();
318
                    }
319
                }
320
            }
321
            foreach ($person->getChildFamilies() as $family) {
322
                foreach ($family->getSpouses() as $parent) {
323
                    $xrefs[] = $parent->getXref();
324
                }
325
                foreach ($family->getChildren() as $sibling) {
326
                    if ($person !== $sibling) {
327
                        $xrefs[] = $sibling->getXref();
328
                    }
329
                }
330
            }
331
        }
332
333
        return $xrefs;
334
    }
335
336
    /**
337
     * Prints the time line scale
338
     */
339
    public function printTimeline()
340
    {
341
        $startYear = $this->timelineMinYear;
342
        while ($startYear < $this->timelineMaxYear) {
343
            $date = new Date($this->calendarEscape . $startYear);
344
            echo $date->display(false, '%Y', false);
345
            $startYear += self::YEAR_SPAN;
346
        }
347
    }
348
349
    /**
350
     * Populate the timeline
351
     *
352
     * @return int
353
     */
354
    public function fillTimeline()
355
    {
356
        $rows = array();
357
        $maxY = self::CHART_TOP;
358
        //base case
359
        if (!$this->people) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->people of type Fisharebest\Webtrees\Individual[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
360
            return $maxY;
361
        }
362
363
        foreach ($this->people as $person) {
364
            $bdate     = $this->getCalendarDate($person->getEstimatedBirthDate()->minimumJulianDay());
365
            $ddate     = $this->getCalendarDate($person->getEstimatedDeathDate()->maximumJulianDay());
366
            $birthYear = $bdate->y;
367
            $age       = min($ddate->y, $this->currentYear) - $birthYear; // truncate the bar at the current year
368
            $width     = max(9, $age * self::PIXELS_PER_YEAR); // min width is width of sex icon
369
            $startPos  = ($birthYear - $this->timelineMinYear) * self::PIXELS_PER_YEAR;
370
371
            //-- calculate a good Y top value
372
            $Y     = self::CHART_TOP;
373
            $ready = false;
374
            while (!$ready) {
375
                if (!isset($rows[$Y])) {
376
                    $ready          = true;
377
                    $rows[$Y]['x1'] = $startPos;
378
                    $rows[$Y]['x2'] = $startPos + $width;
379
                } else {
380
                    if ($rows[$Y]['x1'] > $startPos + $width) {
381
                        $ready          = true;
382
                        $rows[$Y]['x1'] = $startPos;
383
                    } elseif ($rows[$Y]['x2'] < $startPos) {
384
                        $ready          = true;
385
                        $rows[$Y]['x2'] = $startPos + $width;
386
                    } else {
387
                        //move down a line
388
                        $Y += self::BAR_SPACING;
389
                    }
390
                }
391
            }
392
393
            $facts = $person->getFacts();
394
            foreach ($person->getSpouseFamilies() as $family) {
395
                foreach ($family->getFacts() as $fact) {
396
                    $facts[] = $fact;
397
                }
398
            }
399
            Functions::sortFacts($facts);
400
401
            $that          = $this; // PHP5.3 cannot access $this inside a closure
402
            $acceptedFacts = array_filter($facts, function (Fact $fact) use ($that) {
403
                return
404
                    (in_array($fact->getTag(), $that->facts) && $fact->getDate()->isOK()) ||
405
                    (($that->place_obj || $that->startDate) && $that->checkFact($fact));
406
            });
407
408
            $eventList = array();
409
            foreach ($acceptedFacts as $fact) {
410
                $tag = $fact->getTag();
411
                //-- if the fact is a generic EVENt then get the qualifying TYPE
412
                if ($tag == "EVEN") {
413
                    $tag = $fact->getAttribute('TYPE');
414
                }
415
                $eventList[] = array(
416
                    'label' => GedcomTag::getLabel($tag),
417
                    'date'  => $fact->getDate()->display(),
418
                    'place' => $fact->getPlace()->getFullName(),
419
                );
420
            }
421
            $direction  = I18N::direction() === 'ltr' ? 'left' : 'right';
422
            $lifespan   = ' ' . $person->getLifeSpan(); // put the space here so its included in the length calcs
423
            $sex        = $person->getSex();
424
            $popupClass = strtr($sex, array('M' => '', 'U' => 'NN'));
425
            $color      = $sex === 'U' ? '' : sprintf("background-color: %s", $this->colors[$sex]->getNextColor());
426
427
            // following lines are a nasty method of approximating
428
            // the width of a string in pixels from the character count
429
            $name_length       = mb_strlen(strip_tags($person->getFullName())) * 6.5;
430
            $short_name_length = mb_strlen(strip_tags($person->getShortName())) * 6.5;
431
            $lifespan_length   = mb_strlen(strip_tags($lifespan)) * 6.5;
432
433
            if ($width > $name_length + $lifespan_length) {
434
                $printName    = $person->getFullName();
435
                $abbrLifespan = $lifespan;
436
            } elseif ($width > $name_length) {
437
                $printName    = $person->getFullName();
438
                $abbrLifespan = '&hellip;';
439
            } elseif ($width > $short_name_length) {
440
                $printName    = $person->getShortName();
441
                $abbrLifespan = '';
442
            } else {
443
                $printName    = '';
444
                $abbrLifespan = '';
445
            }
446
447
            // Bar framework
448
            printf('
449
                <div class="person_box%s" style="top:%spx; %s:%spx; width:%spx; %s">
450
                        <div class="itr">%s %s %s
451
                            <div class="popup person_box%s">
452
                                <div>
453
                                    <a href="%s">%s%s</a>
454
                                </div>',
455
                $popupClass, $Y, $direction, $startPos, $width, $color,
456
                $person->getSexImage(), $printName, $abbrLifespan,
457
                $popupClass,
458
                $person->getHtmlUrl(), $person->getFullName(), $lifespan
459
            );
460
461
            // Add events to popup
462
            foreach ($eventList as $event) {
463
                printf("<div>%s: %s %s</div>", $event['label'], $event['date'], $event['place']);
464
            }
465
            echo
466
                '</div>' . // class="popup"
467
                '</div>' . // class="itr"
468
                '</div>'; // class=$popupclass
469
470
            $maxY = max($maxY, $Y);
471
        }
472
473
        return $maxY;
474
    }
475
476
    /**
477
     * Function checkFact
478
     *
479
     * Does this fact meet the search criteria?
480
     *
481
     * @todo This function is public to support the PHP5.3 closure workaround.
482
     *
483
     * @param  Fact $fact
484
     *
485
     * @return bool
486
     */
487
    public function checkFact(Fact $fact)
488
    {
489
        $valid = !in_array($fact->getTag(), $this->nonfacts);
490
        if ($valid && $this->place_obj) {
491
            $valid = stripos($fact->getPlace()->getGedcomName(), $this->place_obj->getGedcomName()) !== false;
492
        }
493
        if ($valid && $this->startDate) {
494
            if ($this->strictDate && $this->calendar !== $this->defaultCalendar) {
495
                $valid = stripos($fact->getAttribute('DATE'), $this->calendar) !== false;
0 ignored issues
show
Bug introduced by
It seems like $fact->getAttribute('DATE') can also be of type null; however, parameter $haystack of stripos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

495
                $valid = stripos(/** @scrutinizer ignore-type */ $fact->getAttribute('DATE'), $this->calendar) !== false;
Loading history...
496
            }
497
            if ($valid) {
498
                $date  = $fact->getDate();
499
                $valid = $date->isOK() && Date::compare($date, $this->startDate) >= 0 && Date::compare($date, $this->endDate) <= 0;
0 ignored issues
show
Bug introduced by
It seems like $this->endDate can also be of type null; however, parameter $b of Fisharebest\Webtrees\Date::compare() does only seem to accept Fisharebest\Webtrees\Date, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

499
                $valid = $date->isOK() && Date::compare($date, $this->startDate) >= 0 && Date::compare($date, /** @scrutinizer ignore-type */ $this->endDate) <= 0;
Loading history...
500
            }
501
        }
502
503
        return $valid;
504
    }
505
506
    /**
507
     * Function getCalendarDate
508
     *
509
     * @todo This function is public to support the PHP5.3 closure workaround.
510
     *
511
     * @param int $date
512
     *
513
     * @return object
514
     */
515
    public function getCalendarDate($date)
516
    {
517
        switch ($this->calendar) {
518
            case 'julian':
519
                $caldate = new JulianDate($date);
520
                break;
521
            case 'french':
522
                $caldate = new FrenchDate($date);
523
                break;
524
            case 'jewish':
525
                $caldate = new JewishDate($date);
526
                break;
527
            case 'hijri':
528
                $caldate = new HijriDate($date);
529
                break;
530
            case 'jalali':
531
                $caldate = new JalaliDate($date);
532
                break;
533
            default:
534
                $caldate = new GregorianDate($date);
535
        }
536
537
        return $caldate;
538
    }
539
540
    /**
541
     * Function getCalendarOptionList
542
     *
543
     * @return string
544
     */
545
    public function getCalendarOptionList()
546
    {
547
        $html = '';
548
        foreach (Date::calendarNames() as $calendar => $name) {
549
            $selected = $this->calendar === $calendar ? 'selected' : '';
550
            $html .= sprintf('<option dir="auto" value="%s" %s>%s</option>', $calendar, $selected, $name);
551
        }
552
553
        return $html;
554
    }
555
}
556