SosaRecordsService::sosaNumbers()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 10
c 0
b 0
f 0
nc 1
nop 3
dl 0
loc 12
rs 9.9332
1
<?php
2
3
/**
4
 * webtrees-lib: MyArtJaub library for webtrees
5
 *
6
 * @package MyArtJaub\Webtrees
7
 * @subpackage Sosa
8
 * @author Jonathan Jaubart <[email protected]>
9
 * @copyright Copyright (c) 2009-2022, Jonathan Jaubart
10
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License, version 3
11
 */
12
13
declare(strict_types=1);
14
15
namespace MyArtJaub\Webtrees\Module\Sosa\Services;
16
17
use Brick\Math\BigInteger;
18
use Brick\Math\RoundingMode;
19
use Fisharebest\Webtrees\Individual;
20
use Fisharebest\Webtrees\Registry;
21
use Fisharebest\Webtrees\Tree;
22
use Fisharebest\Webtrees\Contracts\UserInterface;
23
use Illuminate\Database\Capsule\Manager as DB;
24
use Illuminate\Database\Query\Builder;
25
use Illuminate\Database\Query\JoinClause;
26
use Illuminate\Support\Collection;
27
28
/**
29
 * Service for CRUD operations on Sosa records
30
 */
31
class SosaRecordsService
32
{
33
    private ?int $max_system_generations = null;
34
35
    /**
36
     * Maximum number of generation the system is able to hold.
37
     * This is based on the size of the bigint SQL type (2^63) and the maximum PHP integer type
38
     *
39
     * @return int
40
     */
41
    public function maxSystemGenerations(): int
42
    {
43
        if ($this->max_system_generations === null) {
44
            $this->max_system_generations = min(63, $this->generation(PHP_INT_MAX));
45
        }
46
        return $this->max_system_generations;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->max_system_generations could return the type null which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
47
    }
48
49
    /**
50
     * Calculate the generation of a sosa
51
     * Sosa 1 is of generation 1.
52
     *
53
     * @param int $sosa
54
     * @return int
55
     */
56
    public function generation(int $sosa): int
57
    {
58
        return BigInteger::of($sosa)->getBitLength();
59
    }
60
61
    /**
62
     * Calculate the descendant sosa of the given sosa, at the given generation.
63
     * For instance, the descendant of the Sosa 14 at generation 2 is Sosa 3 (mother).
64
     *
65
     * @param int $sosa
66
     * @param int $gen
67
     * @return int
68
     */
69
    public function sosaDescendantOf(int $sosa, int $gen): int
70
    {
71
        $gen_sosa = $this->generation($sosa);
72
        return $gen_sosa <= $gen ? $sosa : BigInteger::of($sosa)
73
            ->dividedBy(BigInteger::of(2)->power($this->generation($sosa) - $gen), RoundingMode::DOWN)
74
            ->toInt();
75
    }
76
77
    /**
78
     * Check whether an individual is a Sosa ancestor.
79
     *
80
     * @param Tree $tree
81
     * @param UserInterface $user
82
     * @param Individual $indi
83
     * @return bool
84
     */
85
    public function isSosa(Tree $tree, UserInterface $user, Individual $indi): bool
86
    {
87
        return $this->sosaNumbers($tree, $user, $indi)->count() > 0;
88
    }
89
90
    /**
91
     * Returns all Sosa numbers associated to an Individual
92
     *
93
     * @param Tree $tree
94
     * @param UserInterface $user
95
     * @param Individual $indi
96
     * @return Collection<int, int>
97
     */
98
    public function sosaNumbers(Tree $tree, UserInterface $user, Individual $indi): Collection
99
    {
100
        return Registry::cache()->array()->remember(
101
            'sosanumbers-' . $indi->xref() . '@' . $tree->id() . '-' . $user->id(),
102
            function () use ($tree, $user, $indi): Collection {
103
                return DB::table('maj_sosa')
104
                    ->select(['majs_sosa', 'majs_gen'])
105
                    ->where('majs_gedcom_id', '=', $tree->id())
106
                    ->where('majs_user_id', '=', $user->id())
107
                    ->where('majs_i_id', '=', $indi->xref())
108
                    ->orderBy('majs_sosa')
109
                    ->get()->pluck('majs_gen', 'majs_sosa');
110
            }
111
        );
112
    }
113
114
    /**
115
     * Return a list of the Sosa ancestors across all generation
116
     *
117
     * @param Tree $tree
118
     * @param UserInterface $user
119
     * @return Collection<\stdClass>
120
     */
121
    public function listAncestors(Tree $tree, UserInterface $user): Collection
122
    {
123
        return DB::table('maj_sosa')
124
            ->select(['majs_sosa', 'majs_i_id'])
125
            ->where('majs_gedcom_id', '=', $tree->id())
126
            ->where('majs_user_id', '=', $user->id())
127
            ->orderBy('majs_sosa')
128
            ->get();
129
    }
130
131
    /**
132
     * Return a list of the Sosa ancestors at a given generation
133
     *
134
     * @param Tree $tree
135
     * @param UserInterface $user
136
     * @param int $gen
137
     * @return Collection<\stdClass>
138
     */
139
    public function listAncestorsAtGeneration(Tree $tree, UserInterface $user, int $gen): Collection
140
    {
141
        return DB::table('maj_sosa')
142
            ->select(['majs_sosa', 'majs_i_id'])
143
            ->where('majs_gedcom_id', '=', $tree->id())
144
            ->where('majs_user_id', '=', $user->id())
145
            ->where('majs_gen', '=', $gen)
146
            ->orderBy('majs_sosa')
147
            ->get();
148
    }
149
150
    /**
151
     * Return a list of the Sosa families at a given generation
152
     *
153
     * @param Tree $tree
154
     * @param UserInterface $user
155
     * @param int $gen
156
     * @return Collection<\stdClass>
157
     */
158
    public function listAncestorFamiliesAtGeneration(Tree $tree, UserInterface $user, int $gen): Collection
159
    {
160
        $table_prefix = DB::connection()->getTablePrefix();
161
        return DB::table('families')
162
            ->join('maj_sosa AS sosa_husb', function (JoinClause $join) use ($tree, $user): void {
163
                // Link to family husband
164
                $join->on('families.f_file', '=', 'sosa_husb.majs_gedcom_id')
165
                    ->on('families.f_husb', '=', 'sosa_husb.majs_i_id')
166
                    ->where('sosa_husb.majs_gedcom_id', '=', $tree->id())
167
                    ->where('sosa_husb.majs_user_id', '=', $user->id());
168
            })
169
            ->join('maj_sosa AS sosa_wife', function (JoinClause $join) use ($tree, $user): void {
170
                // Link to family husband
171
                $join->on('families.f_file', '=', 'sosa_wife.majs_gedcom_id')
172
                ->on('families.f_wife', '=', 'sosa_wife.majs_i_id')
173
                ->where('sosa_wife.majs_gedcom_id', '=', $tree->id())
174
                ->where('sosa_wife.majs_user_id', '=', $user->id());
175
            })
176
            ->select(['sosa_husb.majs_sosa', 'families.f_id'])
177
            ->where('sosa_husb.majs_gen', '=', $gen)
178
            ->whereRaw($table_prefix . 'sosa_husb.majs_sosa + 1 = ' . $table_prefix . 'sosa_wife.majs_sosa')
179
            ->orderBy('sosa_husb.majs_sosa')
180
            ->get();
181
    }
182
183
    /**
184
     * Return a list of Sosa ancestors missing at a given generation.
185
     * It includes the reference of either parent if it is known.
186
     *
187
     * @param Tree $tree
188
     * @param UserInterface $user
189
     * @param int $gen
190
     * @return Collection<\stdClass>
191
     */
192
    public function listMissingAncestorsAtGeneration(Tree $tree, UserInterface $user, int $gen): Collection
193
    {
194
        if ($gen === 1) {
195
            return collect();
196
        }
197
198
        $table_prefix = DB::connection()->getTablePrefix();
199
        return DB::table('maj_sosa AS sosa')
200
            ->select(['sosa.majs_i_id', 'sosa_fat.majs_i_id AS majs_fat_id', 'sosa_mot.majs_i_id AS majs_mot_id'])
201
            ->selectRaw('MIN(' . $table_prefix . 'sosa.majs_sosa) AS majs_sosa')
202
            ->leftJoin('maj_sosa AS sosa_fat', function (JoinClause $join) use ($tree, $user, $table_prefix): void {
203
                // Link to sosa's father
204
                $join->whereRaw($table_prefix . 'sosa_fat.majs_sosa = 2 * ' . $table_prefix . 'sosa.majs_sosa')
205
                    ->where('sosa_fat.majs_gedcom_id', '=', $tree->id())
206
                    ->where('sosa_fat.majs_user_id', '=', $user->id());
207
            })
208
            ->leftJoin('maj_sosa AS sosa_mot', function (JoinClause $join) use ($tree, $user, $table_prefix): void {
209
                // Link to sosa's mother
210
                $join->whereRaw($table_prefix . 'sosa_mot.majs_sosa = 2 * ' . $table_prefix . 'sosa.majs_sosa + 1')
211
                    ->where('sosa_mot.majs_gedcom_id', '=', $tree->id())
212
                    ->where('sosa_mot.majs_user_id', '=', $user->id());
213
            })
214
            ->where('sosa.majs_gedcom_id', '=', $tree->id())
215
            ->where('sosa.majs_user_id', '=', $user->id())
216
            ->where('sosa.majs_gen', '=', $gen - 1)
217
            ->where(function (Builder $query): void {
218
                $query->whereNull('sosa_fat.majs_i_id')
219
                    ->orWhereNull('sosa_mot.majs_i_id');
220
            })
221
            ->groupBy('sosa.majs_i_id', 'sosa_fat.majs_i_id', 'sosa_mot.majs_i_id')
222
            ->orderByRaw('MIN(' . $table_prefix . 'sosa.majs_sosa)')
223
            ->get();
224
    }
225
226
    /**
227
     * Remove all Sosa entries related to the gedcom file and user
228
     *
229
     * @param Tree $tree
230
     * @param UserInterface $user
231
     */
232
    public function deleteAll(Tree $tree, UserInterface $user): void
233
    {
234
        DB::table('maj_sosa')
235
            ->where('majs_gedcom_id', '=', $tree->id())
236
            ->where('majs_user_id', '=', $user->id())
237
            ->delete();
238
    }
239
240
    /**
241
     *
242
     * @param Tree $tree
243
     * @param UserInterface $user
244
     * @param int $sosa
245
     */
246
    public function deleteAncestorsFrom(Tree $tree, UserInterface $user, int $sosa): void
247
    {
248
        DB::table('maj_sosa')
249
            ->where('majs_gedcom_id', '=', $tree->id())
250
            ->where('majs_user_id', '=', $user->id())
251
            ->where('majs_sosa', '>=', $sosa)
252
            ->whereRaw(
253
                'FLOOR(majs_sosa / (POW(2, (majs_gen - ?)))) = ?',
254
                [$this->generation($sosa), $sosa]
255
            )
256
            ->delete();
257
    }
258
259
    /**
260
     * Insert (or update if already existing) a list of Sosa individuals
261
     *
262
     * @param Tree $tree
263
     * @param UserInterface $user
264
     * @param array<array<string,mixed>> $sosa_records
265
     */
266
    public function insertOrUpdate(Tree $tree, UserInterface $user, array $sosa_records): void
267
    {
268
        $mass_update = DB::connection()->getDriverName() === 'mysql';
269
270
        $bindings_placeholders = $bindings_values = [];
271
        $has_records = false;
272
        foreach ($sosa_records as $i => $row) {
273
            $gen = $this->generation($row['sosa']);
274
            if ($gen <=  $this->maxSystemGenerations()) {
275
                $has_records = true;
276
                if ($mass_update) {
277
                    $bindings_placeholders[] = '(:tree_id' . $i . ', :user_id' . $i . ', :sosa' . $i . ',' .
278
                        ' :indi_id' . $i . ', :gen' . $i . ',' .
279
                        ' :byear' . $i . ', :byearest' . $i . ', :dyear' . $i . ', :dyearest' . $i . ')';
280
                    $bindings_values += [
281
                        'tree_id' . $i => $tree->id(),
282
                        'user_id' . $i => $user->id(),
283
                        'sosa' . $i => $row['sosa'],
284
                        'indi_id' . $i => $row['indi'],
285
                        'gen' . $i => $gen,
286
                        'byear' . $i => $row['birth_year'],
287
                        'byearest' . $i => $row['birth_year_est'],
288
                        'dyear' . $i => $row['death_year'],
289
                        'dyearest' . $i => $row['death_year_est']
290
                    ];
291
                } else {
292
                    DB::table('maj_sosa')->updateOrInsert(
293
                        [ 'majs_gedcom_id' => $tree->id(), 'majs_user_id' => $user->id(), 'majs_sosa' => $row['sosa']],
294
                        [
295
                            'majs_i_id' => $row['indi'],
296
                            'majs_gen' => $gen,
297
                            'majs_birth_year' => $row['birth_year'],
298
                            'majs_birth_year_est' => $row['birth_year_est'],
299
                            'majs_death_year' => $row['death_year'],
300
                            'majs_death_year_est' => $row['death_year_est']
301
                        ]
302
                    );
303
                }
304
            }
305
        }
306
307
        if ($has_records && $mass_update) {
308
            DB::connection()->statement(
309
                'INSERT INTO `' . DB::connection()->getTablePrefix() . 'maj_sosa`' .
310
                ' (majs_gedcom_id, majs_user_id, majs_sosa,' .
311
                '   majs_i_id, majs_gen, majs_birth_year, majs_birth_year_est, majs_death_year, majs_death_year_est)' .
312
                ' VALUES ' . implode(',', $bindings_placeholders) .
313
                ' ON DUPLICATE KEY UPDATE majs_i_id = VALUES(majs_i_id), majs_gen = VALUES(majs_gen),' .
314
                '   majs_birth_year = VALUES(majs_birth_year), majs_birth_year_est = VALUES(majs_birth_year_est),' .
315
                '   majs_death_year = VALUES(majs_death_year), majs_death_year_est = VALUES(majs_death_year_est)',
316
                $bindings_values
317
            );
318
        }
319
    }
320
}
321