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