xref: /webtrees/app/Statistics/Google/ChartDistribution.php (revision 5bfc689774bb9a6401271c4ed15a6d50652c991b)
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\Google;
21
22use Fisharebest\Webtrees\I18N;
23use Fisharebest\Webtrees\Statistics\Repository\Interfaces\IndividualRepositoryInterface;
24use Fisharebest\Webtrees\Statistics\Repository\Interfaces\PlaceRepositoryInterface;
25use Fisharebest\Webtrees\Statistics\Service\CountryService;
26use Fisharebest\Webtrees\Tree;
27use Illuminate\Database\Capsule\Manager as DB;
28use Illuminate\Database\Query\Expression;
29use Illuminate\Database\Query\JoinClause;
30
31use function array_key_exists;
32use function preg_match_all;
33use function view;
34
35/**
36 * A chart showing the distribution of different events on a map.
37 */
38class ChartDistribution
39{
40    private Tree $tree;
41
42    private CountryService $country_service;
43
44    private IndividualRepositoryInterface $individual_repository;
45
46    private PlaceRepositoryInterface $place_repository;
47
48    /**
49     * @var array<string>
50     */
51    private array $country_to_iso3166;
52
53    /**
54     * @param Tree                          $tree
55     * @param CountryService                $country_service
56     * @param IndividualRepositoryInterface $individual_repository
57     * @param PlaceRepositoryInterface      $place_repository
58     */
59    public function __construct(
60        Tree $tree,
61        CountryService $country_service,
62        IndividualRepositoryInterface $individual_repository,
63        PlaceRepositoryInterface $place_repository
64    ) {
65        $this->tree                  = $tree;
66        $this->country_service       = $country_service;
67        $this->individual_repository = $individual_repository;
68        $this->place_repository      = $place_repository;
69
70        // Get the country names for each language
71        $this->country_to_iso3166 = $this->getIso3166Countries();
72    }
73
74    /**
75     * Returns the country names for each language.
76     *
77     * @return array<string>
78     */
79    private function getIso3166Countries(): array
80    {
81        // Get the country names for each language
82        $country_to_iso3166 = [];
83
84        $current_language = I18N::languageTag();
85
86        foreach (I18N::activeLocales() as $locale) {
87            I18N::init($locale->languageTag());
88
89            $countries = $this->country_service->getAllCountries();
90
91            foreach ($this->country_service->iso3166() as $three => $two) {
92                $country_to_iso3166[$three]             = $two;
93                $country_to_iso3166[$countries[$three]] = $two;
94            }
95        }
96
97        I18N::init($current_language);
98
99        return $country_to_iso3166;
100    }
101
102    /**
103     * Returns the data structure required by google geochart.
104     *
105     * @param array<int|string,int> $places
106     *
107     * @return array<array<string>>
108     */
109    private function createChartData(array $places): array
110    {
111        $data = [
112            [
113                I18N::translate('Country'),
114                I18N::translate('Total'),
115            ],
116        ];
117
118        // webtrees uses 3-letter country codes and localised country names, but google uses 2 letter codes.
119        foreach ($places as $country => $count) {
120            $data[] = [
121                [
122                    'v' => $country,
123                    'f' => $this->country_service->mapTwoLetterToName($country),
124                ],
125                $count
126            ];
127        }
128
129        return $data;
130    }
131
132    /**
133     * Returns the google geochart data for birth fact.
134     *
135     * @return array<array<string>>
136     */
137    private function getBirthChartData(): array
138    {
139        // Count how many people were born in each country
140        $surn_countries = [];
141        $b_countries    = $this->place_repository->statsPlaces('INDI', 'BIRT', 0, true);
142
143        foreach ($b_countries as $country => $count) {
144            // Consolidate places (Germany, DEU => DE)
145            if (array_key_exists($country, $this->country_to_iso3166)) {
146                $country_code = $this->country_to_iso3166[$country];
147
148                if (array_key_exists($country_code, $surn_countries)) {
149                    $surn_countries[$country_code] += $count;
150                } else {
151                    $surn_countries[$country_code] = $count;
152                }
153            }
154        }
155
156        return $this->createChartData($surn_countries);
157    }
158
159    /**
160     * Returns the google geochart data for death fact.
161     *
162     * @return array<array<string>>
163     */
164    private function getDeathChartData(): array
165    {
166        // Count how many people were death in each country
167        $surn_countries = [];
168        $d_countries    = $this->place_repository->statsPlaces('INDI', 'DEAT', 0, true);
169
170        foreach ($d_countries as $country => $count) {
171            // Consolidate places (Germany, DEU => DE)
172            if (array_key_exists($country, $this->country_to_iso3166)) {
173                $country_code = $this->country_to_iso3166[$country];
174
175                if (array_key_exists($country_code, $surn_countries)) {
176                    $surn_countries[$country_code] += $count;
177                } else {
178                    $surn_countries[$country_code] = $count;
179                }
180            }
181        }
182
183        return $this->createChartData($surn_countries);
184    }
185
186    /**
187     * Returns the google geochart data for marriages.
188     *
189     * @return array<array<string>>
190     */
191    private function getMarriageChartData(): array
192    {
193        // Count how many families got marriage in each country
194        $surn_countries = [];
195        $m_countries    = $this->place_repository->statsPlaces('FAM');
196
197        // webtrees uses 3 letter country codes and localised country names, but google uses 2 letter codes.
198        foreach ($m_countries as $place) {
199            // Consolidate places (Germany, DEU => DE)
200            if (array_key_exists($place->country, $this->country_to_iso3166)) {
201                $country_code = $this->country_to_iso3166[$place->country];
202
203                if (array_key_exists($country_code, $surn_countries)) {
204                    $surn_countries[$country_code] += $place->tot;
205                } else {
206                    $surn_countries[$country_code] = $place->tot;
207                }
208            }
209        }
210
211        return $this->createChartData($surn_countries);
212    }
213
214    /**
215     * Returns the related database records.
216     *
217     * @param string $surname
218     *
219     * @return array<object>
220     */
221    private function queryRecords(string $surname): array
222    {
223        $query = DB::table('individuals')
224            ->select(['i_gedcom'])
225            ->join('name', static function (JoinClause $join): void {
226                $join->on('n_id', '=', 'i_id')
227                    ->on('n_file', '=', 'i_file');
228            })
229            ->where('n_file', '=', $this->tree->id())
230            ->where(new Expression('n_surn /*! COLLATE ' . I18N::collation() . ' */'), '=', $surname);
231
232        return $query->get()->all();
233    }
234
235    /**
236     * Returns the google geochart data for surnames.
237     *
238     * @param string $surname The surname used to create the chart
239     *
240     * @return array<array<string>>
241     */
242    private function getSurnameChartData(string $surname): array
243    {
244        if ($surname === '') {
245            $surname = $this->individual_repository->getCommonSurname();
246        }
247
248        // Count how many people are events in each country
249        $surn_countries = [];
250        $records        = $this->queryRecords($surname);
251
252        foreach ($records as $row) {
253            if (preg_match_all('/^2 PLAC (?:.*, *)*(.*)/m', $row->i_gedcom, $matches)) {
254                // webtrees uses 3 letter country codes and localised country names,
255                // but google uses 2 letter codes.
256                foreach ($matches[1] as $country) {
257                    // Consolidate places (Germany, DEU => DE)
258                    if (array_key_exists($country, $this->country_to_iso3166)) {
259                        $country_code = $this->country_to_iso3166[$country];
260
261                        if (array_key_exists($country_code, $surn_countries)) {
262                            $surn_countries[$country_code]++;
263                        } else {
264                            $surn_countries[$country_code] = 1;
265                        }
266                    }
267                }
268            }
269        }
270
271        return $this->createChartData($surn_countries);
272    }
273
274    /**
275     * Returns the google geochart data for individuals.
276     *
277     * @return array<array<string>>
278     */
279    private function getIndivdualChartData(): array
280    {
281        // Count how many people have events in each country
282        $surn_countries = [];
283        $a_countries    = $this->place_repository->statsPlaces('INDI');
284
285        // webtrees uses 3 letter country codes and localised country names, but google uses 2 letter codes.
286        foreach ($a_countries as $place) {
287            // Consolidate places (Germany, DEU => DE)
288            if (array_key_exists($place->country, $this->country_to_iso3166)) {
289                $country_code = $this->country_to_iso3166[$place->country];
290
291                if (array_key_exists($country_code, $surn_countries)) {
292                    $surn_countries[$country_code] += $place->tot;
293                } else {
294                    $surn_countries[$country_code] = $place->tot;
295                }
296            }
297        }
298
299        return $this->createChartData($surn_countries);
300    }
301
302    /**
303     * Create a chart showing where events occurred.
304     *
305     * @param string $chart_shows The type of chart map to show
306     * @param string $chart_type  The type of chart to show
307     * @param string $surname     The surname for surname based distribution chart
308     *
309     * @return string
310     */
311    public function chartDistribution(
312        string $chart_shows = 'world',
313        string $chart_type = '',
314        string $surname = ''
315    ): string {
316        switch ($chart_type) {
317            case 'surname_distribution_chart':
318                $chart_title = I18N::translate('Surname distribution chart') . ': ' . $surname;
319                $data        = $this->getSurnameChartData($surname);
320                break;
321
322            case 'birth_distribution_chart':
323                $chart_title = I18N::translate('Birth by country');
324                $data        = $this->getBirthChartData();
325                break;
326
327            case 'death_distribution_chart':
328                $chart_title = I18N::translate('Death by country');
329                $data        = $this->getDeathChartData();
330                break;
331
332            case 'marriage_distribution_chart':
333                $chart_title = I18N::translate('Marriage by country');
334                $data        = $this->getMarriageChartData();
335                break;
336
337            case 'indi_distribution_chart':
338            default:
339                $chart_title = I18N::translate('Individual distribution chart');
340                $data        = $this->getIndivdualChartData();
341                break;
342        }
343
344        return view('statistics/other/charts/geo', [
345            'chart_title'  => $chart_title,
346            'chart_color2' => '84beff',
347            'chart_color3' => 'c3dfff',
348            'region'       => $chart_shows,
349            'data'         => $data,
350            'language'     => I18N::languageTag(),
351        ]);
352    }
353}
354