xref: /webtrees/app/Statistics/Repository/EventRepository.php (revision 1270d2767576ed4a83917769b0ee3613e3b010bf)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2023 webtrees development team
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees\Statistics\Repository;
21
22use Fisharebest\Webtrees\Date;
23use Fisharebest\Webtrees\DB;
24use Fisharebest\Webtrees\Elements\UnknownElement;
25use Fisharebest\Webtrees\Fact;
26use Fisharebest\Webtrees\Family;
27use Fisharebest\Webtrees\Gedcom;
28use Fisharebest\Webtrees\GedcomRecord;
29use Fisharebest\Webtrees\Header;
30use Fisharebest\Webtrees\I18N;
31use Fisharebest\Webtrees\Individual;
32use Fisharebest\Webtrees\Registry;
33use Fisharebest\Webtrees\Statistics\Repository\Interfaces\EventRepositoryInterface;
34use Fisharebest\Webtrees\Tree;
35
36use function abs;
37use function array_map;
38use function array_merge;
39use function e;
40use function strncmp;
41use function substr;
42
43/**
44 * A repository providing methods for event related statistics.
45 */
46class EventRepository implements EventRepositoryInterface
47{
48    /**
49     * Sorting directions.
50     */
51    private const SORT_ASC  = 'ASC';
52    private const SORT_DESC = 'DESC';
53
54    /**
55     * Event facts.
56     */
57    private const EVENT_BIRTH    = 'BIRT';
58    private const EVENT_DEATH    = 'DEAT';
59    private const EVENT_MARRIAGE = 'MARR';
60    private const EVENT_DIVORCE  = 'DIV';
61
62    private Tree $tree;
63
64    /**
65     * @param Tree $tree
66     */
67    public function __construct(Tree $tree)
68    {
69        $this->tree = $tree;
70    }
71
72    /**
73     * Returns the total number of a given list of events (with dates).
74     *
75     * @param array<string> $events The list of events to count (e.g. BIRT, DEAT, ...)
76     *
77     * @return int
78     */
79    private function getEventCount(array $events): int
80    {
81        $query = DB::table('dates')
82            ->where('d_file', '=', $this->tree->id());
83
84        $no_types = [
85            'HEAD',
86            'CHAN',
87        ];
88
89        if ($events !== []) {
90            $types = [];
91
92            foreach ($events as $type) {
93                if (strncmp($type, '!', 1) === 0) {
94                    $no_types[] = substr($type, 1);
95                } else {
96                    $types[] = $type;
97                }
98            }
99
100            if ($types !== []) {
101                $query->whereIn('d_fact', $types);
102            }
103        }
104
105        return $query->whereNotIn('d_fact', $no_types)
106            ->count();
107    }
108
109    /**
110     * @param array<string> $events
111     *
112     * @return string
113     */
114    public function totalEvents(array $events = []): string
115    {
116        return I18N::number(
117            $this->getEventCount($events)
118        );
119    }
120
121    /**
122     * @return string
123     */
124    public function totalEventsBirth(): string
125    {
126        return $this->totalEvents(Gedcom::BIRTH_EVENTS);
127    }
128
129    /**
130     * @return string
131     */
132    public function totalBirths(): string
133    {
134        return $this->totalEvents([self::EVENT_BIRTH]);
135    }
136
137    /**
138     * @return string
139     */
140    public function totalEventsDeath(): string
141    {
142        return $this->totalEvents(Gedcom::DEATH_EVENTS);
143    }
144
145    /**
146     * @return string
147     */
148    public function totalDeaths(): string
149    {
150        return $this->totalEvents([self::EVENT_DEATH]);
151    }
152
153    /**
154     * @return string
155     */
156    public function totalEventsMarriage(): string
157    {
158        return $this->totalEvents(Gedcom::MARRIAGE_EVENTS);
159    }
160
161    /**
162     * @return string
163     */
164    public function totalMarriages(): string
165    {
166        return $this->totalEvents([self::EVENT_MARRIAGE]);
167    }
168
169    /**
170     * @return string
171     */
172    public function totalEventsDivorce(): string
173    {
174        return $this->totalEvents(Gedcom::DIVORCE_EVENTS);
175    }
176
177    /**
178     * @return string
179     */
180    public function totalDivorces(): string
181    {
182        return $this->totalEvents([self::EVENT_DIVORCE]);
183    }
184
185    /**
186     * Returns the list of common facts used query the data.
187     *
188     * @return array<string>
189     */
190    private function getCommonFacts(): array
191    {
192        // The list of facts used to limit the query result
193        return array_merge(
194            Gedcom::BIRTH_EVENTS,
195            Gedcom::MARRIAGE_EVENTS,
196            Gedcom::DIVORCE_EVENTS,
197            Gedcom::DEATH_EVENTS
198        );
199    }
200
201    /**
202     * @return string
203     */
204    public function totalEventsOther(): string
205    {
206        $no_facts = array_map(
207            static fn (string $fact): string => '!' . $fact,
208            $this->getCommonFacts()
209        );
210
211        return $this->totalEvents($no_facts);
212    }
213
214    /**
215     * Returns the first/last event record from the given list of event facts.
216     *
217     * @param string $direction The sorting direction of the query (To return first or last record)
218     *
219     * @return object{id:string,year:int,fact:string,type:string}|null
220     */
221    private function eventQuery(string $direction): object|null
222    {
223        return DB::table('dates')
224            ->select(['d_gid as id', 'd_year as year', 'd_fact AS fact', 'd_type AS type'])
225            ->where('d_file', '=', $this->tree->id())
226            ->where('d_gid', '<>', Header::RECORD_TYPE)
227            ->whereIn('d_fact', $this->getCommonFacts())
228            ->where('d_julianday1', '<>', 0)
229            ->orderBy('d_julianday1', $direction)
230            ->orderBy('d_type')
231            ->limit(1)
232            ->get()
233            ->map(static fn (object $row): object => (object) [
234                'id'   => $row->id,
235                'year' => (int) $row->year,
236                'fact' => $row->fact,
237                'type' => $row->type,
238            ])
239            ->first();
240    }
241
242    /**
243     * Returns the formatted first/last occurring event.
244     *
245     * @param string $direction The sorting direction
246     *
247     * @return string
248     */
249    private function getFirstLastEvent(string $direction): string
250    {
251        $row    = $this->eventQuery($direction);
252        $result = I18N::translate('This information is not available.');
253
254        if ($row !== null) {
255            $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree);
256
257            if ($record instanceof GedcomRecord && $record->canShow()) {
258                $result = $record->formatList();
259            } else {
260                $result = I18N::translate('This information is private and cannot be shown.');
261            }
262        }
263
264        return $result;
265    }
266
267    /**
268     * @return string
269     */
270    public function firstEvent(): string
271    {
272        return $this->getFirstLastEvent(self::SORT_ASC);
273    }
274
275    /**
276     * @return string
277     */
278    public function lastEvent(): string
279    {
280        return $this->getFirstLastEvent(self::SORT_DESC);
281    }
282
283    /**
284     * Returns the formatted year of the first/last occurring event.
285     *
286     * @param string $direction The sorting direction
287     *
288     * @return string
289     */
290    private function getFirstLastEventYear(string $direction): string
291    {
292        $row = $this->eventQuery($direction);
293
294        if ($row === null) {
295            return '';
296        }
297
298        if ($row->year < 0) {
299            $row->year = abs($row->year) . ' B.C.';
300        }
301
302        return (new Date($row->type . ' ' . $row->year))
303            ->display();
304    }
305
306    /**
307     * @return string
308     */
309    public function firstEventYear(): string
310    {
311        return $this->getFirstLastEventYear(self::SORT_ASC);
312    }
313
314    /**
315     * @return string
316     */
317    public function lastEventYear(): string
318    {
319        return $this->getFirstLastEventYear(self::SORT_DESC);
320    }
321
322    /**
323     * Returns the formatted type of the first/last occurring event.
324     *
325     * @param string $direction The sorting direction
326     *
327     * @return string
328     */
329    private function getFirstLastEventType(string $direction): string
330    {
331        $row = $this->eventQuery($direction);
332
333        if ($row === null) {
334            return '';
335        }
336
337        foreach ([Individual::RECORD_TYPE, Family::RECORD_TYPE] as $record_type) {
338            $element = Registry::elementFactory()->make($record_type . ':' . $row->fact);
339
340            if (!$element instanceof UnknownElement) {
341                return $element->label();
342            }
343        }
344
345        return $row->fact;
346    }
347
348    /**
349     * @return string
350     */
351    public function firstEventType(): string
352    {
353        return $this->getFirstLastEventType(self::SORT_ASC);
354    }
355
356    /**
357     * @return string
358     */
359    public function lastEventType(): string
360    {
361        return $this->getFirstLastEventType(self::SORT_DESC);
362    }
363
364    /**
365     * Returns the formatted name of the first/last occurring event.
366     *
367     * @param string $direction The sorting direction
368     *
369     * @return string
370     */
371    private function getFirstLastEventName(string $direction): string
372    {
373        $row = $this->eventQuery($direction);
374
375        if ($row !== null) {
376            $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree);
377
378            if ($record instanceof GedcomRecord) {
379                return '<a href="' . e($record->url()) . '">' . $record->fullName() . '</a>';
380            }
381        }
382
383        return '';
384    }
385
386    /**
387     * @return string
388     */
389    public function firstEventName(): string
390    {
391        return $this->getFirstLastEventName(self::SORT_ASC);
392    }
393
394    /**
395     * @return string
396     */
397    public function lastEventName(): string
398    {
399        return $this->getFirstLastEventName(self::SORT_DESC);
400    }
401
402    /**
403     * Returns the formatted place of the first/last occurring event.
404     *
405     * @param string $direction The sorting direction
406     *
407     * @return string
408     */
409    private function getFirstLastEventPlace(string $direction): string
410    {
411        $row = $this->eventQuery($direction);
412
413        if ($row !== null) {
414            $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree);
415            $fact   = null;
416
417            if ($record instanceof GedcomRecord) {
418                $fact = $record->facts([$row->fact])->first();
419            }
420
421            if ($fact instanceof Fact) {
422                return $fact->place()->shortName();
423            }
424        }
425
426        return I18N::translate('Private');
427    }
428
429    /**
430     * @return string
431     */
432    public function firstEventPlace(): string
433    {
434        return $this->getFirstLastEventPlace(self::SORT_ASC);
435    }
436
437    /**
438     * @return string
439     */
440    public function lastEventPlace(): string
441    {
442        return $this->getFirstLastEventPlace(self::SORT_DESC);
443    }
444}
445