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