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