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