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