Completed
Push — master ( 56eca4...2839b8 )
by Greg
06:20
created

CalendarController::applyFilter()   C

Complexity

Conditions 16
Paths 19

Size

Total Lines 37
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
eloc 21
nc 19
nop 3
dl 0
loc 37
rs 5.5666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2019 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
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Http\Controllers;
21
22
use Fisharebest\Localization\Locale\LocaleInterface;
23
use Fisharebest\Webtrees\Carbon;
24
use Fisharebest\Webtrees\Date;
25
use Fisharebest\Webtrees\Date\FrenchDate;
26
use Fisharebest\Webtrees\Date\GregorianDate;
27
use Fisharebest\Webtrees\Date\HijriDate;
28
use Fisharebest\Webtrees\Date\JalaliDate;
29
use Fisharebest\Webtrees\Date\JewishDate;
30
use Fisharebest\Webtrees\Date\JulianDate;
31
use Fisharebest\Webtrees\Fact;
32
use Fisharebest\Webtrees\Family;
33
use Fisharebest\Webtrees\GedcomRecord;
34
use Fisharebest\Webtrees\I18N;
35
use Fisharebest\Webtrees\Individual;
36
use Fisharebest\Webtrees\Services\CalendarService;
37
use Fisharebest\Webtrees\Services\LocalizationService;
38
use Fisharebest\Webtrees\Tree;
39
use Psr\Http\Message\ResponseInterface;
40
use Psr\Http\Message\ServerRequestInterface;
41
42
use function array_unique;
43
use function assert;
44
use function count;
45
use function e;
46
use function explode;
47
use function get_class;
48
use function ob_get_clean;
49
use function ob_start;
50
use function preg_match;
51
use function range;
52
use function redirect;
53
use function response;
54
use function route;
55
use function str_replace;
56
use function strlen;
57
use function substr;
58
59
/**
60
 * Show anniveraries for events in a given day/month/year.
61
 */
