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