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