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