xref: /webtrees/app/Statistics/Repository/PlaceRepository.php (revision 9f9acdbc09170c04c1e150c36ee57d49027e314a)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2019 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\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 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->get()->all();
169    }
170
171    /**
172     * Get the top 10 places list.
173     *
174     * @param array<string,int> $places
175     *
176     * @return array<array<string,mixed>>
177     */
178    private function getTop10Places(array $places): array
179    {
180        $top10 = [];
181        $i     = 0;
182
183        arsort($places);
184
185        foreach ($places as $place => $count) {
186            $tmp     = new Place($place, $this->tree);
187            $top10[] = [
188                'place' => $tmp,
189                'count' => $count,
190            ];
191
192            ++$i;
193
194            if ($i === 10) {
195                break;
196            }
197        }
198
199        return $top10;
200    }
201
202    /**
203     * Renders the top 10 places list.
204     *
205     * @param array<string,int> $places
206     *
207     * @return string
208     */
209    private function renderTop10(array $places): string
210    {
211        $top10Records = $this->getTop10Places($places);
212
213        return view(
214            'statistics/other/top10-list',
215            [
216                'records' => $top10Records,
217            ]
218        );
219    }
220
221    /**
222     * A list of common birth places.
223     *
224     * @return string
225     */
226    public function commonBirthPlacesList(): string
227    {
228        $places = $this->queryFactPlaces('BIRT', 'INDI');
229        return $this->renderTop10($places);
230    }
231
232    /**
233     * A list of common death places.
234     *
235     * @return string
236     */
237    public function commonDeathPlacesList(): string
238    {
239        $places = $this->queryFactPlaces('DEAT', 'INDI');
240        return $this->renderTop10($places);
241    }
242
243    /**
244     * A list of common marriage places.
245     *
246     * @return string
247     */
248    public function commonMarriagePlacesList(): string
249    {
250        $places = $this->queryFactPlaces('MARR', 'FAM');
251        return $this->renderTop10($places);
252    }
253
254    /**
255     * A list of common countries.
256     *
257     * @return string
258     */
259    public function commonCountriesList(): string
260    {
261        $countries = $this->statsPlaces();
262
263        if ($countries === []) {
264            return '';
265        }
266
267        $top10 = [];
268        $i     = 1;
269
270        // Get the country names for each language
271        $country_names = [];
272        $old_language = I18N::languageTag();
273
274        foreach (I18N::activeLocales() as $locale) {
275            I18N::init($locale->languageTag());
276            $all_countries = $this->country_service->getAllCountries();
277            foreach ($all_countries as $country_code => $country_name) {
278                $country_names[$country_name] = $country_code;
279            }
280        }
281
282        I18N::init($old_language);
283
284        $all_db_countries = [];
285        foreach ($countries as $place) {
286            $country = trim($place->country);
287            if (array_key_exists($country, $country_names)) {
288                if (isset($all_db_countries[$country_names[$country]][$country])) {
289                    $all_db_countries[$country_names[$country]][$country] += (int) $place->tot;
290                } else {
291                    $all_db_countries[$country_names[$country]][$country] = (int) $place->tot;
292                }
293            }
294        }
295
296        // get all the user’s countries names
297        $all_countries = $this->country_service->getAllCountries();
298
299        foreach ($all_db_countries as $country_code => $country) {
300            foreach ($country as $country_name => $tot) {
301                $tmp     = new Place($country_name, $this->tree);
302
303                $top10[] = [
304                    'place' => $tmp,
305                    'count' => $tot,
306                    'name'  => $all_countries[$country_code],
307                ];
308            }
309
310            if ($i++ === 10) {
311                break;
312            }
313        }
314
315        return view(
316            'statistics/other/top10-list',
317            [
318                'records' => $top10,
319            ]
320        );
321    }
322
323    /**
324     * Count total places.
325     *
326     * @return int
327     */
328    private function totalPlacesQuery(): int
329    {
330        return DB::table('places')
331            ->where('p_file', '=', $this->tree->id())
332            ->count();
333    }
334
335    /**
336     * Count total places.
337     *
338     * @return string
339     */
340    public function totalPlaces(): string
341    {
342        return I18N::number($this->totalPlacesQuery());
343    }
344
345    /**
346     * Create a chart showing where events occurred.
347     *
348     * @param string $chart_shows
349     * @param string $chart_type
350     * @param string $surname
351     *
352     * @return string
353     */
354    public function chartDistribution(
355        string $chart_shows = 'world',
356        string $chart_type = '',
357        string $surname = ''
358    ): string {
359        return (new ChartDistribution($this->tree))
360            ->chartDistribution($chart_shows, $chart_type, $surname);
361    }
362}
363