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