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