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