xref: /webtrees/app/Module/TimelineChartModule.php (revision e5a6b4d4f6f6e7ff2fba7ae2cf27546ae68a79cc)
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;
21*e5a6b4d4SGreg Roachuse Fisharebest\Webtrees\Contracts\UserInterface;
22eca4a663SGreg Roachuse Fisharebest\Webtrees\Date\GregorianDate;
23eca4a663SGreg Roachuse Fisharebest\Webtrees\Functions\Functions;
24eca4a663SGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
25168ff6f3Sric2016use Fisharebest\Webtrees\I18N;
26168ff6f3Sric2016use Fisharebest\Webtrees\Individual;
27eca4a663SGreg Roachuse Fisharebest\Webtrees\Tree;
28eca4a663SGreg Roachuse Illuminate\Support\Collection;
29eca4a663SGreg Roachuse Symfony\Component\HttpFoundation\Request;
30eca4a663SGreg Roachuse Symfony\Component\HttpFoundation\Response;
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    /**
58168ff6f3Sric2016     * How should this module be labelled on tabs, menus, 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     *
110eca4a663SGreg Roach     * @param Request $request
111eca4a663SGreg Roach     * @param Tree    $tree
112*e5a6b4d4SGreg Roach     * @param UserInterface    $user
113eca4a663SGreg Roach     *
114eca4a663SGreg Roach     * @return Response
115eca4a663SGreg Roach     */
116*e5a6b4d4SGreg Roach    public function getChartAction(Request $request, Tree $tree, UserInterface $user): Response
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()
130eca4a663SGreg Roach            ->map(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
141eca4a663SGreg Roach                ->filter(function (Individual $individual) use ($exclude): bool {
142eca4a663SGreg Roach                    return $individual->xref() !== $exclude->xref();
143eca4a663SGreg Roach                })
144eca4a663SGreg Roach                ->map(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
157eca4a663SGreg Roach        $individuals = array_map(function (string $xref) use ($tree) {
158eca4a663SGreg Roach            return Individual::getInstance($xref, $tree);
159eca4a663SGreg Roach        }, $xrefs);
160eca4a663SGreg Roach
1619b5537c3SGreg Roach        if ($ajax) {
162eca4a663SGreg Roach            return $this->chart($tree, $xrefs, $scale);
163eca4a663SGreg Roach        }
164eca4a663SGreg Roach
165eca4a663SGreg Roach        $ajax_url = route('module', [
1669b5537c3SGreg Roach            'ajax'   => true,
167eca4a663SGreg Roach            'module' => $this->name(),
168eca4a663SGreg Roach            'action' => 'Chart',
169eca4a663SGreg Roach            'ged'    => $tree->name(),
170eca4a663SGreg Roach            'scale'  => $scale,
171eca4a663SGreg Roach            'xrefs'  => $xrefs,
172eca4a663SGreg Roach        ]);
173eca4a663SGreg Roach
174eca4a663SGreg Roach        $reset_url = route('module', [
175eca4a663SGreg Roach            'module' => $this->name(),
176eca4a663SGreg Roach            'action' => 'Chart',
177eca4a663SGreg Roach            'ged'    => $tree->name(),
178eca4a663SGreg Roach        ]);
179eca4a663SGreg Roach
180eca4a663SGreg Roach        $zoom_in_url = route('module', [
181eca4a663SGreg Roach            'module' => $this->name(),
182eca4a663SGreg Roach            'action' => 'Chart',
183eca4a663SGreg Roach            'ged'    => $tree->name(),
184eca4a663SGreg Roach            'scale'  => min(self::SCALE_MAX, $scale + (int) ($scale * 0.2 + 1)),
185eca4a663SGreg Roach            'xrefs'  => $xrefs,
186eca4a663SGreg Roach        ]);
187eca4a663SGreg Roach
188eca4a663SGreg Roach        $zoom_out_url = route('module', [
189eca4a663SGreg Roach            'module' => $this->name(),
190eca4a663SGreg Roach            'action' => 'Chart',
191eca4a663SGreg Roach            'ged'    => $tree->name(),
192eca4a663SGreg Roach            'scale'  => max(self::SCALE_MIN, $scale - (int) ($scale * 0.2 + 1)),
193eca4a663SGreg Roach            'xrefs'  => $xrefs,
194eca4a663SGreg Roach        ]);
195eca4a663SGreg Roach
1969b5537c3SGreg Roach        return $this->viewResponse('modules/timeline-chart/page', [
197eca4a663SGreg Roach            'ajax_url'     => $ajax_url,
198eca4a663SGreg Roach            'individuals'  => $individuals,
199eca4a663SGreg Roach            'module_name'  => $this->name(),
200eca4a663SGreg Roach            'remove_urls'  => $remove_urls,
201eca4a663SGreg Roach            'reset_url'    => $reset_url,
202eca4a663SGreg Roach            'title'        => $this->title(),
203eca4a663SGreg Roach            'scale'        => $scale,
204eca4a663SGreg Roach            'zoom_in_url'  => $zoom_in_url,
205eca4a663SGreg Roach            'zoom_out_url' => $zoom_out_url,
206eca4a663SGreg Roach        ]);
207eca4a663SGreg Roach    }
208eca4a663SGreg Roach
209eca4a663SGreg Roach    /**
210eca4a663SGreg Roach     * @param Tree  $tree
211eca4a663SGreg Roach     * @param array $xrefs
212eca4a663SGreg Roach     * @param int   $scale
213eca4a663SGreg Roach     *
214eca4a663SGreg Roach     * @return Response
215eca4a663SGreg Roach     */
216eca4a663SGreg Roach    protected function chart(Tree $tree, array $xrefs, int $scale): Response
217eca4a663SGreg Roach    {
218eca4a663SGreg Roach        $xrefs = array_unique($xrefs);
219eca4a663SGreg Roach
220eca4a663SGreg Roach        /** @var Individual[] $individuals */
221eca4a663SGreg Roach        $individuals = array_map(function (string $xref) use ($tree) {
222eca4a663SGreg Roach            return Individual::getInstance($xref, $tree);
223eca4a663SGreg Roach        }, $xrefs);
224eca4a663SGreg Roach
225eca4a663SGreg Roach        $individuals = array_filter($individuals, function (Individual $individual = null): bool {
226eca4a663SGreg Roach            return $individual !== null && $individual->canShow();
227eca4a663SGreg Roach        });
228eca4a663SGreg Roach
229eca4a663SGreg Roach        $baseyear    = (int) date('Y');
230eca4a663SGreg Roach        $topyear     = 0;
231eca4a663SGreg Roach        $indifacts   = [];
232eca4a663SGreg Roach        $birthyears  = [];
233eca4a663SGreg Roach        $birthmonths = [];
234eca4a663SGreg Roach        $birthdays   = [];
235eca4a663SGreg Roach
236eca4a663SGreg Roach        foreach ($individuals as $individual) {
237eca4a663SGreg Roach            $bdate = $individual->getBirthDate();
238eca4a663SGreg Roach            if ($bdate->isOK()) {
239eca4a663SGreg Roach                $date = new GregorianDate($bdate->minimumJulianDay());
240eca4a663SGreg Roach
241eca4a663SGreg Roach                $birthyears [$individual->xref()] = $date->year;
242eca4a663SGreg Roach                $birthmonths[$individual->xref()] = max(1, $date->month);
243eca4a663SGreg Roach                $birthdays  [$individual->xref()] = max(1, $date->day);
244eca4a663SGreg Roach            }
245eca4a663SGreg Roach            // find all the fact information
246eca4a663SGreg Roach            $facts = $individual->facts();
247eca4a663SGreg Roach            foreach ($individual->getSpouseFamilies() as $family) {
248eca4a663SGreg Roach                foreach ($family->facts() as $fact) {
249eca4a663SGreg Roach                    $facts[] = $fact;
250eca4a663SGreg Roach                }
251eca4a663SGreg Roach            }
252eca4a663SGreg Roach            foreach ($facts as $event) {
253eca4a663SGreg Roach                // get the fact type
254eca4a663SGreg Roach                $fact = $event->getTag();
255eca4a663SGreg Roach                if (!in_array($fact, self::NON_FACTS)) {
256eca4a663SGreg Roach                    // check for a date
257eca4a663SGreg Roach                    $date = $event->date();
258eca4a663SGreg Roach                    if ($date->isOK()) {
259eca4a663SGreg Roach                        $date     = new GregorianDate($date->minimumJulianDay());
260eca4a663SGreg Roach                        $baseyear = min($baseyear, $date->year);
261eca4a663SGreg Roach                        $topyear  = max($topyear, $date->year);
262eca4a663SGreg Roach
263eca4a663SGreg Roach                        if (!$individual->isDead()) {
264eca4a663SGreg Roach                            $topyear = max($topyear, (int) date('Y'));
265eca4a663SGreg Roach                        }
266eca4a663SGreg Roach
267eca4a663SGreg Roach                        // do not add the same fact twice (prevents marriages from being added multiple times)
268eca4a663SGreg Roach                        if (!in_array($event, $indifacts, true)) {
269eca4a663SGreg Roach                            $indifacts[] = $event;
270eca4a663SGreg Roach                        }
271eca4a663SGreg Roach                    }
272eca4a663SGreg Roach                }
273eca4a663SGreg Roach            }
274eca4a663SGreg Roach        }
275eca4a663SGreg Roach
276eca4a663SGreg Roach        if ($scale === 0) {
277eca4a663SGreg Roach            $scale = (int) (($topyear - $baseyear) / 20 * count($indifacts) / 4);
278eca4a663SGreg Roach            if ($scale < 6) {
279eca4a663SGreg Roach                $scale = 6;
280eca4a663SGreg Roach            }
281eca4a663SGreg Roach        }
282eca4a663SGreg Roach        if ($scale < 2) {
283eca4a663SGreg Roach            $scale = 2;
284eca4a663SGreg Roach        }
285eca4a663SGreg Roach        $baseyear -= 5;
286eca4a663SGreg Roach        $topyear  += 5;
287eca4a663SGreg Roach
288eca4a663SGreg Roach        Functions::sortFacts($indifacts);
289eca4a663SGreg Roach
290eca4a663SGreg Roach        $html = view('modules/timeline-chart/chart', [
291eca4a663SGreg Roach            'baseyear'    => $baseyear,
292eca4a663SGreg Roach            'bheight'     => self::BHEIGHT,
293eca4a663SGreg Roach            'birthdays'   => $birthdays,
294eca4a663SGreg Roach            'birthmonths' => $birthmonths,
295eca4a663SGreg Roach            'birthyears'  => $birthyears,
296eca4a663SGreg Roach            'indifacts'   => $indifacts,
297eca4a663SGreg Roach            'individuals' => $individuals,
298eca4a663SGreg Roach            'placements'  => [],
299eca4a663SGreg Roach            'scale'       => $scale,
300eca4a663SGreg Roach            'topyear'     => $topyear,
301eca4a663SGreg Roach        ]);
302eca4a663SGreg Roach
303eca4a663SGreg Roach        return new Response($html);
304eca4a663SGreg Roach    }
305168ff6f3Sric2016}
306