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">© 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