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