xref: /webtrees/app/Module/TimelineChartModule.php (revision 9b5537c37ce2b6d3b5c1a28c56f73d7ff9b9355c)
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
20eca4a663SGreg Roachuse Fisharebest\Webtrees\Date\GregorianDate;
21eca4a663SGreg Roachuse Fisharebest\Webtrees\Functions\Functions;
22eca4a663SGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
23168ff6f3Sric2016use Fisharebest\Webtrees\I18N;
24168ff6f3Sric2016use Fisharebest\Webtrees\Individual;
25eca4a663SGreg Roachuse Fisharebest\Webtrees\Tree;
26eca4a663SGreg Roachuse Illuminate\Support\Collection;
27eca4a663SGreg Roachuse Symfony\Component\HttpFoundation\Request;
28eca4a663SGreg Roachuse Symfony\Component\HttpFoundation\Response;
29168ff6f3Sric2016
30168ff6f3Sric2016/**
31168ff6f3Sric2016 * Class TimelineChartModule
32168ff6f3Sric2016 */
3349a243cbSGreg Roachclass TimelineChartModule extends AbstractModule implements ModuleInterface, ModuleChartInterface
34c1010edaSGreg Roach{
3549a243cbSGreg Roach    use ModuleChartTrait;
3649a243cbSGreg Roach
37eca4a663SGreg Roach    // The user can alter the vertical scale
38eca4a663SGreg Roach    protected const SCALE_MIN     = 1;
39eca4a663SGreg Roach    protected const SCALE_MAX     = 200;
40eca4a663SGreg Roach    protected const SCALE_DEFAULT = 10;
41eca4a663SGreg Roach
42eca4a663SGreg Roach    // GEDCOM events that may have DATE data, but should not be displayed
43eca4a663SGreg Roach    protected const NON_FACTS = [
44eca4a663SGreg Roach        'BAPL',
45eca4a663SGreg Roach        'ENDL',
46eca4a663SGreg Roach        'SLGC',
47eca4a663SGreg Roach        'SLGS',
48eca4a663SGreg Roach        '_TODO',
49eca4a663SGreg Roach        'CHAN',
50eca4a663SGreg Roach    ];
51eca4a663SGreg Roach
52eca4a663SGreg Roach    // Box height
53eca4a663SGreg Roach    protected const BHEIGHT = 30;
54eca4a663SGreg Roach
55168ff6f3Sric2016    /**
56168ff6f3Sric2016     * How should this module be labelled on tabs, menus, etc.?
57168ff6f3Sric2016     *
58168ff6f3Sric2016     * @return string
59168ff6f3Sric2016     */
6049a243cbSGreg Roach    public function title(): string
61c1010edaSGreg Roach    {
62bbb76c12SGreg Roach        /* I18N: Name of a module/chart */
63bbb76c12SGreg Roach        return I18N::translate('Timeline');
64168ff6f3Sric2016    }
65168ff6f3Sric2016
66168ff6f3Sric2016    /**
67168ff6f3Sric2016     * A sentence describing what this module does.
68168ff6f3Sric2016     *
69168ff6f3Sric2016     * @return string
70168ff6f3Sric2016     */
7149a243cbSGreg Roach    public function description(): string
72c1010edaSGreg Roach    {
73bbb76c12SGreg Roach        /* I18N: Description of the “TimelineChart” module */
74bbb76c12SGreg Roach        return I18N::translate('A timeline displaying individual events.');
75168ff6f3Sric2016    }
76168ff6f3Sric2016
77168ff6f3Sric2016    /**
78377a2979SGreg Roach     * CSS class for the URL.
79377a2979SGreg Roach     *
80377a2979SGreg Roach     * @return string
81377a2979SGreg Roach     */
82377a2979SGreg Roach    public function chartMenuClass(): string
83377a2979SGreg Roach    {
84377a2979SGreg Roach        return 'menu-chart-timeline';
85377a2979SGreg Roach    }
86377a2979SGreg Roach
87377a2979SGreg Roach    /**
88e6562982SGreg Roach     * The URL for this chart.
89168ff6f3Sric2016     *
9060bc3e3fSGreg Roach     * @param Individual $individual
91e6562982SGreg Roach     * @param string[]   $parameters
9260bc3e3fSGreg Roach     *
93e6562982SGreg Roach     * @return string
94168ff6f3Sric2016     */
95e6562982SGreg Roach    public function chartUrl(Individual $individual, array $parameters = []): string
96c1010edaSGreg Roach    {
97eca4a663SGreg Roach        return route('module', [
98eca4a663SGreg Roach                'module'  => $this->name(),
99eca4a663SGreg Roach                'action'  => 'Chart',
100c0935879SGreg Roach                'xrefs[]' => $individual->xref(),
101f4afa648SGreg Roach                'ged'     => $individual->tree()->name(),
102e6562982SGreg Roach            ] + $parameters);
103168ff6f3Sric2016    }
104eca4a663SGreg Roach
105eca4a663SGreg Roach    /**
106eca4a663SGreg Roach     * A form to request the chart parameters.
107eca4a663SGreg Roach     *
108eca4a663SGreg Roach     * @param Request $request
109eca4a663SGreg Roach     * @param Tree    $tree
110eca4a663SGreg Roach     *
111eca4a663SGreg Roach     * @return Response
112eca4a663SGreg Roach     */
113eca4a663SGreg Roach    public function getChartAction(Request $request, Tree $tree): Response
114eca4a663SGreg Roach    {
115*9b5537c3SGreg Roach        $ajax  = (bool) $request->get('ajax');
116eca4a663SGreg Roach        $scale = (int) $request->get('scale', self::SCALE_DEFAULT);
117eca4a663SGreg Roach        $scale = min($scale, self::SCALE_MAX);
118eca4a663SGreg Roach        $scale = max($scale, self::SCALE_MIN);
119eca4a663SGreg Roach
120eca4a663SGreg Roach        $xrefs = $request->get('xrefs', []);
121eca4a663SGreg Roach
122eca4a663SGreg Roach        // Find the requested individuals.
123eca4a663SGreg Roach        $individuals = (new Collection($xrefs))
124eca4a663SGreg Roach            ->unique()
125eca4a663SGreg Roach            ->map(function (string $xref) use ($tree): ?Individual {
126eca4a663SGreg Roach                return Individual::getInstance($xref, $tree);
127eca4a663SGreg Roach            })
128eca4a663SGreg Roach            ->filter()
129eca4a663SGreg Roach            ->filter(GedcomRecord::accessFilter());
130eca4a663SGreg Roach
131eca4a663SGreg Roach        // Generate URLs omitting each xref.
132eca4a663SGreg Roach        $remove_urls = [];
133eca4a663SGreg Roach
134eca4a663SGreg Roach        foreach ($individuals as $exclude) {
135eca4a663SGreg Roach            $xrefs_1 = $individuals
136eca4a663SGreg Roach                ->filter(function (Individual $individual) use ($exclude): bool {
137eca4a663SGreg Roach                    return $individual->xref() !== $exclude->xref();
138eca4a663SGreg Roach                })
139eca4a663SGreg Roach                ->map(function (Individual $individual): string {
140eca4a663SGreg Roach                    return $individual->xref();
141eca4a663SGreg Roach                });
142eca4a663SGreg Roach
143eca4a663SGreg Roach            $remove_urls[$exclude->xref()] = route('module', [
144eca4a663SGreg Roach                'module' => $this->name(),
145eca4a663SGreg Roach                'action' => 'Chart',
146eca4a663SGreg Roach                'ged'    => $tree->name(),
147eca4a663SGreg Roach                'scale'  => $scale,
148eca4a663SGreg Roach                'xrefs'  => $xrefs_1->all(),
149eca4a663SGreg Roach            ]);
150eca4a663SGreg Roach        }
151eca4a663SGreg Roach
152eca4a663SGreg Roach        $individuals = array_map(function (string $xref) use ($tree) {
153eca4a663SGreg Roach            return Individual::getInstance($xref, $tree);
154eca4a663SGreg Roach        }, $xrefs);
155eca4a663SGreg Roach
156*9b5537c3SGreg Roach        if ($ajax) {
157eca4a663SGreg Roach            return $this->chart($tree, $xrefs, $scale);
158eca4a663SGreg Roach        }
159eca4a663SGreg Roach
160eca4a663SGreg Roach        $ajax_url = route('module', [
161*9b5537c3SGreg Roach            'ajax'   => true,
162eca4a663SGreg Roach            'module' => $this->name(),
163eca4a663SGreg Roach            'action' => 'Chart',
164eca4a663SGreg Roach            'ged'    => $tree->name(),
165eca4a663SGreg Roach            'scale'  => $scale,
166eca4a663SGreg Roach            'xrefs'  => $xrefs,
167eca4a663SGreg Roach        ]);
168eca4a663SGreg Roach
169eca4a663SGreg Roach        $reset_url = route('module', [
170eca4a663SGreg Roach            'module' => $this->name(),
171eca4a663SGreg Roach            'action' => 'Chart',
172eca4a663SGreg Roach            'ged'    => $tree->name(),
173eca4a663SGreg Roach        ]);
174eca4a663SGreg Roach
175eca4a663SGreg Roach        $zoom_in_url = route('module', [
176eca4a663SGreg Roach            'module' => $this->name(),
177eca4a663SGreg Roach            'action' => 'Chart',
178eca4a663SGreg Roach            'ged'    => $tree->name(),
179eca4a663SGreg Roach            'scale'  => min(self::SCALE_MAX, $scale + (int) ($scale * 0.2 + 1)),
180eca4a663SGreg Roach            'xrefs'  => $xrefs,
181eca4a663SGreg Roach        ]);
182eca4a663SGreg Roach
183eca4a663SGreg Roach        $zoom_out_url = route('module', [
184eca4a663SGreg Roach            'module' => $this->name(),
185eca4a663SGreg Roach            'action' => 'Chart',
186eca4a663SGreg Roach            'ged'    => $tree->name(),
187eca4a663SGreg Roach            'scale'  => max(self::SCALE_MIN, $scale - (int) ($scale * 0.2 + 1)),
188eca4a663SGreg Roach            'xrefs'  => $xrefs,
189eca4a663SGreg Roach        ]);
190eca4a663SGreg Roach
191*9b5537c3SGreg Roach        return $this->viewResponse('modules/timeline-chart/page', [
192eca4a663SGreg Roach            'ajax_url'     => $ajax_url,
193eca4a663SGreg Roach            'individuals'  => $individuals,
194eca4a663SGreg Roach            'module_name'  => $this->name(),
195eca4a663SGreg Roach            'remove_urls'  => $remove_urls,
196eca4a663SGreg Roach            'reset_url'    => $reset_url,
197eca4a663SGreg Roach            'title'        => $this->title(),
198eca4a663SGreg Roach            'scale'        => $scale,
199eca4a663SGreg Roach            'zoom_in_url'  => $zoom_in_url,
200eca4a663SGreg Roach            'zoom_out_url' => $zoom_out_url,
201eca4a663SGreg Roach        ]);
202eca4a663SGreg Roach    }
203eca4a663SGreg Roach
204eca4a663SGreg Roach    /**
205eca4a663SGreg Roach     * @param Tree  $tree
206eca4a663SGreg Roach     * @param array $xrefs
207eca4a663SGreg Roach     * @param int   $scale
208eca4a663SGreg Roach     *
209eca4a663SGreg Roach     * @return Response
210eca4a663SGreg Roach     */
211eca4a663SGreg Roach    protected function chart(Tree $tree, array $xrefs, int $scale): Response
212eca4a663SGreg Roach    {
213eca4a663SGreg Roach        $xrefs = array_unique($xrefs);
214eca4a663SGreg Roach
215eca4a663SGreg Roach        /** @var Individual[] $individuals */
216eca4a663SGreg Roach        $individuals = array_map(function (string $xref) use ($tree) {
217eca4a663SGreg Roach            return Individual::getInstance($xref, $tree);
218eca4a663SGreg Roach        }, $xrefs);
219eca4a663SGreg Roach
220eca4a663SGreg Roach        $individuals = array_filter($individuals, function (Individual $individual = null): bool {
221eca4a663SGreg Roach            return $individual !== null && $individual->canShow();
222eca4a663SGreg Roach        });
223eca4a663SGreg Roach
224eca4a663SGreg Roach        $baseyear    = (int) date('Y');
225eca4a663SGreg Roach        $topyear     = 0;
226eca4a663SGreg Roach        $indifacts   = [];
227eca4a663SGreg Roach        $birthyears  = [];
228eca4a663SGreg Roach        $birthmonths = [];
229eca4a663SGreg Roach        $birthdays   = [];
230eca4a663SGreg Roach
231eca4a663SGreg Roach        foreach ($individuals as $individual) {
232eca4a663SGreg Roach            $bdate = $individual->getBirthDate();
233eca4a663SGreg Roach            if ($bdate->isOK()) {
234eca4a663SGreg Roach                $date = new GregorianDate($bdate->minimumJulianDay());
235eca4a663SGreg Roach
236eca4a663SGreg Roach                $birthyears [$individual->xref()] = $date->year;
237eca4a663SGreg Roach                $birthmonths[$individual->xref()] = max(1, $date->month);
238eca4a663SGreg Roach                $birthdays  [$individual->xref()] = max(1, $date->day);
239eca4a663SGreg Roach            }
240eca4a663SGreg Roach            // find all the fact information
241eca4a663SGreg Roach            $facts = $individual->facts();
242eca4a663SGreg Roach            foreach ($individual->getSpouseFamilies() as $family) {
243eca4a663SGreg Roach                foreach ($family->facts() as $fact) {
244eca4a663SGreg Roach                    $facts[] = $fact;
245eca4a663SGreg Roach                }
246eca4a663SGreg Roach            }
247eca4a663SGreg Roach            foreach ($facts as $event) {
248eca4a663SGreg Roach                // get the fact type
249eca4a663SGreg Roach                $fact = $event->getTag();
250eca4a663SGreg Roach                if (!in_array($fact, self::NON_FACTS)) {
251eca4a663SGreg Roach                    // check for a date
252eca4a663SGreg Roach                    $date = $event->date();
253eca4a663SGreg Roach                    if ($date->isOK()) {
254eca4a663SGreg Roach                        $date     = new GregorianDate($date->minimumJulianDay());
255eca4a663SGreg Roach                        $baseyear = min($baseyear, $date->year);
256eca4a663SGreg Roach                        $topyear  = max($topyear, $date->year);
257eca4a663SGreg Roach
258eca4a663SGreg Roach                        if (!$individual->isDead()) {
259eca4a663SGreg Roach                            $topyear = max($topyear, (int) date('Y'));
260eca4a663SGreg Roach                        }
261eca4a663SGreg Roach
262eca4a663SGreg Roach                        // do not add the same fact twice (prevents marriages from being added multiple times)
263eca4a663SGreg Roach                        if (!in_array($event, $indifacts, true)) {
264eca4a663SGreg Roach                            $indifacts[] = $event;
265eca4a663SGreg Roach                        }
266eca4a663SGreg Roach                    }
267eca4a663SGreg Roach                }
268eca4a663SGreg Roach            }
269eca4a663SGreg Roach        }
270eca4a663SGreg Roach
271eca4a663SGreg Roach        if ($scale === 0) {
272eca4a663SGreg Roach            $scale = (int) (($topyear - $baseyear) / 20 * count($indifacts) / 4);
273eca4a663SGreg Roach            if ($scale < 6) {
274eca4a663SGreg Roach                $scale = 6;
275eca4a663SGreg Roach            }
276eca4a663SGreg Roach        }
277eca4a663SGreg Roach        if ($scale < 2) {
278eca4a663SGreg Roach            $scale = 2;
279eca4a663SGreg Roach        }
280eca4a663SGreg Roach        $baseyear -= 5;
281eca4a663SGreg Roach        $topyear  += 5;
282eca4a663SGreg Roach
283eca4a663SGreg Roach        Functions::sortFacts($indifacts);
284eca4a663SGreg Roach
285eca4a663SGreg Roach        $html = view('modules/timeline-chart/chart', [
286eca4a663SGreg Roach            'baseyear'    => $baseyear,
287eca4a663SGreg Roach            'bheight'     => self::BHEIGHT,
288eca4a663SGreg Roach            'birthdays'   => $birthdays,
289eca4a663SGreg Roach            'birthmonths' => $birthmonths,
290eca4a663SGreg Roach            'birthyears'  => $birthyears,
291eca4a663SGreg Roach            'indifacts'   => $indifacts,
292eca4a663SGreg Roach            'individuals' => $individuals,
293eca4a663SGreg Roach            'placements'  => [],
294eca4a663SGreg Roach            'scale'       => $scale,
295eca4a663SGreg Roach            'topyear'     => $topyear,
296eca4a663SGreg Roach        ]);
297eca4a663SGreg Roach
298eca4a663SGreg Roach        return new Response($html);
299eca4a663SGreg Roach    }
300168ff6f3Sric2016}
301