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\FunctionsEdit; 23use Fisharebest\Webtrees\I18N; 24use Fisharebest\Webtrees\Individual; 25use Fisharebest\Webtrees\Menu; 26use Fisharebest\Webtrees\Services\ChartService; 27use Fisharebest\Webtrees\Tree; 28use Symfony\Component\HttpFoundation\Request; 29use Symfony\Component\HttpFoundation\Response; 30 31/** 32 * Class PedigreeChartModule 33 */ 34class PedigreeChartModule extends AbstractModule implements ModuleChartInterface 35{ 36 use ModuleChartTrait; 37 38 // Defaults 39 protected const DEFAULT_GENERATIONS = '4'; 40 41 // Limits 42 protected const MAX_GENERATIONS = 12; 43 protected const MIN_GENERATIONS = 2; 44 45 // Chart orientation options. These are used to generate icons, views, etc. 46 public const ORIENTATION_LEFT = 'left'; 47 public const ORIENTATION_RIGHT = 'right'; 48 public const ORIENTATION_UP = 'up'; 49 public const ORIENTATION_DOWN = 'down'; 50 51 protected const MIRROR_ORIENTATION = [ 52 self::ORIENTATION_UP => self::ORIENTATION_DOWN, 53 self::ORIENTATION_DOWN => self::ORIENTATION_UP, 54 self::ORIENTATION_LEFT => self::ORIENTATION_RIGHT, 55 self::ORIENTATION_RIGHT => self::ORIENTATION_LEFT, 56 ]; 57 58 protected const DEFAULT_ORIENTATION = self::ORIENTATION_RIGHT; 59 60 /** 61 * How should this module be identified in the control panel, etc.? 62 * 63 * @return string 64 */ 65 public function title(): string 66 { 67 /* I18N: Name of a module/chart */ 68 return I18N::translate('Pedigree'); 69 } 70 71 /** 72 * A sentence describing what this module does. 73 * 74 * @return string 75 */ 76 public function description(): string 77 { 78 /* I18N: Description of the “PedigreeChart” module */ 79 return I18N::translate('A chart of an individual’s ancestors, formatted as a tree.'); 80 } 81 82 /** 83 * CSS class for the URL. 84 * 85 * @return string 86 */ 87 public function chartMenuClass(): string 88 { 89 return 'menu-chart-pedigree'; 90 } 91 92 /** 93 * Return a menu item for this chart - for use in individual boxes. 94 * 95 * @param Individual $individual 96 * 97 * @return Menu|null 98 */ 99 public function chartBoxMenu(Individual $individual): ?Menu 100 { 101 return $this->chartMenu($individual); 102 } 103 104 /** 105 * The title for a specific instance of this chart. 106 * 107 * @param Individual $individual 108 * 109 * @return string 110 */ 111 public function chartTitle(Individual $individual): string 112 { 113 /* I18N: %s is an individual’s name */ 114 return I18N::translate('Pedigree tree of %s', $individual->fullName()); 115 } 116 117 /** 118 * A form to request the chart parameters. 119 * 120 * @param Request $request 121 * @param Tree $tree 122 * @param UserInterface $user 123 * @param ChartService $chart_service 124 * 125 * @return Response 126 */ 127 public function getChartAction(Request $request, Tree $tree, UserInterface $user, ChartService $chart_service): Response 128 { 129 $ajax = (bool) $request->get('ajax'); 130 $xref = $request->get('xref', ''); 131 $individual = Individual::getInstance($xref, $tree); 132 133 Auth::checkIndividualAccess($individual); 134 Auth::checkComponentAccess($this, 'chart', $tree, $user); 135 136 $orientation = $request->get('orientation', static::DEFAULT_ORIENTATION); 137 $generations = (int) $request->get('generations', static::DEFAULT_GENERATIONS); 138 139 $generations = min(static::MAX_GENERATIONS, $generations); 140 $generations = max(static::MIN_GENERATIONS, $generations); 141 142 $generation_options = $this->generationOptions(); 143 144 if ($ajax) { 145 return $this->chart($individual, $orientation, $generations, $chart_service); 146 } 147 148 $ajax_url = $this->chartUrl($individual, [ 149 'ajax' => true, 150 'generations' => $generations, 151 'orientation' => $orientation, 152 ]); 153 154 return $this->viewResponse('modules/pedigree-chart/page', [ 155 'ajax_url' => $ajax_url, 156 'generations' => $generations, 157 'generation_options' => $generation_options, 158 'individual' => $individual, 159 'module_name' => $this->name(), 160 'orientation' => $orientation, 161 'orientations' => $this->orientations(), 162 'title' => $this->chartTitle($individual), 163 ]); 164 } 165 166 /** 167 * @param Individual $individual 168 * @param string $orientation 169 * @param int $generations 170 * @param ChartService $chart_service 171 * 172 * @return Response 173 */ 174 public function chart(Individual $individual, string $orientation, int $generations, ChartService $chart_service): Response 175 { 176 $ancestors = $chart_service->sosaStradonitzAncestors($individual, $generations); 177 178 // Father’s ancestors link to the father’s pedigree 179 // Mother’s ancestors link to the mother’s pedigree.. 180 $links = $ancestors->map(function (?Individual $individual, $sosa) use ($ancestors, $orientation, $generations): string { 181 if ($individual instanceof Individual && $sosa >= 2 ** $generations / 2 && $individual->childFamilies()->isNotEmpty()) { 182 // The last row/column, and there are more generations. 183 if ($sosa >= 2 ** $generations * 3 / 4) { 184 return $this->nextLink($ancestors->get(3), $orientation, $generations); 185 } 186 187 return $this->nextLink($ancestors->get(2), $orientation, $generations); 188 } 189 190 // A spacer to fix the "Left" layout. 191 return '<span class="invisible px-2">' . view('icons/arrow-' . $orientation) . '</span>'; 192 }); 193 194 // Root individual links to their children. 195 $links->put(1, $this->previousLink($individual, $orientation, $generations)); 196 197 $html = view('modules/pedigree-chart/chart', [ 198 'ancestors' => $ancestors, 199 'generations' => $generations, 200 'orientation' => $orientation, 201 'layout' => 'right', 202 'links' => $links, 203 ]); 204 205 return new Response($html); 206 } 207 208 /** 209 * Build a menu for the chart root individual 210 * 211 * @param Individual $individual 212 * @param string $orientation 213 * @param int $generations 214 * 215 * @return string 216 */ 217 public function nextLink(Individual $individual, string $orientation, int $generations): string 218 { 219 $icon = view('icons/arrow-' . $orientation); 220 $title = $this->chartTitle($individual); 221 $url = $this->chartUrl($individual, [ 222 'orientation' => $orientation, 223 'generations' => $generations, 224 ]); 225 226 return '<a class="px-2" href="' . e($url) . '" title="' . strip_tags($title) . '">' . $icon . '<span class="sr-only">' . $title . '</span></a>'; 227 } 228 229 /** 230 * Build a menu for the chart root individual 231 * 232 * @param Individual $individual 233 * @param string $orientation 234 * @param int $generations 235 * 236 * @return string 237 */ 238 public function previousLink(Individual $individual, string $orientation, int $generations): string 239 { 240 $icon = view('icons/arrow-' . self::MIRROR_ORIENTATION[$orientation]); 241 242 $siblings = []; 243 $spouses = []; 244 $children = []; 245 246 foreach ($individual->childFamilies() as $family) { 247 foreach ($family->children() as $child) { 248 if ($child !== $individual) { 249 $siblings[] = $this->individualLink($child, $orientation, $generations); 250 } 251 } 252 } 253 254 foreach ($individual->spouseFamilies() as $family) { 255 foreach ($family->spouses() as $spouse) { 256 if ($spouse !== $individual) { 257 $spouses[] = $this->individualLink($spouse, $orientation, $generations); 258 } 259 } 260 261 foreach ($family->children() as $child) { 262 $children[] = $this->individualLink($child, $orientation, $generations); 263 } 264 } 265 266 return view('modules/pedigree-chart/previous', [ 267 'icon' => $icon, 268 'individual' => $individual, 269 'generations' => $generations, 270 'orientation' => $orientation, 271 'chart' => $this, 272 'siblings' => $siblings, 273 'spouses' => $spouses, 274 'children' => $children, 275 ]); 276 } 277 278 /** 279 * @param Individual $individual 280 * @param string $orientation 281 * @param int $generations 282 * 283 * @return string 284 */ 285 protected function individualLink(Individual $individual, string $orientation, int $generations): string 286 { 287 $text = $individual->fullName(); 288 $title = $this->chartTitle($individual); 289 $url = $this->chartUrl($individual, [ 290 'orientation' => $orientation, 291 'generations' => $generations, 292 ]); 293 294 return '<a class="dropdown-item" href="' . e($url) . '" title="' . strip_tags($title) . '">' . $text . '</a>'; 295 } 296 297 /** 298 * @return string[] 299 */ 300 protected function generationOptions(): array 301 { 302 return FunctionsEdit::numericOptions(range(static::MIN_GENERATIONS, static::MAX_GENERATIONS)); 303 } 304 305 /** 306 * @return string[] 307 */ 308 protected function orientations(): array 309 { 310 return [ 311 self::ORIENTATION_LEFT => I18N::translate('Left'), 312 self::ORIENTATION_RIGHT => I18N::translate('Right'), 313 self::ORIENTATION_UP => I18N::translate('Up'), 314 self::ORIENTATION_DOWN => I18N::translate('Down'), 315 ]; 316 } 317} 318