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\Date; 23use Fisharebest\Webtrees\Elements\UnknownElement; 24use Fisharebest\Webtrees\Fact; 25use Fisharebest\Webtrees\Family; 26use Fisharebest\Webtrees\Individual; 27use Fisharebest\Webtrees\Registry; 28use Fisharebest\Webtrees\Gedcom; 29use Fisharebest\Webtrees\Header; 30use Fisharebest\Webtrees\I18N; 31use Fisharebest\Webtrees\Statistics\Repository\Interfaces\EventRepositoryInterface; 32use Fisharebest\Webtrees\Tree; 33use Illuminate\Database\Capsule\Manager as DB; 34 35use function array_map; 36use function array_merge; 37use function e; 38use function strncmp; 39use function substr; 40 41/** 42 * A repository providing methods for event related statistics. 43 */ 44class EventRepository implements EventRepositoryInterface 45{ 46 /** 47 * Sorting directions. 48 */ 49 private const SORT_ASC = 'ASC'; 50 private const SORT_DESC = 'DESC'; 51 52 /** 53 * Event facts. 54 */ 55 private const EVENT_BIRTH = 'BIRT'; 56 private const EVENT_DEATH = 'DEAT'; 57 private const EVENT_MARRIAGE = 'MARR'; 58 private const EVENT_DIVORCE = 'DIV'; 59 60 private Tree $tree; 61 62 /** 63 * @param Tree $tree 64 */ 65 public function __construct(Tree $tree) 66 { 67 $this->tree = $tree; 68 } 69 70 /** 71 * Returns the total number of a given list of events (with dates). 72 * 73 * @param array<string> $events The list of events to count (e.g. BIRT, DEAT, ...) 74 * 75 * @return int 76 */ 77 private function getEventCount(array $events): int 78 { 79 $query = DB::table('dates') 80 ->where('d_file', '=', $this->tree->id()); 81 82 $no_types = [ 83 'HEAD', 84 'CHAN', 85 ]; 86 87 if ($events !== []) { 88 $types = []; 89 90 foreach ($events as $type) { 91 if (strncmp($type, '!', 1) === 0) { 92 $no_types[] = substr($type, 1); 93 } else { 94 $types[] = $type; 95 } 96 } 97 98 if ($types !== []) { 99 $query->whereIn('d_fact', $types); 100 } 101 } 102 103 return $query->whereNotIn('d_fact', $no_types) 104 ->count(); 105 } 106 107 /** 108 * @param array<string> $events 109 * 110 * @return string 111 */ 112 public function totalEvents(array $events = []): string 113 { 114 return I18N::number( 115 $this->getEventCount($events) 116 ); 117 } 118 119 /** 120 * @return string 121 */ 122 public function totalEventsBirth(): string 123 { 124 return $this->totalEvents(Gedcom::BIRTH_EVENTS); 125 } 126 127 /** 128 * @return string 129 */ 130 public function totalBirths(): string 131 { 132 return $this->totalEvents([self::EVENT_BIRTH]); 133 } 134 135 /** 136 * @return string 137 */ 138 public function totalEventsDeath(): string 139 { 140 return $this->totalEvents(Gedcom::DEATH_EVENTS); 141 } 142 143 /** 144 * @return string 145 */ 146 public function totalDeaths(): string 147 { 148 return $this->totalEvents([self::EVENT_DEATH]); 149 } 150 151 /** 152 * @return string 153 */ 154 public function totalEventsMarriage(): string 155 { 156 return $this->totalEvents(Gedcom::MARRIAGE_EVENTS); 157 } 158 159 /** 160 * @return string 161 */ 162 public function totalMarriages(): string 163 { 164 return $this->totalEvents([self::EVENT_MARRIAGE]); 165 } 166 167 /** 168 * @return string 169 */ 170 public function totalEventsDivorce(): string 171 { 172 return $this->totalEvents(Gedcom::DIVORCE_EVENTS); 173 } 174 175 /** 176 * @return string 177 */ 178 public function totalDivorces(): string 179 { 180 return $this->totalEvents([self::EVENT_DIVORCE]); 181 } 182 183 /** 184 * Retursn the list of common facts used query the data. 185 * 186 * @return array<string> 187 */ 188 private function getCommonFacts(): array 189 { 190 // The list of facts used to limit the query result 191 return array_merge( 192 Gedcom::BIRTH_EVENTS, 193 Gedcom::MARRIAGE_EVENTS, 194 Gedcom::DIVORCE_EVENTS, 195 Gedcom::DEATH_EVENTS 196 ); 197 } 198 199 /** 200 * @return string 201 */ 202 public function totalEventsOther(): string 203 { 204 $no_facts = array_map( 205 static function (string $fact): string { 206 return '!' . $fact; 207 }, 208 $this->getCommonFacts() 209 ); 210 211 return $this->totalEvents($no_facts); 212 } 213 214 /** 215 * Returns the first/last event record from the given list of event facts. 216 * 217 * @param string $direction The sorting direction of the query (To return first or last record) 218 * 219 * @return object|null 220 */ 221 private function eventQuery(string $direction): ?object 222 { 223 return DB::table('dates') 224 ->select(['d_gid as id', 'd_year as year', 'd_fact AS fact', 'd_type AS type']) 225 ->where('d_file', '=', $this->tree->id()) 226 ->where('d_gid', '<>', Header::RECORD_TYPE) 227 ->whereIn('d_fact', $this->getCommonFacts()) 228 ->where('d_julianday1', '<>', 0) 229 ->orderBy('d_julianday1', $direction) 230 ->orderBy('d_type') 231 ->first(); 232 } 233 234 /** 235 * Returns the formatted first/last occuring event. 236 * 237 * @param string $direction The sorting direction 238 * 239 * @return string 240 */ 241 private function getFirstLastEvent(string $direction): string 242 { 243 $row = $this->eventQuery($direction); 244 $result = I18N::translate('This information is not available.'); 245 246 if ($row) { 247 $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree); 248 249 if ($record && $record->canShow()) { 250 $result = $record->formatList(); 251 } else { 252 $result = I18N::translate('This information is private and cannot be shown.'); 253 } 254 } 255 256 return $result; 257 } 258 259 /** 260 * @return string 261 */ 262 public function firstEvent(): string 263 { 264 return $this->getFirstLastEvent(self::SORT_ASC); 265 } 266 267 /** 268 * @return string 269 */ 270 public function lastEvent(): string 271 { 272 return $this->getFirstLastEvent(self::SORT_DESC); 273 } 274 275 /** 276 * Returns the formatted year of the first/last occuring event. 277 * 278 * @param string $direction The sorting direction 279 * 280 * @return string 281 */ 282 private function getFirstLastEventYear(string $direction): string 283 { 284 $row = $this->eventQuery($direction); 285 286 if (!$row) { 287 return ''; 288 } 289 290 return (new Date($row->type . ' ' . $row->year)) 291 ->display(); 292 } 293 294 /** 295 * @return string 296 */ 297 public function firstEventYear(): string 298 { 299 return $this->getFirstLastEventYear(self::SORT_ASC); 300 } 301 302 /** 303 * @return string 304 */ 305 public function lastEventYear(): string 306 { 307 return $this->getFirstLastEventYear(self::SORT_DESC); 308 } 309 310 /** 311 * Returns the formatted type of the first/last occurring event. 312 * 313 * @param string $direction The sorting direction 314 * 315 * @return string 316 */ 317 private function getFirstLastEventType(string $direction): string 318 { 319 $row = $this->eventQuery($direction); 320 321 if ($row === null) { 322 return ''; 323 } 324 325 foreach ([Individual::RECORD_TYPE, Family::RECORD_TYPE] as $record_type) { 326 $element = Registry::elementFactory()->make($record_type . ':' . $row->fact); 327 328 if (!$element instanceof UnknownElement) { 329 return $element->label(); 330 } 331 } 332 333 return $row->fact; 334 } 335 336 /** 337 * @return string 338 */ 339 public function firstEventType(): string 340 { 341 return $this->getFirstLastEventType(self::SORT_ASC); 342 } 343 344 /** 345 * @return string 346 */ 347 public function lastEventType(): string 348 { 349 return $this->getFirstLastEventType(self::SORT_DESC); 350 } 351 352 /** 353 * Returns the formatted name of the first/last occuring event. 354 * 355 * @param string $direction The sorting direction 356 * 357 * @return string 358 */ 359 private function getFirstLastEventName(string $direction): string 360 { 361 $row = $this->eventQuery($direction); 362 363 if ($row) { 364 $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree); 365 366 if ($record) { 367 return '<a href="' . e($record->url()) . '">' . $record->fullName() . '</a>'; 368 } 369 } 370 371 return ''; 372 } 373 374 /** 375 * @return string 376 */ 377 public function firstEventName(): string 378 { 379 return $this->getFirstLastEventName(self::SORT_ASC); 380 } 381 382 /** 383 * @return string 384 */ 385 public function lastEventName(): string 386 { 387 return $this->getFirstLastEventName(self::SORT_DESC); 388 } 389 390 /** 391 * Returns the formatted place of the first/last occuring event. 392 * 393 * @param string $direction The sorting direction 394 * 395 * @return string 396 */ 397 private function getFirstLastEventPlace(string $direction): string 398 { 399 $row = $this->eventQuery($direction); 400 401 if ($row) { 402 $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree); 403 $fact = null; 404 405 if ($record) { 406 $fact = $record->facts([$row->fact])->first(); 407 } 408 409 if ($fact instanceof Fact) { 410 return $fact->place()->shortName(); 411 } 412 } 413 414 return I18N::translate('Private'); 415 } 416 417 /** 418 * @return string 419 */ 420 public function firstEventPlace(): string 421 { 422 return $this->getFirstLastEventPlace(self::SORT_ASC); 423 } 424 425 /** 426 * @return string 427 */ 428 public function lastEventPlace(): string 429 { 430 return $this->getFirstLastEventPlace(self::SORT_DESC); 431 } 432} 433