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\Http\RequestHandlers; 21 22use Fisharebest\Webtrees\Date; 23use Fisharebest\Webtrees\Date\FrenchDate; 24use Fisharebest\Webtrees\Date\GregorianDate; 25use Fisharebest\Webtrees\Date\HijriDate; 26use Fisharebest\Webtrees\Date\JalaliDate; 27use Fisharebest\Webtrees\Date\JewishDate; 28use Fisharebest\Webtrees\Date\JulianDate; 29use Fisharebest\Webtrees\Fact; 30use Fisharebest\Webtrees\Factory; 31use Fisharebest\Webtrees\Family; 32use Fisharebest\Webtrees\I18N; 33use Fisharebest\Webtrees\Individual; 34use Fisharebest\Webtrees\Services\CalendarService; 35use Fisharebest\Webtrees\Tree; 36use Illuminate\Support\Collection; 37use Psr\Http\Message\ResponseInterface; 38use Psr\Http\Message\ServerRequestInterface; 39use Psr\Http\Server\RequestHandlerInterface; 40 41use function assert; 42use function count; 43use function e; 44use function explode; 45use function get_class; 46use function ob_get_clean; 47use function ob_start; 48use function range; 49use function response; 50use function view; 51 52/** 53 * Show anniversaries for events in a given day/month/year. 54 */ 55class CalendarEvents implements RequestHandlerInterface 56{ 57 /** @var CalendarService */ 58 private $calendar_service; 59 60 /** 61 * CalendarPage constructor. 62 * 63 * @param CalendarService $calendar_service 64 */ 65 public function __construct(CalendarService $calendar_service) 66 { 67 $this->calendar_service = $calendar_service; 68 } 69 70 /** 71 * Show anniversaries that occured on a given day/month/year. 72 * 73 * @param ServerRequestInterface $request 74 * 75 * @return ResponseInterface 76 */ 77 public function handle(ServerRequestInterface $request): ResponseInterface 78 { 79 $tree = $request->getAttribute('tree'); 80 assert($tree instanceof Tree); 81 82 $view = $request->getAttribute('view'); 83 $CALENDAR_FORMAT = $tree->getPreference('CALENDAR_FORMAT'); 84 85 $cal = $request->getQueryParams()['cal'] ?? ''; 86 $day = $request->getQueryParams()['day'] ?? ''; 87 $month = $request->getQueryParams()['month'] ?? ''; 88 $year = $request->getQueryParams()['year'] ?? ''; 89 $filterev = $request->getQueryParams()['filterev'] ?? 'BIRT-MARR-DEAT'; 90 $filterof = $request->getQueryParams()['filterof'] ?? 'all'; 91 $filtersx = $request->getQueryParams()['filtersx'] ?? ''; 92 93 $ged_date = new Date("{$cal} {$day} {$month} {$year}"); 94 $cal_date = $ged_date->minimumDate(); 95 $today = $cal_date->today(); 96 97 $days_in_month = $cal_date->daysInMonth(); 98 $days_in_week = $cal_date->daysInWeek(); 99 100 // Day and year share the same layout. 101 if ($view !== 'month') { 102 if ($view === 'day') { 103 $anniversary_facts = $this->calendar_service->getAnniversaryEvents($cal_date->minimumJulianDay(), $filterev, $tree, $filterof, $filtersx); 104 } else { 105 $ged_year = new Date($cal . ' ' . $year); 106 $anniversary_facts = $this->calendar_service->getCalendarEvents($ged_year->minimumJulianDay(), $ged_year->maximumJulianDay(), $filterev, $tree, $filterof, $filtersx); 107 } 108 109 $anniversaries = Collection::make($anniversary_facts) 110 ->unique() 111 ->sort(static function (Fact $x, Fact $y): int { 112 return $x->date()->minimumJulianDay() <=> $y->date()->minimumJulianDay(); 113 }); 114 115 $family_anniversaries = $anniversaries->filter(static function (Fact $f): bool { 116 return $f->record() instanceof Family; 117 }); 118 119 $individual_anniversaries = $anniversaries->filter(static function (Fact $f): bool { 120 return $f->record() instanceof Individual; 121 }); 122 123 return response(view('calendar-list', [ 124 'family_anniversaries' => $family_anniversaries, 125 'individual_anniversaries' => $individual_anniversaries, 126 ])); 127 } 128 129 $found_facts = []; 130 131 $cal_date->day = 0; 132 $cal_date->setJdFromYmd(); 133 // Make a separate list for each day. Unspecified/invalid days go in day 0. 134 for ($d = 0; $d <= $days_in_month; ++$d) { 135 $found_facts[$d] = []; 136 } 137 // Fetch events for each day 138 $jds = range($cal_date->minimumJulianDay(), $cal_date->maximumJulianDay()); 139 140 foreach ($jds as $jd) { 141 foreach ($this->calendar_service->getAnniversaryEvents($jd, $filterev, $tree, $filterof, $filtersx) as $fact) { 142 $tmp = $fact->date()->minimumDate(); 143 if ($tmp->day >= 1 && $tmp->day <= $tmp->daysInMonth()) { 144 // If the day is valid (for its own calendar), display it in the 145 // anniversary day (for the display calendar). 146 $found_facts[$jd - $cal_date->minimumJulianDay() + 1][] = $fact; 147 } else { 148 // Otherwise, display it in the "Day not set" box. 149 $found_facts[0][] = $fact; 150 } 151 } 152 } 153 154 $cal_facts = []; 155 156 foreach ($found_facts as $d => $facts) { 157 $cal_facts[$d] = []; 158 foreach ($facts as $fact) { 159 $xref = $fact->record()->xref(); 160 $text = $text = $fact->label() . ' — ' . $fact->date()->display(true, null, false); 161 if ($fact->anniv > 0) { 162 $text .= ' (' . I18N::translate('%s year anniversary', $fact->anniv) . ')'; 163 } 164 if (empty($cal_facts[$d][$xref])) { 165 $cal_facts[$d][$xref] = $text; 166 } else { 167 $cal_facts[$d][$xref] .= '<br>' . $text; 168 } 169 } 170 } 171 // We use JD%7 = 0/Mon…6/Sun. Standard definitions use 0/Sun…6/Sat. 172 $week_start = (I18N::locale()->territory()->firstDay() + 6) % 7; 173 $weekend_start = (I18N::locale()->territory()->weekendStart() + 6) % 7; 174 $weekend_end = (I18N::locale()->territory()->weekendEnd() + 6) % 7; 175 // The french calendar has a 10-day week, which starts on primidi 176 if ($days_in_week === 10) { 177 $week_start = 0; 178 $weekend_start = -1; 179 $weekend_end = -1; 180 } 181 182 ob_start(); 183 184 echo '<table class="w-100"><thead><tr>'; 185 for ($week_day = 0; $week_day < $days_in_week; ++$week_day) { 186 $day_name = $cal_date->dayNames(($week_day + $week_start) % $days_in_week); 187 if ($week_day === $weekend_start || $week_day === $weekend_end) { 188 echo '<th class="wt-page-options-label weekend" width="' . (100 / $days_in_week) . '%">', $day_name, '</th>'; 189 } else { 190 echo '<th class="wt-page-options-label" width="' . (100 / $days_in_week) . '%">', $day_name, '</th>'; 191 } 192 } 193 echo '</tr>'; 194 echo '</thead>'; 195 echo '<tbody>'; 196 // Print days 1 to n of the month, but extend to cover "empty" days before/after the month to make whole weeks. 197 // e.g. instead of 1 -> 30 (=30 days), we might have -1 -> 33 (=35 days) 198 $start_d = 1 - ($cal_date->minimumJulianDay() - $week_start) % $days_in_week; 199 $end_d = $days_in_month + ($days_in_week - ($cal_date->maximumJulianDay() - $week_start + 1) % $days_in_week) % $days_in_week; 200 // Make sure that there is an empty box for any leap/missing days 201 if ($start_d === 1 && $end_d === $days_in_month && count($found_facts[0]) > 0) { 202 $end_d += $days_in_week; 203 } 204 for ($d = $start_d; $d <= $end_d; ++$d) { 205 if (($d + $cal_date->minimumJulianDay() - $week_start) % $days_in_week === 1) { 206 echo '<tr>'; 207 } 208 echo '<td class="wt-page-options-value">'; 209 if ($d < 1 || $d > $days_in_month) { 210 if (count($cal_facts[0]) > 0) { 211 echo '<div class="cal_day">', I18N::translate('Day not set'), '</div>'; 212 echo '<div class="small" style="height: 180px; overflow: auto;">'; 213 echo $this->calendarListText($cal_facts[0], '', '', $tree); 214 echo '</div>'; 215 $cal_facts[0] = []; 216 } 217 } else { 218 // Format the day number using the calendar 219 $tmp = new Date($cal_date->format("%@ {$d} %O %E")); 220 $d_fmt = $tmp->minimumDate()->format('%j'); 221 echo '<div class="d-flex d-flex justify-content-between">'; 222 if ($d === $today->day && $cal_date->month === $today->month) { 223 echo '<span class="cal_day current_day">', $d_fmt, '</span>'; 224 } else { 225 echo '<span class="cal_day">', $d_fmt, '</span>'; 226 } 227 // Show a converted date 228 foreach (explode('_and_', $CALENDAR_FORMAT) as $convcal) { 229 switch ($convcal) { 230 case 'french': 231 $alt_date = new FrenchDate($cal_date->minimumJulianDay() + $d - 1); 232 break; 233 case 'gregorian': 234 $alt_date = new GregorianDate($cal_date->minimumJulianDay() + $d - 1); 235 break; 236 case 'jewish': 237 $alt_date = new JewishDate($cal_date->minimumJulianDay() + $d - 1); 238 break; 239 case 'julian': 240 $alt_date = new JulianDate($cal_date->minimumJulianDay() + $d - 1); 241 break; 242 case 'hijri': 243 $alt_date = new HijriDate($cal_date->minimumJulianDay() + $d - 1); 244 break; 245 case 'jalali': 246 $alt_date = new JalaliDate($cal_date->minimumJulianDay() + $d - 1); 247 break; 248 case 'none': 249 default: 250 $alt_date = $cal_date; 251 break; 252 } 253 if (get_class($alt_date) !== get_class($cal_date) && $alt_date->inValidRange()) { 254 echo '<span class="rtl_cal_day">' . $alt_date->format('%j %M') . '</span>'; 255 // Just show the first conversion 256 break; 257 } 258 } 259 echo '</div>'; 260 echo '<div class="small" style="height: 180px; overflow: auto;">'; 261 echo $this->calendarListText($cal_facts[$d], '', '', $tree); 262 echo '</div>'; 263 } 264 echo '</td>'; 265 if (($d + $cal_date->minimumJulianDay() - $week_start) % $days_in_week === 0) { 266 echo '</tr>'; 267 } 268 } 269 echo '</tbody>'; 270 echo '</table>'; 271 272 return response(ob_get_clean()); 273 } 274 275 /** 276 * Format a list of facts for display 277 * 278 * @param string[] $list 279 * @param string $tag1 280 * @param string $tag2 281 * @param Tree $tree 282 * 283 * @return string 284 */ 285 private function calendarListText(array $list, string $tag1, string $tag2, Tree $tree): string 286 { 287 $html = ''; 288 289 foreach ($list as $xref => $facts) { 290 $tmp = Factory::gedcomRecord()->make((string) $xref, $tree); 291 $html .= $tag1 . '<a href="' . e($tmp->url()) . '">' . $tmp->fullName() . '</a> '; 292 $html .= '<div class="indent">' . $facts . '</div>' . $tag2; 293 } 294 295 return $html; 296 } 297} 298