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