xref: /webtrees/app/Module/TimelineChartModule.php (revision d993d560f991544b8dc49e013a8027c6fc967956)
1<?php
2/**
3 * webtrees: online genealogy
4 * Copyright (C) 2019 webtrees development team
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
16declare(strict_types=1);
17
18namespace Fisharebest\Webtrees\Module;
19
20use Fisharebest\Webtrees\Date\GregorianDate;
21use Fisharebest\Webtrees\Functions\Functions;
22use Fisharebest\Webtrees\GedcomRecord;
23use Fisharebest\Webtrees\I18N;
24use Fisharebest\Webtrees\Individual;
25use Fisharebest\Webtrees\Tree;
26use Illuminate\Support\Collection;
27use Symfony\Component\HttpFoundation\Request;
28use Symfony\Component\HttpFoundation\Response;
29
30/**
31 * Class TimelineChartModule
32 */
33class TimelineChartModule extends AbstractModule implements ModuleInterface, ModuleChartInterface
34{
35    use ModuleChartTrait;
36
37    // The user can alter the vertical scale
38    protected const SCALE_MIN     = 1;
39    protected const SCALE_MAX     = 200;
40    protected const SCALE_DEFAULT = 10;
41
42    // GEDCOM events that may have DATE data, but should not be displayed
43    protected const NON_FACTS = [
44        'BAPL',
45        'ENDL',
46        'SLGC',
47        'SLGS',
48        '_TODO',
49        'CHAN',
50    ];
51
52    // Box height
53    protected const BHEIGHT = 30;
54
55    /**
56     * How should this module be labelled on tabs, menus, etc.?
57     *
58     * @return string
59     */
60    public function title(): string
61    {
62        /* I18N: Name of a module/chart */
63        return I18N::translate('Timeline');
64    }
65
66    /**
67     * A sentence describing what this module does.
68     *
69     * @return string
70     */
71    public function description(): string
72    {
73        /* I18N: Description of the “TimelineChart” module */
74        return I18N::translate('A timeline displaying individual events.');
75    }
76
77    /**
78     * CSS class for the URL.
79     *
80     * @return string
81     */
82    public function chartMenuClass(): string
83    {
84        return 'menu-chart-timeline';
85    }
86
87    /**
88     * The URL for this chart.
89     *
90     * @param Individual $individual
91     * @param string[]   $parameters
92     *
93     * @return string
94     */
95    public function chartUrl(Individual $individual, array $parameters = []): string
96    {
97        return route('module', [
98                'module'  => $this->name(),
99                'action'  => 'Chart',
100                'xrefs[]' => $individual->xref(),
101                'ged'     => $individual->tree()->name(),
102            ] + $parameters);
103    }
104
105    /**
106     * A form to request the chart parameters.
107     *
108     * @param Request $request
109     * @param Tree    $tree
110     *
111     * @return Response
112     */
113    public function getChartAction(Request $request, Tree $tree): Response
114    {
115        $ajax  = (bool) $request->get('ajax');
116        $scale = (int) $request->get('scale', self::SCALE_DEFAULT);
117        $scale = min($scale, self::SCALE_MAX);
118        $scale = max($scale, self::SCALE_MIN);
119
120        $xrefs = $request->get('xrefs', []);
121
122        // Find the requested individuals.
123        $individuals = (new Collection($xrefs))
124            ->unique()
125            ->map(function (string $xref) use ($tree): ?Individual {
126                return Individual::getInstance($xref, $tree);
127            })
128            ->filter()
129            ->filter(GedcomRecord::accessFilter());
130
131        // Generate URLs omitting each xref.
132        $remove_urls = [];
133
134        foreach ($individuals as $exclude) {
135            $xrefs_1 = $individuals
136                ->filter(function (Individual $individual) use ($exclude): bool {
137                    return $individual->xref() !== $exclude->xref();
138                })
139                ->map(function (Individual $individual): string {
140                    return $individual->xref();
141                });
142
143            $remove_urls[$exclude->xref()] = route('module', [
144                'module' => $this->name(),
145                'action' => 'Chart',
146                'ged'    => $tree->name(),
147                'scale'  => $scale,
148                'xrefs'  => $xrefs_1->all(),
149            ]);
150        }
151
152        $individuals = array_map(function (string $xref) use ($tree) {
153            return Individual::getInstance($xref, $tree);
154        }, $xrefs);
155
156        if ($ajax) {
157            return $this->chart($tree, $xrefs, $scale);
158        }
159
160        $ajax_url = route('module', [
161            'ajax'   => true,
162            'module' => $this->name(),
163            'action' => 'Chart',
164            'ged'    => $tree->name(),
165            'scale'  => $scale,
166            'xrefs'  => $xrefs,
167        ]);
168
169        $reset_url = route('module', [
170            'module' => $this->name(),
171            'action' => 'Chart',
172            'ged'    => $tree->name(),
173        ]);
174
175        $zoom_in_url = route('module', [
176            'module' => $this->name(),
177            'action' => 'Chart',
178            'ged'    => $tree->name(),
179            'scale'  => min(self::SCALE_MAX, $scale + (int) ($scale * 0.2 + 1)),
180            'xrefs'  => $xrefs,
181        ]);
182
183        $zoom_out_url = route('module', [
184            'module' => $this->name(),
185            'action' => 'Chart',
186            'ged'    => $tree->name(),
187            'scale'  => max(self::SCALE_MIN, $scale - (int) ($scale * 0.2 + 1)),
188            'xrefs'  => $xrefs,
189        ]);
190
191        return $this->viewResponse('modules/timeline-chart/page', [
192            'ajax_url'     => $ajax_url,
193            'individuals'  => $individuals,
194            'module_name'  => $this->name(),
195            'remove_urls'  => $remove_urls,
196            'reset_url'    => $reset_url,
197            'title'        => $this->title(),
198            'scale'        => $scale,
199            'zoom_in_url'  => $zoom_in_url,
200            'zoom_out_url' => $zoom_out_url,
201        ]);
202    }
203
204    /**
205     * @param Tree  $tree
206     * @param array $xrefs
207     * @param int   $scale
208     *
209     * @return Response
210     */
211    protected function chart(Tree $tree, array $xrefs, int $scale): Response
212    {
213        $xrefs = array_unique($xrefs);
214
215        /** @var Individual[] $individuals */
216        $individuals = array_map(function (string $xref) use ($tree) {
217            return Individual::getInstance($xref, $tree);
218        }, $xrefs);
219
220        $individuals = array_filter($individuals, function (Individual $individual = null): bool {
221            return $individual !== null && $individual->canShow();
222        });
223
224        $baseyear    = (int) date('Y');
225        $topyear     = 0;
226        $indifacts   = [];
227        $birthyears  = [];
228        $birthmonths = [];
229        $birthdays   = [];
230
231        foreach ($individuals as $individual) {
232            $bdate = $individual->getBirthDate();
233            if ($bdate->isOK()) {
234                $date = new GregorianDate($bdate->minimumJulianDay());
235
236                $birthyears [$individual->xref()] = $date->year;
237                $birthmonths[$individual->xref()] = max(1, $date->month);
238                $birthdays  [$individual->xref()] = max(1, $date->day);
239            }
240            // find all the fact information
241            $facts = $individual->facts();
242            foreach ($individual->getSpouseFamilies() as $family) {
243                foreach ($family->facts() as $fact) {
244                    $facts[] = $fact;
245                }
246            }
247            foreach ($facts as $event) {
248                // get the fact type
249                $fact = $event->getTag();
250                if (!in_array($fact, self::NON_FACTS)) {
251                    // check for a date
252                    $date = $event->date();
253                    if ($date->isOK()) {
254                        $date     = new GregorianDate($date->minimumJulianDay());
255                        $baseyear = min($baseyear, $date->year);
256                        $topyear  = max($topyear, $date->year);
257
258                        if (!$individual->isDead()) {
259                            $topyear = max($topyear, (int) date('Y'));
260                        }
261
262                        // do not add the same fact twice (prevents marriages from being added multiple times)
263                        if (!in_array($event, $indifacts, true)) {
264                            $indifacts[] = $event;
265                        }
266                    }
267                }
268            }
269        }
270
271        if ($scale === 0) {
272            $scale = (int) (($topyear - $baseyear) / 20 * count($indifacts) / 4);
273            if ($scale < 6) {
274                $scale = 6;
275            }
276        }
277        if ($scale < 2) {
278            $scale = 2;
279        }
280        $baseyear -= 5;
281        $topyear  += 5;
282
283        Functions::sortFacts($indifacts);
284
285        $html = view('modules/timeline-chart/chart', [
286            'baseyear'    => $baseyear,
287            'bheight'     => self::BHEIGHT,
288            'birthdays'   => $birthdays,
289            'birthmonths' => $birthmonths,
290            'birthyears'  => $birthyears,
291            'indifacts'   => $indifacts,
292            'individuals' => $individuals,
293            'placements'  => [],
294            'scale'       => $scale,
295            'topyear'     => $topyear,
296        ]);
297
298        return new Response($html);
299    }
300}
301