xref: /webtrees/app/Module/TimelineChartModule.php (revision 22d65e5ad7724941da33d875027b68b86648a321)
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;
21e5a6b4d4SGreg Roachuse Fisharebest\Webtrees\Contracts\UserInterface;
22eca4a663SGreg Roachuse Fisharebest\Webtrees\Date\GregorianDate;
23580a4d11SGreg Roachuse Fisharebest\Webtrees\Fact;
24eca4a663SGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
25168ff6f3Sric2016use Fisharebest\Webtrees\I18N;
26168ff6f3Sric2016use Fisharebest\Webtrees\Individual;
27eca4a663SGreg Roachuse Fisharebest\Webtrees\Tree;
28eca4a663SGreg Roachuse Illuminate\Support\Collection;
296ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface;
306ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
31168ff6f3Sric2016
32168ff6f3Sric2016/**
33168ff6f3Sric2016 * Class TimelineChartModule
34168ff6f3Sric2016 */
3537eb8894SGreg Roachclass TimelineChartModule extends AbstractModule implements ModuleChartInterface
36c1010edaSGreg Roach{
3749a243cbSGreg Roach    use ModuleChartTrait;
3849a243cbSGreg Roach
39eca4a663SGreg Roach    // The user can alter the vertical scale
40eca4a663SGreg Roach    protected const SCALE_MIN     = 1;
41eca4a663SGreg Roach    protected const SCALE_MAX     = 200;
42eca4a663SGreg Roach    protected const SCALE_DEFAULT = 10;
43eca4a663SGreg Roach
44eca4a663SGreg Roach    // GEDCOM events that may have DATE data, but should not be displayed
45eca4a663SGreg Roach    protected const NON_FACTS = [
46eca4a663SGreg Roach        'BAPL',
47eca4a663SGreg Roach        'ENDL',
48eca4a663SGreg Roach        'SLGC',
49eca4a663SGreg Roach        'SLGS',
50eca4a663SGreg Roach        '_TODO',
51eca4a663SGreg Roach        'CHAN',
52eca4a663SGreg Roach    ];
53eca4a663SGreg Roach
54eca4a663SGreg Roach    // Box height
55eca4a663SGreg Roach    protected const BHEIGHT = 30;
56eca4a663SGreg Roach
57168ff6f3Sric2016    /**
580cfd6963SGreg Roach     * How should this module be identified in the control panel, etc.?
59168ff6f3Sric2016     *
60168ff6f3Sric2016     * @return string
61168ff6f3Sric2016     */
6249a243cbSGreg Roach    public function title(): string
63c1010edaSGreg Roach    {
64bbb76c12SGreg Roach        /* I18N: Name of a module/chart */
65bbb76c12SGreg Roach        return I18N::translate('Timeline');
66168ff6f3Sric2016    }
67168ff6f3Sric2016
68168ff6f3Sric2016    /**
69168ff6f3Sric2016     * A sentence describing what this module does.
70168ff6f3Sric2016     *
71168ff6f3Sric2016     * @return string
72168ff6f3Sric2016     */
7349a243cbSGreg Roach    public function description(): string
74c1010edaSGreg Roach    {
75bbb76c12SGreg Roach        /* I18N: Description of the “TimelineChart” module */
76bbb76c12SGreg Roach        return I18N::translate('A timeline displaying individual events.');
77168ff6f3Sric2016    }
78168ff6f3Sric2016
79168ff6f3Sric2016    /**
80377a2979SGreg Roach     * CSS class for the URL.
81377a2979SGreg Roach     *
82377a2979SGreg Roach     * @return string
83377a2979SGreg Roach     */
84377a2979SGreg Roach    public function chartMenuClass(): string
85377a2979SGreg Roach    {
86377a2979SGreg Roach        return 'menu-chart-timeline';
87377a2979SGreg Roach    }
88377a2979SGreg Roach
89377a2979SGreg Roach    /**
90e6562982SGreg Roach     * The URL for this chart.
91168ff6f3Sric2016     *
9260bc3e3fSGreg Roach     * @param Individual $individual
93e6562982SGreg Roach     * @param string[]   $parameters
9460bc3e3fSGreg Roach     *
95e6562982SGreg Roach     * @return string
96168ff6f3Sric2016     */
97e6562982SGreg Roach    public function chartUrl(Individual $individual, array $parameters = []): string
98c1010edaSGreg Roach    {
99eca4a663SGreg Roach        return route('module', [
100eca4a663SGreg Roach                'module'  => $this->name(),
101eca4a663SGreg Roach                'action'  => 'Chart',
102c0935879SGreg Roach                'xrefs[]' => $individual->xref(),
103f4afa648SGreg Roach                'ged'     => $individual->tree()->name(),
104e6562982SGreg Roach            ] + $parameters);
105168ff6f3Sric2016    }
106eca4a663SGreg Roach
107eca4a663SGreg Roach    /**
108eca4a663SGreg Roach     * A form to request the chart parameters.
109eca4a663SGreg Roach     *
1106ccdf4f0SGreg Roach     * @param ServerRequestInterface $request
111eca4a663SGreg Roach     * @param Tree                   $tree
112e5a6b4d4SGreg Roach     * @param UserInterface          $user
113eca4a663SGreg Roach     *
1146ccdf4f0SGreg Roach     * @return ResponseInterface
115eca4a663SGreg Roach     */
1166ccdf4f0SGreg Roach    public function getChartAction(ServerRequestInterface $request, Tree $tree, UserInterface $user): ResponseInterface
117eca4a663SGreg Roach    {
1189867b2f0SGreg Roach        Auth::checkComponentAccess($this, 'chart', $tree, $user);
1199867b2f0SGreg Roach
1209b5537c3SGreg Roach        $ajax  = (bool) $request->get('ajax');
121eca4a663SGreg Roach        $scale = (int) $request->get('scale', self::SCALE_DEFAULT);
122eca4a663SGreg Roach        $scale = min($scale, self::SCALE_MAX);
123eca4a663SGreg Roach        $scale = max($scale, self::SCALE_MIN);
124eca4a663SGreg Roach
125eca4a663SGreg Roach        $xrefs = $request->get('xrefs', []);
126eca4a663SGreg Roach
127eca4a663SGreg Roach        // Find the requested individuals.
128eca4a663SGreg Roach        $individuals = (new Collection($xrefs))
129eca4a663SGreg Roach            ->unique()
1300b5fd0a6SGreg Roach            ->map(static function (string $xref) use ($tree): ?Individual {
131eca4a663SGreg Roach                return Individual::getInstance($xref, $tree);
132eca4a663SGreg Roach            })
133eca4a663SGreg Roach            ->filter()
134eca4a663SGreg Roach            ->filter(GedcomRecord::accessFilter());
135eca4a663SGreg Roach
136eca4a663SGreg Roach        // Generate URLs omitting each xref.
137eca4a663SGreg Roach        $remove_urls = [];
138eca4a663SGreg Roach
139eca4a663SGreg Roach        foreach ($individuals as $exclude) {
140eca4a663SGreg Roach            $xrefs_1 = $individuals
1410b5fd0a6SGreg Roach                ->filter(static function (Individual $individual) use ($exclude): bool {
142eca4a663SGreg Roach                    return $individual->xref() !== $exclude->xref();
143eca4a663SGreg Roach                })
1440b5fd0a6SGreg Roach                ->map(static function (Individual $individual): string {
145eca4a663SGreg Roach                    return $individual->xref();
146eca4a663SGreg Roach                });
147eca4a663SGreg Roach
148eca4a663SGreg Roach            $remove_urls[$exclude->xref()] = route('module', [
149eca4a663SGreg Roach                'module' => $this->name(),
150eca4a663SGreg Roach                'action' => 'Chart',
151eca4a663SGreg Roach                'ged'    => $tree->name(),
152eca4a663SGreg Roach                'scale'  => $scale,
153eca4a663SGreg Roach                'xrefs'  => $xrefs_1->all(),
154eca4a663SGreg Roach            ]);
155eca4a663SGreg Roach        }
156eca4a663SGreg Roach
1570b5fd0a6SGreg Roach        $individuals = array_map(static function (string $xref) use ($tree): ?Individual {
158eca4a663SGreg Roach            return Individual::getInstance($xref, $tree);
159eca4a663SGreg Roach        }, $xrefs);
160eca4a663SGreg Roach
1610b5fd0a6SGreg Roach        $individuals = array_filter($individuals, static function (?Individual $individual): bool {
16225d7fe95SGreg Roach            return $individual instanceof Individual && $individual->canShow();
16325d7fe95SGreg Roach        });
16425d7fe95SGreg Roach
1659b5537c3SGreg Roach        if ($ajax) {
166eca4a663SGreg Roach            return $this->chart($tree, $xrefs, $scale);
167eca4a663SGreg Roach        }
168eca4a663SGreg Roach
169eca4a663SGreg Roach        $ajax_url = route('module', [
1709b5537c3SGreg Roach            'ajax'   => true,
171eca4a663SGreg Roach            'module' => $this->name(),
172eca4a663SGreg Roach            'action' => 'Chart',
173eca4a663SGreg Roach            'ged'    => $tree->name(),
174eca4a663SGreg Roach            'scale'  => $scale,
175eca4a663SGreg Roach            'xrefs'  => $xrefs,
176eca4a663SGreg Roach        ]);
177eca4a663SGreg Roach
178eca4a663SGreg Roach        $reset_url = route('module', [
179eca4a663SGreg Roach            'module' => $this->name(),
180eca4a663SGreg Roach            'action' => 'Chart',
181eca4a663SGreg Roach            'ged'    => $tree->name(),
182eca4a663SGreg Roach        ]);
183eca4a663SGreg Roach
184eca4a663SGreg Roach        $zoom_in_url = route('module', [
185eca4a663SGreg Roach            'module' => $this->name(),
186eca4a663SGreg Roach            'action' => 'Chart',
187eca4a663SGreg Roach            'ged'    => $tree->name(),
188eca4a663SGreg Roach            'scale'  => min(self::SCALE_MAX, $scale + (int) ($scale * 0.2 + 1)),
189eca4a663SGreg Roach            'xrefs'  => $xrefs,
190eca4a663SGreg Roach        ]);
191eca4a663SGreg Roach
192eca4a663SGreg Roach        $zoom_out_url = route('module', [
193eca4a663SGreg Roach            'module' => $this->name(),
194eca4a663SGreg Roach            'action' => 'Chart',
195eca4a663SGreg Roach            'ged'    => $tree->name(),
196eca4a663SGreg Roach            'scale'  => max(self::SCALE_MIN, $scale - (int) ($scale * 0.2 + 1)),
197eca4a663SGreg Roach            'xrefs'  => $xrefs,
198eca4a663SGreg Roach        ]);
199eca4a663SGreg Roach
2009b5537c3SGreg Roach        return $this->viewResponse('modules/timeline-chart/page', [
201eca4a663SGreg Roach            'ajax_url'     => $ajax_url,
202eca4a663SGreg Roach            'individuals'  => $individuals,
203eca4a663SGreg Roach            'module_name'  => $this->name(),
204eca4a663SGreg Roach            'remove_urls'  => $remove_urls,
205eca4a663SGreg Roach            'reset_url'    => $reset_url,
206eca4a663SGreg Roach            'title'        => $this->title(),
207eca4a663SGreg Roach            'scale'        => $scale,
208eca4a663SGreg Roach            'zoom_in_url'  => $zoom_in_url,
209eca4a663SGreg Roach            'zoom_out_url' => $zoom_out_url,
210eca4a663SGreg Roach        ]);
211eca4a663SGreg Roach    }
212eca4a663SGreg Roach
213eca4a663SGreg Roach    /**
214eca4a663SGreg Roach     * @param Tree  $tree
215eca4a663SGreg Roach     * @param array $xrefs
216eca4a663SGreg Roach     * @param int   $scale
217eca4a663SGreg Roach     *
2186ccdf4f0SGreg Roach     * @return ResponseInterface
219eca4a663SGreg Roach     */
2206ccdf4f0SGreg Roach    protected function chart(Tree $tree, array $xrefs, int $scale): ResponseInterface
221eca4a663SGreg Roach    {
222eca4a663SGreg Roach        $xrefs = array_unique($xrefs);
223eca4a663SGreg Roach
224eca4a663SGreg Roach        /** @var Individual[] $individuals */
2250b5fd0a6SGreg Roach        $individuals = array_map(static function (string $xref) use ($tree): ?Individual {
226eca4a663SGreg Roach            return Individual::getInstance($xref, $tree);
227eca4a663SGreg Roach        }, $xrefs);
228eca4a663SGreg Roach
2290b5fd0a6SGreg Roach        $individuals = array_filter($individuals, static function (?Individual $individual): bool {
23025d7fe95SGreg Roach            return $individual instanceof Individual && $individual->canShow();
231eca4a663SGreg Roach        });
232eca4a663SGreg Roach
233eca4a663SGreg Roach        $baseyear    = (int) date('Y');
234eca4a663SGreg Roach        $topyear     = 0;
2358af3e5c1SGreg Roach        $indifacts   = new Collection();
236eca4a663SGreg Roach        $birthyears  = [];
237eca4a663SGreg Roach        $birthmonths = [];
238eca4a663SGreg Roach        $birthdays   = [];
239eca4a663SGreg Roach
240eca4a663SGreg Roach        foreach ($individuals as $individual) {
241eca4a663SGreg Roach            $bdate = $individual->getBirthDate();
242eca4a663SGreg Roach            if ($bdate->isOK()) {
243eca4a663SGreg Roach                $date = new GregorianDate($bdate->minimumJulianDay());
244eca4a663SGreg Roach
245eca4a663SGreg Roach                $birthyears [$individual->xref()] = $date->year;
246eca4a663SGreg Roach                $birthmonths[$individual->xref()] = max(1, $date->month);
247eca4a663SGreg Roach                $birthdays  [$individual->xref()] = max(1, $date->day);
248eca4a663SGreg Roach            }
249eca4a663SGreg Roach            // find all the fact information
250eca4a663SGreg Roach            $facts = $individual->facts();
25139ca88baSGreg Roach            foreach ($individual->spouseFamilies() as $family) {
252eca4a663SGreg Roach                foreach ($family->facts() as $fact) {
25339ca88baSGreg Roach                    $facts->push($fact);
254eca4a663SGreg Roach                }
255eca4a663SGreg Roach            }
256eca4a663SGreg Roach            foreach ($facts as $event) {
257eca4a663SGreg Roach                // get the fact type
258eca4a663SGreg Roach                $fact = $event->getTag();
259*22d65e5aSGreg Roach                if (!in_array($fact, self::NON_FACTS, true)) {
260eca4a663SGreg Roach                    // check for a date
261eca4a663SGreg Roach                    $date = $event->date();
262eca4a663SGreg Roach                    if ($date->isOK()) {
263eca4a663SGreg Roach                        $date     = new GregorianDate($date->minimumJulianDay());
264eca4a663SGreg Roach                        $baseyear = min($baseyear, $date->year);
265eca4a663SGreg Roach                        $topyear  = max($topyear, $date->year);
266eca4a663SGreg Roach
267eca4a663SGreg Roach                        if (!$individual->isDead()) {
268eca4a663SGreg Roach                            $topyear = max($topyear, (int) date('Y'));
269eca4a663SGreg Roach                        }
270eca4a663SGreg Roach
2718af3e5c1SGreg Roach                        $indifacts->push($event);
272eca4a663SGreg Roach                    }
273eca4a663SGreg Roach                }
274eca4a663SGreg Roach            }
275eca4a663SGreg Roach        }
276eca4a663SGreg Roach
2778af3e5c1SGreg Roach        // do not add the same fact twice (prevents marriages from being added multiple times)
2788af3e5c1SGreg Roach        $indifacts = $indifacts->unique();
2798af3e5c1SGreg Roach
280eca4a663SGreg Roach        if ($scale === 0) {
2818af3e5c1SGreg Roach            $scale = (int) (($topyear - $baseyear) / 20 * $indifacts->count() / 4);
282eca4a663SGreg Roach            if ($scale < 6) {
283eca4a663SGreg Roach                $scale = 6;
284eca4a663SGreg Roach            }
285eca4a663SGreg Roach        }
286eca4a663SGreg Roach        if ($scale < 2) {
287eca4a663SGreg Roach            $scale = 2;
288eca4a663SGreg Roach        }
289eca4a663SGreg Roach        $baseyear -= 5;
290eca4a663SGreg Roach        $topyear  += 5;
291eca4a663SGreg Roach
292580a4d11SGreg Roach        $indifacts = Fact::sortFacts($indifacts);
293eca4a663SGreg Roach
294eca4a663SGreg Roach        $html = view('modules/timeline-chart/chart', [
295eca4a663SGreg Roach            'baseyear'    => $baseyear,
296eca4a663SGreg Roach            'bheight'     => self::BHEIGHT,
297eca4a663SGreg Roach            'birthdays'   => $birthdays,
298eca4a663SGreg Roach            'birthmonths' => $birthmonths,
299eca4a663SGreg Roach            'birthyears'  => $birthyears,
300eca4a663SGreg Roach            'indifacts'   => $indifacts,
301eca4a663SGreg Roach            'individuals' => $individuals,
302eca4a663SGreg Roach            'placements'  => [],
303eca4a663SGreg Roach            'scale'       => $scale,
304eca4a663SGreg Roach            'topyear'     => $topyear,
305eca4a663SGreg Roach        ]);
306eca4a663SGreg Roach
3076ccdf4f0SGreg Roach        return response($html);
308eca4a663SGreg Roach    }
309168ff6f3Sric2016}
310