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 string SORT_ASC = 'ASC'; 52 private const string SORT_DESC = 'DESC'; 53 54 /** 55 * Event facts. 56 */ 57 private const string EVENT_BIRTH = 'BIRT'; 58 private const string EVENT_DEATH = 'DEAT'; 59 private const string EVENT_MARRIAGE = 'MARR'; 60 private const string 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 fn (string $fact): string => '!' . $fact, 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{id:string,year:int,fact:string,type:string}|null 220 */ 221 private function eventQuery(string $direction): object|null 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 ->limit(1) 232 ->get() 233 ->map(static fn (object $row): object => (object) [ 234 'id' => $row->id, 235 'year' => (int) $row->year, 236 'fact' => $row->fact, 237 'type' => $row->type, 238 ]) 239 ->first(); 240 } 241 242 /** 243 * Returns the formatted first/last occurring event. 244 * 245 * @param string $direction The sorting direction 246 * 247 * @return string 248 */ 249 private function getFirstLastEvent(string $direction): string 250 { 251 $row = $this->eventQuery($direction); 252 $result = I18N::translate('This information is not available.'); 253 254 if ($row !== null) { 255 $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree); 256 257 if ($record instanceof GedcomRecord && $record->canShow()) { 258 $result = $record->formatList(); 259 } else { 260 $result = I18N::translate('This information is private and cannot be shown.'); 261 } 262 } 263 264 return $result; 265 } 266 267 /** 268 * @return string 269 */ 270 public function firstEvent(): string 271 { 272 return $this->getFirstLastEvent(self::SORT_ASC); 273 } 274 275 /** 276 * @return string 277 */ 278 public function lastEvent(): string 279 { 280 return $this->getFirstLastEvent(self::SORT_DESC); 281 } 282 283 /** 284 * Returns the formatted year of the first/last occurring event. 285 * 286 * @param string $direction The sorting direction 287 * 288 * @return string 289 */ 290 private function getFirstLastEventYear(string $direction): string 291 { 292 $row = $this->eventQuery($direction); 293 294 if ($row === null) { 295 return ''; 296 } 297 298 if ($row->year < 0) { 299 $row->year = abs($row->year) . ' B.C.'; 300 } 301 302 return (new Date($row->type . ' ' . $row->year)) 303 ->display(); 304 } 305 306 /** 307 * @return string 308 */ 309 public function firstEventYear(): string 310 { 311 return $this->getFirstLastEventYear(self::SORT_ASC); 312 } 313 314 /** 315 * @return string 316 */ 317 public function lastEventYear(): string 318 { 319 return $this->getFirstLastEventYear(self::SORT_DESC); 320 } 321 322 /** 323 * Returns the formatted type of the first/last occurring event. 324 * 325 * @param string $direction The sorting direction 326 * 327 * @return string 328 */ 329 private function getFirstLastEventType(string $direction): string 330 { 331 $row = $this->eventQuery($direction); 332 333 if ($row === null) { 334 return ''; 335 } 336 337 foreach ([Individual::RECORD_TYPE, Family::RECORD_TYPE] as $record_type) { 338 $element = Registry::elementFactory()->make($record_type . ':' . $row->fact); 339 340 if (!$element instanceof UnknownElement) { 341 return $element->label(); 342 } 343 } 344 345 return $row->fact; 346 } 347 348 /** 349 * @return string 350 */ 351 public function firstEventType(): string 352 { 353 return $this->getFirstLastEventType(self::SORT_ASC); 354 } 355 356 /** 357 * @return string 358 */ 359 public function lastEventType(): string 360 { 361 return $this->getFirstLastEventType(self::SORT_DESC); 362 } 363 364 /** 365 * Returns the formatted name of the first/last occurring event. 366 * 367 * @param string $direction The sorting direction 368 * 369 * @return string 370 */ 371 private function getFirstLastEventName(string $direction): string 372 { 373 $row = $this->eventQuery($direction); 374 375 if ($row !== null) { 376 $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree); 377 378 if ($record instanceof GedcomRecord) { 379 return '<a href="' . e($record->url()) . '">' . $record->fullName() . '</a>'; 380 } 381 } 382 383 return ''; 384 } 385 386 /** 387 * @return string 388 */ 389 public function firstEventName(): string 390 { 391 return $this->getFirstLastEventName(self::SORT_ASC); 392 } 393 394 /** 395 * @return string 396 */ 397 public function lastEventName(): string 398 { 399 return $this->getFirstLastEventName(self::SORT_DESC); 400 } 401 402 /** 403 * Returns the formatted place of the first/last occurring event. 404 * 405 * @param string $direction The sorting direction 406 * 407 * @return string 408 */ 409 private function getFirstLastEventPlace(string $direction): string 410 { 411 $row = $this->eventQuery($direction); 412 413 if ($row !== null) { 414 $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree); 415 $fact = null; 416 417 if ($record instanceof GedcomRecord) { 418 $fact = $record->facts([$row->fact])->first(); 419 } 420 421 if ($fact instanceof Fact) { 422 return $fact->place()->shortName(); 423 } 424 } 425 426 return I18N::translate('Private'); 427 } 428 429 /** 430 * @return string 431 */ 432 public function firstEventPlace(): string 433 { 434 return $this->getFirstLastEventPlace(self::SORT_ASC); 435 } 436 437 /** 438 * @return string 439 */ 440 public function lastEventPlace(): string 441 { 442 return $this->getFirstLastEventPlace(self::SORT_DESC); 443 } 444} 445