Issues (2502)

app/Module/PedigreeMapModule.php (1 issue)

Labels
Severity
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2025 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 <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Module;
21
22
use Fig\Http\Message\RequestMethodInterface;
23
use Fisharebest\Webtrees\Auth;
24
use Fisharebest\Webtrees\Fact;
25
use Fisharebest\Webtrees\Gedcom;
26
use Fisharebest\Webtrees\Http\Middleware\AuthNotRobot;
27
use Fisharebest\Webtrees\I18N;
28
use Fisharebest\Webtrees\Individual;
29
use Fisharebest\Webtrees\Menu;
30
use Fisharebest\Webtrees\PlaceLocation;
31
use Fisharebest\Webtrees\Registry;
32
use Fisharebest\Webtrees\Services\ChartService;
33
use Fisharebest\Webtrees\Services\LeafletJsService;
34
use Fisharebest\Webtrees\Services\RelationshipService;
35
use Fisharebest\Webtrees\Validator;
36
use Psr\Http\Message\ResponseInterface;
37
use Psr\Http\Message\ServerRequestInterface;
38
use Psr\Http\Server\RequestHandlerInterface;
39
40
use function array_key_exists;
41
use function intdiv;
42
use function redirect;
43
use function route;
44
use function ucfirst;
45
use function view;
46
47
class PedigreeMapModule extends AbstractModule implements ModuleChartInterface, RequestHandlerInterface
48
{
49
    use ModuleChartTrait;
50
51
    protected const string ROUTE_URL = '/tree/{tree}/pedigree-map-{generations}/{xref}';
0 ignored issues
show
A parse error occurred: Syntax error, unexpected T_STRING, expecting '=' on line 51 at column 27
Loading history...
52
53
    // Defaults
54
    public const string DEFAULT_GENERATIONS = '4';
55
    public const array  DEFAULT_PARAMETERS  = [
56
        'generations' => self::DEFAULT_GENERATIONS,
57
    ];
58
59
    // Limits
60
    public const int MINIMUM_GENERATIONS = 1;
61
    public const int MAXIMUM_GENERATIONS = 10;
62
63
    // CSS colors for each generation
64
    protected const int COUNT_CSS_COLORS = 12;
65
66
    protected ChartService $chart_service;
67
68
    protected LeafletJsService $leaflet_js_service;
69
70
    protected RelationshipService $relationship_service;
71
72
    /**
73
     * @param ChartService        $chart_service
74
     * @param LeafletJsService    $leaflet_js_service
75
     * @param RelationshipService $relationship_service
76
     */
77
    public function __construct(
78
        ChartService $chart_service,
79
        LeafletJsService $leaflet_js_service,
80
        RelationshipService $relationship_service
81
    ) {
82
        $this->chart_service      = $chart_service;
83
        $this->leaflet_js_service = $leaflet_js_service;
84
        $this->relationship_service = $relationship_service;
85
    }
86
87
    /**
88
     * Initialization.
89
     *
90
     * @return void
91
     */
92
    public function boot(): void
93
    {
94
        Registry::routeFactory()->routeMap()
95
            ->get(static::class, static::ROUTE_URL, $this)
96
            ->allows(RequestMethodInterface::METHOD_POST)
97
            ->extras(['middleware' => [AuthNotRobot::class]]);
98
    }
99
100
    public function title(): string
101
    {
102
        /* I18N: Name of a module */
103
        return I18N::translate('Pedigree map');
104
    }
105
106
    public function description(): string
107
    {
108
        /* I18N: Description of the “Pedigree map” module */
109
        return I18N::translate('Show the birthplace of ancestors on a map.');
110
    }
111
112
    /**
113
     * CSS class for the URL.
114
     *
115
     * @return string
116
     */
117
    public function chartMenuClass(): string
118
    {
119
        return 'menu-chart-pedigreemap';
120
    }
121
122
    /**
123
     * Return a menu item for this chart - for use in individual boxes.
124
     *
125
     * @param Individual $individual
126
     *
127
     * @return Menu|null
128
     */
129
    public function chartBoxMenu(Individual $individual): Menu|null
130
    {
131
        return $this->chartMenu($individual);
132
    }
133
134
    /**
135
     * The title for a specific instance of this chart.
136
     *
137
     * @param Individual $individual
138
     *
139
     * @return string
140
     */
141
    public function chartTitle(Individual $individual): string
142
    {
143
        /* I18N: %s is an individual’s name */
144
        return I18N::translate('Pedigree map of %s', $individual->fullName());
145
    }
146
147
    /**
148
     * The URL for a page showing chart options.
149
     *
150
     * @param Individual                                $individual
151
     * @param array<bool|int|string|array<string>|null> $parameters
152
     *
153
     * @return string
154
     */
155
    public function chartUrl(Individual $individual, array $parameters = []): string
156
    {
157
        return route(static::class, [
158
                'tree' => $individual->tree()->name(),
159
                'xref' => $individual->xref(),
160
            ] + $parameters + self::DEFAULT_PARAMETERS);
161
    }
162
163
    /**
164
     * @param ServerRequestInterface $request
165
     *
166
     * @return ResponseInterface
167
     */
168
    public function handle(ServerRequestInterface $request): ResponseInterface
169
    {
170
        $tree        = Validator::attributes($request)->tree();
171
        $user        = Validator::attributes($request)->user();
172
        $generations = Validator::attributes($request)->isBetween(self::MINIMUM_GENERATIONS, self::MAXIMUM_GENERATIONS)->integer('generations');
173
        $xref        = Validator::attributes($request)->isXref()->string('xref');
174
175
        // Convert POST requests into GET requests for pretty URLs.
176
        if ($request->getMethod() === RequestMethodInterface::METHOD_POST) {
177
            return redirect(route(static::class, [
178
                'tree'        => $tree->name(),
179
                'xref'        => Validator::parsedBody($request)->isXref()->string('xref'),
180
                'generations' => Validator::parsedBody($request)->isBetween(self::MINIMUM_GENERATIONS, self::MAXIMUM_GENERATIONS)->integer('generations'),
181
            ]));
182
        }
183
184
        Auth::checkComponentAccess($this, ModuleChartInterface::class, $tree, $user);
185
186
        $individual  = Registry::individualFactory()->make($xref, $tree);
187
        $individual  = Auth::checkIndividualAccess($individual, false, true);
188
189
        $map = view('modules/pedigree-map/chart', [
190
            'data'           => $this->getMapData($request),
191
            'leaflet_config' => $this->leaflet_js_service->config(),
192
        ]);
193
194
        return $this->viewResponse('modules/pedigree-map/page', [
195
            'module'         => $this->name(),
196
            /* I18N: %s is an individual’s name */
197
            'title'          => I18N::translate('Pedigree map of %s', $individual->fullName()),
198
            'tree'           => $tree,
199
            'individual'     => $individual,
200
            'generations'    => $generations,
201
            'maxgenerations' => self::MAXIMUM_GENERATIONS,
202
            'map'            => $map,
203
        ]);
204
    }
205
206
    /**
207
     * @param ServerRequestInterface $request
208
     *
209
     * @return array<mixed> $geojson
210
     */
211
    protected function getMapData(ServerRequestInterface $request): array
212
    {
213
        $facts = $this->getPedigreeMapFacts($request, $this->chart_service);
214
215
        $geojson = [
216
            'type'     => 'FeatureCollection',
217
            'features' => [],
218
        ];
219
220
        $sosa_points = [];
221
222
        foreach ($facts as $sosa => $fact) {
223
            $location = new PlaceLocation($fact->place()->gedcomName());
224
225
            // Use the co-ordinates from the fact (if they exist).
226
            $latitude  = $fact->latitude();
227
            $longitude = $fact->longitude();
228
229
            // Use the co-ordinates from the location otherwise.
230
            if ($latitude === null || $longitude === null) {
231
                $latitude  = $location->latitude();
232
                $longitude = $location->longitude();
233
            }
234
235
            if ($latitude !== null && $longitude !== null) {
236
                $polyline           = null;
237
                $sosa_points[$sosa] = [$latitude, $longitude];
238
                $sosa_child         = intdiv($sosa, 2);
239
                $generation         = (int) log($sosa, 2);
240
                $color              = 'var(--wt-pedigree-map-gen-' . $generation % self::COUNT_CSS_COLORS . ')';
241
                $class              = 'wt-pedigree-map-gen-' . $generation % self::COUNT_CSS_COLORS;
242
243
                if (array_key_exists($sosa_child, $sosa_points)) {
244
                    // Would like to use a GeometryCollection to hold LineStrings
245
                    // rather than generate polylines but the MarkerCluster library
246
                    // doesn't seem to like them
247
                    $polyline = [
248
                        'points'  => [
249
                            $sosa_points[$sosa_child],
250
                            [$latitude, $longitude],
251
                        ],
252
                        'options' => [
253
                            'color' => $color,
254
                        ],
255
                    ];
256
                }
257
                $geojson['features'][] = [
258
                    'type'       => 'Feature',
259
                    'id'         => $sosa,
260
                    'geometry'   => [
261
                        'type'        => 'Point',
262
                        'coordinates' => [$longitude, $latitude],
263
                    ],
264
                    'properties' => [
265
                        'polyline'  => $polyline,
266
                        'iconcolor' => $color,
267
                        'tooltip'   => null,
268
                        'summary'   => view('modules/pedigree-map/events', [
269
                            'class'        => $class,
270
                            'fact'         => $fact,
271
                            'relationship' => $this->getSosaName($sosa),
272
                            'sosa'         => $sosa,
273
                        ]),
274
                    ],
275
                ];
276
            }
277
        }
278
279
        return $geojson;
280
    }
281
282
    /**
283
     * @param ServerRequestInterface $request
284
     * @param ChartService           $chart_service
285
     *
286
     * @return array<Fact>
287
     */
288
    protected function getPedigreeMapFacts(ServerRequestInterface $request, ChartService $chart_service): array
289
    {
290
        $tree        = Validator::attributes($request)->tree();
291
        $generations = Validator::attributes($request)->isBetween(self::MINIMUM_GENERATIONS, self::MAXIMUM_GENERATIONS)->integer('generations');
292
        $xref        = Validator::attributes($request)->isXref()->string('xref');
293
        $individual  = Registry::individualFactory()->make($xref, $tree);
294
        $individual  = Auth::checkIndividualAccess($individual, false, true);
295
        $ancestors   = $chart_service->sosaStradonitzAncestors($individual, $generations);
296
        $facts       = [];
297
298
        foreach ($ancestors as $sosa => $person) {
299
            if ($person->canShow()) {
300
                $birth = $person->facts(Gedcom::BIRTH_EVENTS, true)
301
                    ->first(static fn (Fact $fact): bool => $fact->place()->gedcomName() !== '');
302
303
                if ($birth instanceof Fact) {
304
                    $facts[$sosa] = $birth;
305
                }
306
            }
307
        }
308
309
        return $facts;
310
    }
311
312
    /**
313
     * builds and returns sosa relationship name in the active language
314
     *
315
     * @param int $sosa Sosa number
316
     *
317
     * @return string
318
     */
319
    protected function getSosaName(int $sosa): string
320
    {
321
        $path = '';
322
323
        while ($sosa > 1) {
324
            if ($sosa % 2 === 1) {
325
                $path = 'mot' . $path;
326
            } else {
327
                $path = 'fat' . $path;
328
            }
329
            $sosa = intdiv($sosa, 2);
330
        }
331
332
        return ucfirst($this->relationship_service->legacyNameAlgorithm($path));
333
    }
334
}
335