Passed
Push — master ( 9fd6b6...ce1e09 )
by Jonathan
05:22
created

SosaProvider::getUser()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 2
rs 10
1
<?php
2
/**
3
 * webtrees-lib: MyArtJaub library for webtrees
4
 *
5
 * @package MyArtJaub\Webtrees
6
 * @subpackage Sosa
7
 * @author Jonathan Jaubart <[email protected]>
8
 * @copyright Copyright (c) 2009-2016, Jonathan Jaubart
9
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License, version 3
10
 */
11
namespace MyArtJaub\Webtrees\Module\Sosa\Model;
12
13
use Fisharebest\Webtrees\Auth;
14
use Fisharebest\Webtrees\Database;
15
use Fisharebest\Webtrees\Individual;
16
use Fisharebest\Webtrees\Tree;
17
use Fisharebest\Webtrees\User;
18
use MyArtJaub\Webtrees\Functions\Functions;
19
20
/**
21
 * Provide Sosa data access
22
 */
23
class SosaProvider {
24
    
25
    /**
26
     * Maximum number of generation the database is able to hold.
27
     * @var int MAX_DB_GENERATIONS
28
     */
29
    const MAX_DB_GENERATIONS = 64;
30
    
31
    /**
32
     * System's default user (ID -1 in the database
33
     * @var User $default_user
34
     */
35
    protected static $default_user;
36
    
37
    /**
38
     * Reference user
39
     * @var User $user
40
     */
41
    protected $user;
42
    
43
    /**
44
     * Reference tree
45
     * @var Tree $tree
46
     */
47
    protected $tree;
48
    
49
    /**
50
     * Cached list of Sosa Individuals by generation
51
     * Format: key = generation, value = array ( sosa => Individual ID)
52
     * @var array $sosa_list_by_gen
53
     */
54
    protected $sosa_list_by_gen;
55
    
56
    /**
57
     * Cached list of Sosa Families by generation
58
     * Format: key = generation, value = array ( sosa => Family ID)
59
     * @var unknown $sosa_fam_list_by_gen
0 ignored issues
show
Bug introduced by
The type MyArtJaub\Webtrees\Module\Sosa\Model\unknown was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
60
     */
61
    protected $sosa_fam_list_by_gen;
62
    
63
    /**
64
     * Cached array of statistics by generation
65
     * Format:  key = generation, 
66
     *          value = array(
67
     *              sosaCount, sosaTotalCount, diffSosaTotalCount, firstBirth, lastBirth, avgBirth
68
     *           )
69
     * @var array $statistics_tab
70
     */
71
    protected $statistics_tab;
72
    
73
    /**
74
     * Has the provider's initialisation completed
75
     * @var bool $is_setup
76
     */
77
    protected $is_setup;
78
    
79
    /**
80
     * Constructor for Sosa Provider.
81
     * A provider is defined in relation to a specific tree and reference user.
82
     * 
83
     * @param Tree $tree
84
     * @param User $user
85
     */
86
    public function __construct(Tree $tree, User $user = null) {
87
        if(self::$default_user === null) 
88
            self::$default_user = User::find(-1);
89
        
90
        $this->tree = $tree;
91
        $this->user = $user;
92
        $this->is_setup = true;
93
        if($this->user === null) $this->user = Auth::user();
94
        if(strlen($this->user->getUserId()) == 0) $this->user = self::$default_user;
95
        
96
        // Check if the user, or the default user, has a root already setup;
97
        if(empty($this->getRootIndiId())) {
98
            if($this->user == self::$default_user) {  // If the default user is not setup
99
                $this->is_setup = false;
100
            }
101
            else {
102
                $this->user = self::$default_user;
103
                $this->is_setup = $this->getRootIndiId() === null;
104
            }            
105
        }
106
    }
107
    
108
    /**
109
     * Returns is the Provider has been successfully set up
110
     * @return bool
111
     */
112
    public function isSetup() {
113
        return $this->is_setup;
114
    }
115
    
116
    /**
117
     * Return the reference tree
118
     * 
119
     *  @return Tree Reference tree
120
     */
121
    public function getTree() {
122
        return $this->tree;
123
    }
124
    
125
    /**
126
     * Return the reference user
127
     * 
128
     * @return User
129
     */
130
    public function getUser() {
131
        return $this->user;
132
    }
133
    
134
    /**
135
     * Return the root individual ID for the reference tree and user.
136
     * @return string Individual ID
137
     */
138
    public function getRootIndiId() {
139
        return $this->tree->getUserPreference($this->user, 'MAJ_SOSA_ROOT_ID');
140
    }
141
    
142
    /**
143
     * Return the root individual for the reference tree and user.
144
     * @return Individual Individual
145
     */
146
    public function getRootIndi() {
147
        $root_indi_id = $this->getRootIndiId();
148
        if(!empty($root_indi_id)) {
149
            return Individual::getInstance($root_indi_id, $this->tree);
150
        }
151
        return null;
152
    }
153
       
154
    /*****************
155
     * DATA CRUD LAYER
156
     *****************/
157
    
158
    /**
159
     * Remove all Sosa entries related to the gedcom file and user
160
     */
161
    public function deleteAll() {
162
        if(!$this->is_setup) return;
163
        Database::prepare(
164
            'DELETE FROM `##maj_sosa`'.
165
            ' WHERE majs_gedcom_id= :tree_id and majs_user_id = :user_id ')
166
            ->execute(array(
167
                'tree_id' => $this->tree->getTreeId(), 
168
                'user_id' => $this->user->getUserId()                
169
            ));
170
    }
171
    
172
    /**
173
     * Remove all ancestors of a sosa number
174
     * 
175
     * @param int $sosa
176
     */
177
    public function deleteAncestors($sosa) {
178
        if(!$this->is_setup) return;
179
        $gen = Functions::getGeneration($sosa);
180
        Database::prepare(
181
            'DELETE FROM `##maj_sosa`'.
182
            ' WHERE majs_gedcom_id=:tree_id and majs_user_id = :user_id' .
183
            ' AND majs_gen >= :gen' .
184
            ' AND FLOOR(majs_sosa / (POW(2, (majs_gen - :gen)))) = :sosa'
185
        )->execute(array(
186
            'tree_id' => $this->tree->getTreeId(), 
187
            'user_id' => $this->user->getUserId(),
188
            'gen' => $gen,
189
            'sosa' => $sosa
190
        ));
191
    }    
192
    
193
    /**
194
     * Insert (or update if already existing) a list of Sosa individuals
195
     * @param array $sosa_records
196
     */
197
    public function insertOrUpdate($sosa_records) {
198
        if(!$this->is_setup) return;
199
        
200
        $treeid = $this->tree->getTreeId();
201
        $userid = $this->user->getUserId();
202
        $questionmarks_table = array();
203
        $values_table = array();
204
        
205
        $i = 0;
206
        foreach  ($sosa_records as $row) {
207
            $gen = Functions::getGeneration($row['sosa']);
208
            if($gen <= self::MAX_DB_GENERATIONS) {
209
                $questionmarks_table[] = 
210
                    '(:tree_id'.$i.', :user_id'.$i.', :sosa'.$i.', :indi_id'.$i.', :gen'.$i.', :byear'.$i.', :byearest'.$i.', :dyear'.$i.', :dyearest'.$i.')';
211
                $values_table = array_merge(
212
                    $values_table, 
213
                    array(
214
                        'tree_id'.$i => $treeid, 
215
                        'user_id'.$i => $userid, 
216
                        'sosa'.$i => $row['sosa'], 
217
                        'indi_id'.$i => $row['indi'], 
218
                        'gen'.$i => Functions::getGeneration($row['sosa']),
219
                        'byear'.$i => $row['birth_year'],
220
                        'byearest'.$i => $row['birth_year_est'],
221
                        'dyear'.$i => $row['death_year'],
222
                        'dyearest'.$i => $row['death_year_est']
223
                    )
224
                );
225
            }
226
            $i++;
227
        }
228
        
229
        $sql = 'REPLACE INTO `##maj_sosa`' .
230
            ' (majs_gedcom_id, majs_user_id, majs_sosa, majs_i_id, majs_gen, majs_birth_year, majs_birth_year_est, majs_death_year, majs_death_year_est)' .
231
            ' VALUES '. implode(',', $questionmarks_table);
232
        Database::prepare($sql)->execute($values_table);
233
    }
234
    
235
    /****************
236
     * SIMPLE QUERIES
237
     ****************/
238
    
239
    /**
240
     * Returns the list of Sosa numbers to which an individual is related.
241
     * Format: key = sosa number, value = generation for the Sosa number
242
     * 
243
     * @param Individual $indi
244
     * @return array Array of sosa numbers
245
     */
246
    public function getSosaNumbers(Individual $indi) {
247
        if(!$this->is_setup) return array();
248
        return Database::prepare(
249
                'SELECT majs_sosa, majs_gen FROM `##maj_sosa`'.
250
                ' WHERE majs_i_id=:indi_id AND majs_gedcom_id=:tree_id AND majs_user_id=:user_id'
251
            )->execute(array(
252
                'indi_id' => $indi->getXref(), 
253
                'tree_id' => $this->tree->getTreeId(), 
254
                'user_id' => $this->user->getUserId()
255
            ))->fetchAssoc();
256
    }
257
    
258
    /**
259
     * Get the last generation of Sosa ancestors
260
     *
261
     * @return number Last generation if found, 1 otherwise
262
     */
263
    public function getLastGeneration() {
264
        if(!$this->is_setup) return;
265
        return Database::prepare(
0 ignored issues
show
Bug Best Practice introduced by
The expression return Fisharebest\Webtr...Id()))->fetchOne() ?: 1 also could return the type string which is incompatible with the documented return type double|integer.
Loading history...
266
                'SELECT MAX(majs_gen) FROM `##maj_sosa`'.
267
                ' WHERE majs_gedcom_id=:tree_id AND majs_user_id=:user_id'
268
            )->execute(array(
269
                'tree_id' => $this->tree->getTreeId(), 
270
                'user_id' => $this->user->getUserId()                
271
            ))->fetchOne() ?: 1;
