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