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 '<span class="cal_day">', I18N::translate('Day not set'), '</span><br style="clear: both;">'; 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 if ($d === $today->day && $cal_date->month === $today->month) { 222 echo '<span class="cal_day current_day">', $d_fmt, '</span>'; 223 } else { 224 echo '<span class="cal_day">', $d_fmt, '</span>'; 225 } 226 // Show a converted date 227 foreach (explode('_and_', $CALENDAR_FORMAT) as $convcal) { 228 switch ($convcal) { 229 case 'french': 230 $alt_date = new FrenchDate($cal_date->minimumJulianDay() + $d - 1); 231 break; 232 case 'gregorian': 233 $alt_date = new GregorianDate($cal_date->minimumJulianDay() + $d - 1); 234 break; 235 case 'jewish': 236 $alt_date = new JewishDate($cal_date->minimumJulianDay() + $d - 1); 237 break; 238 case 'julian': 239 $alt_date = new JulianDate($cal_date->minimumJulianDay() + $d - 1); 240 break; 241 case 'hijri': 242 $alt_date = new HijriDate($cal_date->minimumJulianDay() + $d - 1); 243 break; 244 case 'jalali': 245 $alt_date = new JalaliDate($cal_date->minimumJulianDay() + $d - 1); 246 break; 247 case 'none': 248 default: 249 $alt_date = $cal_date; 250 break; 251 } 252 if (get_class($alt_date) !== get_class($cal_date) && $alt_date->inValidRange()) { 253 echo '<span class="rtl_cal_day">' . $alt_date->format('%j %M') . '</span>'; 254 // Just show the first conversion 255 break; 256 } 257 } 258 echo '<br style="clear: both;"><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 string[] $list 277 * @param string $tag1 278 * @param string $tag2 279 * @param Tree $tree 280 * 281 * @return string 282 */ 283 private function calendarListText(array $list, string $tag1, string $tag2, Tree $tree): string 284 { 285 $html = ''; 286 287 foreach ($list as $xref => $facts) { 288 $tmp = Factory::gedcomRecord()->make((string) $xref, $tree); 289 $html .= $tag1 . '<a href="' . e($tmp->url()) . '">' . $tmp->fullName() . '</a> '; 290 $html .= '<div class="indent">' . $facts . '</div>' . $tag2; 291 } 292 293 return $html; 294 } 295} 296