272
    }
273
    
274
    /*************
275
     * SOSA LISTS
276
     *************/
277
    
278
    /**
279
     * Return the list of all sosas, with the generations it belongs to
280
     *
281
     * @return array Associative array of Sosa ancestors, with their generation, comma separated
282
     */
283
    public function getAllSosaWithGenerations(){
284
        if(!$this->is_setup) return array();
285
        return Database::prepare(
286
            'SELECT majs_i_id AS indi,' .
287
            ' GROUP_CONCAT(DISTINCT majs_gen ORDER BY majs_gen ASC SEPARATOR ",") AS generations' .
288
            ' FROM `##maj_sosa`' .
289
            ' WHERE majs_gedcom_id=:tree_id AND majs_user_id=:user_id' .
290
            ' GROUP BY majs_i_id'
291
        )->execute(array(
292
            'tree_id' => $this->tree->getTreeId(),
293
            'user_id' => $this->user->getUserId()
294
        ))->fetchAssoc();
295
    }
296
    
297
    /**
298
     * Get an associative array of Sosa individuals in generation G. Keys are Sosa numbers, values individuals.
299
     *
300
     * @param number $gen Generation
301
     * @return array Array of Sosa individuals
302
     */
303
    public function getSosaListAtGeneration($gen){
304
        if(!$this->is_setup) return array();
305
        if(!$this->sosa_list_by_gen)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->sosa_list_by_gen of type array 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...
306
            $this->sosa_list_by_gen = array();
307
        
308
        if($gen){
309
            if(!isset($this->sosa_list_by_gen[$gen])){
310
                $this->sosa_list_by_gen[$gen] = Database::prepare(
311
                    'SELECT majs_sosa AS sosa, majs_i_id AS indi'.
312
                    ' FROM `##maj_sosa`'.
313
                    ' WHERE majs_gedcom_id=:tree_id AND majs_user_id=:user_id'.
314
                    ' AND majs_gen = :gen'.
315
                    ' ORDER BY majs_sosa ASC')
316
                ->execute(array(
317
                    'tree_id' => $this->tree->getTreeId(),
318
                    'user_id' => $this->user->getUserId(),
319
                    'gen' => $gen
320
                ))
321
                ->fetchAssoc();
322
            }
323
            return $this->sosa_list_by_gen[$gen];
324
        }
325
        return array();
326
    }
