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