1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2019 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 <http://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Module; 21 22use Fisharebest\Webtrees\Auth; 23use Fisharebest\Webtrees\Date; 24use Fisharebest\Webtrees\Fact; 25use Fisharebest\Webtrees\Family; 26use Fisharebest\Webtrees\Gedcom; 27use Fisharebest\Webtrees\I18N; 28use Fisharebest\Webtrees\Individual; 29use Fisharebest\Webtrees\Services\ClipboardService; 30use Fisharebest\Webtrees\Services\ModuleService; 31use Illuminate\Support\Collection; 32 33/** 34 * Class IndividualFactsTabModule 35 */ 36class IndividualFactsTabModule extends AbstractModule implements ModuleTabInterface 37{ 38 use ModuleTabTrait; 39 40 /** @var ModuleService */ 41 private $module_service; 42 43 /** @var ClipboardService */ 44 private $clipboard_service; 45 46 /** 47 * UserWelcomeModule constructor. 48 * 49 * @param ModuleService $module_service 50 * @param ClipboardService $clipboard_service 51 */ 52 public function __construct(ModuleService $module_service, ClipboardService $clipboard_service) 53 { 54 $this->module_service = $module_service; 55 $this->clipboard_service = $clipboard_service; 56 } 57 58 /** 59 * How should this module be identified in the control panel, etc.? 60 * 61 * @return string 62 */ 63 public function title(): string 64 { 65 /* I18N: Name of a module/tab on the individual page. */ 66 return I18N::translate('Facts and events'); 67 } 68 69 /** 70 * A sentence describing what this module does. 71 * 72 * @return string 73 */ 74 public function description(): string 75 { 76 /* I18N: Description of the “Facts and events” module */ 77 return I18N::translate('A tab showing the facts and events of an individual.'); 78 } 79 80 /** 81 * The default position for this tab. It can be changed in the control panel. 82 * 83 * @return int 84 */ 85 public function defaultTabOrder(): int 86 { 87 return 1; 88 } 89 90 /** 91 * A greyed out tab has no actual content, but may perhaps have 92 * options to create content. 93 * 94 * @param Individual $individual 95 * 96 * @return bool 97 */ 98 public function isGrayedOut(Individual $individual): bool 99 { 100 return false; 101 } 102 103 /** 104 * Generate the HTML content of this tab. 105 * 106 * @param Individual $individual 107 * 108 * @return string 109 */ 110 public function getTabContent(Individual $individual): string 111 { 112 // Only include events of close relatives that are between birth and death 113 $min_date = $individual->getEstimatedBirthDate(); 114 $max_date = $individual->getEstimatedDeathDate(); 115 116 // Which facts and events are handled by other modules? 117 $sidebar_facts = $this->module_service 118 ->findByComponent(ModuleSidebarInterface::class, $individual->tree(), Auth::user()) 119 ->map(static function (ModuleSidebarInterface $sidebar): Collection { 120 return $sidebar->supportedFacts(); 121 }); 122 123 $tab_facts = $this->module_service 124 ->findByComponent(ModuleTabInterface::class, $individual->tree(), Auth::user()) 125 ->map(static function (ModuleTabInterface $sidebar): Collection { 126 return $sidebar->supportedFacts(); 127 }); 128 129 $exclude_facts = $sidebar_facts->merge($tab_facts)->flatten(); 130 131 132 // The individual’s own facts 133 $indifacts = $individual->facts() 134 ->filter(static function (Fact $fact) use ($exclude_facts): bool { 135 return !$exclude_facts->contains($fact->getTag()); 136 }); 137 138 // Add spouse-family facts 139 foreach ($individual->spouseFamilies() as $family) { 140 foreach ($family->facts() as $fact) { 141 if (!$exclude_facts->contains($fact->getTag()) && $fact->getTag() !== 'CHAN') { 142 $indifacts->push($fact); 143 } 144 } 145 146 $spouse = $family->spouse($individual); 147 148 if ($spouse instanceof Individual) { 149 $spouse_facts = $this->spouseFacts($individual, $spouse, $min_date, $max_date); 150 $indifacts = $indifacts->merge($spouse_facts); 151 } 152 153 $child_facts = $this->childFacts($individual, $family, '_CHIL', '', $min_date, $max_date); 154 $indifacts = $indifacts->merge($child_facts); 155 } 156 157 $parent_facts = $this->parentFacts($individual, 1, $min_date, $max_date); 158 $associate_facts = $this->associateFacts($individual); 159 $historical_facts = $this->historicalFacts($individual); 160 161 $indifacts = $indifacts 162 ->merge($parent_facts) 163 ->merge($associate_facts) 164 ->merge($historical_facts); 165 166 $indifacts = Fact::sortFacts($indifacts); 167 168 return view('modules/personal_facts/tab', [ 169 'can_edit' => $individual->canEdit(), 170 'clipboard_facts' => $this->clipboard_service->pastableFacts($individual, $exclude_facts), 171 'has_historical_facts' => $historical_facts !== [], 172 'individual' => $individual, 173 'facts' => $indifacts, 174 ]); 175 } 176 177 /** 178 * Does a relative event occur within a date range (i.e. the individual's lifetime)? 179 * 180 * @param Fact $fact 181 * @param Date $min_date 182 * @param Date $max_date 183 * 184 * @return bool 185 */ 186 private function includeFact(Fact $fact, Date $min_date, Date $max_date): bool 187 { 188 $fact_date = $fact->date(); 189 190 return $fact_date->isOK() && Date::compare($min_date, $fact_date) <= 0 && Date::compare($fact_date, $max_date) <= 0; 191 } 192 193 /** 194 * Is this tab empty? If so, we don't always need to display it. 195 * 196 * @param Individual $individual 197 * 198 * @return bool 199 */ 200 public function hasTabContent(Individual $individual): bool 201 { 202 return true; 203 } 204 205 /** 206 * Can this tab load asynchronously? 207 * 208 * @return bool 209 */ 210 public function canLoadAjax(): bool 211 { 212 return false; 213 } 214 215 /** 216 * Spouse facts that are shown on an individual’s page. 217 * 218 * @param Individual $individual Show events that occured during the lifetime of this individual 219 * @param Individual $spouse Show events of this individual 220 * @param Date $min_date 221 * @param Date $max_date 222 * 223 * @return Fact[] 224 */ 225 private function spouseFacts(Individual $individual, Individual $spouse, Date $min_date, Date $max_date): array 226 { 227 $SHOW_RELATIVES_EVENTS = $individual->tree()->getPreference('SHOW_RELATIVES_EVENTS'); 228 229 $facts = []; 230 if (strstr($SHOW_RELATIVES_EVENTS, '_DEAT_SPOU')) { 231 foreach ($spouse->facts(Gedcom::DEATH_EVENTS) as $fact) { 232 if ($this->includeFact($fact, $min_date, $max_date)) { 233 // Convert the event to a close relatives event. 234 $rela_fact = clone $fact; 235 $rela_fact->setTag('_' . $fact->getTag() . '_SPOU'); 236 $facts[] = $rela_fact; 237 } 238 } 239 } 240 241 return $facts; 242 } 243 244 /** 245 * Get the events of children and grandchildren. 246 * 247 * @param Individual $person 248 * @param Family $family 249 * @param string $option 250 * @param string $relation 251 * @param Date $min_date 252 * @param Date $max_date 253 * 254 * @return Fact[] 255 */ 256 private function childFacts(Individual $person, Family $family, $option, $relation, Date $min_date, Date $max_date): array 257 { 258 $SHOW_RELATIVES_EVENTS = $person->tree()->getPreference('SHOW_RELATIVES_EVENTS'); 259 260 $facts = []; 261 262 // Deal with recursion. 263 switch ($option) { 264 case '_CHIL': 265 // Add grandchildren 266 foreach ($family->children() as $child) { 267 foreach ($child->spouseFamilies() as $cfamily) { 268 switch ($child->sex()) { 269 case 'M': 270 foreach ($this->childFacts($person, $cfamily, '_GCHI', 'son', $min_date, $max_date) as $fact) { 271 $facts[] = $fact; 272 } 273 break; 274 case 'F': 275 foreach ($this->childFacts($person, $cfamily, '_GCHI', 'dau', $min_date, $max_date) as $fact) { 276 $facts[] = $fact; 277 } 278 break; 279 default: 280 foreach ($this->childFacts($person, $cfamily, '_GCHI', 'chi', $min_date, $max_date) as $fact) { 281 $facts[] = $fact; 282 } 283 break; 284 } 285 } 286 } 287 break; 288 } 289 290 // For each child in the family 291 foreach ($family->children() as $child) { 292 if ($child->xref() === $person->xref()) { 293 // We are not our own sibling! 294 continue; 295 } 296 // add child’s birth 297 if (strpos($SHOW_RELATIVES_EVENTS, '_BIRT' . str_replace('_HSIB', '_SIBL', $option)) !== false) { 298 foreach ($child->facts(Gedcom::BIRTH_EVENTS) as $fact) { 299 // Always show _BIRT_CHIL, even if the dates are not known 300 if ($option === '_CHIL' || $this->includeFact($fact, $min_date, $max_date)) { 301 if ($option === '_GCHI' && $relation === 'dau') { 302 // Convert the event to a close relatives event. 303 $rela_fact = clone $fact; 304 $rela_fact->setTag('_' . $fact->getTag() . '_GCH1'); 305 $facts[] = $rela_fact; 306 } elseif ($option === '_GCHI' && $relation === 'son') { 307 // Convert the event to a close relatives event. 308 $rela_fact = clone $fact; 309 $rela_fact->setTag('_' . $fact->getTag() . '_GCH2'); 310 $facts[] = $rela_fact; 311 } else { 312 // Convert the event to a close relatives event. 313 $rela_fact = clone $fact; 314 $rela_fact->setTag('_' . $fact->getTag() . $option); 315 $facts[] = $rela_fact; 316 } 317 } 318 } 319 } 320 // add child’s death 321 if (strpos($SHOW_RELATIVES_EVENTS, '_DEAT' . str_replace('_HSIB', '_SIBL', $option)) !== false) { 322 foreach ($child->facts(Gedcom::DEATH_EVENTS) as $fact) { 323 if ($this->includeFact($fact, $min_date, $max_date)) { 324 if ($option === '_GCHI' && $relation === 'dau') { 325 // Convert the event to a close relatives event. 326 $rela_fact = clone $fact; 327 $rela_fact->setTag('_' . $fact->getTag() . '_GCH1'); 328 $facts[] = $rela_fact; 329 } elseif ($option === '_GCHI' && $relation === 'son') { 330 // Convert the event to a close relatives event. 331 $rela_fact = clone $fact; 332 $rela_fact->setTag('_' . $fact->getTag() . '_GCH2'); 333 $facts[] = $rela_fact; 334 } else { 335 // Convert the event to a close relatives event. 336 $rela_fact = clone $fact; 337 $rela_fact->setTag('_' . $fact->getTag() . $option); 338 $facts[] = $rela_fact; 339 } 340 } 341 } 342 } 343 // add child’s marriage 344 if (strstr($SHOW_RELATIVES_EVENTS, '_MARR' . str_replace('_HSIB', '_SIBL', $option))) { 345 foreach ($child->spouseFamilies() as $sfamily) { 346 foreach ($sfamily->facts(['MARR']) as $fact) { 347 if ($this->includeFact($fact, $min_date, $max_date)) { 348 if ($option === '_GCHI' && $relation === 'dau') { 349 // Convert the event to a close relatives event. 350 $rela_fact = clone $fact; 351 $rela_fact->setTag('_' . $fact->getTag() . '_GCH1'); 352 $facts[] = $rela_fact; 353 } elseif ($option === '_GCHI' && $relation === 'son') { 354 // Convert the event to a close relatives event. 355 $rela_fact = clone $fact; 356 $rela_fact->setTag('_' . $fact->getTag() . '_GCH2'); 357 $facts[] = $rela_fact; 358 } else { 359 // Convert the event to a close relatives event. 360 $rela_fact = clone $fact; 361 $rela_fact->setTag('_' . $fact->getTag() . $option); 362 $facts[] = $rela_fact; 363 } 364 } 365 } 366 } 367 } 368 } 369 370 return $facts; 371 } 372 373 /** 374 * Get the events of parents and grandparents. 375 * 376 * @param Individual $person 377 * @param int $sosa 378 * @param Date $min_date 379 * @param Date $max_date 380 * 381 * @return Fact[] 382 */ 383 private function parentFacts(Individual $person, $sosa, Date $min_date, Date $max_date): array 384 { 385 $SHOW_RELATIVES_EVENTS = $person->tree()->getPreference('SHOW_RELATIVES_EVENTS'); 386 387 $facts = []; 388 389 if ($sosa == 1) { 390 foreach ($person->childFamilies() as $family) { 391 // Add siblings 392 foreach ($this->childFacts($person, $family, '_SIBL', '', $min_date, $max_date) as $fact) { 393 $facts[] = $fact; 394 } 395 foreach ($family->spouses() as $spouse) { 396 foreach ($spouse->spouseFamilies() as $sfamily) { 397 if ($family !== $sfamily) { 398 // Add half-siblings 399 foreach ($this->childFacts($person, $sfamily, '_HSIB', '', $min_date, $max_date) as $fact) { 400 $facts[] = $fact; 401 } 402 } 403 } 404 // Add grandparents 405 foreach ($this->parentFacts($spouse, $spouse->sex() === 'F' ? 3 : 2, $min_date, $max_date) as $fact) { 406 $facts[] = $fact; 407 } 408 } 409 } 410 411 if (strstr($SHOW_RELATIVES_EVENTS, '_MARR_PARE')) { 412 // add father/mother marriages 413 foreach ($person->childFamilies() as $sfamily) { 414 foreach ($sfamily->facts(['MARR']) as $fact) { 415 if ($this->includeFact($fact, $min_date, $max_date)) { 416 // marriage of parents (to each other) 417 $rela_fact = clone $fact; 418 $rela_fact->setTag('_' . $fact->getTag() . '_FAMC'); 419 $facts[] = $rela_fact; 420 } 421 } 422 } 423 foreach ($person->childStepFamilies() as $sfamily) { 424 foreach ($sfamily->facts(['MARR']) as $fact) { 425 if ($this->includeFact($fact, $min_date, $max_date)) { 426 // marriage of a parent (to another spouse) 427 // Convert the event to a close relatives event 428 $rela_fact = clone $fact; 429 $rela_fact->setTag('_' . $fact->getTag() . '_PARE'); 430 $facts[] = $rela_fact; 431 } 432 } 433 } 434 } 435 } 436 437 foreach ($person->childFamilies() as $family) { 438 foreach ($family->spouses() as $parent) { 439 if (strstr($SHOW_RELATIVES_EVENTS, '_DEAT' . ($sosa == 1 ? '_PARE' : '_GPAR'))) { 440 foreach ($parent->facts(Gedcom::DEATH_EVENTS) as $fact) { 441 if ($this->includeFact($fact, $min_date, $max_date)) { 442 switch ($sosa) { 443 case 1: 444 // Convert the event to a close relatives event. 445 $rela_fact = clone $fact; 446 $rela_fact->setTag('_' . $fact->getTag() . '_PARE'); 447 $facts[] = $rela_fact; 448 break; 449 case 2: 450 // Convert the event to a close relatives event 451 $rela_fact = clone $fact; 452 $rela_fact->setTag('_' . $fact->getTag() . '_GPA1'); 453 $facts[] = $rela_fact; 454 break; 455 case 3: 456 // Convert the event to a close relatives event 457 $rela_fact = clone $fact; 458 $rela_fact->setTag('_' . $fact->getTag() . '_GPA2'); 459 $facts[] = $rela_fact; 460 break; 461 } 462 } 463 } 464 } 465 } 466 } 467 468 return $facts; 469 } 470 471 /** 472 * Get any historical events. 473 * 474 * @param Individual $individual 475 * 476 * @return Fact[] 477 */ 478 private function historicalFacts(Individual $individual): array 479 { 480 return $this->module_service->findByInterface(ModuleHistoricEventsInterface::class) 481 ->map(static function (ModuleHistoricEventsInterface $module) use ($individual): Collection { 482 return $module->historicEventsForIndividual($individual); 483 }) 484 ->flatten() 485 ->all(); 486 } 487 488 /** 489 * Get the events of associates. 490 * 491 * @param Individual $person 492 * 493 * @return Fact[] 494 */ 495 private function associateFacts(Individual $person): array 496 { 497 $facts = []; 498 499 /** @var Individual[] $associates */ 500 $asso1 = $person->linkedIndividuals('ASSO'); 501 $asso2 = $person->linkedIndividuals('_ASSO'); 502 $asso3 = $person->linkedFamilies('ASSO'); 503 $asso4 = $person->linkedFamilies('_ASSO'); 504 505 $associates = $asso1->merge($asso2)->merge($asso3)->merge($asso4); 506 507 foreach ($associates as $associate) { 508 foreach ($associate->facts() as $fact) { 509 if (preg_match('/\n\d _?ASSO @' . $person->xref() . '@/', $fact->gedcom())) { 510 // Extract the important details from the fact 511 $factrec = '1 ' . $fact->getTag(); 512 if (preg_match('/\n2 DATE .*/', $fact->gedcom(), $match)) { 513 $factrec .= $match[0]; 514 } 515 if (preg_match('/\n2 PLAC .*/', $fact->gedcom(), $match)) { 516 $factrec .= $match[0]; 517 } 518 if ($associate instanceof Family) { 519 foreach ($associate->spouses() as $spouse) { 520 $factrec .= "\n2 _ASSO @" . $spouse->xref() . '@'; 521 } 522 } else { 523 $factrec .= "\n2 _ASSO @" . $associate->xref() . '@'; 524 } 525 $facts[] = new Fact($factrec, $associate, 'asso'); 526 } 527 } 528 } 529 530 return $facts; 531 } 532 533 /** 534 * This module handles the following facts - so don't show them on the "Facts and events" tab. 535 * 536 * @return Collection<string> 537 */ 538 public function supportedFacts(): Collection 539 { 540 // We don't actually displaye these facts, but they are displayed 541 // outside the tabs/sidebar systems. This just forces them to be excluded here. 542 return new Collection(['NAME', 'SEX']); 543 } 544} 545