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