167992b6aSRichard Cissee<?php 23976b470SGreg Roach 367992b6aSRichard Cissee/** 467992b6aSRichard Cissee * webtrees: online genealogy 590949315SGreg Roach * Copyright (C) 2021 webtrees development team 667992b6aSRichard Cissee * This program is free software: you can redistribute it and/or modify 767992b6aSRichard Cissee * it under the terms of the GNU General Public License as published by 867992b6aSRichard Cissee * the Free Software Foundation, either version 3 of the License, or 967992b6aSRichard Cissee * (at your option) any later version. 1067992b6aSRichard Cissee * This program is distributed in the hope that it will be useful, 1167992b6aSRichard Cissee * but WITHOUT ANY WARRANTY; without even the implied warranty of 1267992b6aSRichard Cissee * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 1367992b6aSRichard Cissee * GNU General Public License for more details. 1467992b6aSRichard Cissee * You should have received a copy of the GNU General Public License 1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>. 1667992b6aSRichard Cissee */ 17fcfa147eSGreg Roach 1867992b6aSRichard Cisseedeclare(strict_types=1); 1967992b6aSRichard Cissee 2067992b6aSRichard Cisseenamespace Fisharebest\Webtrees\Module; 2167992b6aSRichard Cissee 224b2f1dbbSGreg Roachuse Aura\Router\RouterContainer; 2367992b6aSRichard Cisseeuse Fisharebest\Webtrees\Auth; 244b2f1dbbSGreg Roachuse Fisharebest\Webtrees\Contracts\UserInterface; 2567992b6aSRichard Cisseeuse Fisharebest\Webtrees\I18N; 264b2f1dbbSGreg Roachuse Fisharebest\Webtrees\Place; 274b2f1dbbSGreg Roachuse Fisharebest\Webtrees\PlaceLocation; 284b2f1dbbSGreg Roachuse Fisharebest\Webtrees\Services\ModuleService; 294b2f1dbbSGreg Roachuse Fisharebest\Webtrees\Services\SearchService; 304b2f1dbbSGreg Roachuse Fisharebest\Webtrees\Services\UserService; 314b2f1dbbSGreg Roachuse Fisharebest\Webtrees\Site; 324b2f1dbbSGreg Roachuse Fisharebest\Webtrees\Statistics; 335229eadeSGreg Roachuse Fisharebest\Webtrees\Tree; 344b2f1dbbSGreg Roachuse Fisharebest\Webtrees\Webtrees; 354b2f1dbbSGreg Roachuse Illuminate\Database\Capsule\Manager as DB; 366ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface; 376ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface; 384b2f1dbbSGreg Roachuse Psr\Http\Server\RequestHandlerInterface; 39f3874e19SGreg Roach 404b2f1dbbSGreg Roachuse function array_chunk; 414b2f1dbbSGreg Roachuse function array_pop; 424b2f1dbbSGreg Roachuse function array_reverse; 435229eadeSGreg Roachuse function assert; 444b2f1dbbSGreg Roachuse function ceil; 454b2f1dbbSGreg Roachuse function count; 464b2f1dbbSGreg Roachuse function is_file; 474b2f1dbbSGreg Roachuse function redirect; 484b2f1dbbSGreg Roachuse function route; 494b2f1dbbSGreg Roachuse function view; 5067992b6aSRichard Cissee 5167992b6aSRichard Cissee/** 5267992b6aSRichard Cissee * Class IndividualListModule 5367992b6aSRichard Cissee */ 544b2f1dbbSGreg Roachclass PlaceHierarchyListModule extends AbstractModule implements ModuleListInterface, RequestHandlerInterface 5567992b6aSRichard Cissee{ 5667992b6aSRichard Cissee use ModuleListTrait; 5767992b6aSRichard Cissee 584b2f1dbbSGreg Roach protected const ROUTE_URL = '/tree/{tree}/place-list'; 594b2f1dbbSGreg Roach 604b2f1dbbSGreg Roach /** @var int The default access level for this module. It can be changed in the control panel. */ 614b2f1dbbSGreg Roach protected $access_level = Auth::PRIV_USER; 624b2f1dbbSGreg Roach 634b2f1dbbSGreg Roach /** @var SearchService */ 644b2f1dbbSGreg Roach private $search_service; 654b2f1dbbSGreg Roach 664b2f1dbbSGreg Roach /** 674b2f1dbbSGreg Roach * PlaceHierarchy constructor. 684b2f1dbbSGreg Roach * 694b2f1dbbSGreg Roach * @param SearchService $search_service 704b2f1dbbSGreg Roach */ 714b2f1dbbSGreg Roach public function __construct(SearchService $search_service) 724b2f1dbbSGreg Roach { 734b2f1dbbSGreg Roach $this->search_service = $search_service; 744b2f1dbbSGreg Roach } 754b2f1dbbSGreg Roach 764b2f1dbbSGreg Roach /** 774b2f1dbbSGreg Roach * Initialization. 784b2f1dbbSGreg Roach * 794b2f1dbbSGreg Roach * @return void 804b2f1dbbSGreg Roach */ 814b2f1dbbSGreg Roach public function boot(): void 824b2f1dbbSGreg Roach { 834b2f1dbbSGreg Roach $router_container = app(RouterContainer::class); 844b2f1dbbSGreg Roach assert($router_container instanceof RouterContainer); 854b2f1dbbSGreg Roach 864b2f1dbbSGreg Roach $router_container->getMap() 874b2f1dbbSGreg Roach ->get(static::class, static::ROUTE_URL, $this); 884b2f1dbbSGreg Roach } 894b2f1dbbSGreg Roach 9067992b6aSRichard Cissee /** 910cfd6963SGreg Roach * How should this module be identified in the control panel, etc.? 9267992b6aSRichard Cissee * 9367992b6aSRichard Cissee * @return string 9467992b6aSRichard Cissee */ 9567992b6aSRichard Cissee public function title(): string 9667992b6aSRichard Cissee { 9767992b6aSRichard Cissee /* I18N: Name of a module/list */ 9867992b6aSRichard Cissee return I18N::translate('Place hierarchy'); 9967992b6aSRichard Cissee } 10067992b6aSRichard Cissee 10167992b6aSRichard Cissee /** 10267992b6aSRichard Cissee * A sentence describing what this module does. 10367992b6aSRichard Cissee * 10467992b6aSRichard Cissee * @return string 10567992b6aSRichard Cissee */ 10667992b6aSRichard Cissee public function description(): string 10767992b6aSRichard Cissee { 1089e0868cbSGreg Roach /* I18N: Description of the “Place hierarchy” module */ 10967992b6aSRichard Cissee return I18N::translate('The place hierarchy.'); 11067992b6aSRichard Cissee } 11167992b6aSRichard Cissee 11267992b6aSRichard Cissee /** 11367992b6aSRichard Cissee * CSS class for the URL. 11467992b6aSRichard Cissee * 11567992b6aSRichard Cissee * @return string 11667992b6aSRichard Cissee */ 11767992b6aSRichard Cissee public function listMenuClass(): string 11867992b6aSRichard Cissee { 11967992b6aSRichard Cissee return 'menu-list-plac'; 12067992b6aSRichard Cissee } 12167992b6aSRichard Cissee 1224db4b4a9SGreg Roach /** 1234b2f1dbbSGreg Roach * @param Tree $tree 1244b2f1dbbSGreg Roach * @param mixed[] $parameters 1254db4b4a9SGreg Roach * 1264b2f1dbbSGreg Roach * @return string 1274db4b4a9SGreg Roach */ 1284b2f1dbbSGreg Roach public function listUrl(Tree $tree, array $parameters = []): string 12967992b6aSRichard Cissee { 1304b2f1dbbSGreg Roach $parameters['tree'] = $tree->name(); 1315229eadeSGreg Roach 1324b2f1dbbSGreg Roach return route(static::class, $parameters); 13367992b6aSRichard Cissee } 13467992b6aSRichard Cissee 1354db4b4a9SGreg Roach /** 136*24f2a3afSGreg Roach * @return array<string> 1374db4b4a9SGreg Roach */ 13867992b6aSRichard Cissee public function listUrlAttributes(): array 13967992b6aSRichard Cissee { 14067992b6aSRichard Cissee return []; 14167992b6aSRichard Cissee } 1424b2f1dbbSGreg Roach 1434b2f1dbbSGreg Roach /** 1444b2f1dbbSGreg Roach * @param Tree $tree 1454b2f1dbbSGreg Roach * 1464b2f1dbbSGreg Roach * @return bool 1474b2f1dbbSGreg Roach */ 1484b2f1dbbSGreg Roach public function listIsEmpty(Tree $tree): bool 1494b2f1dbbSGreg Roach { 1504b2f1dbbSGreg Roach return !DB::table('places') 1514b2f1dbbSGreg Roach ->where('p_file', '=', $tree->id()) 1524b2f1dbbSGreg Roach ->exists(); 1534b2f1dbbSGreg Roach } 1544b2f1dbbSGreg Roach 1554b2f1dbbSGreg Roach /** 1564b2f1dbbSGreg Roach * Handle URLs generated by older versions of webtrees 1574b2f1dbbSGreg Roach * 1584b2f1dbbSGreg Roach * @param ServerRequestInterface $request 1594b2f1dbbSGreg Roach * 1604b2f1dbbSGreg Roach * @return ResponseInterface 1614b2f1dbbSGreg Roach */ 1624b2f1dbbSGreg Roach public function getListAction(ServerRequestInterface $request): ResponseInterface 1634b2f1dbbSGreg Roach { 1644b2f1dbbSGreg Roach return redirect($this->listUrl($request->getAttribute('tree'), $request->getQueryParams())); 1654b2f1dbbSGreg Roach } 1664b2f1dbbSGreg Roach 1674b2f1dbbSGreg Roach /** 1684b2f1dbbSGreg Roach * @param ServerRequestInterface $request 1694b2f1dbbSGreg Roach * 1704b2f1dbbSGreg Roach * @return ResponseInterface 1714b2f1dbbSGreg Roach */ 1724b2f1dbbSGreg Roach public function handle(ServerRequestInterface $request): ResponseInterface 1734b2f1dbbSGreg Roach { 1744b2f1dbbSGreg Roach $tree = $request->getAttribute('tree'); 1754b2f1dbbSGreg Roach assert($tree instanceof Tree); 1764b2f1dbbSGreg Roach 1774b2f1dbbSGreg Roach $user = $request->getAttribute('user'); 1784b2f1dbbSGreg Roach assert($user instanceof UserInterface); 1794b2f1dbbSGreg Roach 1804b2f1dbbSGreg Roach Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user); 1814b2f1dbbSGreg Roach 1824b2f1dbbSGreg Roach $action2 = $request->getQueryParams()['action2'] ?? 'hierarchy'; 1834b2f1dbbSGreg Roach $place_id = (int) ($request->getQueryParams()['place_id'] ?? 0); 1844b2f1dbbSGreg Roach $place = Place::find($place_id, $tree); 1854b2f1dbbSGreg Roach 1864b2f1dbbSGreg Roach // Request for a non-existent place? 1874b2f1dbbSGreg Roach if ($place_id !== $place->id()) { 1884b2f1dbbSGreg Roach return redirect($place->url()); 1894b2f1dbbSGreg Roach } 1904b2f1dbbSGreg Roach 1914b2f1dbbSGreg Roach $content = ''; 1924b2f1dbbSGreg Roach $showmap = Site::getPreference('map-provider') !== ''; 1934b2f1dbbSGreg Roach $data = null; 1944b2f1dbbSGreg Roach 1954b2f1dbbSGreg Roach if ($showmap) { 1964b2f1dbbSGreg Roach $content .= view('modules/place-hierarchy/map', [ 1974b2f1dbbSGreg Roach 'data' => $this->mapData($tree, $place), 1984b2f1dbbSGreg Roach 'provider' => [ 1994b2f1dbbSGreg Roach 'url' => 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 2004b2f1dbbSGreg Roach 'options' => [ 2014b2f1dbbSGreg Roach 'attribution' => '<a href="https://www.openstreetmap.org/copyright">© OpenStreetMap</a> contributors', 2024b2f1dbbSGreg Roach 'max_zoom' => 19 2034b2f1dbbSGreg Roach ] 2044b2f1dbbSGreg Roach ] 2054b2f1dbbSGreg Roach ]); 2064b2f1dbbSGreg Roach } 2074b2f1dbbSGreg Roach 2084b2f1dbbSGreg Roach switch ($action2) { 2094b2f1dbbSGreg Roach case 'list': 2104b2f1dbbSGreg Roach default: 2114b2f1dbbSGreg Roach $alt_link = I18N::translate('Show place hierarchy'); 2124b2f1dbbSGreg Roach $alt_url = $this->listUrl($tree, ['action2' => 'hierarchy', 'place_id' => $place_id]); 2134b2f1dbbSGreg Roach $content .= view('modules/place-hierarchy/list', ['columns' => $this->getList($tree)]); 2144b2f1dbbSGreg Roach break; 2154b2f1dbbSGreg Roach case 'hierarchy': 2164b2f1dbbSGreg Roach case 'hierarchy-e': 2174b2f1dbbSGreg Roach $alt_link = I18N::translate('Show all places in a list'); 2184b2f1dbbSGreg Roach $alt_url = $this->listUrl($tree, ['action2' => 'list', 'place_id' => 0]); 2194b2f1dbbSGreg Roach $data = $this->getHierarchy($place); 2204b2f1dbbSGreg Roach $content .= (null === $data || $showmap) ? '' : view('place-hierarchy', $data); 2214b2f1dbbSGreg Roach if (null === $data || $action2 === 'hierarchy-e') { 2224b2f1dbbSGreg Roach $content .= view('modules/place-hierarchy/events', [ 2234b2f1dbbSGreg Roach 'indilist' => $this->search_service->searchIndividualsInPlace($place), 2244b2f1dbbSGreg Roach 'famlist' => $this->search_service->searchFamiliesInPlace($place), 2254b2f1dbbSGreg Roach 'tree' => $place->tree(), 2264b2f1dbbSGreg Roach ]); 2274b2f1dbbSGreg Roach } 2284b2f1dbbSGreg Roach } 2294b2f1dbbSGreg Roach 2304b2f1dbbSGreg Roach if ($data !== null && $action2 !== 'hierarchy-e' && $place->gedcomName() !== '') { 2314b2f1dbbSGreg Roach $events_link = $this->listUrl($tree, ['action2' => 'hierarchy-e', 'place_id' => $place_id]); 2324b2f1dbbSGreg Roach } else { 2334b2f1dbbSGreg Roach $events_link = ''; 2344b2f1dbbSGreg Roach } 2354b2f1dbbSGreg Roach 2364b2f1dbbSGreg Roach $breadcrumbs = $this->breadcrumbs($place); 2374b2f1dbbSGreg Roach 2384b2f1dbbSGreg Roach return $this->viewResponse('modules/place-hierarchy/page', [ 2394b2f1dbbSGreg Roach 'alt_link' => $alt_link, 2404b2f1dbbSGreg Roach 'alt_url' => $alt_url, 2414b2f1dbbSGreg Roach 'breadcrumbs' => $breadcrumbs['breadcrumbs'], 2424b2f1dbbSGreg Roach 'content' => $content, 2434b2f1dbbSGreg Roach 'current' => $breadcrumbs['current'], 2444b2f1dbbSGreg Roach 'events_link' => $events_link, 2454b2f1dbbSGreg Roach 'place' => $place, 2464b2f1dbbSGreg Roach 'title' => I18N::translate('Place hierarchy'), 2474b2f1dbbSGreg Roach 'tree' => $tree, 2484b2f1dbbSGreg Roach 'world_url' => $this->listUrl($tree) 2494b2f1dbbSGreg Roach ]); 2504b2f1dbbSGreg Roach } 2514b2f1dbbSGreg Roach 2524b2f1dbbSGreg Roach /** 2534b2f1dbbSGreg Roach * @param Tree $tree 2544b2f1dbbSGreg Roach * 2554b2f1dbbSGreg Roach * @return array<array<Place>> 2564b2f1dbbSGreg Roach */ 2574b2f1dbbSGreg Roach private function getList(Tree $tree): array 2584b2f1dbbSGreg Roach { 2594b2f1dbbSGreg Roach $places = $this->search_service->searchPlaces($tree, '') 2604b2f1dbbSGreg Roach ->sort(static function (Place $x, Place $y): int { 2614b2f1dbbSGreg Roach return $x->gedcomName() <=> $y->gedcomName(); 2624b2f1dbbSGreg Roach }) 2634b2f1dbbSGreg Roach ->all(); 2644b2f1dbbSGreg Roach 2654b2f1dbbSGreg Roach $count = count($places); 2664b2f1dbbSGreg Roach 2674b2f1dbbSGreg Roach if ($places === []) { 2684b2f1dbbSGreg Roach return []; 2694b2f1dbbSGreg Roach } 2704b2f1dbbSGreg Roach 2714b2f1dbbSGreg Roach $columns = $count > 20 ? 3 : 2; 2724b2f1dbbSGreg Roach 2734b2f1dbbSGreg Roach return array_chunk($places, (int) ceil($count / $columns)); 2744b2f1dbbSGreg Roach } 2754b2f1dbbSGreg Roach 2764b2f1dbbSGreg Roach 2774b2f1dbbSGreg Roach /** 2784b2f1dbbSGreg Roach * @param Place $place 2794b2f1dbbSGreg Roach * 2804b2f1dbbSGreg Roach * @return array{'tree':Tree,'col_class':string,'columns':array<array<Place>>,'place':Place}|null 2814b2f1dbbSGreg Roach */ 2824b2f1dbbSGreg Roach private function getHierarchy(Place $place): ?array 2834b2f1dbbSGreg Roach { 2844b2f1dbbSGreg Roach $child_places = $place->getChildPlaces(); 2854b2f1dbbSGreg Roach $numfound = count($child_places); 2864b2f1dbbSGreg Roach 2874b2f1dbbSGreg Roach if ($numfound > 0) { 2884b2f1dbbSGreg Roach $divisor = $numfound > 20 ? 3 : 2; 2894b2f1dbbSGreg Roach 2904b2f1dbbSGreg Roach return [ 2914b2f1dbbSGreg Roach 'tree' => $place->tree(), 2924b2f1dbbSGreg Roach 'col_class' => 'w-' . ($divisor === 2 ? '25' : '50'), 2934b2f1dbbSGreg Roach 'columns' => array_chunk($child_places, (int) ceil($numfound / $divisor)), 2944b2f1dbbSGreg Roach 'place' => $place, 2954b2f1dbbSGreg Roach ]; 2964b2f1dbbSGreg Roach } 2974b2f1dbbSGreg Roach 2984b2f1dbbSGreg Roach return null; 2994b2f1dbbSGreg Roach } 3004b2f1dbbSGreg Roach 3014b2f1dbbSGreg Roach /** 3024b2f1dbbSGreg Roach * @param Place $place 3034b2f1dbbSGreg Roach * 3044b2f1dbbSGreg Roach * @return array{'breadcrumbs':array<Place>,'current':Place|null} 3054b2f1dbbSGreg Roach */ 3064b2f1dbbSGreg Roach private function breadcrumbs(Place $place): array 3074b2f1dbbSGreg Roach { 3084b2f1dbbSGreg Roach $breadcrumbs = []; 3094b2f1dbbSGreg Roach if ($place->gedcomName() !== '') { 3104b2f1dbbSGreg Roach $breadcrumbs[] = $place; 3114b2f1dbbSGreg Roach $parent_place = $place->parent(); 3124b2f1dbbSGreg Roach while ($parent_place->gedcomName() !== '') { 3134b2f1dbbSGreg Roach $breadcrumbs[] = $parent_place; 3144b2f1dbbSGreg Roach $parent_place = $parent_place->parent(); 3154b2f1dbbSGreg Roach } 3164b2f1dbbSGreg Roach $breadcrumbs = array_reverse($breadcrumbs); 3174b2f1dbbSGreg Roach $current = array_pop($breadcrumbs); 3184b2f1dbbSGreg Roach } else { 3194b2f1dbbSGreg Roach $current = null; 3204b2f1dbbSGreg Roach } 3214b2f1dbbSGreg Roach 3224b2f1dbbSGreg Roach return [ 3234b2f1dbbSGreg Roach 'breadcrumbs' => $breadcrumbs, 3244b2f1dbbSGreg Roach 'current' => $current, 3254b2f1dbbSGreg Roach ]; 3264b2f1dbbSGreg Roach } 3274b2f1dbbSGreg Roach 3284b2f1dbbSGreg Roach /** 3294b2f1dbbSGreg Roach * @param Tree $tree 3304b2f1dbbSGreg Roach * @param Place $placeObj 3314b2f1dbbSGreg Roach * 3324b2f1dbbSGreg Roach * @return array<mixed> 3334b2f1dbbSGreg Roach */ 3344b2f1dbbSGreg Roach protected function mapData(Tree $tree, Place $placeObj): array 3354b2f1dbbSGreg Roach { 3364b2f1dbbSGreg Roach $places = $placeObj->getChildPlaces(); 3374b2f1dbbSGreg Roach $features = []; 3384b2f1dbbSGreg Roach $sidebar = ''; 3394b2f1dbbSGreg Roach $show_link = true; 3404b2f1dbbSGreg Roach 3414b2f1dbbSGreg Roach if ($places === []) { 3424b2f1dbbSGreg Roach $places[] = $placeObj; 3434b2f1dbbSGreg Roach $show_link = false; 3444b2f1dbbSGreg Roach } 3454b2f1dbbSGreg Roach 3464b2f1dbbSGreg Roach foreach ($places as $id => $place) { 3474b2f1dbbSGreg Roach $location = new PlaceLocation($place->gedcomName()); 3484b2f1dbbSGreg Roach 34990949315SGreg Roach if ($location->latitude() === null || $location->longitude() === null) { 3504b2f1dbbSGreg Roach $sidebar_class = 'unmapped'; 3514b2f1dbbSGreg Roach } else { 3524b2f1dbbSGreg Roach $sidebar_class = 'mapped'; 3534b2f1dbbSGreg Roach $features[] = [ 3544b2f1dbbSGreg Roach 'type' => 'Feature', 3554b2f1dbbSGreg Roach 'id' => $id, 3564b2f1dbbSGreg Roach 'geometry' => [ 3574b2f1dbbSGreg Roach 'type' => 'Point', 3584b2f1dbbSGreg Roach 'coordinates' => [$location->longitude(), $location->latitude()], 3594b2f1dbbSGreg Roach ], 3604b2f1dbbSGreg Roach 'properties' => [ 3614b2f1dbbSGreg Roach 'tooltip' => $place->gedcomName(), 3624b2f1dbbSGreg Roach 'popup' => view('modules/place-hierarchy/popup', [ 3634b2f1dbbSGreg Roach 'showlink' => $show_link, 3644b2f1dbbSGreg Roach 'place' => $place, 3654b2f1dbbSGreg Roach 'latitude' => $location->latitude(), 3664b2f1dbbSGreg Roach 'longitude' => $location->longitude(), 3674b2f1dbbSGreg Roach ]), 3684b2f1dbbSGreg Roach ], 3694b2f1dbbSGreg Roach ]; 3704b2f1dbbSGreg Roach } 3714b2f1dbbSGreg Roach 3724b2f1dbbSGreg Roach $statistics = new Statistics(app(ModuleService::class), $tree, app(UserService::class)); 3734b2f1dbbSGreg Roach 3744b2f1dbbSGreg Roach //Stats 3754b2f1dbbSGreg Roach $placeStats = []; 3764b2f1dbbSGreg Roach foreach (['INDI', 'FAM'] as $type) { 3774b2f1dbbSGreg Roach $tmp = $statistics->statsPlaces($type, '', $place->id()); 3784b2f1dbbSGreg Roach $placeStats[$type] = $tmp === [] ? 0 : $tmp[0]->tot; 3794b2f1dbbSGreg Roach } 3804b2f1dbbSGreg Roach $sidebar .= view('modules/place-hierarchy/sidebar', [ 3814b2f1dbbSGreg Roach 'showlink' => $show_link, 3824b2f1dbbSGreg Roach 'id' => $id, 3834b2f1dbbSGreg Roach 'place' => $place, 3844b2f1dbbSGreg Roach 'sidebar_class' => $sidebar_class, 3854b2f1dbbSGreg Roach 'stats' => $placeStats, 3864b2f1dbbSGreg Roach ]); 3874b2f1dbbSGreg Roach } 3884b2f1dbbSGreg Roach 3894b2f1dbbSGreg Roach return [ 3904b2f1dbbSGreg Roach 'bounds' => (new PlaceLocation($placeObj->gedcomName()))->boundingRectangle(), 3914b2f1dbbSGreg Roach 'sidebar' => $sidebar, 3924b2f1dbbSGreg Roach 'markers' => [ 3934b2f1dbbSGreg Roach 'type' => 'FeatureCollection', 3944b2f1dbbSGreg Roach 'features' => $features, 3954b2f1dbbSGreg Roach ] 3964b2f1dbbSGreg Roach ]; 3974b2f1dbbSGreg Roach } 39867992b6aSRichard Cissee} 399