327
    
328
    /**
329
     * Get an associative array of Sosa families in generation G. Keys are Sosa numbers for the husband, values families.
330
     *
331
     * @param number $gen Generation
332
     * @return array Array of Sosa families
333
     */
334
    public function getFamilySosaListAtGeneration($gen){
335
        if(!$this->is_setup) return array();
336
        if(!$this->sosa_fam_list_by_gen)
337
            $this->sosa_fam_list_by_gen = array();
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type MyArtJaub\Webtrees\Module\Sosa\Model\unknown of property $sosa_fam_list_by_gen.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
338
        
339
        if($gen){
340
            if(!isset($this->sosa_fam_list_by_gen[$gen])){
341
                $this->sosa_fam_list_by_gen[$gen] = Database::prepare(
342
                    'SELECT s1.majs_sosa AS sosa, f_id AS fam'.
343
                    ' FROM `##families`'.
344
                    ' INNER JOIN `##maj_sosa` AS s1 ON (`##families`.f_husb = s1.majs_i_id AND `##families`.f_file = s1.majs_gedcom_id)'.
345
                    ' INNER JOIN `##maj_sosa` AS s2 ON (`##families`.f_wife = s2.majs_i_id AND `##families`.f_file = s2.majs_gedcom_id)'.
346
                    ' WHERE s1.majs_sosa + 1 = s2.majs_sosa'.
347
                    ' AND s1.majs_gedcom_id= :tree_id AND s1.majs_user_id=:user_id'.
348
                    ' AND s2.majs_gedcom_id= :tree_id AND s2.majs_user_id=:user_id'.
349
                    ' AND s1.majs_gen = :gen'.
350
                    ' ORDER BY s1.majs_sosa ASC'
351
                    )
352
                    ->execute(array(
353
                        'tree_id' => $this->tree->getTreeId(),
354
                        'user_id' => $this->user->getUserId(),
355
                        'gen' => $gen
356
                    ))
357
                    ->fetchAssoc();
358
            }
359
            return $this->sosa_fam_list_by_gen[$gen];
360
        }
361
        return array();
362
    }
