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