1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2020 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\Statistics\Repository; 21 22use Fisharebest\Webtrees\Date; 23use Fisharebest\Webtrees\Fact; 24use Fisharebest\Webtrees\Factory; 25use Fisharebest\Webtrees\Functions\FunctionsPrint; 26use Fisharebest\Webtrees\Gedcom; 27use Fisharebest\Webtrees\GedcomTag; 28use Fisharebest\Webtrees\Header; 29use Fisharebest\Webtrees\I18N; 30use Fisharebest\Webtrees\Statistics\Repository\Interfaces\EventRepositoryInterface; 31use Fisharebest\Webtrees\Tree; 32use Illuminate\Database\Capsule\Manager as DB; 33use Illuminate\Database\Eloquent\Model; 34use Illuminate\Database\Query\Builder; 35use stdClass; 36 37/** 38 * A repository providing methods for event related statistics. 39 */ 40class EventRepository implements EventRepositoryInterface 41{ 42 /** 43 * Sorting directions. 44 */ 45 private const SORT_ASC = 'ASC'; 46 private const SORT_DESC = 'DESC'; 47 48 /** 49 * Event facts. 50 */ 51 private const EVENT_BIRTH = 'BIRT'; 52 private const EVENT_DEATH = 'DEAT'; 53 private const EVENT_MARRIAGE = 'MARR'; 54 private const EVENT_DIVORCE = 'DIV'; 55 private const EVENT_ADOPTION = 'ADOP'; 56 private const EVENT_BURIAL = 'BURI'; 57 private const EVENT_CENSUS = 'CENS'; 58 59 /** 60 * @var Tree 61 */ 62 private $tree; 63 64 /** 65 * Constructor. 66 * 67 * @param Tree $tree 68 */ 69 public function __construct(Tree $tree) 70 { 71 $this->tree = $tree; 72 } 73 74 /** 75 * Returns the total number of a given list of events (with dates). 76 * 77 * @param array<string> $events The list of events to count (e.g. BIRT, DEAT, ...) 78 * 79 * @return int 80 */ 81 private function getEventCount(array $events): int 82 { 83 $query = DB::table('dates') 84 ->where('d_file', '=', $this->tree->id()); 85 86 $no_types = [ 87 'HEAD', 88 'CHAN', 89 ]; 90 91 if ($events !== []) { 92 $types = []; 93 94 foreach ($events as $type) { 95 if (strncmp($type, '!', 1) === 0) { 96 $no_types[] = substr($type, 1); 97 } else { 98 $types[] = $type; 99 } 100 } 101 102 if ($types !== []) { 103 $query->whereIn('d_fact', $types); 104 } 105 } 106 107 return $query->whereNotIn('d_fact', $no_types) 108 ->count(); 109 } 110 111 /** 112 * @param string[] $events 113 * 114 * @return string 115 */ 116 public function totalEvents(array $events = []): string 117 { 118 return I18N::number( 119 $this->getEventCount($events) 120 ); 121 } 122 123 /** 124 * @return string 125 */ 126 public function totalEventsBirth(): string 127 { 128 return $this->totalEvents(Gedcom::BIRTH_EVENTS); 129 } 130 131 /** 132 * @return string 133 */ 134 public function totalBirths(): string 135 { 136 return $this->totalEvents([self::EVENT_BIRTH]); 137 } 138 139 /** 140 * @return string 141 */ 142 public function totalEventsDeath(): string 143 { 144 return $this->totalEvents(Gedcom::DEATH_EVENTS); 145 } 146 147 /** 148 * @return string 149 */ 150 public function totalDeaths(): string 151 { 152 return $this->totalEvents([self::EVENT_DEATH]); 153 } 154 155 /** 156 * @return string 157 */ 158 public function totalEventsMarriage(): string 159 { 160 return $this->totalEvents(Gedcom::MARRIAGE_EVENTS); 161 } 162 163 /** 164 * @return string 165 */ 166 public function totalMarriages(): string 167 { 168 return $this->totalEvents([self::EVENT_MARRIAGE]); 169 } 170 171 /** 172 * @return string 173 */ 174 public function totalEventsDivorce(): string 175 { 176 return $this->totalEvents(Gedcom::DIVORCE_EVENTS); 177 } 178 179 /** 180 * @return string 181 */ 182 public function totalDivorces(): string 183 { 184 return $this->totalEvents([self::EVENT_DIVORCE]); 185 } 186 187 /** 188 * Retursn the list of common facts used query the data. 189 * 190 * @return array<string> 191 */ 192 private function getCommonFacts(): array 193 { 194 // The list of facts used to limit the query result 195 return array_merge( 196 Gedcom::BIRTH_EVENTS, 197 Gedcom::MARRIAGE_EVENTS, 198 Gedcom::DIVORCE_EVENTS, 199 Gedcom::DEATH_EVENTS 200 ); 201 } 202 203 /** 204 * @return string 205 */ 206 public function totalEventsOther(): string 207 { 208 $no_facts = array_map( 209 static function (string $fact): string { 210 return '!' . $fact; 211 }, 212 $this->getCommonFacts() 213 ); 214 215 return $this->totalEvents($no_facts); 216 } 217 218 /** 219 * Returns the first/last event record from the given list of event facts. 220 * 221 * @param string $direction The sorting direction of the query (To return first or last record) 222 * 223 * @return stdClass|null 224 */ 225 private function eventQuery(string $direction): ?stdClass 226 { 227 return DB::table('dates') 228 ->select(['d_gid as id', 'd_year as year', 'd_fact AS fact', 'd_type AS type']) 229 ->where('d_file', '=', $this->tree->id()) 230 ->where('d_gid', '<>', Header::RECORD_TYPE) 231 ->whereIn('d_fact', $this->getCommonFacts()) 232 ->where('d_julianday1', '<>', 0) 233 ->orderBy('d_julianday1', $direction) 234 ->orderBy('d_type') 235 ->first(); 236 } 237 238 /** 239 * Returns the formatted first/last occuring event. 240 * 241 * @param string $direction The sorting direction 242 * 243 * @return string 244 */ 245 private function getFirstLastEvent(string $direction): string 246 { 247 $row = $this->eventQuery($direction); 248 $result = I18N::translate('This information is not available.'); 249 250 if ($row) { 251 $record = Factory::gedcomRecord()->make($row->id, $this->tree); 252 253 if ($record && $record->canShow()) { 254 $result = $record->formatList(); 255 } else { 256 $result = I18N::translate('This information is private and cannot be shown.'); 257 } 258 } 259 260 return $result; 261 } 262 263 /** 264 * @return string 265 */ 266 public function firstEvent(): string 267 { 268 return $this->getFirstLastEvent(self::SORT_ASC); 269 } 270 271 /** 272 * @return string 273 */ 274 public function lastEvent(): string 275 { 276 return $this->getFirstLastEvent(self::SORT_DESC); 277 } 278 279 /** 280 * Returns the formatted year of the first/last occuring event. 281 * 282 * @param string $direction The sorting direction 283 * 284 * @return string 285 */ 286 private function getFirstLastEventYear(string $direction): string 287 { 288 $row = $this->eventQuery($direction); 289 290 if (!$row) { 291 return ''; 292 } 293 294 return (new Date($row->type . ' ' . $row->year)) 295 ->display(); 296 } 297 298 /** 299 * @return string 300 */ 301 public function firstEventYear(): string 302 { 303 return $this->getFirstLastEventYear(self::SORT_ASC); 304 } 305 306 /** 307 * @return string 308 */ 309 public function lastEventYear(): string 310 { 311 return $this->getFirstLastEventYear(self::SORT_DESC); 312 } 313 314 /** 315 * Returns the formatted type of the first/last occuring event. 316 * 317 * @param string $direction The sorting direction 318 * 319 * @return string 320 */ 321 private function getFirstLastEventType(string $direction): string 322 { 323 $row = $this->eventQuery($direction); 324 325 if ($row) { 326 $event_types = [ 327 self::EVENT_BIRTH => I18N::translate('birth'), 328 self::EVENT_DEATH => I18N::translate('death'), 329 self::EVENT_MARRIAGE => I18N::translate('marriage'), 330 self::EVENT_ADOPTION => I18N::translate('adoption'), 331 self::EVENT_BURIAL => I18N::translate('burial'), 332 self::EVENT_CENSUS => I18N::translate('census added'), 333 ]; 334 335 return $event_types[$row->fact] ?? GedcomTag::getLabel($row->fact); 336 } 337 338 return ''; 339 } 340 341 /** 342 * @return string 343 */ 344 public function firstEventType(): string 345 { 346 return $this->getFirstLastEventType(self::SORT_ASC); 347 } 348 349 /** 350 * @return string 351 */ 352 public function lastEventType(): string 353 { 354 return $this->getFirstLastEventType(self::SORT_DESC); 355 } 356 357 /** 358 * Returns the formatted name of the first/last occuring event. 359 * 360 * @param string $direction The sorting direction 361 * 362 * @return string 363 */ 364 private function getFirstLastEventName(string $direction): string 365 { 366 $row = $this->eventQuery($direction); 367 368 if ($row) { 369 $record = Factory::gedcomRecord()->make($row->id, $this->tree); 370 371 if ($record) { 372 return '<a href="' . e($record->url()) . '">' . $record->fullName() . '</a>'; 373 } 374 } 375 376 return ''; 377 } 378 379 /** 380 * @return string 381 */ 382 public function firstEventName(): string 383 { 384 return $this->getFirstLastEventName(self::SORT_ASC); 385 } 386 387 /** 388 * @return string 389 */ 390 public function lastEventName(): string 391 { 392 return $this->getFirstLastEventName(self::SORT_DESC); 393 } 394 395 /** 396 * Returns the formatted place of the first/last occuring event. 397 * 398 * @param string $direction The sorting direction 399 * 400 * @return string 401 */ 402 private function getFirstLastEventPlace(string $direction): string 403 { 404 $row = $this->eventQuery($direction); 405 406 if ($row) { 407 $record = Factory::gedcomRecord()->make($row->id, $this->tree); 408 $fact = null; 409 410 if ($record) { 411 $fact = $record->facts([$row->fact])->first(); 412 } 413 414 if ($fact instanceof Fact) { 415 return FunctionsPrint::formatFactPlace($fact, true, true, true); 416 } 417 } 418 419 return I18N::translate('Private'); 420 } 421 422 /** 423 * @return string 424 */ 425 public function firstEventPlace(): string 426 { 427 return $this->getFirstLastEventPlace(self::SORT_ASC); 428 } 429 430 /** 431 * @return string 432 */ 433 public function lastEventPlace(): string 434 { 435 return $this->getFirstLastEventPlace(self::SORT_DESC); 436 } 437} 438