xref: /webtrees/app/Module/PedigreeMapModule.php (revision 3976b4703df669696105ed6b024b96d433c8fbdb)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2019 webtrees development team
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17declare(strict_types=1);
18
19namespace Fisharebest\Webtrees\Module;
20
21use Fig\Http\Message\StatusCodeInterface;
22use Fisharebest\Webtrees\Exceptions\IndividualAccessDeniedException;
23use Fisharebest\Webtrees\Exceptions\IndividualNotFoundException;
24use Fisharebest\Webtrees\Fact;
25use Fisharebest\Webtrees\Family;
26use Fisharebest\Webtrees\Functions\Functions;
27use Fisharebest\Webtrees\GedcomTag;
28use Fisharebest\Webtrees\I18N;
29use Fisharebest\Webtrees\Individual;
30use Fisharebest\Webtrees\Location;
31use Fisharebest\Webtrees\Menu;
32use Fisharebest\Webtrees\Services\ChartService;
33use Fisharebest\Webtrees\Tree;
34use Psr\Http\Message\ResponseInterface;
35use Psr\Http\Message\ServerRequestInterface;
36
37use function intdiv;
38
39/**
40 * Class PedigreeMapModule
41 */
42class PedigreeMapModule extends AbstractModule implements ModuleChartInterface
43{
44    use ModuleChartTrait;
45
46    // Defaults
47    public const DEFAULT_GENERATIONS = '4';
48
49    // Limits
50    public const MAXIMUM_GENERATIONS = 10;
51
52    private const LINE_COLORS = [
53        '#FF0000',
54        // Red
55        '#00FF00',
56        // Green
57        '#0000FF',
58        // Blue
59        '#FFB300',
60        // Gold
61        '#00FFFF',
62        // Cyan
63        '#FF00FF',
64        // Purple
65        '#7777FF',
66        // Light blue
67        '#80FF80'
68        // Light green
69    ];
70
71    /** @var ChartService */
72    private $chart_service;
73
74    /**
75     * PedigreeMapModule constructor.
76     *
77     * @param ChartService $chart_service
78     */
79    public function __construct(ChartService $chart_service)
80    {
81        $this->chart_service = $chart_service;
82    }
83
84    /**
85     * How should this module be identified in the control panel, etc.?
86     *
87     * @return string
88     */
89    public function title(): string
90    {
91        /* I18N: Name of a module */
92        return I18N::translate('Pedigree map');
93    }
94
95    /**
96     * A sentence describing what this module does.
97     *
98     * @return string
99     */
100    public function description(): string
101    {
102        /* I18N: Description of the “OSM” module */
103        return I18N::translate('Show the birthplace of ancestors on a map.');
104    }
105
106    /**
107     * CSS class for the URL.
108     *
109     * @return string
110     */
111    public function chartMenuClass(): string
112    {
113        return 'menu-chart-pedigreemap';
114    }
115
116    /**
117     * Return a menu item for this chart - for use in individual boxes.
118     *
119     * @param Individual $individual
120     *
121     * @return Menu|null
122     */
123    public function chartBoxMenu(Individual $individual): ?Menu
124    {
125        return $this->chartMenu($individual);
126    }
127
128    /**
129     * The title for a specific instance of this chart.
130     *
131     * @param Individual $individual
132     *
133     * @return string
134     */
135    public function chartTitle(Individual $individual): string
136    {
137        /* I18N: %s is an individual’s name */
138        return I18N::translate('Pedigree map of %s', $individual->fullName());
139    }
140
141    /**
142     * The URL for this chart.
143     *
144     * @param Individual $individual
145     * @param string[]   $parameters
146     *
147     * @return string
148     */
149    public function chartUrl(Individual $individual, array $parameters = []): string
150    {
151        return route('module', [
152                'module' => $this->name(),
153                'action' => 'PedigreeMap',
154                'xref'   => $individual->xref(),
155                'ged'    => $individual->tree()->name(),
156            ] + $parameters);
157    }
158
159    /**
160     * @param ServerRequestInterface $request
161     *
162     * @return ResponseInterface
163     */
164    public function getMapDataAction(ServerRequestInterface $request): ResponseInterface
165    {
166        $tree        = $request->getAttribute('tree');
167        $xref        = $request->getQueryParams()['reference'];
168        $indi        = Individual::getInstance($xref, $tree);
169        $color_count = count(self::LINE_COLORS);
170
171        $facts = $this->getPedigreeMapFacts($request, $this->chart_service);
172
173        $geojson = [
174            'type'     => 'FeatureCollection',
175            'features' => [],
176        ];
177
178        $sosa_points = [];
179
180        foreach ($facts as $id => $fact) {
181            $location = new Location($fact->place()->gedcomName());
182
183            // Use the co-ordinates from the fact (if they exist).
184            $latitude  = $fact->latitude();
185            $longitude = $fact->longitude();
186
187            // Use the co-ordinates from the location otherwise.
188            if ($latitude === 0.0 && $longitude === 0.0) {
189                $latitude  = $location->latitude();
190                $longitude = $location->longitude();
191            }
192
193            $icon = ['color' => 'Gold', 'name' => 'bullseye '];
194            if ($latitude !== 0.0 || $longitude !== 0.0) {
195                $polyline         = null;
196                $color            = self::LINE_COLORS[log($id, 2) % $color_count];
197                $icon['color']    = $color; //make icon color the same as the line
198                $sosa_points[$id] = [$latitude, $longitude];
199                $sosa_parent      = intdiv($id, 2);
200                if (array_key_exists($sosa_parent, $sosa_points)) {
201                    // Would like to use a GeometryCollection to hold LineStrings
202                    // rather than generate polylines but the MarkerCluster library
203                    // doesn't seem to like them
204                    $polyline = [
205                        'points'  => [
206                            $sosa_points[$sosa_parent],
207                            [$latitude, $longitude],
208                        ],
209                        'options' => [
210                            'color' => $color,
211                        ],
212                    ];
213                }
214                $geojson['features'][] = [
215                    'type'       => 'Feature',
216                    'id'         => $id,
217                    'valid'      => true,
218                    'geometry'   => [
219                        'type'        => 'Point',
220                        'coordinates' => [$longitude, $latitude],
221                    ],
222                    'properties' => [
223                        'polyline' => $polyline,
224                        'icon'     => $icon,
225                        'tooltip'  => strip_tags($fact->place()->fullName()),
226                        'summary'  => view('modules/pedigree-map/events', $this->summaryData($indi, $fact, $id)),
227                        'zoom'     => $location->zoom() ?: 2,
228                    ],
229                ];
230            }
231        }
232
233        $code = empty($facts) ? StatusCodeInterface::STATUS_NO_CONTENT : StatusCodeInterface::STATUS_OK;
234
235        return response($geojson, $code);
236    }
237
238    /**
239     * @param ServerRequestInterface $request
240     * @param ChartService           $chart_service
241     *
242     * @return array
243     */
244    private function getPedigreeMapFacts(ServerRequestInterface $request, ChartService $chart_service): array
245    {
246        $tree        = $request->getAttribute('tree');
247        $xref        = $request->getQueryParams()['reference'];
248        $individual  = Individual::getInstance($xref, $tree);
249        $generations = (int) $request->getQueryParams()['generations'];
250        $ancestors   = $chart_service->sosaStradonitzAncestors($individual, $generations);
251        $facts       = [];
252        foreach ($ancestors as $sosa => $person) {
253            if ($person->canShow()) {
254                $birth = $person->facts(['BIRT'])->first();
255                if ($birth instanceof Fact && $birth->place()->gedcomName() !== '') {
256                    $facts[$sosa] = $birth;
257                }
258            }
259        }
260
261        return $facts;
262    }
263
264    /**
265     * @param Individual $individual
266     * @param Fact       $fact
267     * @param int        $sosa
268     *
269     * @return array
270     */
271    private function summaryData(Individual $individual, Fact $fact, int $sosa): array
272    {
273        $record      = $fact->record();
274        $name        = '';
275        $url         = '';
276        $tag         = $fact->label();
277        $addbirthtag = false;
278
279        if ($record instanceof Family) {
280            // Marriage
281            $spouse = $record->spouse($individual);
282            if ($spouse) {
283                $url  = $spouse->url();
284                $name = $spouse->fullName();
285            }
286        } elseif ($record !== $individual) {
287            // Birth of a child
288            $url  = $record->url();
289            $name = $record->fullName();
290            $tag  = GedcomTag::getLabel('_BIRT_CHIL', $record);
291        }
292
293        if ($sosa > 1) {
294            $addbirthtag = true;
295            $tag         = ucfirst($this->getSosaName($sosa));
296        }
297
298        return [
299            'tag'    => $tag,
300            'url'    => $url,
301            'name'   => $name,
302            'value'  => $fact->value(),
303            'date'   => $fact->date()->display(true),
304            'place'  => $fact->place(),
305            'addtag' => $addbirthtag,
306        ];
307    }
308
309    /**
310     * @param ServerRequestInterface $request
311     *
312     * @return object
313     */
314    public function getPedigreeMapAction(ServerRequestInterface $request)
315    {
316        $tree        = $request->getAttribute('tree');
317        $xref        = $request->getQueryParams()['xref'];
318        $individual  = Individual::getInstance($xref, $tree);
319        $generations = $request->getQueryParams()['generations'] ?? self::DEFAULT_GENERATIONS;
320
321        if ($individual === null) {
322            throw new IndividualNotFoundException();
323        }
324
325        if (!$individual->canShow()) {
326            throw new IndividualAccessDeniedException();
327        }
328
329        return $this->viewResponse('modules/pedigree-map/page', [
330            'module_name'    => $this->name(),
331            /* I18N: %s is an individual’s name */
332            'title'          => I18N::translate('Pedigree map of %s', $individual->fullName()),
333            'tree'           => $tree,
334            'individual'     => $individual,
335            'generations'    => $generations,
336            'maxgenerations' => self::MAXIMUM_GENERATIONS,
337            'map'            => view(
338                'modules/pedigree-map/chart',
339                [
340                    'module'      => $this->name(),
341                    'ref'         => $individual->xref(),
342                    'type'        => 'pedigree',
343                    'generations' => $generations,
344                ]
345            ),
346        ]);
347    }
348
349    /**
350     * builds and returns sosa relationship name in the active language
351     *
352     * @param int $sosa Sosa number
353     *
354     * @return string
355     */
356    private function getSosaName(int $sosa): string
357    {
358        $path = '';
359
360        while ($sosa > 1) {
361            if ($sosa % 2 === 1) {
362                $path = 'mot' . $path;
363            } else {
364                $path = 'fat' . $path;
365            }
366            $sosa = intdiv($sosa, 2);
367        }
368
369        return Functions::getRelationshipNameFromPath($path);
370    }
371}
372