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