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