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