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