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