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