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