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