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