62
class CalendarController extends AbstractBaseController
63
{
64
    /** @var CalendarService */
65
    private $calendar_service;
66
67
    /** @var LocalizationService */
68
    private $localization_service;
69
70
    /**
71
     * CalendarController constructor.
72
     *
73
     * @param CalendarService     $calendar_service
74
     * @param LocalizationService $localization_service
75
     */
76
    public function __construct(CalendarService $calendar_service, LocalizationService $localization_service)
77
    {
78
        $this->calendar_service     = $calendar_service;
79
        $this->localization_service = $localization_service;
80
    }
81
82
    /**
83
     * A form to request the page parameters.
84
     *
85
     * @param ServerRequestInterface $request
86
     *
87
     * @return ResponseInterface
88
     */
89
    public function page(ServerRequestInterface $request): ResponseInterface
90
    {
91
        $tree = $request->getAttribute('tree');
92
        assert($tree instanceof Tree);
93
94
        $locale = $request->getAttribute('locale');
95
        assert($locale instanceof LocaleInterface);
96
97
        $view     = $request->getAttribute('view');
98
        $cal      = $request->getQueryParams()['cal'] ?? '';
99
        $day      = $request->getQueryParams()['day'] ?? '';
100
        $month    = $request->getQueryParams()['month'] ?? '';
101
        $year     = $request->getQueryParams()['year'] ?? '';
102
        $filterev = $request->getQueryParams()['filterev'] ?? 'BIRT-MARR-DEAT';
103
        $filterof = $request->getQueryParams()['filterof'] ?? 'all';
104
        $filtersx = $request->getQueryParams()['filtersx'] ?? '';
105
106
        if ($cal . $day . $month . $year === '') {
107
            // No date specified? Use the most likely calendar
108
            $cal = $this->localization_service->calendar($locale)->gedcomCalendarEscape();
109
        }
110
111
        // need BC to parse date
112
        if ($year < 0) {
113
            $year = (-$year) . ' B.C.';
114
        }
115
        $ged_date = new Date("{$cal} {$day} {$month} {$year}");
116
        // need negative year for year entry field.
117
        $year     = $ged_date->minimumDate()->year;
118
        $cal_date = $ged_date->minimumDate();
119
120
        // Fill in any missing bits with todays date
121
        $today = $cal_date->today();
122
        if ($cal_date->day === 0) {
123
            $cal_date->day = $today->day;
124
        }
125
        if ($cal_date->month === 0) {
126
            $cal_date->month = $today->month;
127
        }
128
        if ($cal_date->year === 0) {
129
            $cal_date->year = $today->year;
130
        }
131
132
        $cal_date->setJdFromYmd();
133
134
        if ($year === 0) {
135
            $year = $cal_date->year;
136
        }
137
138
        // Extract values from date
139
        $days_in_month = $cal_date->daysInMonth();
140
        $cal_month     = $cal_date->format('%O');
141
        $today_month   = $today->format('%O');
142
143
        // Invalid dates? Go to monthly view, where they'll be found.
144
        if ($cal_date->day > $days_in_month && $view === 'day') {
145
            $view = 'month';
146
        }
147
148
        $title = I18N::translate('Anniversary calendar');
149
150
        switch ($view) {
151
            case 'day':
152
                $title = I18N::translate('On this day…') . ' ' . $ged_date->display(false);
153
                break;
154
            case 'month':
155
                $title = I18N::translate('In this month…') . ' ' . $ged_date->display(false, '%F %Y');
156
                break;
157
            case 'year':
158
                $title = I18N::translate('In this year…') . ' ' . $ged_date->display(false, '%Y');
159
                break;
160
        }
161
162
        return $this->viewResponse('calendar-page', [
163
            'cal'           => $cal,
164
            'cal_date'      => $cal_date,
165
            'cal_month'     => $cal_month,
166
            'day'           => $day,
167
            'days_in_month' => $days_in_month,
168
            'filterev'      => $filterev,
169
            'filterof'      => $filterof,
170
            'filtersx'      => $filtersx,
171
            'month'         => $month,
172
            'months'        => $this->calendar_service->calendarMonthsInYear($cal, $year),
173
            'title'         => $title,
174
            'today'         => $today,
175
            'today_month'   => $today_month,
176
            'tree'          => $tree,
177
            'view'          => $view,
178
            'year'          => $year,
179
        ]);
180
    }
181
182
    /**
183
     * @param ServerRequestInterface $request
184
     *
185
     * @return ResponseInterface
186
     */
187
    public function select(ServerRequestInterface $request): ResponseInterface
188
    {
189
        $tree = $request->getAttribute('tree');
190
        assert($tree instanceof Tree);
191
192
        $view = $request->getAttribute('view');
193
194
        return redirect(route('calendar', [
195
            'cal'      => $request->getParsedBody()['cal'],
196
            'day'      => $request->getParsedBody()['day'],
197
            'filterev' => $request->getParsedBody()['filterev'],
198
            'filterof' => $request->getParsedBody()['filterof'],
199
            'filtersx' => $request->getParsedBody()['filtersx'],
200
            'month'    => $request->getParsedBody()['month'],
201
            'tree'     => $tree->name(),
202
            'view'     => $view,
203
            'year'     => $request->getParsedBody()['year'],
204
        ]));
205
    }
206
207
    /**
208
     * Show anniveraries that occured on a given day/month/year.
209
     *
210
     * @param ServerRequestInterface $request
211
     *
212
     * @return ResponseInterface
213
     */
214
    public function calendar(ServerRequestInterface $request): ResponseInterface
215
    {
216
        $tree = $request->getAttribute('tree');
217
        assert($tree instanceof Tree);
218
219
        $locale = $request->getAttribute('locale');
220
        assert($locale instanceof LocaleInterface);
221
222
        $view            = $request->getAttribute('view');
223
        $CALENDAR_FORMAT = $tree->getPreference('CALENDAR_FORMAT');
224
225
        $cal      = $request->getQueryParams()['cal'] ?? '';
226
        $day      = $request->getQueryParams()['day'] ?? '';
227
        $month    = $request->getQueryParams()['month'] ?? '';
228
        $year     = $request->getQueryParams()['year'] ?? '';
229
        $filterev = $request->getQueryParams()['filterev'] ?? 'BIRT-MARR-DEAT';
230
        $filterof = $request->getQueryParams()['filterof'] ?? 'all';
231
        $filtersx = $request->getQueryParams()['filtersx'] ?? '';
232
233
        if ($cal . $day . $month . $year === '') {
234
            // No date specified? Use the most likely calendar
235
            $cal = $this->localization_service->calendar($locale)->gedcomCalendarEscape();
236
        }
237
238
        // Create a CalendarDate from the parameters
239
240
        // We cannot display new-style/old-style years, so convert to new style
241
        if (preg_match('/^(\d\d)\d\d\/(\d\d)$/', $year, $match)) {
242
            $year = $match[1] . $match[2];
243
        }
244
245
        // advanced-year "year range"
246
        if (preg_match('/^(\d+)-(\d+)$/', $year, $match)) {
247
            if (strlen($match[1]) > strlen($match[2])) {
248
                $match[2] = substr($match[1], 0, strlen($match[1]) - strlen($match[2])) . $match[2];
249
            }
250
            $ged_date = new Date("FROM {$cal} {$match[1]} TO {$cal} {$match[2]}");
251
            $view     = 'year';
252
        } elseif (preg_match('/^(\d+)(\?+)$/', $year, $match)) {
253
            // advanced-year "decade/century wildcard"
254
            $y1       = $match[1] . str_replace('?', '0', $match[2]);
255
            $y2       = $match[1] . str_replace('?', '9', $match[2]);
256
            $ged_date = new Date("FROM {$cal} {$y1} TO {$cal} {$y2}");
257
            $view     = 'year';
258
        } else {
259
            if ($year < 0) {
260
                $year = (-$year) . ' B.C.';
261
            } // need BC to parse date
262
            $ged_date = new Date("{$cal} {$day} {$month} {$year}");
263
        }
264
        $cal_date = $ged_date->minimumDate();
265
266
        // Fill in any missing bits with todays date
267
        $today = $cal_date->today();
268
        if ($cal_date->day === 0) {
269
            $cal_date->day = $today->day;
270
        }
271
        if ($cal_date->month === 0) {
272
            $cal_date->month = $today->month;
273
        }
274
        if ($cal_date->year === 0) {
275
            $cal_date->year = $today->year;
276
        }
277
278
        $cal_date->setJdFromYmd();
279
280
        // Extract values from date
281
        $days_in_month = $cal_date->daysInMonth();
282
        $days_in_week  = $cal_date->daysInWeek();
283
284
        // Invalid dates? Go to monthly view, where they'll be found.
285
        if ($cal_date->day > $days_in_month && $view === 'day') {
286
            $view = 'month';
287
        }
288
289
        /** @var Fact[]|Fact[][] $found_facts */
290
        $found_facts = [];
291
292
        switch ($view) {
293
            case 'day':
294
                $found_facts = $this->applyFilter($this->calendar_service->getAnniversaryEvents($cal_date->minimumJulianDay(), $filterev, $tree), $filterof, $filtersx);
295
                break;
296
            case 'month':
297
                $cal_date->day = 0;
298
                $cal_date->setJdFromYmd();
299
                // Make a separate list for each day. Unspecified/invalid days go in day 0.
300
                for ($d = 0; $d <= $days_in_month; ++$d) {
301
                    $found_facts[$d] = [];
302
                }
303
                // Fetch events for each day
304
                $jds = range($cal_date->minimumJulianDay(), $cal_date->maximumJulianDay());
305
306
                foreach ($jds as $jd) {
307
                    foreach ($this->applyFilter($this->calendar_service->getAnniversaryEvents($jd, $filterev, $tree), $filterof, $filtersx) as $fact) {
308
                        $tmp = $fact->date()->minimumDate();
309
                        if ($tmp->day >= 1 && $tmp->day <= $tmp->daysInMonth()) {
310
                            // If the day is valid (for its own calendar), display it in the
311
                            // anniversary day (for the display calendar).
312
                            $found_facts[$jd - $cal_date->minimumJulianDay() + 1][] = $fact;
313
                        } else {
314
                            // Otherwise, display it in the "Day not set" box.
315
                            $found_facts[0][] = $fact;
316
                        }
317
                    }
318
                }
319
                break;
320
            case 'year':
321
                $cal_date->month = 0;
322
                $cal_date->setJdFromYmd();
323
                $found_facts = $this->applyFilter($this->calendar_service->getCalendarEvents($ged_date->minimumJulianDay(), $ged_date->maximumJulianDay(), $filterev, $tree), $filterof, $filtersx);
324
                // Eliminate duplicates (e.g. BET JUL 1900 AND SEP 1900 will appear twice in 1900)
325
                $found_facts = array_unique($found_facts);
326
                break;
327
        }
328
329
        // Group the facts by family/individual
330
        $indis     = [];
331
        $fams      = [];
332
        $cal_facts = [];
333
334
        switch ($view) {
335
            case 'year':
336
            case 'day':
337
                foreach ($found_facts as $fact) {
338
                    $record = $fact->record();
339
                    $xref   = $record->xref();
340
                    if ($record instanceof Individual) {
341
                        if (empty($indis[$xref])) {
342
                            $indis[$xref] = $this->calendarFactText($fact, true);
343
                        } else {
344
                            $indis[$xref] .= '<br>' . $this->calendarFactText($fact, true);
345
                        }
346
                    } elseif ($record instanceof Family) {
347
                        if (empty($indis[$xref])) {
348
                            $fams[$xref] = $this->calendarFactText($fact, true);
349
                        } else {
350
                            $fams[$xref] .= '<br>' . $this->calendarFactText($fact, true);
351
                        }
352
                    }
353
                }
354
                break;
355
            case 'month':
356
                foreach ($found_facts as $d => $facts) {
357
                    $cal_facts[$d] = [];
358
                    foreach ($facts as $fact) {
359
                        $xref = $fact->record()->xref();
360
                        if (empty($cal_facts[$d][$xref])) {
361
                            $cal_facts[$d][$xref] = $this->calendarFactText($fact, false);
362
                        } else {
363
                            $cal_facts[$d][$xref] .= '<br>' . $this->calendarFactText($fact, false);
364
                        }
365
                    }
366
                }
367
                break;
368
        }
369
370
        ob_start();
371
372
        switch ($view) {
373
            case 'year':
374
            case 'day':
375
                echo '<table class="w-100"><tr>';
376
                echo '<td class="descriptionbox center"><i class="icon-indis"></i>', I18N::translate('Individuals'), '</td>';
377
                echo '<td class="descriptionbox center"><i class="icon-cfamily"></i>', I18N::translate('Families'), '</td>';
378
                echo '</tr><tr>';
379
                echo '<td class="optionbox wrap">';
380
381
                $content = $this->calendarListText($indis, '<li>', '</li>', $tree);
382
                if ($content) {
383
                    echo '<ul>', $content, '</ul>';
384
                }
385
386
                echo '</td>';
387
                echo '<td class="optionbox wrap">';
388
389
                $content = $this->calendarListText($fams, '<li>', '</li>', $tree);
390
                if ($content) {
391
                    echo '<ul>', $content, '</ul>';
392
                }
393
394
                echo '</td>';
395
                echo '</tr><tr>';
396
                echo '<td class="descriptionbox">', I18N::translate('Total individuals: %s', I18N::number(count($indis))), '</td>';
397
                echo '<td class="descriptionbox">', I18N::translate('Total families: %s', I18N::number(count($fams))), '</td>';
398
                echo '</tr></table>';
399
400
                break;
401
            case 'month':
402
                // We use JD%7 = 0/Mon…6/Sun. Standard definitions use 0/Sun…6/Sat.
403
                $week_start    = (I18N::firstDay() + 6) % 7;
404
                $weekend_start = ($locale->territory()->weekendStart() + 6) % 7;
405
                $weekend_end   = ($locale->territory()->weekendEnd() + 6) % 7;
406
                // The french  calendar has a 10-day week, which starts on primidi
407
                if ($days_in_week === 10) {
408
                    $week_start    = 0;
409
                    $weekend_start = -1;
410
                    $weekend_end   = -1;
411
                }
412
                echo '<table class="w-100"><thead><tr>';
413
                for ($week_day = 0; $week_day < $days_in_week; ++$week_day) {
414
                    $day_name = $cal_date->dayNames(($week_day + $week_start) % $days_in_week);
415
                    if ($week_day == $weekend_start || $week_day == $weekend_end) {
416
                        echo '<th class="descriptionbox weekend" width="' . (100 / $days_in_week) . '%">', $day_name, '</th>';
417
                    } else {
418
                        echo '<th class="descriptionbox" width="' . (100 / $days_in_week) . '%">', $day_name, '</th>';
419
                    }
420
                }
421
                echo '</tr>';
422
                echo '</thead>';
423
                echo '<tbody>';
424
                // Print days 1 to n of the month, but extend to cover "empty" days before/after the month to make whole weeks.
425
                // e.g. instead of 1 -> 30 (=30 days), we might have -1 -> 33 (=35 days)
426
                $start_d = 1 - ($cal_date->minimumJulianDay() - $week_start) % $days_in_week;
427
                $end_d   = $days_in_month + ($days_in_week - ($cal_date->maximumJulianDay() - $week_start + 1) % $days_in_week) % $days_in_week;
428
                // Make sure that there is an empty box for any leap/missing days
429
                if ($start_d === 1 && $end_d === $days_in_month && count($found_facts[0]) > 0) {
430
                    $end_d += $days_in_week;
431
                }
432
                for ($d = $start_d; $d <= $end_d; ++$d) {
433
                    if (($d + $cal_date->minimumJulianDay() - $week_start) % $days_in_week === 1) {
434
                        echo '<tr>';
435
                    }
436
                    echo '<td class="optionbox wrap">';
437
                    if ($d < 1 || $d > $days_in_month) {
438
                        if (count($cal_facts[0]) > 0) {
439
                            echo '<span class="cal_day">', I18N::translate('Day not set'), '</span><br style="clear: both;">';
440
                            echo '<div class="details1" style="height: 180px; overflow: auto;">';
441
                            echo $this->calendarListText($cal_facts[0], '', '', $tree);
442
                            echo '</div>';
443
                            $cal_facts[0] = [];
444
                        }
445
                    } else {
446
                        // Format the day number using the calendar
447
                        $tmp   = new Date($cal_date->format("%@ {$d} %O %E"));
448
                        $d_fmt = $tmp->minimumDate()->format('%j');
449
                        if ($d === $today->day && $cal_date->month === $today->month) {
450
                            echo '<span class="cal_day current_day">', $d_fmt, '</span>';
451
                        } else {
452
                            echo '<span class="cal_day">', $d_fmt, '</span>';
453
                        }
454
                        // Show a converted date
455
                        foreach (explode('_and_', $CALENDAR_FORMAT) as $convcal) {
456
                            switch ($convcal) {
457
                                case 'french':
458
                                    $alt_date = new FrenchDate($cal_date->minimumJulianDay() + $d - 1);
459
                                    break;
460
                                case 'gregorian':
461
                                    $alt_date = new GregorianDate($cal_date->minimumJulianDay() + $d - 1);
462
                                    break;
463
                                case 'jewish':
464
                                    $alt_date = new JewishDate($cal_date->minimumJulianDay() + $d - 1);
465
                                    break;
466
                                case 'julian':
467
                                    $alt_date = new JulianDate($cal_date->minimumJulianDay() + $d - 1);
468
                                    break;
469
                                case 'hijri':
470
                                    $alt_date = new HijriDate($cal_date->minimumJulianDay() + $d - 1);
471
                                    break;
472
                                case 'jalali':
473
                                    $alt_date = new JalaliDate($cal_date->minimumJulianDay() + $d - 1);
474
                                    break;
475
                                case 'none':
476
                                default:
477
                                    $alt_date = $cal_date;
478
                                    break;
479
                            }
480
                            if (get_class($alt_date) !== get_class($cal_date) && $alt_date->inValidRange()) {
481
                                echo '<span class="rtl_cal_day">' . $alt_date->format('%j %M') . '</span>';
482
                                // Just show the first conversion
483
                                break;
484
                            }
485
                        }
486
                        echo '<br style="clear: both;"><div class="details1" style="height: 180px; overflow: auto;">';
487
                        echo $this->calendarListText($cal_facts[$d], '', '', $tree);
488
                        echo '</div>';
489
                    }
490
                    echo '</td>';
491
                    if (($d + $cal_date->minimumJulianDay() - $week_start) % $days_in_week === 0) {
492
                        echo '</tr>';
493
                    }
494
                }
495
                echo '</tbody>';
496
                echo '</table>';
497
                break;
498
        }
499
500
        $html = ob_get_clean();
501
502
        return response($html);
503
    }
504
505
    /**
506
     * Filter a list of anniversaries
507
     *
508
     * @param Fact[] $facts
509
     * @param string $filterof
510
     * @param string $filtersx
511
     *
512
     * @return Fact[]
513
     */
514
    private function applyFilter(array $facts, string $filterof, string $filtersx): array
515
    {
516
        $filtered      = [];
517
        $hundred_years = Carbon::now()->subYears(100)->julianDay();
518
        foreach ($facts as $fact) {
519
            $record = $fact->record();
520
            if ($filtersx) {
521
                // Filter on sex
522
                if ($record instanceof Individual && $filtersx !== $record->sex()) {
523
                    continue;
524
                }
525
                // Can't display families if the sex filter is on.
526
                if ($record instanceof Family) {
527
                    continue;
528
                }
529
            }
530
            // Filter living individuals
531
            if ($filterof === 'living') {
532
                if ($record instanceof Individual && $record->isDead()) {
533
                    continue;
534
                }
535
                if ($record instanceof Family) {
536
                    $husb = $record->husband();
537
                    $wife = $record->wife();
538
                    if ($husb && $husb->isDead() || $wife && $wife->isDead()) {
539
                        continue;
540
                    }
541
                }
542
            }
543
            // Filter on recent events
544
            if ($filterof === 'recent' && $fact->date()->maximumJulianDay() < $hundred_years) {
545
                continue;
546
            }
547
            $filtered[] = $fact;
548
        }
549
550
        return $filtered;
551
    }
552
553
    /**
554
     * Format an anniversary display.
555
     *
556
     * @param Fact $fact
557
     * @param bool $show_places
558
     *
559
     * @return string
560
     */
561
    private function calendarFactText(Fact $fact, bool $show_places): string
562
    {
563
        $text = $fact->label() . ' — ' . $fact->date()->display(true, null, false);
564
        if ($fact->anniv) {
565
            $text .= ' (' . I18N::translate('%s year anniversary', $fact->anniv) . ')';
566
        }
567
        if ($show_places && $fact->attribute('PLAC')) {
568
            $text .= ' — ' . $fact->attribute('PLAC');
569
        }
570
571
        return $text;
572
    }
573
574
    /**
575
     * Format a list of facts for display
576
     *
577
     * @param Fact[] $list
578
     * @param string $tag1
579
     * @param string $tag2
580
     * @param Tree   $tree
581
     *
582
     * @return string
583
     */
584
    private function calendarListText(array $list, string $tag1, string $tag2, Tree $tree): string
585
    {
586
        $html = '';
587
588
        foreach ($list as $id => $facts) {
589
            $tmp  = GedcomRecord::getInstance($id, $tree);
590
            $html .= $tag1 . '<a href="' . e($tmp->url()) . '">' . $tmp->fullName() . '</a> ';
591
            $html .= '<div class="indent">' . $facts . '</div>' . $tag2;
592
        }
593
594
        return $html;
595
    }
596
}
597