363
    
364
    /**
365
     * Get an associative array of Sosa individuals in generation G who are missing parents. Keys are Sosa numbers, values individuals.
366
     *
367
     * @param number $gen Generation
368
     * @return array Array of Sosa individuals
369
     */
370
    public function getMissingSosaListAtGeneration($gen){
371
        if(!$this->is_setup) return array();    
372
        if($gen){
373
            return $this->sosa_list_by_gen[$gen] = Database::prepare(
374
                'SELECT schild.majs_sosa sosa, schild.majs_i_id indi, sfat.majs_sosa IS NOT NULL has_father, smot.majs_sosa IS NOT NULL has_mother'.
375
                ' FROM `##maj_sosa` schild'.
376
                ' LEFT JOIN `##maj_sosa` sfat ON ((schild.majs_sosa * 2) = sfat.majs_sosa AND schild.majs_gedcom_id = sfat.majs_gedcom_id AND schild.majs_user_id = sfat.majs_user_id)'.
377
                ' LEFT JOIN `##maj_sosa` smot ON ((schild.majs_sosa * 2 + 1) = smot.majs_sosa AND schild.majs_gedcom_id = smot.majs_gedcom_id AND schild.majs_user_id = smot.majs_user_id)'.
378
                ' WHERE schild.majs_gedcom_id = :tree_id AND schild.majs_user_id = :user_id'.
379
                ' AND schild.majs_gen = :gen'.
380
                ' AND (sfat.majs_sosa IS NULL OR smot.majs_sosa IS NULL)'.
381
                ' ORDER BY schild.majs_sosa ASC')
382
                ->execute(array(
383
                    'tree_id' => $this->tree->getTreeId(),
384
                    'user_id' => $this->user->getUserId(),
385
                    'gen' => $gen - 1
386
                ))->fetchAll(\PDO::FETCH_ASSOC);
387
        }
388
        return array();
389
    }
390
    
391
    
392
    
393
    /*************
394
     * STATISTICS
395
     *************/
396
    /**
397
     * Get the statistic array detailed by generation.
398
     * Statistics for each generation are:
399
     * 	- The number of Sosa in generation
400
     * 	- The number of Sosa up to generation
401
     *  - The number of distinct Sosa up to generation
402
     *  - The year of the first birth in generation
403
     *  - The year of the first estimated birth in generation
404
     *  - The year of the last birth in generation
405
     *  - The year of the last estimated birth in generation
406
     *  - The average year of birth in generation
407
     *
408
     * @return array Statistics array
409
     */
410
    public function getStatisticsByGeneration() {
411
        if(!$this->is_setup) return array();
412
        if(!$this->statistics_tab) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->statistics_tab of type array 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...
413
            $this->statistics_tab = array();
414
            if($maxGeneration = $this->getLastGeneration()) {
415
                for ($gen = 1; $gen <= $maxGeneration; $gen++) {
416
                    $birthStats = $this->getStatsBirthYearInGeneration($gen);
417
                    $this->statistics_tab[$gen] = array(
418
                        'sosaCount'				=>	$this->getSosaCountAtGeneration($gen),
419
                        'sosaTotalCount'		=>	$this->getSosaCountUpToGeneration($gen),
420
                        'diffSosaTotalCount'	=>	$this->getDifferentSosaCountUpToGeneration($gen),
421
                        'firstBirth'			=>	$birthStats['first'],
422
                        'firstEstimatedBirth'	=>	$birthStats['first_est'],
423
                        'lastBirth'				=>	$birthStats['last'],
424
                        'lastEstimatedBirth'	=>	$birthStats['last_est'],
425
                        'avgBirth'				=>	$birthStats['avg']
426
                    );
427
                }
428
            }
429
        }
430
        return $this->statistics_tab;        
431
    }
432
    
433
	/**
434
	 * How many individuals exist in the tree.
435
	 *
436
	 * @return int
437
	 */
438
	public function getTotalIndividuals() {
439
	    if(!$this->is_setup) return 0;
440
	    return Database::prepare(
0 ignored issues
show
Bug Best Practice introduced by
The expression return Fisharebest\Webtr...Id()))->fetchOne() ?: 0 also could return the type string which is incompatible with the documented return type integer.
Loading history...
441
	        'SELECT COUNT(*) FROM `##individuals`' .
442
	        ' WHERE i_file = :tree_id')
443
	        ->execute(array('tree_id' => $this->tree->getTreeId()))
444
	        ->fetchOne() ?: 0;
