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