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 labelled on tabs, menus, 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 2; 86 } 87 88 /** {@inheritdoc} */ 89 public function isGrayedOut(Individual $individual): bool 90 { 91 return false; 92 } 93 94 /** {@inheritdoc} */ 95 public function getTabContent(Individual $individual): string 96 { 97 // Only include events of close relatives that are between birth and death 98 $min_date = $individual->getEstimatedBirthDate(); 99 $max_date = $individual->getEstimatedDeathDate(); 100 101 // Which facts and events are handled by other modules? 102 $sidebar_facts = $this->module_service 103 ->findByComponent(ModuleSidebarInterface::class, $individual->tree(), Auth::user()) 104 ->map(function (ModuleSidebarInterface $sidebar): Collection { 105 return $sidebar->supportedFacts(); 106 }); 107 108 $tab_facts = $this->module_service 109 ->findByComponent(ModuleTabInterface::class, $individual->tree(), Auth::user()) 110 ->map(function (ModuleTabInterface $sidebar): Collection { 111 return $sidebar->supportedFacts(); 112 }); 113 114 $exclude_facts = $sidebar_facts->merge($tab_facts)->flatten(); 115 116 117 // The individual’s own facts 118 $indifacts = $individual->facts() 119 ->filter(function (Fact $fact) use ($exclude_facts): bool { 120 return !$exclude_facts->contains($fact->getTag()); 121 }); 122 123 // Add spouse-family facts 124 foreach ($individual->spouseFamilies() as $family) { 125 foreach ($family->facts() as $fact) { 126 if (!$exclude_facts->contains($fact->getTag()) && $fact->getTag() !== 'CHAN') { 127 $indifacts->push($fact); 128 } 129 } 130 131 $spouse = $family->spouse($individual); 132 133 if ($spouse instanceof Individual) { 134 $spouse_facts = $this->spouseFacts($individual, $spouse, $min_date, $max_date); 135 $indifacts = $indifacts->merge($spouse_facts); 136 } 137 138 $child_facts = $this->childFacts($individual, $family, '_CHIL', '', $min_date, $max_date); 139 $indifacts = $indifacts->merge($child_facts); 140 } 141 142 $parent_facts = $this->parentFacts($individual, 1, $min_date, $max_date); 143 $associate_facts = $this->associateFacts($individual); 144 $historical_facts = $this->historicalFacts($individual); 145 146 $indifacts = $indifacts 147 ->merge($parent_facts) 148 ->merge($associate_facts) 149 ->merge($historical_facts); 150 151 $indifacts = Fact::sortFacts($indifacts); 152 153 return view('modules/personal_facts/tab', [ 154 'can_edit' => $individual->canEdit(), 155 'clipboard_facts' => $this->clipboard_service->pastableFacts($individual, $exclude_facts), 156 'has_historical_facts' => !empty($historical_facts), 157 'individual' => $individual, 158 'facts' => $indifacts, 159 ]); 160 } 161 162 /** 163 * Does a relative event occur within a date range (i.e. the individual's lifetime)? 164 * 165 * @param Fact $fact 166 * @param Date $min_date 167 * @param Date $max_date 168 * 169 * @return bool 170 */ 171 private function includeFact(Fact $fact, Date $min_date, Date $max_date): bool 172 { 173 $fact_date = $fact->date(); 174 175 return $fact_date->isOK() && Date::compare($min_date, $fact_date) <= 0 && Date::compare($fact_date, $max_date) <= 0; 176 } 177 178 /** {@inheritdoc} */ 179 public function hasTabContent(Individual $individual): bool 180 { 181 return true; 182 } 183 184 /** {@inheritdoc} */ 185 public function canLoadAjax(): bool 186 { 187 return false; 188 } 189 190 /** 191 * Spouse facts that are shown on an individual’s page. 192 * 193 * @param Individual $individual Show events that occured during the lifetime of this individual 194 * @param Individual $spouse Show events of this individual 195 * @param Date $min_date 196 * @param Date $max_date 197 * 198 * @return Fact[] 199 */ 200 private function spouseFacts(Individual $individual, Individual $spouse, Date $min_date, Date $max_date): array 201 { 202 $SHOW_RELATIVES_EVENTS = $individual->tree()->getPreference('SHOW_RELATIVES_EVENTS'); 203 204 $facts = []; 205 if (strstr($SHOW_RELATIVES_EVENTS, '_DEAT_SPOU')) { 206 foreach ($spouse->facts(Gedcom::DEATH_EVENTS) as $fact) { 207 if ($this->includeFact($fact, $min_date, $max_date)) { 208 // Convert the event to a close relatives event. 209 $rela_fact = clone($fact); 210 $rela_fact->setTag('_' . $fact->getTag() . '_SPOU'); 211 $facts[] = $rela_fact; 212 } 213 } 214 } 215 216 return $facts; 217 } 218 219 /** 220 * Get the events of children and grandchildren. 221 * 222 * @param Individual $person 223 * @param Family $family 224 * @param string $option 225 * @param string $relation 226 * @param Date $min_date 227 * @param Date $max_date 228 * 229 * @return Fact[] 230 */ 231 private function childFacts(Individual $person, Family $family, $option, $relation, Date $min_date, Date $max_date): array 232 { 233 $SHOW_RELATIVES_EVENTS = $person->tree()->getPreference('SHOW_RELATIVES_EVENTS'); 234 235 $facts = []; 236 237 // Deal with recursion. 238 switch ($option) { 239 case '_CHIL': 240 // Add grandchildren 241 foreach ($family->children() as $child) { 242 foreach ($child->spouseFamilies() as $cfamily) { 243 switch ($child->sex()) { 244 case 'M': 245 foreach ($this->childFacts($person, $cfamily, '_GCHI', 'son', $min_date, $max_date) as $fact) { 246 $facts[] = $fact; 247 } 248 break; 249 case 'F': 250 foreach ($this->childFacts($person, $cfamily, '_GCHI', 'dau', $min_date, $max_date) as $fact) { 251 $facts[] = $fact; 252 } 253 break; 254 default: 255 foreach ($this->childFacts($person, $cfamily, '_GCHI', 'chi', $min_date, $max_date) as $fact) { 256 $facts[] = $fact; 257 } 258 break; 259 } 260 } 261 } 262 break; 263 } 264 265 // For each child in the family 266 foreach ($family->children() as $child) { 267 if ($child->xref() == $person->xref()) { 268 // We are not our own sibling! 269 continue; 270 } 271 // add child’s birth 272 if (strpos($SHOW_RELATIVES_EVENTS, '_BIRT' . str_replace('_HSIB', '_SIBL', $option)) !== false) { 273 foreach ($child->facts(Gedcom::BIRTH_EVENTS) as $fact) { 274 // Always show _BIRT_CHIL, even if the dates are not known 275 if ($option == '_CHIL' || $this->includeFact($fact, $min_date, $max_date)) { 276 if ($option == '_GCHI' && $relation == 'dau') { 277 // Convert the event to a close relatives event. 278 $rela_fact = clone($fact); 279 $rela_fact->setTag('_' . $fact->getTag() . '_GCH1'); 280 $facts[] = $rela_fact; 281 } elseif ($option == '_GCHI' && $relation == 'son') { 282 // Convert the event to a close relatives event. 283 $rela_fact = clone($fact); 284 $rela_fact->setTag('_' . $fact->getTag() . '_GCH2'); 285 $facts[] = $rela_fact; 286 } else { 287 // Convert the event to a close relatives event. 288 $rela_fact = clone($fact); 289 $rela_fact->setTag('_' . $fact->getTag() . $option); 290 $facts[] = $rela_fact; 291 } 292 } 293 } 294 } 295 // add child’s death 296 if (strpos($SHOW_RELATIVES_EVENTS, '_DEAT' . str_replace('_HSIB', '_SIBL', $option)) !== false) { 297 foreach ($child->facts(Gedcom::DEATH_EVENTS) as $fact) { 298 if ($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 marriage 319 if (strstr($SHOW_RELATIVES_EVENTS, '_MARR' . str_replace('_HSIB', '_SIBL', $option))) { 320 foreach ($child->spouseFamilies() as $sfamily) { 321 foreach ($sfamily->facts(['MARR']) 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 } 343 } 344 345 return $facts; 346 } 347 348 /** 349 * Get the events of parents and grandparents. 350 * 351 * @param Individual $person 352 * @param int $sosa 353 * @param Date $min_date 354 * @param Date $max_date 355 * 356 * @return Fact[] 357 */ 358 private function parentFacts(Individual $person, $sosa, Date $min_date, Date $max_date): array 359 { 360 $SHOW_RELATIVES_EVENTS = $person->tree()->getPreference('SHOW_RELATIVES_EVENTS'); 361 362 $facts = []; 363 364 if ($sosa == 1) { 365 foreach ($person->childFamilies() as $family) { 366 // Add siblings 367 foreach ($this->childFacts($person, $family, '_SIBL', '', $min_date, $max_date) as $fact) { 368 $facts[] = $fact; 369 } 370 foreach ($family->spouses() as $spouse) { 371 foreach ($spouse->spouseFamilies() as $sfamily) { 372 if ($family !== $sfamily) { 373 // Add half-siblings 374 foreach ($this->childFacts($person, $sfamily, '_HSIB', '', $min_date, $max_date) as $fact) { 375 $facts[] = $fact; 376 } 377 } 378 } 379 // Add grandparents 380 foreach ($this->parentFacts($spouse, $spouse->sex() == 'F' ? 3 : 2, $min_date, $max_date) as $fact) { 381 $facts[] = $fact; 382 } 383 } 384 } 385 386 if (strstr($SHOW_RELATIVES_EVENTS, '_MARR_PARE')) { 387 // add father/mother marriages 388 foreach ($person->childFamilies() as $sfamily) { 389 foreach ($sfamily->facts(['MARR']) as $fact) { 390 if ($this->includeFact($fact, $min_date, $max_date)) { 391 // marriage of parents (to each other) 392 $rela_fact = clone($fact); 393 $rela_fact->setTag('_' . $fact->getTag() . '_FAMC'); 394 $facts[] = $rela_fact; 395 } 396 } 397 } 398 foreach ($person->childStepFamilies() as $sfamily) { 399 foreach ($sfamily->facts(['MARR']) as $fact) { 400 if ($this->includeFact($fact, $min_date, $max_date)) { 401 // marriage of a parent (to another spouse) 402 // Convert the event to a close relatives event 403 $rela_fact = clone($fact); 404 $rela_fact->setTag('_' . $fact->getTag() . '_PARE'); 405 $facts[] = $rela_fact; 406 } 407 } 408 } 409 } 410 } 411 412 foreach ($person->childFamilies() as $family) { 413 foreach ($family->spouses() as $parent) { 414 if (strstr($SHOW_RELATIVES_EVENTS, '_DEAT' . ($sosa == 1 ? '_PARE' : '_GPAR'))) { 415 foreach ($parent->facts(Gedcom::DEATH_EVENTS) as $fact) { 416 if ($this->includeFact($fact, $min_date, $max_date)) { 417 switch ($sosa) { 418 case 1: 419 // Convert the event to a close relatives event. 420 $rela_fact = clone($fact); 421 $rela_fact->setTag('_' . $fact->getTag() . '_PARE'); 422 $facts[] = $rela_fact; 423 break; 424 case 2: 425 // Convert the event to a close relatives event 426 $rela_fact = clone($fact); 427 $rela_fact->setTag('_' . $fact->getTag() . '_GPA1'); 428 $facts[] = $rela_fact; 429 break; 430 case 3: 431 // Convert the event to a close relatives event 432 $rela_fact = clone($fact); 433 $rela_fact->setTag('_' . $fact->getTag() . '_GPA2'); 434 $facts[] = $rela_fact; 435 break; 436 } 437 } 438 } 439 } 440 } 441 } 442 443 return $facts; 444 } 445 446 /** 447 * Get any historical events. 448 * 449 * @param Individual $individual 450 * 451 * @return Fact[] 452 */ 453 private function historicalFacts(Individual $individual): array 454 { 455 return $this->module_service->findByInterface(ModuleHistoricEventsInterface::class) 456 ->map(function (ModuleHistoricEventsInterface $module) use ($individual): Collection { 457 return $module->historicEventsForIndividual($individual); 458 }) 459 ->flatten() 460 ->all(); 461 } 462 463 /** 464 * Get the events of associates. 465 * 466 * @param Individual $person 467 * 468 * @return Fact[] 469 */ 470 private function associateFacts(Individual $person): array 471 { 472 $facts = []; 473 474 /** @var Individual[] $associates */ 475 $associates = array_merge( 476 $person->linkedIndividuals('ASSO'), 477 $person->linkedIndividuals('_ASSO'), 478 $person->linkedFamilies('ASSO'), 479 $person->linkedFamilies('_ASSO') 480 ); 481 foreach ($associates as $associate) { 482 foreach ($associate->facts() as $fact) { 483 $arec = $fact->attribute('_ASSO'); 484 if (!$arec) { 485 $arec = $fact->attribute('ASSO'); 486 } 487 if ($arec && trim($arec, '@') === $person->xref()) { 488 // Extract the important details from the fact 489 $factrec = '1 ' . $fact->getTag(); 490 if (preg_match('/\n2 DATE .*/', $fact->gedcom(), $match)) { 491 $factrec .= $match[0]; 492 } 493 if (preg_match('/\n2 PLAC .*/', $fact->gedcom(), $match)) { 494 $factrec .= $match[0]; 495 } 496 if ($associate instanceof Family) { 497 foreach ($associate->spouses() as $spouse) { 498 $factrec .= "\n2 _ASSO @" . $spouse->xref() . '@'; 499 } 500 } else { 501 $factrec .= "\n2 _ASSO @" . $associate->xref() . '@'; 502 } 503 $facts[] = new Fact($factrec, $associate, 'asso'); 504 } 505 } 506 } 507 508 return $facts; 509 } 510 511 /** 512 * This module handles the following facts - so don't show them on the "Facts and events" tab. 513 * 514 * @return Collection|string[] 515 */ 516 public function supportedFacts(): Collection 517 { 518 // We don't actually displaye these facts, but they are displayed 519 // outside the tabs/sidebar systems. This just forces them to be excluded here. 520 return new Collection(['NAME', 'SEX']); 521 } 522} 523