445
	}
446
    
447
    /**
448
     * Get the total Sosa count for all generations
449
     *
450
     * @return number Number of Sosas
451
     */
452
    public function getSosaCount(){
453
        if(!$this->is_setup) return 0;
454
        return Database::prepare(
0 ignored issues
show
Bug Best Practice introduced by
The expression return Fisharebest\Webtr...Id()))->fetchOne() ?: 0 also could return the type string which is incompatible with the documented return type double|integer.
Loading history...
455
            'SELECT COUNT(majs_sosa) FROM `##maj_sosa`' .
456
            ' WHERE majs_gedcom_id=:tree_id AND majs_user_id=:user_id')
457
            ->execute(array(
458
                'tree_id' => $this->tree->getTreeId(), 
459
                'user_id' => $this->user->getUserId() 
460
            ))->fetchOne() ?: 0;
461
    }
462
    
463
    /**
464
     * Get the number of Sosa in a specific generation.
465
     *
466
     * @param number $gen Generation
467
     * @return number Number of Sosas in generation
468
     */
469
    public function getSosaCountAtGeneration($gen){
470
        if(!$this->is_setup) return 0;
471
        return Database::prepare(
0 ignored issues
show
Bug Best Practice introduced by
The expression return Fisharebest\Webtr...$gen))->fetchOne() ?: 0 also could return the type string which is incompatible with the documented return type double|integer.
Loading history...
472
            'SELECT COUNT(majs_sosa) FROM `##maj_sosa`' .
473
            ' WHERE majs_gedcom_id=:tree_id AND majs_user_id=:user_id'.
474
            ' AND majs_gen= :gen')
475
        ->execute(array(
476
                'tree_id' => $this->tree->getTreeId(), 
477
                'user_id' => $this->user->getUserId(),
478
                'gen' => $gen            
479
        ))->fetchOne() ?: 0;
480
    }
481
    
482
    /**
483
     * Get the total number of Sosa up to a specific generation.
484
     *
485
     * @param number $gen Generation
486
     * @return number Total number of Sosas up to generation
487
     */
488
    public function getSosaCountUpToGeneration($gen){
489
        if(!$this->is_setup) return 0;
490
        return Database::prepare(
0 ignored issues
show
Bug Best Practice introduced by
The expression return Fisharebest\Webtr...$gen))->fetchOne() ?: 0 also could return the type string which is incompatible with the documented return type double|integer.
Loading history...
491
            'SELECT COUNT(majs_sosa) FROM `##maj_sosa`' .
492
            ' WHERE majs_gedcom_id=:tree_id AND majs_user_id=:user_id'.
493
            ' AND majs_gen <= :gen')
494
        ->execute(array(
495
                'tree_id' => $this->tree->getTreeId(), 
496
                'user_id' => $this->user->getUserId(),
497
                'gen' => $gen 
498
        ))->fetchOne() ?: 0;
499
    }
500
    
501
    /**
502
     * Get the total number of distinct Sosa individual for all generations.
503
     *
504
     * @return number Total number of distinct individual
505
     */
506
    public function getDifferentSosaCount(){
507
        if(!$this->is_setup) return 0;
508
        return Database::prepare(
0 ignored issues
show
Bug Best Practice introduced by
The expression return Fisharebest\Webtr...Id()))->fetchOne() ?: 0 also could return the type string which is incompatible with the documented return type double|integer.
Loading history...
509
            'SELECT COUNT(DISTINCT majs_i_id) FROM `##maj_sosa`' .
510
            ' WHERE majs_gedcom_id=:tree_id AND majs_user_id=:user_id')
511
        ->execute(array(
512
                'tree_id' => $this->tree->getTreeId(), 
513
                'user_id' => $this->user->getUserId()
514
        ))->fetchOne() ?: 0;
515
    }
516
    
517
    /**
518
     * Get the number of distinct Sosa individual up to a specific generation.
519
     *
520
     * @param number $gen Generation
521
     * @return number Number of distinct Sosa individuals up to generation
522
     */
523
    public function getDifferentSosaCountUpToGeneration($gen){
524
        if(!$this->is_setup) return 0;
525
        return Database::prepare(
0 ignored issues
show
Bug Best Practice introduced by
The expression return Fisharebest\Webtr...$gen))->fetchOne() ?: 0 also could return the type string which is incompatible with the documented return type double|integer.
Loading history...
526
            'SELECT COUNT(DISTINCT majs_i_id) FROM `##maj_sosa`' .
527
            ' WHERE majs_gedcom_id=:tree_id AND majs_user_id=:user_id'.
528
            ' AND majs_gen <= :gen')
529
        ->execute(array(
530
                'tree_id' => $this->tree->getTreeId(), 
531
                'user_id' => $this->user->getUserId(),
532
                'gen' => $gen 
533
        ))->fetchOne() ?: 0;
