1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2021 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 <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Statistics\Repository; 21 22use Fisharebest\Webtrees\Family; 23use Fisharebest\Webtrees\Gedcom; 24use Fisharebest\Webtrees\I18N; 25use Fisharebest\Webtrees\Individual; 26use Fisharebest\Webtrees\Location; 27use Fisharebest\Webtrees\Place; 28use Fisharebest\Webtrees\Statistics\Google\ChartDistribution; 29use Fisharebest\Webtrees\Statistics\Repository\Interfaces\PlaceRepositoryInterface; 30use Fisharebest\Webtrees\Statistics\Service\CountryService; 31use Fisharebest\Webtrees\Tree; 32use Illuminate\Database\Capsule\Manager as DB; 33use Illuminate\Database\Query\JoinClause; 34use stdClass; 35 36use function array_key_exists; 37 38/** 39 * A repository providing methods for place related statistics. 40 */ 41class PlaceRepository implements PlaceRepositoryInterface 42{ 43 /** 44 * @var Tree 45 */ 46 private $tree; 47 48 /** 49 * @var CountryService 50 */ 51 private $country_service; 52 53 /** 54 * BirthPlaces constructor. 55 * 56 * @param Tree $tree 57 */ 58 public function __construct(Tree $tree) 59 { 60 $this->tree = $tree; 61 $this->country_service = new CountryService(); 62 } 63 64 /** 65 * Places 66 * 67 * @param string $fact 68 * @param string $what 69 * @param bool $country 70 * 71 * @return int[] 72 */ 73 private function queryFactPlaces(string $fact, string $what = 'ALL', bool $country = false): array 74 { 75 $rows = []; 76 77 if ($what === 'INDI') { 78 $rows = DB::table('individuals')->select(['i_gedcom as tree'])->where( 79 'i_file', 80 '=', 81 $this->tree->id() 82 )->where( 83 'i_gedcom', 84 'LIKE', 85 "%\n2 PLAC %" 86 )->get()->all(); 87 } elseif ($what === 'FAM') { 88 $rows = DB::table('families')->select(['f_gedcom as tree'])->where( 89 'f_file', 90 '=', 91 $this->tree->id() 92 )->where( 93 'f_gedcom', 94 'LIKE', 95 "%\n2 PLAC %" 96 )->get()->all(); 97 } 98 99 $placelist = []; 100 101 foreach ($rows as $row) { 102 if (preg_match('/\n1 ' . $fact . '(?:\n[2-9].*)*\n2 PLAC (.+)/', $row->tree, $match)) { 103 if ($country) { 104 $tmp = explode(Gedcom::PLACE_SEPARATOR, $match[1]); 105 $place = end($tmp); 106 } else { 107 $place = $match[1]; 108 } 109 110 if (isset($placelist[$place])) { 111 ++$placelist[$place]; 112 } else { 113 $placelist[$place] = 1; 114 } 115 } 116 } 117 118 return $placelist; 119 } 120 121 /** 122 * Query places. 123 * 124 * @param string $what 125 * @param string $fact 126 * @param int $parent 127 * @param bool $country 128 * 129 * @return array<int|stdClass> 130 */ 131 public function statsPlaces(string $what = 'ALL', string $fact = '', int $parent = 0, bool $country = false): array 132 { 133 if ($fact) { 134 return $this->queryFactPlaces($fact, $what, $country); 135 } 136 137 $query = DB::table('places') 138 ->join('placelinks', static function (JoinClause $join): void { 139 $join->on('pl_file', '=', 'p_file') 140 ->on('pl_p_id', '=', 'p_id'); 141 }) 142 ->where('p_file', '=', $this->tree->id()); 143 144 if ($parent > 0) { 145 // Used by placehierarchy map modules 146 $query->select(['p_place AS place']) 147 ->selectRaw('COUNT(*) AS tot') 148 ->where('p_id', '=', $parent) 149 ->groupBy(['place']); 150 } else { 151 $query->select(['p_place AS country']) 152 ->selectRaw('COUNT(*) AS tot') 153 ->where('p_parent_id', '=', 0) 154 ->groupBy(['country']) 155 ->orderByDesc('tot') 156 ->orderBy('country'); 157 } 158 159 if ($what === Individual::RECORD_TYPE) { 160 $query->join('individuals', static function (JoinClause $join): void { 161 $join->on('pl_file', '=', 'i_file') 162 ->on('pl_gid', '=', 'i_id'); 163 }); 164 } elseif ($what === Family::RECORD_TYPE) { 165 $query->join('families', static function (JoinClause $join): void { 166 $join->on('pl_file', '=', 'f_file') 167 ->on('pl_gid', '=', 'f_id'); 168 }); 169 } elseif ($what === Location::RECORD_TYPE) { 170 $query->join('other', static function (JoinClause $join): void { 171 $join->on('pl_file', '=', 'o_file') 172 ->on('pl_gid', '=', 'o_id'); 173 }) 174 ->where('o_type', '=', Location::RECORD_TYPE); 175 } 176 177 return $query 178 ->get() 179 ->map(static function (stdClass $entry) { 180 // Map total value to integer 181 $entry->tot = (int) $entry->tot; 182 183 return $entry; 184 }) 185 ->all(); 186 } 187 188 /** 189 * Get the top 10 places list. 190 * 191 * @param array<string,int> $places 192 * 193 * @return array<array<string,mixed>> 194 */ 195 private function getTop10Places(array $places): array 196 { 197 $top10 = []; 198 $i = 0; 199 200 arsort($places); 201 202 foreach ($places as $place => $count) { 203 $tmp = new Place($place, $this->tree); 204 $top10[] = [ 205 'place' => $tmp, 206 'count' => $count, 207 ]; 208 209 ++$i; 210 211 if ($i === 10) { 212 break; 213 } 214 } 215 216 return $top10; 217 } 218 219 /** 220 * Renders the top 10 places list. 221 * 222 * @param array<string,int> $places 223 * 224 * @return string 225 */ 226 private function renderTop10(array $places): string 227 { 228 $top10Records = $this->getTop10Places($places); 229 230 return view( 231 'statistics/other/top10-list', 232 [ 233 'records' => $top10Records, 234 ] 235 ); 236 } 237 238 /** 239 * A list of common birth places. 240 * 241 * @return string 242 */ 243 public function commonBirthPlacesList(): string 244 { 245 $places = $this->queryFactPlaces('BIRT', 'INDI'); 246 return $this->renderTop10($places); 247 } 248 249 /** 250 * A list of common death places. 251 * 252 * @return string 253 */ 254 public function commonDeathPlacesList(): string 255 { 256 $places = $this->queryFactPlaces('DEAT', 'INDI'); 257 return $this->renderTop10($places); 258 } 259 260 /** 261 * A list of common marriage places. 262 * 263 * @return string 264 */ 265 public function commonMarriagePlacesList(): string 266 { 267 $places = $this->queryFactPlaces('MARR', 'FAM'); 268 return $this->renderTop10($places); 269 } 270 271 /** 272 * A list of common countries. 273 * 274 * @return string 275 */ 276 public function commonCountriesList(): string 277 { 278 $countries = $this->statsPlaces(); 279 280 if ($countries === []) { 281 return ''; 282 } 283 284 $top10 = []; 285 $i = 1; 286 287 // Get the country names for each language 288 $country_names = []; 289 $old_language = I18N::languageTag(); 290 291 foreach (I18N::activeLocales() as $locale) { 292 I18N::init($locale->languageTag()); 293 $all_countries = $this->country_service->getAllCountries(); 294 foreach ($all_countries as $country_code => $country_name) { 295 $country_names[$country_name] = $country_code; 296 } 297 } 298 299 I18N::init($old_language); 300 301 $all_db_countries = []; 302 foreach ($countries as $place) { 303 $country = trim($place->country); 304 if (array_key_exists($country, $country_names)) { 305 if (isset($all_db_countries[$country_names[$country]][$country])) { 306 $all_db_countries[$country_names[$country]][$country] += (int) $place->tot; 307 } else { 308 $all_db_countries[$country_names[$country]][$country] = (int) $place->tot; 309 } 310 } 311 } 312 313 // get all the user’s countries names 314 $all_countries = $this->country_service->getAllCountries(); 315 316 foreach ($all_db_countries as $country_code => $country) { 317 foreach ($country as $country_name => $tot) { 318 $tmp = new Place($country_name, $this->tree); 319 320 $top10[] = [ 321 'place' => $tmp, 322 'count' => $tot, 323 'name' => $all_countries[$country_code], 324 ]; 325 } 326 327 if ($i++ === 10) { 328 break; 329 } 330 } 331 332 return view( 333 'statistics/other/top10-list', 334 [ 335 'records' => $top10, 336 ] 337 ); 338 } 339 340 /** 341 * Count total places. 342 * 343 * @return int 344 */ 345 private function totalPlacesQuery(): int 346 { 347 return DB::table('places') 348 ->where('p_file', '=', $this->tree->id()) 349 ->count(); 350 } 351 352 /** 353 * Count total places. 354 * 355 * @return string 356 */ 357 public function totalPlaces(): string 358 { 359 return I18N::number($this->totalPlacesQuery()); 360 } 361 362 /** 363 * Create a chart showing where events occurred. 364 * 365 * @param string $chart_shows 366 * @param string $chart_type 367 * @param string $surname 368 * 369 * @return string 370 */ 371 public function chartDistribution( 372 string $chart_shows = 'world', 373 string $chart_type = '', 374 string $surname = '' 375 ): string { 376 return (new ChartDistribution($this->tree)) 377 ->chartDistribution($chart_shows, $chart_type, $surname); 378 } 379} 380