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