534
    }
535
    
536
    /**
537
     * Get an array of birth statistics for a specific generation
538
     * Statistics are :
539
     * 	- first : First birth year in generation
540
     *  - first_est: First estimated birth year in generation
541
     *  - last : Last birth year in generation
542
     *  - last_est : Last estimated birth year in generation
543
     *  - avg : Average birth year (based on non-estimated birth date)
544
     *
545
     * @param number $gen Generation
546
     * @return array Birth statistics array
547
     */
548
    public function getStatsBirthYearInGeneration($gen){
549
        if(!$this->is_setup) return array('first' => 0, 'first_est' => 0, 'avg' => 0, 'last' => 0, 'last_est' => 0);
550
        return Database::prepare(
0 ignored issues
show
Bug Best Practice introduced by
The expression return Fisharebest\Webtr... => 0, 'last_est' => 0) also could return the type stdClass which is incompatible with the documented return type array.
Loading history...
551
            'SELECT'.
552
            ' MIN(majs_birth_year) AS first, MIN(majs_birth_year_est) AS first_est,'.
553
            ' AVG(majs_birth_year) AS avg,'.
554
            ' MAX(majs_birth_year) AS last, MAX(majs_birth_year_est) AS last_est'.
555
            ' FROM `##maj_sosa`'.
556
            ' WHERE majs_gedcom_id=:tree_id AND majs_user_id=:user_id'.
557
            ' AND majs_gen=:gen')
558
            ->execute(array(
559
                'tree_id' => $this->tree->getTreeId(), 
560
                'user_id' => $this->user->getUserId(),
561
                'gen' => $gen))
562
            ->fetchOneRow(\PDO::FETCH_ASSOC) ?: array('first' => 0, 'first_est' => 0, 'avg' => 0, 'last' => 0, 'last_est' => 0);
563
    }
564
    
565
    /**
566
     * Get the mean generation time, based on a linear regression of birth years and generations
567
     *
568
     * @return number|NULL Mean generation time
569
     */
570
    public function getMeanGenerationTime(){
571
        if(!$this->is_setup) return;
572
        if(!$this->statistics_tab){
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->statistics_tab of type array 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...
573
            $this->getStatisticsByGeneration();
574
        }
575
        //Linear regression on x=generation and y=birthdate
576
        $sum_xy = 0;
577
        $sum_x=0;
578
        $sum_y=0;
579
        $sum_x2=0;
580
        $n=count($this->statistics_tab);
581
        foreach($this->statistics_tab as $gen=>$stats){
582
            $sum_xy+=$gen*$stats['avgBirth'];
583
            $sum_x+=$gen;
584
            $sum_y+=$stats['avgBirth'];
585
            $sum_x2+=$gen*$gen;
586
        }
587
        $denom=($n*$sum_x2)-($sum_x*$sum_x);
588
        if($denom!=0){
589
            return -(($n*$sum_xy)-($sum_x*$sum_y))/($denom);
590
        }
591
        return null;
592
    }
593
    
594
    /**
595
     * Return an array of the mean generation depth and standard deviation for all Sosa ancestors at a given generation.
596
     * Sosa 1 is of generation 1.
597
     * 
598
     * Mean generation depth and deviation are calculated based on the works of Marie-Héléne Cazes and Pierre Cazes,
599
     * published in Population (French Edition), Vol. 51, No. 1 (Jan. - Feb., 1996), pp. 117-140
600
     * http://kintip.net/index.php?option=com_jdownloads&task=download.send&id=9&catid=4&m=0
601
     * 
602
     * Format: 
603
     *  - key : sosa number of the ancestor
604
     *  - values: array
605
     *      - root_ancestor_id : ID of the ancestor
606
     *      - mean_gen_depth : Mean generation depth
607
     *      - stddev_gen_depth : Standard deviation of generation depth
608
     *  
609
     * @param number $gen Sosa generation
610
     * @return array
611
     */
