xref: /webtrees/app/Module/PlaceHierarchyListModule.php (revision 22c135fa2a9d8aaf68a70032269940d5314a9818)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2021 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
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees\Module;
21
22use Aura\Router\RouterContainer;
23use Fisharebest\Webtrees\Auth;
24use Fisharebest\Webtrees\Contracts\UserInterface;
25use Fisharebest\Webtrees\I18N;
26use Fisharebest\Webtrees\Place;
27use Fisharebest\Webtrees\PlaceLocation;
28use Fisharebest\Webtrees\Services\ModuleService;
29use Fisharebest\Webtrees\Services\SearchService;
30use Fisharebest\Webtrees\Services\UserService;
31use Fisharebest\Webtrees\Site;
32use Fisharebest\Webtrees\Statistics;
33use Fisharebest\Webtrees\Tree;
34use Illuminate\Database\Capsule\Manager as DB;
35use Psr\Http\Message\ResponseInterface;
36use Psr\Http\Message\ServerRequestInterface;
37use Psr\Http\Server\RequestHandlerInterface;
38
39use function array_chunk;
40use function array_pop;
41use function array_reverse;
42use function assert;
43use function ceil;
44use function count;
45use function redirect;
46use function route;
47use function view;
48
49/**
50 * Class IndividualListModule
51 */
52class PlaceHierarchyListModule extends AbstractModule implements ModuleListInterface, RequestHandlerInterface
53{
54    use ModuleListTrait;
55
56    protected const ROUTE_URL = '/tree/{tree}/place-list';
57
58    /** @var int The default access level for this module.  It can be changed in the control panel. */
59    protected $access_level = Auth::PRIV_USER;
60
61    /** @var SearchService */
62    private $search_service;
63
64    /**
65     * PlaceHierarchy constructor.
66     *
67     * @param SearchService $search_service
68     */
69    public function __construct(SearchService $search_service)
70    {
71        $this->search_service = $search_service;
72    }
73
74    /**
75     * Initialization.
76     *
77     * @return void
78     */
79    public function boot(): void
80    {
81        $router_container = app(RouterContainer::class);
82        assert($router_container instanceof RouterContainer);
83
84        $router_container->getMap()
85            ->get(static::class, static::ROUTE_URL, $this);
86    }
87
88    /**
89     * How should this module be identified in the control panel, etc.?
90     *
91     * @return string
92     */
93    public function title(): string
94    {
95        /* I18N: Name of a module/list */
96        return I18N::translate('Place hierarchy');
97    }
98
99    /**
100     * A sentence describing what this module does.
101     *
102     * @return string
103     */
104    public function description(): string
105    {
106        /* I18N: Description of the “Place hierarchy” module */
107        return I18N::translate('The place hierarchy.');
108    }
109
110    /**
111     * CSS class for the URL.
112     *
113     * @return string
114     */
115    public function listMenuClass(): string
116    {
117        return 'menu-list-plac';
118    }
119
120    /**
121     * @param Tree    $tree
122     * @param mixed[] $parameters
123     *
124     * @return string
125     */
126    public function listUrl(Tree $tree, array $parameters = []): string
127    {
128        $parameters['tree'] = $tree->name();
129
130        return route(static::class, $parameters);
131    }
132
133    /**
134     * @return array<string>
135     */
136    public function listUrlAttributes(): array
137    {
138        return [];
139    }
140
141    /**
142     * @param Tree $tree
143     *
144     * @return bool
145     */
146    public function listIsEmpty(Tree $tree): bool
147    {
148        return !DB::table('places')
149            ->where('p_file', '=', $tree->id())
150            ->exists();
151    }
152
153    /**
154     * Handle URLs generated by older versions of webtrees
155     *
156     * @param ServerRequestInterface $request
157     *
158     * @return ResponseInterface
159     */
160    public function getListAction(ServerRequestInterface $request): ResponseInterface
161    {
162        return redirect($this->listUrl($request->getAttribute('tree'), $request->getQueryParams()));
163    }
164
165    /**
166     * @param ServerRequestInterface $request
167     *
168     * @return ResponseInterface
169     */
170    public function handle(ServerRequestInterface $request): ResponseInterface
171    {
172        $tree = $request->getAttribute('tree');
173        assert($tree instanceof Tree);
174
175        $user = $request->getAttribute('user');
176        assert($user instanceof UserInterface);
177
178        Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user);
179
180        $action2  = $request->getQueryParams()['action2'] ?? 'hierarchy';
181        $place_id = (int) ($request->getQueryParams()['place_id'] ?? 0);
182        $place    = Place::find($place_id, $tree);
183
184        // Request for a non-existent place?
185        if ($place_id !== $place->id()) {
186            return redirect($place->url());
187        }
188
189        $content = '';
190        $showmap = Site::getPreference('map-provider') !== '';
191        $data    = null;
192
193        if ($showmap) {
194            $content .= view('modules/place-hierarchy/map', [
195                'data'     => $this->mapData($tree, $place),
196                'provider' => [
197                    'url'    => 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
198                    'options' => [
199                        'attribution' => '<a href="https://www.openstreetmap.org/copyright">&copy; OpenStreetMap</a> contributors',
200                        'max_zoom'    => 19
201                    ]
202                ]
203            ]);
204        }
205
206        switch ($action2) {
207            case 'list':
208            default:
209                $alt_link = I18N::translate('Show place hierarchy');
210                $alt_url  = $this->listUrl($tree, ['action2' => 'hierarchy', 'place_id' => $place_id]);
211                $content .= view('modules/place-hierarchy/list', ['columns' => $this->getList($tree)]);
212                break;
213            case 'hierarchy':
214            case 'hierarchy-e':
215                $alt_link = I18N::translate('Show all places in a list');
216                $alt_url  = $this->listUrl($tree, ['action2' => 'list', 'place_id' => 0]);
217                $data     = $this->getHierarchy($place);
218                $content .= (null === $data || $showmap) ? '' : view('place-hierarchy', $data);
219                if (null === $data || $action2 === 'hierarchy-e') {
220                    $content .= view('modules/place-hierarchy/events', [
221                        'indilist' => $this->search_service->searchIndividualsInPlace($place),
222                        'famlist'  => $this->search_service->searchFamiliesInPlace($place),
223                        'tree'     => $place->tree(),
224                    ]);
225                }
226        }
227
228        if ($data !== null && $action2 !== 'hierarchy-e' && $place->gedcomName() !== '') {
229            $events_link = $this->listUrl($tree, ['action2' => 'hierarchy-e', 'place_id' => $place_id]);
230        } else {
231            $events_link = '';
232        }
233
234        $breadcrumbs = $this->breadcrumbs($place);
235
236        return $this->viewResponse('modules/place-hierarchy/page', [
237            'alt_link'    => $alt_link,
238            'alt_url'     => $alt_url,
239            'breadcrumbs' => $breadcrumbs['breadcrumbs'],
240            'content'     => $content,
241            'current'     => $breadcrumbs['current'],
242            'events_link' => $events_link,
243            'place'       => $place,
244            'title'       => I18N::translate('Place hierarchy'),
245            'tree'        => $tree,
246            'world_url'   => $this->listUrl($tree)
247        ]);
248    }
249
250    /**
251     * @param Tree $tree
252     *
253     * @return array<array<Place>>
254     */
255    private function getList(Tree $tree): array
256    {
257        $places = $this->search_service->searchPlaces($tree, '')
258            ->sort(static function (Place $x, Place $y): int {
259                return $x->gedcomName() <=> $y->gedcomName();
260            })
261            ->all();
262
263        $count = count($places);
264
265        if ($places === []) {
266            return [];
267        }
268
269        $columns = $count > 20 ? 3 : 2;
270
271        return array_chunk($places, (int) ceil($count / $columns));
272    }
273
274
275    /**
276     * @param Place $place
277     *
278     * @return array{'tree':Tree,'col_class':string,'columns':array<array<Place>>,'place':Place}|null
279     */
280    private function getHierarchy(Place $place): ?array
281    {
282        $child_places = $place->getChildPlaces();
283        $numfound     = count($child_places);
284
285        if ($numfound > 0) {
286            $divisor = $numfound > 20 ? 3 : 2;
287
288            return [
289                'tree'      => $place->tree(),
290                'col_class' => 'w-' . ($divisor === 2 ? '25' : '50'),
291                'columns'   => array_chunk($child_places, (int) ceil($numfound / $divisor)),
292                'place'     => $place,
293            ];
294        }
295
296        return null;
297    }
298
299    /**
300     * @param Place $place
301     *
302     * @return array{'breadcrumbs':array<Place>,'current':Place|null}
303     */
304    private function breadcrumbs(Place $place): array
305    {
306        $breadcrumbs = [];
307        if ($place->gedcomName() !== '') {
308            $breadcrumbs[] = $place;
309            $parent_place  = $place->parent();
310            while ($parent_place->gedcomName() !== '') {
311                $breadcrumbs[] = $parent_place;
312                $parent_place  = $parent_place->parent();
313            }
314            $breadcrumbs = array_reverse($breadcrumbs);
315            $current     = array_pop($breadcrumbs);
316        } else {
317            $current = null;
318        }
319
320        return [
321            'breadcrumbs' => $breadcrumbs,
322            'current'     => $current,
323        ];
324    }
325
326    /**
327     * @param Tree  $tree
328     * @param Place $placeObj
329     *
330     * @return array<mixed>
331     */
332    protected function mapData(Tree $tree, Place $placeObj): array
333    {
334        $places    = $placeObj->getChildPlaces();
335        $features  = [];
336        $sidebar   = '';
337        $show_link = true;
338
339        if ($places === []) {
340            $places[] = $placeObj;
341            $show_link = false;
342        }
343
344        foreach ($places as $id => $place) {
345            $location = new PlaceLocation($place->gedcomName());
346
347            if ($location->latitude() === null || $location->longitude() === null) {
348                $sidebar_class = 'unmapped';
349            } else {
350                $sidebar_class = 'mapped';
351                $features[]    = [
352                    'type'       => 'Feature',
353                    'id'         => $id,
354                    'geometry'   => [
355                        'type'        => 'Point',
356                        'coordinates' => [$location->longitude(), $location->latitude()],
357                    ],
358                    'properties' => [
359                        'tooltip' => $place->gedcomName(),
360                        'popup'   => view('modules/place-hierarchy/popup', [
361                            'showlink'  => $show_link,
362                            'place'     => $place,
363                            'latitude'  => $location->latitude(),
364                            'longitude' => $location->longitude(),
365                        ]),
366                    ],
367                ];
368            }
369
370            $statistics = new Statistics(app(ModuleService::class), $tree, app(UserService::class));
371
372            //Stats
373            $placeStats = [];
374            foreach (['INDI', 'FAM'] as $type) {
375                $tmp               = $statistics->statsPlaces($type, '', $place->id());
376                $placeStats[$type] = $tmp === [] ? 0 : $tmp[0]->tot;
377            }
378            $sidebar .= view('modules/place-hierarchy/sidebar', [
379                'showlink'      => $show_link,
380                'id'            => $id,
381                'place'         => $place,
382                'sidebar_class' => $sidebar_class,
383                'stats'         => $placeStats,
384            ]);
385        }
386
387        return [
388            'bounds'  => (new PlaceLocation($placeObj->gedcomName()))->boundingRectangle(),
389            'sidebar' => $sidebar,
390            'markers' => [
391                'type'     => 'FeatureCollection',
392                'features' => $features,
393            ]
394        ];
395    }
396}
397