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