xref: /webtrees/app/Statistics/Google/ChartDistribution.php (revision 6392c6f1dcc7aa958a8eaabd5c73b77e37bbcc2f)
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 string[]
68     */
69    private $country_to_iso3166;
70
71    /**
72     * Constructor.
73     *
74     * @param Tree $tree
75     */
76    public function __construct(Tree $tree)
77    {
78        $this->tree                 = $tree;
79        $this->theme                = app(ModuleThemeInterface::class);
80        $this->country_service      = new CountryService();
81        $this->individualRepository = new IndividualRepository($tree);
82        $this->placeRepository      = new PlaceRepository($tree);
83
84        // Get the country names for each language
85        $this->country_to_iso3166 = $this->getIso3166Countries();
86    }
87
88    /**
89     * Returns the country names for each language.
90     *
91     * @return array<string>
92     */
93    private function getIso3166Countries(): array
94    {
95        // Get the country names for each language
96        $country_to_iso3166 = [];
97
98        $current_language = I18N::languageTag();
99
100        foreach (I18N::activeLocales() as $locale) {
101            I18N::init($locale->languageTag());
102
103            $countries = $this->country_service->getAllCountries();
104
105            foreach ($this->country_service->iso3166() as $three => $two) {
106                $country_to_iso3166[$three]             = $two;
107                $country_to_iso3166[$countries[$three]] = $two;
108            }
109        }
110
111        I18N::init($current_language);
112
113        return $country_to_iso3166;
114    }
115
116    /**
117     * Returns the data structure required by google geochart.
118     *
119     * @param array $places
120     *
121     * @return array
122     */
123    private function createChartData(array $places): array
124    {
125        $data = [
126            [
127                I18N::translate('Country'),
128                I18N::translate('Total'),
129            ],
130        ];
131
132        // webtrees uses 3 letter country codes and localised country names, but google uses 2 letter codes.
133        foreach ($places as $country => $count) {
134            $data[] = [
135                [
136                    'v' => $country,
137                    'f' => $this->country_service->mapTwoLetterToName($country),
138                ],
139                $count
140            ];
141        }
142
143        return $data;
144    }
145
146    /**
147     * Returns the google geochart data for birth fact.
148     *
149     * @return array
150     */
151    private function getBirthChartData(): array
152    {
153        // Count how many people were born in each country
154        $surn_countries = [];
155        $b_countries    = $this->placeRepository->statsPlaces('INDI', 'BIRT', 0, true);
156
157        foreach ($b_countries as $country => $count) {
158            // Consolidate places (Germany, DEU => DE)
159            if (array_key_exists($country, $this->country_to_iso3166)) {
160                $country_code = $this->country_to_iso3166[$country];
161
162                if (array_key_exists($country_code, $surn_countries)) {
163                    $surn_countries[$country_code] += $count;
164                } else {
165                    $surn_countries[$country_code] = $count;
166                }
167            }
168        }
169
170        return $this->createChartData($surn_countries);
171    }
172
173    /**
174     * Returns the google geochart data for death fact.
175     *
176     * @return array
177     */
178    private function getDeathChartData(): array
179    {
180        // Count how many people were death in each country
181        $surn_countries = [];
182        $d_countries    = $this->placeRepository->statsPlaces('INDI', 'DEAT', 0, true);
183
184        foreach ($d_countries as $country => $count) {
185            // Consolidate places (Germany, DEU => DE)
186            if (array_key_exists($country, $this->country_to_iso3166)) {
187                $country_code = $this->country_to_iso3166[$country];
188
189                if (array_key_exists($country_code, $surn_countries)) {
190                    $surn_countries[$country_code] += $count;
191                } else {
192                    $surn_countries[$country_code] = $count;
193                }
194            }
195        }
196
197        return $this->createChartData($surn_countries);
198    }
199
200    /**
201     * Returns the google geochart data for marriages.
202     *
203     * @return array
204     */
205    private function getMarriageChartData(): array
206    {
207        // Count how many families got marriage in each country
208        $surn_countries = [];
209        $m_countries    = $this->placeRepository->statsPlaces('FAM');
210
211        // webtrees uses 3 letter country codes and localised country names, but google uses 2 letter codes.
212        foreach ($m_countries as $place) {
213            // Consolidate places (Germany, DEU => DE)
214            if (array_key_exists($place->country, $this->country_to_iso3166)) {
215                $country_code = $this->country_to_iso3166[$place->country];
216
217                if (array_key_exists($country_code, $surn_countries)) {
218                    $surn_countries[$country_code] += $place->tot;
219                } else {
220                    $surn_countries[$country_code] = $place->tot;
221                }
222            }
223        }
224
225        return $this->createChartData($surn_countries);
226    }
227
228    /**
229     * Returns the related database records.
230     *
231     * @param string $surname
232     *
233     * @return array<stdClass>
234     */
235    private function queryRecords(string $surname): array
236    {
237        $query = DB::table('individuals')
238            ->select(['i_gedcom'])
239            ->join('name', static function (JoinClause $join): void {
240                $join->on('n_id', '=', 'i_id')
241                    ->on('n_file', '=', 'i_file');
242            })
243            ->where('n_file', '=', $this->tree->id())
244            ->where(new Expression('n_surn /*! COLLATE ' . I18N::collation() . ' */'), '=', $surname);
245
246        return $query->get()->all();
247    }
248
249    /**
250     * Returns the google geochart data for surnames.
251     *
252     * @param string $surname The surname used to create the chart
253     *
254     * @return array
255     */
256    private function getSurnameChartData(string $surname): array
257    {
258        if ($surname === '') {
259            $surname = $this->individualRepository->getCommonSurname();
260        }
261
262        // Count how many people are events in each country
263        $surn_countries = [];
264        $records        = $this->queryRecords($surname);
265
266        foreach ($records as $row) {
267            /** @var string[][] $matches */
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