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