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