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