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