xref: /webtrees/app/Statistics/Google/ChartDistribution.php (revision 90da5d67aa35f0ef1e16c59451752e64fe40c87d)
18add1155SRico Sonntag<?php
23976b470SGreg Roach
38add1155SRico Sonntag/**
48add1155SRico Sonntag * webtrees: online genealogy
5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team
68add1155SRico Sonntag * This program is free software: you can redistribute it and/or modify
78add1155SRico Sonntag * it under the terms of the GNU General Public License as published by
88add1155SRico Sonntag * the Free Software Foundation, either version 3 of the License, or
98add1155SRico Sonntag * (at your option) any later version.
108add1155SRico Sonntag * This program is distributed in the hope that it will be useful,
118add1155SRico Sonntag * but WITHOUT ANY WARRANTY; without even the implied warranty of
128add1155SRico Sonntag * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
138add1155SRico Sonntag * GNU General Public License for more details.
148add1155SRico Sonntag * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
168add1155SRico Sonntag */
17fcfa147eSGreg Roach
188add1155SRico Sonntagdeclare(strict_types=1);
198add1155SRico Sonntag
208add1155SRico Sonntagnamespace Fisharebest\Webtrees\Statistics\Google;
218add1155SRico Sonntag
226f4ec3caSGreg Roachuse Fisharebest\Webtrees\DB;
238add1155SRico Sonntaguse Fisharebest\Webtrees\I18N;
24f78da678SGreg Roachuse Fisharebest\Webtrees\Statistics\Repository\Interfaces\IndividualRepositoryInterface;
2593ccd686SRico Sonntaguse Fisharebest\Webtrees\Statistics\Service\CountryService;
268add1155SRico Sonntaguse Fisharebest\Webtrees\Tree;
27a020b8bdSGreg Roachuse Illuminate\Database\Query\Builder;
28a69f5655SGreg Roachuse Illuminate\Database\Query\Expression;
2988de55fdSRico Sonntaguse Illuminate\Database\Query\JoinClause;
308add1155SRico Sonntag
31a020b8bdSGreg Roachuse function preg_match;
32a020b8bdSGreg Roachuse function preg_quote;
3334b20f29SGreg Roachuse function view;
3471378461SGreg Roach
358add1155SRico Sonntag/**
3693ccd686SRico Sonntag * A chart showing the distribution of different events on a map.
378add1155SRico Sonntag */
3893ccd686SRico Sonntagclass ChartDistribution
398add1155SRico Sonntag{
4034b20f29SGreg Roach    private Tree $tree;
4193ccd686SRico Sonntag
4234b20f29SGreg Roach    private CountryService $country_service;
4393ccd686SRico Sonntag
44f78da678SGreg Roach    private IndividualRepositoryInterface $individual_repository;
458add1155SRico Sonntag
468add1155SRico Sonntag    /**
4709482a55SGreg Roach     * @var array<string>
4888de55fdSRico Sonntag     */
4909482a55SGreg Roach    private array $country_to_iso3166;
5088de55fdSRico Sonntag
5188de55fdSRico Sonntag    /**
528add1155SRico Sonntag     * @param Tree                          $tree
534c78e066SGreg Roach     * @param CountryService                $country_service
547f50305dSGreg Roach     * @param IndividualRepositoryInterface $individual_repository
558add1155SRico Sonntag     */
56f78da678SGreg Roach    public function __construct(
57f78da678SGreg Roach        Tree $tree,
58f78da678SGreg Roach        CountryService $country_service,
59a020b8bdSGreg Roach        IndividualRepositoryInterface $individual_repository
60f78da678SGreg Roach    ) {
6193ccd686SRico Sonntag        $this->tree                  = $tree;
62f78da678SGreg Roach        $this->country_service       = $country_service;
63f78da678SGreg Roach        $this->individual_repository = $individual_repository;
6488de55fdSRico Sonntag
6588de55fdSRico Sonntag        // Get the country names for each language
6688de55fdSRico Sonntag        $this->country_to_iso3166 = $this->getIso3166Countries();
6788de55fdSRico Sonntag    }
6888de55fdSRico Sonntag
6988de55fdSRico Sonntag    /**
7088de55fdSRico Sonntag     * Returns the country names for each language.
7188de55fdSRico Sonntag     *
7224f2a3afSGreg Roach     * @return array<string>
7388de55fdSRico Sonntag     */
7488de55fdSRico Sonntag    private function getIso3166Countries(): array
7588de55fdSRico Sonntag    {
7688de55fdSRico Sonntag        // Get the country names for each language
7793ccd686SRico Sonntag        $country_to_iso3166 = [];
7888de55fdSRico Sonntag
7956eca4e8SGreg Roach        $current_language = I18N::languageTag();
8056eca4e8SGreg Roach
8188de55fdSRico Sonntag        foreach (I18N::activeLocales() as $locale) {
8288de55fdSRico Sonntag            I18N::init($locale->languageTag());
8388de55fdSRico Sonntag
8456eca4e8SGreg Roach            $countries = $this->country_service->getAllCountries();
8556eca4e8SGreg Roach
8693ccd686SRico Sonntag            foreach ($this->country_service->iso3166() as $three => $two) {
8793ccd686SRico Sonntag                $country_to_iso3166[$three]             = $two;
8893ccd686SRico Sonntag                $country_to_iso3166[$countries[$three]] = $two;
8988de55fdSRico Sonntag            }
9088de55fdSRico Sonntag        }
9188de55fdSRico Sonntag
9256eca4e8SGreg Roach        I18N::init($current_language);
9356eca4e8SGreg Roach
9493ccd686SRico Sonntag        return $country_to_iso3166;
9588de55fdSRico Sonntag    }
9688de55fdSRico Sonntag
9788de55fdSRico Sonntag    /**
9888de55fdSRico Sonntag     * Returns the data structure required by google geochart.
9988de55fdSRico Sonntag     *
100a020b8bdSGreg Roach     * @param array<int> $places
10188de55fdSRico Sonntag     *
102a020b8bdSGreg Roach     * @return array<int,array<int|string|array<string,string>>>
10388de55fdSRico Sonntag     */
10488de55fdSRico Sonntag    private function createChartData(array $places): array
10588de55fdSRico Sonntag    {
10688de55fdSRico Sonntag        $data = [
10788de55fdSRico Sonntag            [
10888de55fdSRico Sonntag                I18N::translate('Country'),
10988de55fdSRico Sonntag                I18N::translate('Total'),
11088de55fdSRico Sonntag            ],
11188de55fdSRico Sonntag        ];
11288de55fdSRico Sonntag
113ac701fbdSGreg Roach        // webtrees uses 3-letter country codes and localised country names, but google uses 2 letter codes.
11488de55fdSRico Sonntag        foreach ($places as $country => $count) {
11588de55fdSRico Sonntag            $data[] = [
11688de55fdSRico Sonntag                [
11788de55fdSRico Sonntag                    'v' => $country,
11893ccd686SRico Sonntag                    'f' => $this->country_service->mapTwoLetterToName($country),
11988de55fdSRico Sonntag                ],
12088de55fdSRico Sonntag                $count
12188de55fdSRico Sonntag            ];
12288de55fdSRico Sonntag        }
12388de55fdSRico Sonntag
12488de55fdSRico Sonntag        return $data;
12588de55fdSRico Sonntag    }
12688de55fdSRico Sonntag
12788de55fdSRico Sonntag    /**
128a020b8bdSGreg Roach     * @param Tree   $tree
12988de55fdSRico Sonntag     *
130a020b8bdSGreg Roach     * @return array<int>
13188de55fdSRico Sonntag     */
132a020b8bdSGreg Roach    private function countIndividualsByCountry(Tree $tree): array
13388de55fdSRico Sonntag    {
134a020b8bdSGreg Roach        $rows = DB::table('places')
135a020b8bdSGreg Roach            ->where('p_file', '=', $tree->id())
136a020b8bdSGreg Roach            ->where('p_parent_id', '=', 0)
137a020b8bdSGreg Roach            ->join('placelinks', static function (JoinClause $join): void {
138a020b8bdSGreg Roach                $join
139a020b8bdSGreg Roach                    ->on('pl_file', '=', 'p_file')
140a020b8bdSGreg Roach                    ->on('pl_p_id', '=', 'p_id');
141a020b8bdSGreg Roach            })
142a020b8bdSGreg Roach            ->join('individuals', static function (JoinClause $join): void {
143a020b8bdSGreg Roach                $join
144a020b8bdSGreg Roach                    ->on('pl_file', '=', 'i_file')
145a020b8bdSGreg Roach                    ->on('pl_gid', '=', 'i_id');
146a020b8bdSGreg Roach            })
147a020b8bdSGreg Roach            ->groupBy('p_place')
148*90da5d67SGreg Roach            ->pluck(new Expression('COUNT(*) AS total'), 'p_place');
14988de55fdSRico Sonntag
150a020b8bdSGreg Roach        $totals = [];
15188de55fdSRico Sonntag
152a020b8bdSGreg Roach        foreach ($rows as $country => $count) {
153a020b8bdSGreg Roach            $country_code = $this->country_to_iso3166[$country] ?? null;
154a020b8bdSGreg Roach
155a020b8bdSGreg Roach            if ($country_code !== null) {
156a020b8bdSGreg Roach                $totals[$country_code] = $count + ($totals[$country_code] ?? 0);
15788de55fdSRico Sonntag            }
15888de55fdSRico Sonntag        }
15988de55fdSRico Sonntag
160a020b8bdSGreg Roach        return $totals;
16188de55fdSRico Sonntag    }
16288de55fdSRico Sonntag
16388de55fdSRico Sonntag    /**
164a020b8bdSGreg Roach     * @param Tree   $tree
16588de55fdSRico Sonntag     * @param string $surname
16688de55fdSRico Sonntag     *
167a020b8bdSGreg Roach     * @return array<int>
16888de55fdSRico Sonntag     */
169a020b8bdSGreg Roach    private function countSurnamesByCountry(Tree $tree, string $surname): array
17088de55fdSRico Sonntag    {
171a020b8bdSGreg Roach        $rows =
172a020b8bdSGreg Roach            DB::table('places')
173a020b8bdSGreg Roach                ->where('p_file', '=', $tree->id())
174a020b8bdSGreg Roach                ->where('p_parent_id', '=', 0)
175a020b8bdSGreg Roach                ->join('placelinks', static function (JoinClause $join): void {
176a020b8bdSGreg Roach                    $join
177a020b8bdSGreg Roach                        ->on('pl_file', '=', 'p_file')
178a020b8bdSGreg Roach                        ->on('pl_p_id', '=', 'p_id');
17988de55fdSRico Sonntag                })
180a020b8bdSGreg Roach                ->join('name', static function (JoinClause $join): void {
181a020b8bdSGreg Roach                    $join
182a020b8bdSGreg Roach                        ->on('n_file', '=', 'pl_file')
183a020b8bdSGreg Roach                        ->on('n_id', '=', 'pl_gid');
184a020b8bdSGreg Roach                })
18552288ec7SGreg Roach                ->where('n_surn', '=', $surname)
186a020b8bdSGreg Roach                ->groupBy('p_place')
187*90da5d67SGreg Roach                ->pluck(new Expression('COUNT(*) AS total'), 'p_place');
18888de55fdSRico Sonntag
189a020b8bdSGreg Roach        $totals = [];
190a020b8bdSGreg Roach
191a020b8bdSGreg Roach        foreach ($rows as $country => $count) {
192a020b8bdSGreg Roach            $country_code = $this->country_to_iso3166[$country] ?? null;
193a020b8bdSGreg Roach
194a020b8bdSGreg Roach            if ($country_code !== null) {
195a020b8bdSGreg Roach                $totals[$country_code] = $count + ($totals[$country_code] ?? 0);
196a020b8bdSGreg Roach            }
197a020b8bdSGreg Roach        }
198a020b8bdSGreg Roach
199a020b8bdSGreg Roach        return $totals;
20088de55fdSRico Sonntag    }
20188de55fdSRico Sonntag
20288de55fdSRico Sonntag    /**
203a020b8bdSGreg Roach     * @param Tree   $tree
204a020b8bdSGreg Roach     * @param string $fact
20588de55fdSRico Sonntag     *
206a020b8bdSGreg Roach     * @return array<int>
20788de55fdSRico Sonntag     */
208a020b8bdSGreg Roach    private function countFamilyEventsByCountry(Tree $tree, string $fact): array
20988de55fdSRico Sonntag    {
210a020b8bdSGreg Roach        $query = DB::table('places')
211a020b8bdSGreg Roach            ->where('p_file', '=', $tree->id())
212a020b8bdSGreg Roach            ->where('p_parent_id', '=', 0)
213a020b8bdSGreg Roach            ->join('placelinks', static function (JoinClause $join): void {
214a020b8bdSGreg Roach                $join
215a020b8bdSGreg Roach                    ->on('pl_file', '=', 'p_file')
216a020b8bdSGreg Roach                    ->on('pl_p_id', '=', 'p_id');
217a020b8bdSGreg Roach            })
218a020b8bdSGreg Roach            ->join('families', static function (JoinClause $join): void {
219a020b8bdSGreg Roach                $join
220a020b8bdSGreg Roach                    ->on('pl_file', '=', 'f_file')
221a020b8bdSGreg Roach                    ->on('pl_gid', '=', 'f_id');
222a020b8bdSGreg Roach            })
223059898c9SGreg Roach            ->select(['p_place AS place', 'f_gedcom AS gedcom']);
22488de55fdSRico Sonntag
225a020b8bdSGreg Roach        return $this->filterEventPlaces($query, $fact);
22688de55fdSRico Sonntag    }
22788de55fdSRico Sonntag
22888de55fdSRico Sonntag    /**
229a020b8bdSGreg Roach     * @param Tree   $tree
230a020b8bdSGreg Roach     * @param string $fact
23188de55fdSRico Sonntag     *
232a020b8bdSGreg Roach     * @return array<int>
23388de55fdSRico Sonntag     */
234a020b8bdSGreg Roach    private function countIndividualEventsByCountry(Tree $tree, string $fact): array
23588de55fdSRico Sonntag    {
236a020b8bdSGreg Roach        $query = DB::table('places')
237a020b8bdSGreg Roach            ->where('p_file', '=', $tree->id())
238a020b8bdSGreg Roach            ->where('p_parent_id', '=', 0)
239a020b8bdSGreg Roach            ->join('placelinks', static function (JoinClause $join): void {
240a020b8bdSGreg Roach                $join
241a020b8bdSGreg Roach                    ->on('pl_file', '=', 'p_file')
242a020b8bdSGreg Roach                    ->on('pl_p_id', '=', 'p_id');
243a020b8bdSGreg Roach            })
244a020b8bdSGreg Roach            ->join('individuals', static function (JoinClause $join): void {
245a020b8bdSGreg Roach                $join
246a020b8bdSGreg Roach                    ->on('pl_file', '=', 'i_file')
247a020b8bdSGreg Roach                    ->on('pl_gid', '=', 'i_id');
248a020b8bdSGreg Roach            })
249059898c9SGreg Roach            ->select(['p_place AS place', 'i_gedcom AS gedcom']);
25088de55fdSRico Sonntag
251a020b8bdSGreg Roach        return $this->filterEventPlaces($query, $fact);
252a020b8bdSGreg Roach    }
25388de55fdSRico Sonntag
254a020b8bdSGreg Roach    /**
255a020b8bdSGreg Roach     * @param Builder $query
256a020b8bdSGreg Roach     * @param string  $fact
257a020b8bdSGreg Roach     *
258a020b8bdSGreg Roach     * @return array<int>
259a020b8bdSGreg Roach     */
260a020b8bdSGreg Roach    private function filterEventPlaces(Builder $query, string $fact): array
261a020b8bdSGreg Roach    {
262a020b8bdSGreg Roach        $totals = [];
263a020b8bdSGreg Roach
264a020b8bdSGreg Roach        foreach ($query->cursor() as $row) {
265a020b8bdSGreg Roach            $country_code = $this->country_to_iso3166[$row->place] ?? null;
266a020b8bdSGreg Roach
267a020b8bdSGreg Roach            if ($country_code !== null) {
268a020b8bdSGreg Roach                $place_regex = '/\n1 ' . $fact . '(?:\n[2-9].*)*\n2 PLAC.*[, ]' . preg_quote($row->place, '(?:\n|$)/i') . '\n/';
269a020b8bdSGreg Roach
270a020b8bdSGreg Roach                if (preg_match($place_regex, $row->gedcom) === 1) {
271a020b8bdSGreg Roach                    $totals[$country_code] = 1 + ($totals[$country_code] ?? 0);
27288de55fdSRico Sonntag                }
27388de55fdSRico Sonntag            }
27488de55fdSRico Sonntag        }
27588de55fdSRico Sonntag
276a020b8bdSGreg Roach        return $totals;
2778add1155SRico Sonntag    }
2788add1155SRico Sonntag
2798add1155SRico Sonntag    /**
2808add1155SRico Sonntag     * Create a chart showing where events occurred.
2818add1155SRico Sonntag     *
28288de55fdSRico Sonntag     * @param string $chart_shows The type of chart map to show
28388de55fdSRico Sonntag     * @param string $chart_type  The type of chart to show
28488de55fdSRico Sonntag     * @param string $surname     The surname for surname based distribution chart
2858add1155SRico Sonntag     *
2868add1155SRico Sonntag     * @return string
2878add1155SRico Sonntag     */
2888add1155SRico Sonntag    public function chartDistribution(
2898add1155SRico Sonntag        string $chart_shows = 'world',
2908add1155SRico Sonntag        string $chart_type = '',
2918add1155SRico Sonntag        string $surname = ''
2928add1155SRico Sonntag    ): string {
2938add1155SRico Sonntag        switch ($chart_type) {
2948add1155SRico Sonntag            case 'surname_distribution_chart':
2958add1155SRico Sonntag                $chart_title = I18N::translate('Surname distribution chart') . ': ' . $surname;
296a020b8bdSGreg Roach                $surname     = $surname ?: $this->individual_repository->getCommonSurname();
297a020b8bdSGreg Roach                $data        = $this->createChartData($this->countSurnamesByCountry($this->tree, $surname));
2988add1155SRico Sonntag                break;
2998add1155SRico Sonntag
3008add1155SRico Sonntag            case 'birth_distribution_chart':
3018add1155SRico Sonntag                $chart_title = I18N::translate('Birth by country');
302a020b8bdSGreg Roach                $data        = $this->createChartData($this->countIndividualEventsByCountry($this->tree, 'BIRT'));
3038add1155SRico Sonntag                break;
3048add1155SRico Sonntag
3058add1155SRico Sonntag            case 'death_distribution_chart':
3068add1155SRico Sonntag                $chart_title = I18N::translate('Death by country');
307a020b8bdSGreg Roach                $data        = $this->createChartData($this->countIndividualEventsByCountry($this->tree, 'DEAT'));
3088add1155SRico Sonntag                break;
3098add1155SRico Sonntag
3108add1155SRico Sonntag            case 'marriage_distribution_chart':
3118add1155SRico Sonntag                $chart_title = I18N::translate('Marriage by country');
312a020b8bdSGreg Roach                $data        = $this->createChartData($this->countFamilyEventsByCountry($this->tree, 'MARR'));
3138add1155SRico Sonntag                break;
3148add1155SRico Sonntag
3158add1155SRico Sonntag            case 'indi_distribution_chart':
3168add1155SRico Sonntag            default:
3178add1155SRico Sonntag                $chart_title = I18N::translate('Individual distribution chart');
318a020b8bdSGreg Roach                $data        = $this->createChartData($this->countIndividualsByCountry($this->tree));
3198add1155SRico Sonntag                break;
3208add1155SRico Sonntag        }
3218add1155SRico Sonntag
32290a2f718SGreg Roach        return view('statistics/other/charts/geo', [
3238add1155SRico Sonntag            'chart_title'  => $chart_title,
32434b20f29SGreg Roach            'chart_color2' => '84beff',
32534b20f29SGreg Roach            'chart_color3' => 'c3dfff',
32688de55fdSRico Sonntag            'region'       => $chart_shows,
32788de55fdSRico Sonntag            'data'         => $data,
32865cf5706SGreg Roach            'language'     => I18N::languageTag(),
32990a2f718SGreg Roach        ]);
3308add1155SRico Sonntag    }
3318add1155SRico Sonntag}
332