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