xref: /webtrees/app/Statistics/Repository/PlaceRepository.php (revision f70bcff589628705e50ed72a6fc6cbad68558677)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2021 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\Family;
23use Fisharebest\Webtrees\Gedcom;
24use Fisharebest\Webtrees\I18N;
25use Fisharebest\Webtrees\Individual;
26use Fisharebest\Webtrees\Location;
27use Fisharebest\Webtrees\Place;
28use Fisharebest\Webtrees\Statistics\Google\ChartDistribution;
29use Fisharebest\Webtrees\Statistics\Repository\Interfaces\PlaceRepositoryInterface;
30use Fisharebest\Webtrees\Statistics\Service\CountryService;
31use Fisharebest\Webtrees\Tree;
32use Illuminate\Database\Capsule\Manager as DB;
33use Illuminate\Database\Query\JoinClause;
34
35use function array_key_exists;
36use function arsort;
37use function end;
38use function explode;
39use function preg_match;
40use function trim;
41use function view;
42
43/**
44 * A repository providing methods for place related statistics.
45 */
46class PlaceRepository implements PlaceRepositoryInterface
47{
48    private Tree $tree;
49
50    private CountryService $country_service;
51
52    /**
53     * @param Tree           $tree
54     * @param CountryService $country_service
55     */
56    public function __construct(Tree $tree, CountryService $country_service)
57    {
58        $this->tree            = $tree;
59        $this->country_service = $country_service;
60    }
61
62    /**
63     * Places
64     *
65     * @param string $fact
66     * @param string $what
67     * @param bool   $country
68     *
69     * @return array<int|string,int>
70     */
71    private function queryFactPlaces(string $fact, string $what = 'ALL', bool $country = false): array
72    {
73        $rows = [];
74
75        if ($what === 'INDI') {
76            $rows = DB::table('individuals')
77                ->select(['i_gedcom as tree'])
78                ->where('i_file', '=', $this->tree->id())
79                ->where('i_gedcom', 'LIKE', "%\n2 PLAC %")
80                ->get()
81                ->all();
82        } elseif ($what === 'FAM') {
83            $rows = DB::table('families')->select(['f_gedcom as tree'])
84                ->where('f_file', '=', $this->tree->id())
85                ->where('f_gedcom', 'LIKE', "%\n2 PLAC %")
86                ->get()
87                ->all();
88        }
89
90        $placelist = [];
91
92        foreach ($rows as $row) {
93            if (preg_match('/\n1 ' . $fact . '(?:\n[2-9].*)*\n2 PLAC (.+)/', $row->tree, $match)) {
94                if ($country) {
95                    $tmp   = explode(Gedcom::PLACE_SEPARATOR, $match[1]);
96                    $place = end($tmp);
97                } else {
98                    $place = $match[1];
99                }
100
101                $placelist[$place] = ($placelist[$place] ?? 0) + 1;
102            }
103        }
104
105        return $placelist;
106    }
107
108    /**
109     * Query places.
110     *
111     * @param string $what
112     * @param string $fact
113     * @param int    $parent
114     * @param bool   $country
115     *
116     * @return array<int|object>
117     */
118    public function statsPlaces(string $what = 'ALL', string $fact = '', int $parent = 0, bool $country = false): array
119    {
120        if ($fact) {
121            return $this->queryFactPlaces($fact, $what, $country);
122        }
123
124        $query = DB::table('places')
125            ->join('placelinks', static function (JoinClause $join): void {
126                $join->on('pl_file', '=', 'p_file')
127                    ->on('pl_p_id', '=', 'p_id');
128            })
129            ->where('p_file', '=', $this->tree->id());
130
131        if ($parent > 0) {
132            // Used by placehierarchy map modules
133            $query->select(['p_place AS place'])
134                ->selectRaw('COUNT(*) AS tot')
135                ->where('p_id', '=', $parent)
136                ->groupBy(['place']);
137        } else {
138            $query->select(['p_place AS country'])
139                ->selectRaw('COUNT(*) AS tot')
140                ->where('p_parent_id', '=', 0)
141                ->groupBy(['country'])
142                ->orderByDesc('tot')
143                ->orderBy('country');
144        }
145
146        if ($what === Individual::RECORD_TYPE) {
147            $query->join('individuals', static function (JoinClause $join): void {
148                $join->on('pl_file', '=', 'i_file')
149                    ->on('pl_gid', '=', 'i_id');
150            });
151        } elseif ($what === Family::RECORD_TYPE) {
152            $query->join('families', static function (JoinClause $join): void {
153                $join->on('pl_file', '=', 'f_file')
154                    ->on('pl_gid', '=', 'f_id');
155            });
156        } elseif ($what === Location::RECORD_TYPE) {
157            $query->join('other', static function (JoinClause $join): void {
158                $join->on('pl_file', '=', 'o_file')
159                    ->on('pl_gid', '=', 'o_id');
160            })
161                ->where('o_type', '=', Location::RECORD_TYPE);
162        }
163
164        return $query
165            ->get()
166            ->map(static function (object $entry) {
167                // Map total value to integer
168                $entry->tot = (int) $entry->tot;
169
170                return $entry;
171            })
172            ->all();
173    }
174
175    /**
176     * Get the top 10 places list.
177     *
178     * @param array<int|string,int> $places
179     *
180     * @return array<array<string,mixed>>
181     */
182    private function getTop10Places(array $places): array
183    {
184        $top10 = [];
185        $i     = 0;
186
187        arsort($places);
188
189        foreach ($places as $place => $count) {
190            $tmp     = new Place((string) $place, $this->tree);
191            $top10[] = [
192                'place' => $tmp,
193                'count' => $count,
194            ];
195
196            ++$i;
197
198            if ($i === 10) {
199                break;
200            }
201        }
202
203        return $top10;
204    }
205
206    /**
207     * Renders the top 10 places list.
208     *
209     * @param array<int|string,int> $places
210     *
211     * @return string
212     */
213    private function renderTop10(array $places): string
214    {
215        $top10Records = $this->getTop10Places($places);
216
217        return view(
218            'statistics/other/top10-list',
219            [
220                'records' => $top10Records,
221            ]
222        );
223    }
224
225    /**
226     * A list of common birth places.
227     *
228     * @return string
229     */
230    public function commonBirthPlacesList(): string
231    {
232        $places = $this->queryFactPlaces('BIRT', 'INDI');
233        return $this->renderTop10($places);
234    }
235
236    /**
237     * A list of common death places.
238     *
239     * @return string
240     */
241    public function commonDeathPlacesList(): string
242    {
243        $places = $this->queryFactPlaces('DEAT', 'INDI');
244        return $this->renderTop10($places);
245    }
246
247    /**
248     * A list of common marriage places.
249     *
250     * @return string
251     */
252    public function commonMarriagePlacesList(): string
253    {
254        $places = $this->queryFactPlaces('MARR', 'FAM');
255        return $this->renderTop10($places);
256    }
257
258    /**
259     * A list of common countries.
260     *
261     * @return string
262     */
263    public function commonCountriesList(): string
264    {
265        $countries = $this->statsPlaces();
266
267        if ($countries === []) {
268            return '';
269        }
270
271        $top10 = [];
272        $i     = 1;
273
274        // Get the country names for each language
275        $country_names = [];
276        $old_language = I18N::languageTag();
277
278        foreach (I18N::activeLocales() as $locale) {
279            I18N::init($locale->languageTag());
280            $all_countries = $this->country_service->getAllCountries();
281            foreach ($all_countries as $country_code => $country_name) {
282                $country_names[$country_name] = $country_code;
283            }
284        }
285
286        I18N::init($old_language);
287
288        $all_db_countries = [];
289        foreach ($countries as $place) {
290            $country = trim($place->country);
291            if (array_key_exists($country, $country_names)) {
292                if (isset($all_db_countries[$country_names[$country]][$country])) {
293                    $all_db_countries[$country_names[$country]][$country] += (int) $place->tot;
294                } else {
295                    $all_db_countries[$country_names[$country]][$country] = (int) $place->tot;
296                }
297            }
298        }
299
300        // get all the user’s countries names
301        $all_countries = $this->country_service->getAllCountries();
302
303        foreach ($all_db_countries as $country_code => $country) {
304            foreach ($country as $country_name => $tot) {
305                $tmp     = new Place($country_name, $this->tree);
306
307                $top10[] = [
308                    'place' => $tmp,
309                    'count' => $tot,
310                    'name'  => $all_countries[$country_code],
311                ];
312            }
313
314            if ($i++ === 10) {
315                break;
316            }
317        }
318
319        return view(
320            'statistics/other/top10-list',
321            [
322                'records' => $top10,
323            ]
324        );
325    }
326
327    /**
328     * Count total places.
329     *
330     * @return int
331     */
332    private function totalPlacesQuery(): int
333    {
334        return DB::table('places')
335            ->where('p_file', '=', $this->tree->id())
336            ->count();
337    }
338
339    /**
340     * Count total places.
341     *
342     * @return string
343     */
344    public function totalPlaces(): string
345    {
346        return I18N::number($this->totalPlacesQuery());
347    }
348
349    /**
350     * Create a chart showing where events occurred.
351     *
352     * @param string $chart_shows
353     * @param string $chart_type
354     * @param string $surname
355     *
356     * @return string
357     */
358    public function chartDistribution(
359        string $chart_shows = 'world',
360        string $chart_type = '',
361        string $surname = ''
362    ): string {
363        return (new ChartDistribution($this->tree, $this->country_service))
364            ->chartDistribution($chart_shows, $chart_type, $surname);
365    }
366}
367