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