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 * @param bool $country 77 * 78 * @return array<int|string,int> 79 */ 80 private function queryFactPlaces(string $fact, string $what = 'ALL', bool $country = false): array 81 { 82 $rows = []; 83 84 if ($what === 'INDI') { 85 $rows = DB::table('individuals') 86 ->select(['i_gedcom as tree']) 87 ->where('i_file', '=', $this->tree->id()) 88 ->where('i_gedcom', 'LIKE', "%\n2 PLAC %") 89 ->get() 90 ->all(); 91 } elseif ($what === 'FAM') { 92 $rows = DB::table('families')->select(['f_gedcom as tree']) 93 ->where('f_file', '=', $this->tree->id()) 94 ->where('f_gedcom', 'LIKE', "%\n2 PLAC %") 95 ->get() 96 ->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 $placelist[$place] = ($placelist[$place] ?? 0) + 1; 111 } 112 } 113 114 return $placelist; 115 } 116 117 /** 118 * Get the top 10 places list. 119 * 120 * @param array<int> $places 121 * 122 * @return array<array<string,int|Place>> 123 */ 124 private function getTop10Places(array $places): array 125 { 126 $top10 = []; 127 $i = 0; 128 129 arsort($places); 130 131 foreach ($places as $place => $count) { 132 $tmp = new Place((string) $place, $this->tree); 133 $top10[] = [ 134 'place' => $tmp, 135 'count' => $count, 136 ]; 137 138 ++$i; 139 140 if ($i === 10) { 141 break; 142 } 143 } 144 145 return $top10; 146 } 147 148 /** 149 * Renders the top 10 places list. 150 * 151 * @param array<int|string,int> $places 152 * 153 * @return string 154 */ 155 private function renderTop10(array $places): string 156 { 157 $top10Records = $this->getTop10Places($places); 158 159 return view( 160 'statistics/other/top10-list', 161 [ 162 'records' => $top10Records, 163 ] 164 ); 165 } 166 167 /** 168 * A list of common birth places. 169 * 170 * @return string 171 */ 172 public function commonBirthPlacesList(): string 173 { 174 $places = $this->queryFactPlaces('BIRT', 'INDI'); 175 return $this->renderTop10($places); 176 } 177 178 /** 179 * A list of common death places. 180 * 181 * @return string 182 */ 183 public function commonDeathPlacesList(): string 184 { 185 $places = $this->queryFactPlaces('DEAT', 'INDI'); 186 return $this->renderTop10($places); 187 } 188 189 /** 190 * A list of common marriage places. 191 * 192 * @return string 193 */ 194 public function commonMarriagePlacesList(): string 195 { 196 $places = $this->queryFactPlaces('MARR', 'FAM'); 197 return $this->renderTop10($places); 198 } 199 200 /** 201 * A list of common countries. 202 * 203 * @return string 204 */ 205 public function commonCountriesList(): string 206 { 207 $countries = DB::table('places') 208 ->join('placelinks', static function (JoinClause $join): void { 209 $join 210 ->on('pl_file', '=', 'p_file') 211 ->on('pl_p_id', '=', 'p_id'); 212 }) 213 ->where('p_file', '=', $this->tree->id()) 214 ->where('p_parent_id', '=', 0) 215 ->groupBy(['p_place']) 216 ->orderByDesc(new Expression('COUNT(*)')) 217 ->orderBy('p_place') 218 ->pluck(new Expression('COUNT(*)'), 'p_place') 219 ->map(static fn(string $col): int => (int) $col) 220 ->all(); 221 222 if ($countries === []) { 223 return I18N::translate('This information is not available.'); 224 } 225 226 $top10 = []; 227 $i = 1; 228 229 // Get the country names for each language 230 $country_names = []; 231 $old_language = I18N::languageTag(); 232 233 foreach (I18N::activeLocales() as $locale) { 234 I18N::init($locale->languageTag()); 235 $all_countries = $this->country_service->getAllCountries(); 236 foreach ($all_countries as $country_code => $country_name) { 237 $country_names[$country_name] = $country_code; 238 } 239 } 240 241 I18N::init($old_language); 242 243 $all_db_countries = []; 244 245 foreach ($countries as $country => $count) { 246 if (array_key_exists($country, $country_names)) { 247 if (isset($all_db_countries[$country_names[$country]][$country])) { 248 $all_db_countries[$country_names[$country]][$country] += (int) $count; 249 } else { 250 $all_db_countries[$country_names[$country]][$country] = (int) $count; 251 } 252 } 253 } 254 255 // get all the user’s countries names 256 $all_countries = $this->country_service->getAllCountries(); 257 258 foreach ($all_db_countries as $country_code => $country) { 259 foreach ($country as $country_name => $tot) { 260 $tmp = new Place($country_name, $this->tree); 261 262 $top10[] = [ 263 'place' => $tmp, 264 'count' => $tot, 265 'name' => $all_countries[$country_code], 266 ]; 267 } 268 269 if ($i++ === 10) { 270 break; 271 } 272 } 273 274 return view( 275 'statistics/other/top10-list', 276 [ 277 'records' => $top10, 278 ] 279 ); 280 } 281 282 /** 283 * Count total places. 284 * 285 * @return int 286 */ 287 private function totalPlacesQuery(): int 288 { 289 return DB::table('places') 290 ->where('p_file', '=', $this->tree->id()) 291 ->count(); 292 } 293 294 /** 295 * Count total places. 296 * 297 * @return string 298 */ 299 public function totalPlaces(): string 300 { 301 return I18N::number($this->totalPlacesQuery()); 302 } 303 304 /** 305 * Create a chart showing where events occurred. 306 * 307 * @param string $chart_shows 308 * @param string $chart_type 309 * @param string $surname 310 * 311 * @return string 312 */ 313 public function chartDistribution( 314 string $chart_shows = 'world', 315 string $chart_type = '', 316 string $surname = '' 317 ): string { 318 return (new ChartDistribution($this->tree, $this->country_service, $this->individual_repository)) 319 ->chartDistribution($chart_shows, $chart_type, $surname); 320 } 321} 322