612
    public function getGenerationDepthStatsAtGen($gen) {
613
        if(!$this->is_setup) return array();
614
        $gen_depth_stats_raw = Database::prepare(
615
            'SELECT stats_by_gen.root_ancestor AS root_ancestor_sosa,'.
616
            '   sosa_list.majs_i_id as root_ancestor_id,'.
617
            '   1 + SUM( (majs_gen_norm) * ( 2 * full_root_count + semi_root_count) /  (2 * POWER(2, majs_gen_norm))) AS mean_gen_depth,'.
618
            '   SQRT('. 
619
            '       SUM(POWER(majs_gen_norm, 2) * ( 2 * full_root_count + semi_root_count) /  (2 * POWER(2, majs_gen_norm)))'.
620
            '       - POWER( SUM( (majs_gen_norm) * ( 2 * full_root_count + semi_root_count) /  (2 * POWER(2, majs_gen_norm))), 2)'.
621
            '   ) AS stddev_gen_depth'.
622
            ' FROM('.
623
            '   SELECT'.
624
            '       sosa.majs_gedcom_id,'.
625
            '       sosa.majs_user_id,'.
626
            '       sosa.majs_gen - :gen AS majs_gen_norm,'.
627
            '       FLOOR(((sosa.majs_sosa / POW(2, sosa.majs_gen -1 )) - 1) * POWER(2, :gen - 1)) + POWER(2, :gen - 1) AS root_ancestor,'.
628
            '       SUM(case when sosa_fat.majs_i_id IS NULL AND sosa_mot.majs_i_id IS NULL THEN 1 ELSE 0 END) AS full_root_count,'.
629
            '       SUM(case when sosa_fat.majs_i_id IS NULL AND sosa_mot.majs_i_id IS NULL THEN 0 ELSE 1 END) As semi_root_count'.
630
            '   FROM `##maj_sosa` AS sosa'.
631
            '   LEFT JOIN `##maj_sosa` AS sosa_fat ON sosa_fat.majs_sosa = 2 * sosa.majs_sosa'.
632
            '       AND sosa_fat.majs_gedcom_id = sosa.majs_gedcom_id'.
633
            '       AND sosa_fat.majs_user_id = sosa.majs_user_id'.
634
            '   LEFT JOIN `##maj_sosa` AS sosa_mot ON sosa_mot.majs_sosa = 2 * sosa.majs_sosa + 1'.
635
            '       AND sosa_mot.majs_gedcom_id = sosa.majs_gedcom_id'.
636
            '       AND sosa_mot.majs_user_id = sosa.majs_user_id'.
637
            '   WHERE sosa.majs_gedcom_id = :tree_id'.
638
            '       AND sosa.majs_user_id = :user_id'.
639
            '       AND sosa.majs_gen >=  :gen'.
640
            '       AND (sosa_fat.majs_i_id IS NULL OR sosa_mot.majs_i_id IS NULL)'.
641
            '   GROUP BY sosa.majs_gen, root_ancestor'.
642
            ' ) AS stats_by_gen'.
643
            ' INNER JOIN `##maj_sosa` sosa_list ON sosa_list.majs_gedcom_id = stats_by_gen.majs_gedcom_id'.
644
            '   AND sosa_list.majs_user_id = stats_by_gen.majs_user_id'.
645
            '   AND sosa_list.majs_sosa = stats_by_gen.root_ancestor'.
646
            ' GROUP BY stats_by_gen.root_ancestor, sosa_list.majs_i_id'.
647
            ' ORDER BY stats_by_gen.root_ancestor')
648
        ->execute(array(
649
            'tree_id' => $this->tree->getTreeId(),
650
            'user_id' => $this->user->getUserId(),
651
            'gen' => $gen
652
        ))->fetchAll() ?: array();
653
        
654
        $gen_depth_stats = array();
655
        foreach ($gen_depth_stats_raw as $gen_depth_stat) {
656
            $gen_depth_stats[$gen_depth_stat->root_ancestor_sosa] = array(
657
                'root_ancestor_id' => $gen_depth_stat->root_ancestor_id,
658
                'mean_gen_depth' => $gen_depth_stat->mean_gen_depth,
659
                'stddev_gen_depth' => $gen_depth_stat->stddev_gen_depth
660
            );
661
        }
662
        return $gen_depth_stats;
663
    }
664
    
665
    /**
666
     * Return a computed array of statistics about the dispersion of ancestors across the ancestors
667
     * at a specified generation.
668
     * This statistics cannot be used for generations above 11, as it would cause a out of range in MySQL
669
     * 
670
     * Format: 
671
     *  - key : a base-2 representation of the ancestor at generation G for which exclusive ancestors have been found,
672
     *          -1 is used for shared ancestors
673
     *          For instance base2(0100) = base10(4) represent the maternal grand father
674
     *  - values: number of ancestors exclusively in the ancestors of the ancestor in key
675
     *  
676
     *  For instance a result at generation 3 could be :
677
     *      array (   -1        =>  12      -> 12 ancestors are shared by the grand-parents
678
     *                base10(1) =>  32      -> 32 ancestors are exclusive to the paternal grand-father
679
     *                base10(2) =>  25      -> 25 ancestors are exclusive to the paternal grand-mother
680
     *                base10(4) =>  12      -> 12 ancestors are exclusive to the maternal grand-father
681
     *                base10(8) =>  30      -> 30 ancestors are exclusive to the maternal grand-mother
682
     *            )
683
     *  
684
     * @param int $gen Reference generation
685
     * @return array
686
     */
