1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2019 webtrees development team 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * You should have received a copy of the GNU General Public License 14 * along with this program. If not, see <http://www.gnu.org/licenses/>. 15 */ 16declare(strict_types=1); 17 18namespace Fisharebest\Webtrees\Module; 19 20use Fisharebest\Webtrees\Auth; 21use Fisharebest\Webtrees\FontAwesome; 22use Fisharebest\Webtrees\Functions\FunctionsCharts; 23use Fisharebest\Webtrees\Functions\FunctionsPrint; 24use Fisharebest\Webtrees\Gedcom; 25use Fisharebest\Webtrees\I18N; 26use Fisharebest\Webtrees\Individual; 27use Fisharebest\Webtrees\Menu; 28use Fisharebest\Webtrees\Services\ChartService; 29use Fisharebest\Webtrees\Tree; 30use Fisharebest\Webtrees\User; 31use Illuminate\Support\Collection; 32use Symfony\Component\HttpFoundation\Request; 33use Symfony\Component\HttpFoundation\Response; 34use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; 35 36/** 37 * Class AncestorsChartModule 38 */ 39class AncestorsChartModule extends AbstractModule implements ModuleChartInterface 40{ 41 use ModuleChartTrait; 42 43 // Chart styles 44 protected const CHART_STYLE_LIST = 'list'; 45 protected const CHART_STYLE_BOOKLET = 'booklet'; 46 protected const CHART_STYLE_INDIVIDUALS = 'individuals'; 47 protected const CHART_STYLE_FAMILIES = 'families'; 48 49 // Defaults 50 protected const DEFAULT_COUSINS = false; 51 protected const DEFAULT_STYLE = self::CHART_STYLE_LIST; 52 protected const DEFAULT_GENERATIONS = '3'; 53 protected const DEFAULT_MAXIMUM_GENERATIONS = '9'; 54 55 /** 56 * How should this module be labelled on tabs, menus, etc.? 57 * 58 * @return string 59 */ 60 public function title(): string 61 { 62 /* I18N: Name of a module/chart */ 63 return I18N::translate('Ancestors'); 64 } 65 66 /** 67 * A sentence describing what this module does. 68 * 69 * @return string 70 */ 71 public function description(): string 72 { 73 /* I18N: Description of the “AncestorsChart” module */ 74 return I18N::translate('A chart of an individual’s ancestors.'); 75 } 76 77 /** 78 * CSS class for the URL. 79 * 80 * @return string 81 */ 82 public function chartMenuClass(): string 83 { 84 return 'menu-chart-ancestry'; 85 } 86 87 /** 88 * Return a menu item for this chart - for use in individual boxes. 89 * 90 * @param Individual $individual 91 * 92 * @return Menu|null 93 */ 94 public function chartBoxMenu(Individual $individual): ?Menu 95 { 96 return $this->chartMenu($individual); 97 } 98 99 /** 100 * The title for a specific instance of this chart. 101 * 102 * @param Individual $individual 103 * 104 * @return string 105 */ 106 public function chartTitle(Individual $individual): string 107 { 108 /* I18N: %s is an individual’s name */ 109 return I18N::translate('Ancestors of %s', $individual->getFullName()); 110 } 111 112 /** 113 * A form to request the chart parameters. 114 * 115 * @param Request $request 116 * @param Tree $tree 117 * @param User $user 118 * @param ChartService $chart_service 119 * 120 * @return Response 121 */ 122 public function getChartAction(Request $request, Tree $tree, User $user, ChartService $chart_service): Response 123 { 124 $ajax = (bool) $request->get('ajax'); 125 $xref = $request->get('xref', ''); 126 $individual = Individual::getInstance($xref, $tree); 127 128 Auth::checkIndividualAccess($individual); 129 Auth::checkComponentAccess($this, 'chart', $tree, $user); 130 131 $minimum_generations = 2; 132 $maximum_generations = (int) $tree->getPreference('MAX_PEDIGREE_GENERATIONS', self::DEFAULT_MAXIMUM_GENERATIONS); 133 $default_generations = (int) $tree->getPreference('DEFAULT_PEDIGREE_GENERATIONS', self::DEFAULT_GENERATIONS); 134 135 $show_cousins = (bool) $request->get('show_cousins', self::DEFAULT_COUSINS); 136 $chart_style = $request->get('chart_style', self::DEFAULT_STYLE); 137 $generations = (int) $request->get('generations', $default_generations); 138 139 $generations = min($generations, $maximum_generations); 140 $generations = max($generations, $minimum_generations); 141 142 if ($ajax) { 143 $ancestors = $chart_service->sosaStradonitzAncestors($individual, $generations); 144 145 switch ($chart_style) { 146 default: 147 case self::CHART_STYLE_LIST: 148 return $this->ancestorsList($individual, $generations); 149 150 case self::CHART_STYLE_BOOKLET: 151 return $this->ancestorsBooklet($ancestors, $show_cousins); 152 153 case self::CHART_STYLE_INDIVIDUALS: 154 return $this->ancestorsIndividuals($tree, $ancestors); 155 156 case self::CHART_STYLE_FAMILIES: 157 return $this->ancestorsFamilies($tree, $ancestors); 158 } 159 } 160 161 $ajax_url = $this->chartUrl($individual, [ 162 'generations' => $generations, 163 'chart_style' => $chart_style, 164 'show_cousins' => $show_cousins, 165 'ajax' => true, 166 ]); 167 168 return $this->viewResponse('modules/ancestors-chart/page', [ 169 'ajax_url' => $ajax_url, 170 'chart_style' => $chart_style, 171 'chart_styles' => $this->chartStyles(), 172 'default_generations' => $default_generations, 173 'generations' => $generations, 174 'individual' => $individual, 175 'maximum_generations' => $maximum_generations, 176 'minimum_generations' => $minimum_generations, 177 'module_name' => $this->name(), 178 'show_cousins' => $show_cousins, 179 'title' => $this->chartTitle($individual), 180 ]); 181 } 182 183 /** 184 * Show a hierarchical list of ancestors 185 * 186 * @TODO replace ob_start() with views. 187 * 188 * @param Individual $individual 189 * @param int $generations 190 * 191 * @return Response 192 */ 193 protected function ancestorsList(Individual $individual, int $generations): Response 194 { 195 ob_start(); 196 197 $this->printChildAscendancy($individual, 1, $generations - 1); 198 199 $html = ob_get_clean(); 200 201 $html = '<ul class="chart_common">' . $html . '</ul>'; 202 203 return new Response($html); 204 } 205 206 /** 207 * print a child ascendancy 208 * 209 * @param Individual $individual 210 * @param int $sosa 211 * @param int $generations 212 * 213 * @return void 214 */ 215 protected function printChildAscendancy(Individual $individual, $sosa, $generations) 216 { 217 echo '<li class="wt-ancestors-chart-list-item">'; 218 echo '<table><tbody><tr><td>'; 219 if ($sosa === 1) { 220 echo '<img src="', app()->make(ModuleThemeInterface::class)->parameter('image-spacer'), '" height="3" width="15"></td><td>'; 221 } else { 222 echo '<img src="', app()->make(ModuleThemeInterface::class)->parameter('image-spacer'), '" height="3" width="2">'; 223 echo '<img src="', app()->make(ModuleThemeInterface::class)->parameter('image-hline'), '" height="3" width="13"></td><td>'; 224 } 225 echo FunctionsPrint::printPedigreePerson($individual); 226 echo '</td><td>'; 227 if ($sosa > 1) { 228 echo FontAwesome::linkIcon('arrow-down', $this->chartTitle($individual), [ 229 'href' => $this->chartUrl($individual, [ 230 'generations' => $generations, 231 'chart_style' => self::CHART_STYLE_LIST, 232 ]) 233 ]); 234 } 235 echo '</td><td class="details1"> <span class="person_box' . ($sosa === 1 ? 'NN' : ($sosa % 2 ? 'F' : '')) . '">', I18N::number($sosa), '</span> '; 236 echo '</td><td class="details1"> ', FunctionsCharts::getSosaName($sosa), '</td>'; 237 echo '</tr></tbody></table>'; 238 239 // Parents 240 $family = $individual->getPrimaryChildFamily(); 241 if ($family && $generations > 0) { 242 // Marriage details 243 echo '<span class="details1">'; 244 echo '<img src="', app()->make(ModuleThemeInterface::class)->parameter('image-spacer'), '" height="2" width="15"><a href="#" onclick="return expand_layer(\'sosa_', $sosa, '\');" class="top"><i id="sosa_', $sosa, '_img" class="icon-minus" title="', I18N::translate('View this family'), '"></i></a>'; 245 echo ' <span class="person_box">', I18N::number($sosa * 2), '</span> ', I18N::translate('and'); 246 echo ' <span class="person_boxF">', I18N::number($sosa * 2 + 1), '</span>'; 247 if ($family->canShow()) { 248 foreach ($family->facts(Gedcom::MARRIAGE_EVENTS) as $fact) { 249 echo ' <a href="', e($family->url()), '" class="details1">', $fact->summary(), '</a>'; 250 } 251 } 252 echo '</span>'; 253 echo '<ul class="wt-ancestors-chart-list" id="sosa_', $sosa, '">'; 254 if ($family->getHusband()) { 255 $this->printChildAscendancy($family->getHusband(), $sosa * 2, $generations - 1); 256 } 257 if ($family->getWife()) { 258 $this->printChildAscendancy($family->getWife(), $sosa * 2 + 1, $generations - 1); 259 } 260 echo '</ul>'; 261 } 262 echo '</li>'; 263 } 264 265 /** 266 * Show a tabular list of individual ancestors. 267 * 268 * @param Tree $tree 269 * @param Collection $ancestors 270 * 271 * @return Response 272 */ 273 protected function ancestorsIndividuals(Tree $tree, Collection $ancestors): Response 274 { 275 $this->layout = 'layouts/ajax'; 276 277 return $this->viewResponse('lists/individuals-table', [ 278 'individuals' => $ancestors, 279 'sosa' => true, 280 'tree' => $tree, 281 ]); 282 } 283 284 /** 285 * Show a tabular list of individual ancestors. 286 * 287 * @param Tree $tree 288 * @param Collection $ancestors 289 * 290 * @return Response 291 */ 292 protected function ancestorsFamilies(Tree $tree, Collection $ancestors): Response 293 { 294 $this->layout = 'layouts/ajax'; 295 296 $families = []; 297 foreach ($ancestors as $individual) { 298 foreach ($individual->getChildFamilies() as $family) { 299 $families[$family->xref()] = $family; 300 } 301 } 302 303 return $this->viewResponse('lists/families-table', [ 304 'families' => $families, 305 'tree' => $tree, 306 ]); 307 } 308 309 /** 310 * Show a booklet view of ancestors 311 * 312 * @TODO replace ob_start() with views. 313 * 314 * @param Collection $ancestors 315 * @param bool $show_cousins 316 * 317 * @return Response 318 */ 319 protected function ancestorsBooklet(Collection $ancestors, bool $show_cousins): Response 320 { 321 ob_start(); 322 323 echo FunctionsPrint::printPedigreePerson($ancestors[1]); 324 foreach ($ancestors as $sosa => $individual) { 325 foreach ($individual->getChildFamilies() as $family) { 326 FunctionsCharts::printSosaFamily($family, $individual->xref(), $sosa, '', '', '', $show_cousins); 327 } 328 } 329 330 $html = ob_get_clean(); 331 332 return new Response($html); 333 } 334 335 /** 336 * This chart can display its output in a number of styles 337 * 338 * @return array 339 */ 340 protected function chartStyles(): array 341 { 342 return [ 343 self::CHART_STYLE_LIST => I18N::translate('List'), 344 self::CHART_STYLE_BOOKLET => I18N::translate('Booklet'), 345 self::CHART_STYLE_INDIVIDUALS => I18N::translate('Individuals'), 346 self::CHART_STYLE_FAMILIES => I18N::translate('Families'), 347 ]; 348 } 349} 350