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