1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2022 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\Family; 25use Fisharebest\Webtrees\I18N; 26use Fisharebest\Webtrees\Individual; 27use Fisharebest\Webtrees\Location; 28use Fisharebest\Webtrees\Place; 29use Fisharebest\Webtrees\PlaceLocation; 30use Fisharebest\Webtrees\Registry; 31use Fisharebest\Webtrees\Services\LeafletJsService; 32use Fisharebest\Webtrees\Services\ModuleService; 33use Fisharebest\Webtrees\Services\SearchService; 34use Fisharebest\Webtrees\Statistics; 35use Fisharebest\Webtrees\Tree; 36use Fisharebest\Webtrees\Validator; 37use Illuminate\Database\Capsule\Manager as DB; 38use Psr\Http\Message\ResponseInterface; 39use Psr\Http\Message\ServerRequestInterface; 40use Psr\Http\Server\RequestHandlerInterface; 41 42use function app; 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 int $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 Registry::routeFactory()->routeMap() 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 $tree = Validator::attributes($request)->tree(); 158 159 return redirect($this->listUrl($tree, $request->getQueryParams())); 160 } 161 162 /** 163 * @param Tree $tree 164 * @param array<bool|int|string|array<string>|null> $parameters 165 * 166 * @return string 167 */ 168 public function listUrl(Tree $tree, array $parameters = []): string 169 { 170 $parameters['tree'] = $tree->name(); 171 172 return route(static::class, $parameters); 173 } 174 175 /** 176 * @param ServerRequestInterface $request 177 * 178 * @return ResponseInterface 179 */ 180 public function handle(ServerRequestInterface $request): ResponseInterface 181 { 182 $tree = Validator::attributes($request)->tree(); 183 $user = Validator::attributes($request)->user(); 184 185 Auth::checkComponentAccess($this, ModuleListInterface::class, $tree, $user); 186 187 $action2 = $request->getQueryParams()['action2'] ?? 'hierarchy'; 188 $place_id = (int) ($request->getQueryParams()['place_id'] ?? 0); 189 $place = Place::find($place_id, $tree); 190 191 // Request for a non-existent place? 192 if ($place_id !== $place->id()) { 193 return redirect($place->url()); 194 } 195 196 $map_providers = $this->module_service->findByInterface(ModuleMapProviderInterface::class); 197 198 $content = ''; 199 $showmap = $map_providers->isNotEmpty(); 200 $data = null; 201 202 if ($showmap) { 203 $content .= view('modules/place-hierarchy/map', [ 204 'data' => $this->mapData($tree, $place), 205 'leaflet_config' => $this->leaflet_js_service->config(), 206 ]); 207 } 208 209 switch ($action2) { 210 case 'list': 211 default: 212 $alt_link = I18N::translate('Show place hierarchy'); 213 $alt_url = $this->listUrl($tree, ['action2' => 'hierarchy', 'place_id' => $place_id]); 214 $content .= view('modules/place-hierarchy/list', ['columns' => $this->getList($tree)]); 215 break; 216 case 'hierarchy': 217 case 'hierarchy-e': 218 $alt_link = I18N::translate('Show all places in a list'); 219 $alt_url = $this->listUrl($tree, ['action2' => 'list', 'place_id' => 0]); 220 $data = $this->getHierarchy($place); 221 $content .= ($data === null || $showmap) ? '' : view('place-hierarchy', $data); 222 if ($data === null || $action2 === 'hierarchy-e') { 223 $content .= view('modules/place-hierarchy/events', [ 224 'indilist' => $this->search_service->searchIndividualsInPlace($place), 225 'famlist' => $this->search_service->searchFamiliesInPlace($place), 226 'tree' => $place->tree(), 227 ]); 228 } 229 } 230 231 if ($data !== null && $action2 !== 'hierarchy-e' && $place->gedcomName() !== '') { 232 $events_link = $this->listUrl($tree, ['action2' => 'hierarchy-e', 'place_id' => $place_id]); 233 } else { 234 $events_link = ''; 235 } 236 237 $breadcrumbs = $this->breadcrumbs($place); 238 239 return $this->viewResponse('modules/place-hierarchy/page', [ 240 'alt_link' => $alt_link, 241 'alt_url' => $alt_url, 242 'breadcrumbs' => $breadcrumbs['breadcrumbs'], 243 'content' => $content, 244 'current' => $breadcrumbs['current'], 245 'events_link' => $events_link, 246 'place' => $place, 247 'title' => I18N::translate('Place hierarchy'), 248 'tree' => $tree, 249 'world_url' => $this->listUrl($tree), 250 ]); 251 } 252 253 /** 254 * @param Tree $tree 255 * @param Place $placeObj 256 * 257 * @return array<mixed> 258 */ 259 protected function mapData(Tree $tree, Place $placeObj): array 260 { 261 $places = $placeObj->getChildPlaces(); 262 $features = []; 263 $sidebar = ''; 264 $show_link = true; 265 266 if ($places === []) { 267 $places[] = $placeObj; 268 $show_link = false; 269 } 270 271 foreach ($places as $id => $place) { 272 $location = new PlaceLocation($place->gedcomName()); 273 274 if ($location->latitude() === null || $location->longitude() === null) { 275 $sidebar_class = 'unmapped'; 276 } else { 277 $sidebar_class = 'mapped'; 278 $features[] = [ 279 'type' => 'Feature', 280 'id' => $id, 281 'geometry' => [ 282 'type' => 'Point', 283 'coordinates' => [$location->longitude(), $location->latitude()], 284 ], 285 'properties' => [ 286 'tooltip' => $place->gedcomName(), 287 'popup' => view('modules/place-hierarchy/popup', [ 288 'showlink' => $show_link, 289 'place' => $place, 290 'latitude' => $location->latitude(), 291 'longitude' => $location->longitude(), 292 ]), 293 ], 294 ]; 295 } 296 297 $statistics = app(Statistics::class); 298 299 //Stats 300 $stats = []; 301 foreach ([Individual::RECORD_TYPE, Family::RECORD_TYPE, Location::RECORD_TYPE] as $type) { 302 $tmp = $statistics->statsPlaces($type, '', $place->id()); 303 $stats[$type] = $tmp === [] ? 0 : $tmp[0]->tot; 304 } 305 $sidebar .= view('modules/place-hierarchy/sidebar', [ 306 'showlink' => $show_link, 307 'id' => $id, 308 'place' => $place, 309 'sidebar_class' => $sidebar_class, 310 'stats' => $stats, 311 ]); 312 } 313 314 return [ 315 'bounds' => (new PlaceLocation($placeObj->gedcomName()))->boundingRectangle(), 316 'sidebar' => $sidebar, 317 'markers' => [ 318 'type' => 'FeatureCollection', 319 'features' => $features, 320 ], 321 ]; 322 } 323 324 /** 325 * @param Tree $tree 326 * 327 * @return array<array<Place>> 328 */ 329 private function getList(Tree $tree): array 330 { 331 $places = $this->search_service->searchPlaces($tree, '') 332 ->sort(static function (Place $x, Place $y): int { 333 return $x->gedcomName() <=> $y->gedcomName(); 334 }) 335 ->all(); 336 337 $count = count($places); 338 339 if ($places === []) { 340 return []; 341 } 342 343 $columns = $count > 20 ? 3 : 2; 344 345 return array_chunk($places, (int) ceil($count / $columns)); 346 } 347 348 /** 349 * @param Place $place 350 * 351 * @return array{'tree':Tree,'col_class':string,'columns':array<array<Place>>,'place':Place}|null 352 */ 353 private function getHierarchy(Place $place): ?array 354 { 355 $child_places = $place->getChildPlaces(); 356 $numfound = count($child_places); 357 358 if ($numfound > 0) { 359 $divisor = $numfound > 20 ? 3 : 2; 360 361 return [ 362 'tree' => $place->tree(), 363 'col_class' => 'w-' . ($divisor === 2 ? '25' : '50'), 364 'columns' => array_chunk($child_places, (int) ceil($numfound / $divisor)), 365 'place' => $place, 366 ]; 367 } 368 369 return null; 370 } 371 372 /** 373 * @param Place $place 374 * 375 * @return array{'breadcrumbs':array<Place>,'current':Place|null} 376 */ 377 private function breadcrumbs(Place $place): array 378 { 379 $breadcrumbs = []; 380 if ($place->gedcomName() !== '') { 381 $breadcrumbs[] = $place; 382 $parent_place = $place->parent(); 383 while ($parent_place->gedcomName() !== '') { 384 $breadcrumbs[] = $parent_place; 385 $parent_place = $parent_place->parent(); 386 } 387 $breadcrumbs = array_reverse($breadcrumbs); 388 $current = array_pop($breadcrumbs); 389 } else { 390 $current = null; 391 } 392 393 return [ 394 'breadcrumbs' => $breadcrumbs, 395 'current' => $current, 396 ]; 397 } 398} 399