687
    public function getAncestorDispersionForGen($gen) {
688
        if(!$this->is_setup || $gen > 11) return array();  // Going further than 11 gen will be out of range in the query
689
        return Database::prepare(
690
            'SELECT branches, count(i_id)'.
691
            ' FROM ('.
692
            '   SELECT i_id,'.
693
            '       CASE'.
694
            '           WHEN CEIL(LOG2(SUM(branch))) = LOG2(SUM(branch)) THEN SUM(branch)'.
695
            '           ELSE -1'.   // We put all ancestors shared between some branches in the same bucket
696
            '       END branches'.
697
            '   FROM ('.
698
            '       SELECT DISTINCT majs_i_id i_id,'.
699
            '           POW(2, FLOOR(majs_sosa / POW(2, (majs_gen - :gen))) - POW(2, :gen -1)) branch'.
700
            '       FROM `##maj_sosa`'.
701
            '       WHERE majs_gedcom_id = :tree_id AND majs_user_id = :user_id'.
702
            '           AND majs_gen >= :gen'.
703
            '   ) indistat'.
704
            '   GROUP BY i_id'.
705
            ') grouped'.
706
            ' GROUP BY branches')
707
            ->execute(array(
708
                'tree_id' => $this->tree->getTreeId(), 
709
                'user_id' => $this->user->getUserId(),
710
                'gen' => $gen
711
            ))->fetchAssoc() ?: array();
712
    }
713
    
714
    /**
715
     * Return an array of the most duplicated root Sosa ancestors.
716
     * The number of ancestors to return is limited by the parameter $limit.
717
     * If several individuals are tied when reaching the limit, none of them are returned,
718
     * which means that there can be less individuals returned than requested.
719
     * 
720
     * Format: 
721
     *  - key : root Sosa individual
722
     *  - value: number of duplications of the ancestor (e.g. 3 if it appears 3 times)
723
     * 
724
     * @param number $limit Maximum number of individuals to return
725
     * @return array 
726
     */
727
    public function getTopMultiSosaAncestorsNoTies($limit) {
728
        if(!$this->is_setup) return array();
729
        return Database::prepare(
730
            'SELECT sosa_i_id, sosa_count FROM ('.
731
            '   SELECT'.
732
            '       top_sosa.sosa_i_id, top_sosa.sosa_count, top_sosa.sosa_min,'.
733
            '       @keep := IF(@prev_count = 0 OR sosa_count = @prev_count, @keep, 1) AS keep,'.
734
            '       @prev_count := top_sosa.sosa_count AS prev_count'.
735
            '   FROM ('.
736
            '       SELECT'.
737
            '           sosa.majs_i_id sosa_i_id,'.
738
            '           COUNT(sosa.majs_sosa) sosa_count,'.
739
            '           MIN(sosa.majs_sosa) sosa_min'.
740
            '       FROM ##maj_sosa AS sosa'.
741
            '       LEFT JOIN ##maj_sosa AS sosa_fat ON sosa_fat.majs_sosa = 2 * sosa.majs_sosa'.   // Link to sosa's father
742
            '           AND sosa.majs_gedcom_id = sosa_fat.majs_gedcom_id'.
743
            '           AND sosa.majs_user_id = sosa_fat.majs_user_id'.
744
            '       LEFT JOIN ##maj_sosa AS sosa_mot on sosa_mot.majs_sosa = (2 * sosa.majs_sosa + 1)'.  // Link to sosa's mother
745
            '           AND sosa.majs_gedcom_id = sosa_fat.majs_gedcom_id'.
746
            '           AND sosa.majs_user_id = sosa_fat.majs_user_id'.
747
            '       WHERE sosa.majs_gedcom_id = :tree_id'.
748
            '       AND sosa.majs_user_id = :user_id'.
749
            '       AND sosa_fat.majs_sosa IS NULL'.    // We keep only root individuals, i.e. those with no father or mother
750
            '       AND sosa_mot.majs_sosa IS NULL'. 
751
            '       GROUP BY sosa.majs_i_id'.
752
            '       HAVING COUNT(sosa.majs_sosa) > 1'.   // Limit to the duplicate sosas.
753
            '       ORDER BY COUNT(sosa.majs_sosa) DESC'.
754
            '       LIMIT ' . ($limit + 1) . // We want to select one more than required
755
            '   ) AS top_sosa,'.
756
            '   (SELECT @prev_count := 0, @keep := 0) x'.
757
            '   ORDER BY top_sosa.sosa_count ASC'.
758
            ' ) top_sosa_list'.
759
            ' WHERE keep = 1'.
760
            ' ORDER BY sosa_count DESC, sosa_min ASC'
761
            )->execute(array(
762
                'tree_id' => $this->tree->getTreeId(),
763
                'user_id' => $this->user->getUserId()
764
            ))->fetchAssoc() ?: array();
765
    }
766
    
767
               
768
}
769