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