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