xref: /webtrees/app/Module/TimelineChartModule.php (revision 57ab22314b2599feb432b1a1ed71643cfc2f0452)
1168ff6f3Sric2016<?php
2168ff6f3Sric2016/**
3168ff6f3Sric2016 * webtrees: online genealogy
48fcd0d32SGreg Roach * Copyright (C) 2019 webtrees development team
5168ff6f3Sric2016 * This program is free software: you can redistribute it and/or modify
6168ff6f3Sric2016 * it under the terms of the GNU General Public License as published by
7168ff6f3Sric2016 * the Free Software Foundation, either version 3 of the License, or
8168ff6f3Sric2016 * (at your option) any later version.
9168ff6f3Sric2016 * This program is distributed in the hope that it will be useful,
10168ff6f3Sric2016 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11168ff6f3Sric2016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12168ff6f3Sric2016 * GNU General Public License for more details.
13168ff6f3Sric2016 * You should have received a copy of the GNU General Public License
14168ff6f3Sric2016 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15168ff6f3Sric2016 */
16e7f56f2aSGreg Roachdeclare(strict_types=1);
17e7f56f2aSGreg Roach
18168ff6f3Sric2016namespace Fisharebest\Webtrees\Module;
19168ff6f3Sric2016
209867b2f0SGreg Roachuse Fisharebest\Webtrees\Auth;
21eca4a663SGreg Roachuse Fisharebest\Webtrees\Date\GregorianDate;
22580a4d11SGreg Roachuse Fisharebest\Webtrees\Fact;
23eca4a663SGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
24168ff6f3Sric2016use Fisharebest\Webtrees\I18N;
25168ff6f3Sric2016use Fisharebest\Webtrees\Individual;
26eca4a663SGreg Roachuse Fisharebest\Webtrees\Tree;
27eca4a663SGreg Roachuse Illuminate\Support\Collection;
286ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface;
296ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
30168ff6f3Sric2016
31168ff6f3Sric2016/**
32168ff6f3Sric2016 * Class TimelineChartModule
33168ff6f3Sric2016 */
3437eb8894SGreg Roachclass TimelineChartModule extends AbstractModule implements ModuleChartInterface
35c1010edaSGreg Roach{
3649a243cbSGreg Roach    use ModuleChartTrait;
3749a243cbSGreg Roach
38eca4a663SGreg Roach    // The user can alter the vertical scale
39eca4a663SGreg Roach    protected const SCALE_MIN     = 1;
40eca4a663SGreg Roach    protected const SCALE_MAX     = 200;
41eca4a663SGreg Roach    protected const SCALE_DEFAULT = 10;
42eca4a663SGreg Roach
43eca4a663SGreg Roach    // GEDCOM events that may have DATE data, but should not be displayed
44eca4a663SGreg Roach    protected const NON_FACTS = [
45eca4a663SGreg Roach        'BAPL',
46eca4a663SGreg Roach        'ENDL',
47eca4a663SGreg Roach        'SLGC',
48eca4a663SGreg Roach        'SLGS',
49eca4a663SGreg Roach        '_TODO',
50eca4a663SGreg Roach        'CHAN',
51eca4a663SGreg Roach    ];
52eca4a663SGreg Roach
53eca4a663SGreg Roach    // Box height
54eca4a663SGreg Roach    protected const BHEIGHT = 30;
55eca4a663SGreg Roach
56168ff6f3Sric2016    /**
570cfd6963SGreg Roach     * How should this module be identified in the control panel, etc.?
58168ff6f3Sric2016     *
59168ff6f3Sric2016     * @return string
60168ff6f3Sric2016     */
6149a243cbSGreg Roach    public function title(): string
62c1010edaSGreg Roach    {
63bbb76c12SGreg Roach        /* I18N: Name of a module/chart */
64bbb76c12SGreg Roach        return I18N::translate('Timeline');
65168ff6f3Sric2016    }
66168ff6f3Sric2016
67168ff6f3Sric2016    /**
68168ff6f3Sric2016     * A sentence describing what this module does.
69168ff6f3Sric2016     *
70168ff6f3Sric2016     * @return string
71168ff6f3Sric2016     */
7249a243cbSGreg Roach    public function description(): string
73c1010edaSGreg Roach    {
74bbb76c12SGreg Roach        /* I18N: Description of the “TimelineChart” module */
75bbb76c12SGreg Roach        return I18N::translate('A timeline displaying individual events.');
76168ff6f3Sric2016    }
77168ff6f3Sric2016
78168ff6f3Sric2016    /**
79377a2979SGreg Roach     * CSS class for the URL.
80377a2979SGreg Roach     *
81377a2979SGreg Roach     * @return string
82377a2979SGreg Roach     */
83377a2979SGreg Roach    public function chartMenuClass(): string
84377a2979SGreg Roach    {
85377a2979SGreg Roach        return 'menu-chart-timeline';
86377a2979SGreg Roach    }
87377a2979SGreg Roach
88377a2979SGreg Roach    /**
89e6562982SGreg Roach     * The URL for this chart.
90168ff6f3Sric2016     *
9160bc3e3fSGreg Roach     * @param Individual $individual
92e6562982SGreg Roach     * @param string[]   $parameters
9360bc3e3fSGreg Roach     *
94e6562982SGreg Roach     * @return string
95168ff6f3Sric2016     */
96e6562982SGreg Roach    public function chartUrl(Individual $individual, array $parameters = []): string
97c1010edaSGreg Roach    {
98eca4a663SGreg Roach        return route('module', [
99eca4a663SGreg Roach                'module'  => $this->name(),
100eca4a663SGreg Roach                'action'  => 'Chart',
101c0935879SGreg Roach                'xrefs[]' => $individual->xref(),
102f4afa648SGreg Roach                'ged'     => $individual->tree()->name(),
103e6562982SGreg Roach            ] + $parameters);
104168ff6f3Sric2016    }
105eca4a663SGreg Roach
106eca4a663SGreg Roach    /**
107eca4a663SGreg Roach     * A form to request the chart parameters.
108eca4a663SGreg Roach     *
1096ccdf4f0SGreg Roach     * @param ServerRequestInterface $request
110eca4a663SGreg Roach     *
1116ccdf4f0SGreg Roach     * @return ResponseInterface
112eca4a663SGreg Roach     */
113*57ab2231SGreg Roach    public function getChartAction(ServerRequestInterface $request): ResponseInterface
114eca4a663SGreg Roach    {
115*57ab2231SGreg Roach        $tree = $request->getAttribute('tree');
116*57ab2231SGreg Roach        $user = $request->getAttribute('user');
117*57ab2231SGreg Roach
1189867b2f0SGreg Roach        Auth::checkComponentAccess($this, 'chart', $tree, $user);
1199867b2f0SGreg Roach
1200b93976aSGreg Roach        $ajax  = $request->getQueryParams()['ajax'] ?? '';
1210b93976aSGreg Roach        $scale = (int) ($request->getQueryParams()['scale'] ?? self::SCALE_DEFAULT);
122eca4a663SGreg Roach        $scale = min($scale, self::SCALE_MAX);
123eca4a663SGreg Roach        $scale = max($scale, self::SCALE_MIN);
1240b93976aSGreg Roach        $xrefs = $request->getQueryParams()['xrefs'] ?? [];
125eca4a663SGreg Roach
126eca4a663SGreg Roach        // Find the requested individuals.
127eca4a663SGreg Roach        $individuals = (new Collection($xrefs))
128eca4a663SGreg Roach            ->unique()
1290b5fd0a6SGreg Roach            ->map(static function (string $xref) use ($tree): ?Individual {
130eca4a663SGreg Roach                return Individual::getInstance($xref, $tree);
131eca4a663SGreg Roach            })
132eca4a663SGreg Roach            ->filter()
133eca4a663SGreg Roach            ->filter(GedcomRecord::accessFilter());
134eca4a663SGreg Roach
135eca4a663SGreg Roach        // Generate URLs omitting each xref.
136eca4a663SGreg Roach        $remove_urls = [];
137eca4a663SGreg Roach
138eca4a663SGreg Roach        foreach ($individuals as $exclude) {
139eca4a663SGreg Roach            $xrefs_1 = $individuals
1400b5fd0a6SGreg Roach                ->filter(static function (Individual $individual) use ($exclude): bool {
141eca4a663SGreg Roach                    return $individual->xref() !== $exclude->xref();
142eca4a663SGreg Roach                })
1430b5fd0a6SGreg Roach                ->map(static function (Individual $individual): string {
144eca4a663SGreg Roach                    return $individual->xref();
145eca4a663SGreg Roach                });
146eca4a663SGreg Roach
147eca4a663SGreg Roach            $remove_urls[$exclude->xref()] = route('module', [
148eca4a663SGreg Roach                'module' => $this->name(),
149eca4a663SGreg Roach                'action' => 'Chart',
150eca4a663SGreg Roach                'ged'    => $tree->name(),
151eca4a663SGreg Roach                'scale'  => $scale,
152eca4a663SGreg Roach                'xrefs'  => $xrefs_1->all(),
153eca4a663SGreg Roach            ]);
154eca4a663SGreg Roach        }
155eca4a663SGreg Roach
1560b5fd0a6SGreg Roach        $individuals = array_map(static function (string $xref) use ($tree): ?Individual {
157eca4a663SGreg Roach            return Individual::getInstance($xref, $tree);
158eca4a663SGreg Roach        }, $xrefs);
159eca4a663SGreg Roach
1600b5fd0a6SGreg Roach        $individuals = array_filter($individuals, static function (?Individual $individual): bool {
16125d7fe95SGreg Roach            return $individual instanceof Individual && $individual->canShow();
16225d7fe95SGreg Roach        });
16325d7fe95SGreg Roach
1640b93976aSGreg Roach        if ($ajax === '1') {
165eca4a663SGreg Roach            return $this->chart($tree, $xrefs, $scale);
166eca4a663SGreg Roach        }
167eca4a663SGreg Roach
168eca4a663SGreg Roach        $ajax_url = route('module', [
1699b5537c3SGreg Roach            'ajax'   => true,
170eca4a663SGreg Roach            'module' => $this->name(),
171eca4a663SGreg Roach            'action' => 'Chart',
172eca4a663SGreg Roach            'ged'    => $tree->name(),
173eca4a663SGreg Roach            'scale'  => $scale,
174eca4a663SGreg Roach            'xrefs'  => $xrefs,
175eca4a663SGreg Roach        ]);
176eca4a663SGreg Roach
177eca4a663SGreg Roach        $reset_url = route('module', [
178eca4a663SGreg Roach            'module' => $this->name(),
179eca4a663SGreg Roach            'action' => 'Chart',
180eca4a663SGreg Roach            'ged'    => $tree->name(),
181eca4a663SGreg Roach        ]);
182eca4a663SGreg Roach
183eca4a663SGreg Roach        $zoom_in_url = route('module', [
184eca4a663SGreg Roach            'module' => $this->name(),
185eca4a663SGreg Roach            'action' => 'Chart',
186eca4a663SGreg Roach            'ged'    => $tree->name(),
187eca4a663SGreg Roach            'scale'  => min(self::SCALE_MAX, $scale + (int) ($scale * 0.2 + 1)),
188eca4a663SGreg Roach            'xrefs'  => $xrefs,
189eca4a663SGreg Roach        ]);
190eca4a663SGreg Roach
191eca4a663SGreg Roach        $zoom_out_url = route('module', [
192eca4a663SGreg Roach            'module' => $this->name(),
193eca4a663SGreg Roach            'action' => 'Chart',
194eca4a663SGreg Roach            'ged'    => $tree->name(),
195eca4a663SGreg Roach            'scale'  => max(self::SCALE_MIN, $scale - (int) ($scale * 0.2 + 1)),
196eca4a663SGreg Roach            'xrefs'  => $xrefs,
197eca4a663SGreg Roach        ]);
198eca4a663SGreg Roach
1999b5537c3SGreg Roach        return $this->viewResponse('modules/timeline-chart/page', [
200eca4a663SGreg Roach            'ajax_url'     => $ajax_url,
201eca4a663SGreg Roach            'individuals'  => $individuals,
202eca4a663SGreg Roach            'module_name'  => $this->name(),
203eca4a663SGreg Roach            'remove_urls'  => $remove_urls,
204eca4a663SGreg Roach            'reset_url'    => $reset_url,
205eca4a663SGreg Roach            'title'        => $this->title(),
206eca4a663SGreg Roach            'scale'        => $scale,
207eca4a663SGreg Roach            'zoom_in_url'  => $zoom_in_url,
208eca4a663SGreg Roach            'zoom_out_url' => $zoom_out_url,
209eca4a663SGreg Roach        ]);
210eca4a663SGreg Roach    }
211eca4a663SGreg Roach
212eca4a663SGreg Roach    /**
213eca4a663SGreg Roach     * @param Tree  $tree
214eca4a663SGreg Roach     * @param array $xrefs
215eca4a663SGreg Roach     * @param int   $scale
216eca4a663SGreg Roach     *
2176ccdf4f0SGreg Roach     * @return ResponseInterface
218eca4a663SGreg Roach     */
2196ccdf4f0SGreg Roach    protected function chart(Tree $tree, array $xrefs, int $scale): ResponseInterface
220eca4a663SGreg Roach    {
221eca4a663SGreg Roach        $xrefs = array_unique($xrefs);
222eca4a663SGreg Roach
223eca4a663SGreg Roach        /** @var Individual[] $individuals */
2240b5fd0a6SGreg Roach        $individuals = array_map(static function (string $xref) use ($tree): ?Individual {
225eca4a663SGreg Roach            return Individual::getInstance($xref, $tree);
226eca4a663SGreg Roach        }, $xrefs);
227eca4a663SGreg Roach
2280b5fd0a6SGreg Roach        $individuals = array_filter($individuals, static function (?Individual $individual): bool {
22925d7fe95SGreg Roach            return $individual instanceof Individual && $individual->canShow();
230eca4a663SGreg Roach        });
231eca4a663SGreg Roach
232eca4a663SGreg Roach        $baseyear    = (int) date('Y');
233eca4a663SGreg Roach        $topyear     = 0;
2348af3e5c1SGreg Roach        $indifacts   = new Collection();
235eca4a663SGreg Roach        $birthyears  = [];
236eca4a663SGreg Roach        $birthmonths = [];
237eca4a663SGreg Roach        $birthdays   = [];
238eca4a663SGreg Roach
239eca4a663SGreg Roach        foreach ($individuals as $individual) {
240eca4a663SGreg Roach            $bdate = $individual->getBirthDate();
241eca4a663SGreg Roach            if ($bdate->isOK()) {
242eca4a663SGreg Roach                $date = new GregorianDate($bdate->minimumJulianDay());
243eca4a663SGreg Roach
244eca4a663SGreg Roach                $birthyears [$individual->xref()] = $date->year;
245eca4a663SGreg Roach                $birthmonths[$individual->xref()] = max(1, $date->month);
246eca4a663SGreg Roach                $birthdays  [$individual->xref()] = max(1, $date->day);
247eca4a663SGreg Roach            }
248eca4a663SGreg Roach            // find all the fact information
249eca4a663SGreg Roach            $facts = $individual->facts();
25039ca88baSGreg Roach            foreach ($individual->spouseFamilies() as $family) {
251eca4a663SGreg Roach                foreach ($family->facts() as $fact) {
25239ca88baSGreg Roach                    $facts->push($fact);
253eca4a663SGreg Roach                }
254eca4a663SGreg Roach            }
255eca4a663SGreg Roach            foreach ($facts as $event) {
256eca4a663SGreg Roach                // get the fact type
257eca4a663SGreg Roach                $fact = $event->getTag();
25822d65e5aSGreg Roach                if (!in_array($fact, self::NON_FACTS, true)) {
259eca4a663SGreg Roach                    // check for a date
260eca4a663SGreg Roach                    $date = $event->date();
261eca4a663SGreg Roach                    if ($date->isOK()) {
262eca4a663SGreg Roach                        $date     = new GregorianDate($date->minimumJulianDay());
263eca4a663SGreg Roach                        $baseyear = min($baseyear, $date->year);
264eca4a663SGreg Roach                        $topyear  = max($topyear, $date->year);
265eca4a663SGreg Roach
266eca4a663SGreg Roach                        if (!$individual->isDead()) {
267eca4a663SGreg Roach                            $topyear = max($topyear, (int) date('Y'));
268eca4a663SGreg Roach                        }
269eca4a663SGreg Roach
2708af3e5c1SGreg Roach                        $indifacts->push($event);
271eca4a663SGreg Roach                    }
272eca4a663SGreg Roach                }
273eca4a663SGreg Roach            }
274eca4a663SGreg Roach        }
275eca4a663SGreg Roach
2768af3e5c1SGreg Roach        // do not add the same fact twice (prevents marriages from being added multiple times)
2778af3e5c1SGreg Roach        $indifacts = $indifacts->unique();
2788af3e5c1SGreg Roach
279eca4a663SGreg Roach        if ($scale === 0) {
2808af3e5c1SGreg Roach            $scale = (int) (($topyear - $baseyear) / 20 * $indifacts->count() / 4);
281eca4a663SGreg Roach            if ($scale < 6) {
282eca4a663SGreg Roach                $scale = 6;
283eca4a663SGreg Roach            }
284eca4a663SGreg Roach        }
285eca4a663SGreg Roach        if ($scale < 2) {
286eca4a663SGreg Roach            $scale = 2;
287eca4a663SGreg Roach        }
288eca4a663SGreg Roach        $baseyear -= 5;
289eca4a663SGreg Roach        $topyear  += 5;
290eca4a663SGreg Roach
291580a4d11SGreg Roach        $indifacts = Fact::sortFacts($indifacts);
292eca4a663SGreg Roach
293eca4a663SGreg Roach        $html = view('modules/timeline-chart/chart', [
294eca4a663SGreg Roach            'baseyear'    => $baseyear,
295eca4a663SGreg Roach            'bheight'     => self::BHEIGHT,
296eca4a663SGreg Roach            'birthdays'   => $birthdays,
297eca4a663SGreg Roach            'birthmonths' => $birthmonths,
298eca4a663SGreg Roach            'birthyears'  => $birthyears,
299eca4a663SGreg Roach            'indifacts'   => $indifacts,
300eca4a663SGreg Roach            'individuals' => $individuals,
301eca4a663SGreg Roach            'placements'  => [],
302eca4a663SGreg Roach            'scale'       => $scale,
303eca4a663SGreg Roach            'topyear'     => $topyear,
304eca4a663SGreg Roach        ]);
305eca4a663SGreg Roach
3066ccdf4f0SGreg Roach        return response($html);
307eca4a663SGreg Roach    }
308168ff6f3Sric2016}
309