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 $places 175 * 176 * @return array 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 $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 (empty($countries)) { 264 return ''; 265 } 266 267 $top10 = []; 268 $i = 1; 269 270 // Get the country names for each language 271 $country_names = []; 272 foreach (I18N::activeLocales() as $locale) { 273 I18N::init($locale->languageTag()); 274 $all_countries = $this->country_service->getAllCountries(); 275 foreach ($all_countries as $country_code => $country_name) { 276 $country_names[$country_name] = $country_code; 277 } 278 } 279 280 I18N::init(WT_LOCALE); 281 282 $all_db_countries = []; 283 foreach ($countries as $place) { 284 $country = trim($place->country); 285 if (array_key_exists($country, $country_names)) { 286 if (isset($all_db_countries[$country_names[$country]][$country])) { 287 $all_db_countries[$country_names[$country]][$country] += (int) $place->tot; 288 } else { 289 $all_db_countries[$country_names[$country]][$country] = (int) $place->tot; 290 } 291 } 292 } 293 294 // get all the user’s countries names 295 $all_countries = $this->country_service->getAllCountries(); 296 297 foreach ($all_db_countries as $country_code => $country) { 298 foreach ($country as $country_name => $tot) { 299 $tmp = new Place($country_name, $this->tree); 300 301 $top10[] = [ 302 'place' => $tmp, 303 'count' => $tot, 304 'name' => $all_countries[$country_code], 305 ]; 306 } 307 308 if ($i++ === 10) { 309 break; 310 } 311 } 312 313 return view( 314 'statistics/other/top10-list', 315 [ 316 'records' => $top10, 317 ] 318 ); 319 } 320 321 /** 322 * Count total places. 323 * 324 * @return int 325 */ 326 private function totalPlacesQuery(): int 327 { 328 return DB::table('places') 329 ->where('p_file', '=', $this->tree->id()) 330 ->count(); 331 } 332 333 /** 334 * Count total places. 335 * 336 * @return string 337 */ 338 public function totalPlaces(): string 339 { 340 return I18N::number($this->totalPlacesQuery()); 341 } 342 343 /** 344 * Create a chart showing where events occurred. 345 * 346 * @param string $chart_shows 347 * @param string $chart_type 348 * @param string $surname 349 * 350 * @return string 351 */ 352 public function chartDistribution( 353 string $chart_shows = 'world', 354 string $chart_type = '', 355 string $surname = '' 356 ): string { 357 return (new ChartDistribution($this->tree)) 358 ->chartDistribution($chart_shows, $chart_type, $surname); 359 } 360} 361