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