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 */ 16namespace Fisharebest\Webtrees\Controller; 17 18use Fisharebest\Webtrees\Filter; 19use Fisharebest\Webtrees\I18N; 20use Fisharebest\Webtrees\Theme; 21 22/** 23 * Controller for the fan chart 24 */ 25class FanchartController extends ChartController 26{ 27 /** @var int Style of fanchart */ 28 public $fan_style; 29 30 /** @var int Width of fanchart (a percentage) */ 31 public $fan_width; 32 33 /** @var int Number of generations to display */ 34 public $generations; 35 36 /** 37 * Create the controller 38 */ 39 public function __construct() 40 { 41 global $WT_TREE; 42 43 parent::__construct(); 44 45 $default_generations = $WT_TREE->getPreference('DEFAULT_PEDIGREE_GENERATIONS'); 46 47 // Extract the request parameters 48 $this->fan_style = Filter::getInteger('fan_style', 2, 4, 3); 49 $this->fan_width = Filter::getInteger('fan_width', 50, 500, 100); 50 $this->generations = Filter::getInteger('generations', 2, 9, $default_generations); 51 52 if ($this->root && $this->root->canShowName()) { 53 $this->setPageTitle( 54 /* I18N: http://en.wikipedia.org/wiki/Family_tree#Fan_chart - %s is an individual’s name */ 55 I18N::translate('Fan chart of %s', $this->root->getFullName()) 56 ); 57 } else { 58 $this->setPageTitle(I18N::translate('Fan chart')); 59 } 60 } 61 62 /** 63 * A list of options for the chart style. 64 * 65 * @return string[] 66 */ 67 public function getFanStyles() 68 { 69 return array( 70 2 => /* I18N: layout option for the fan chart */ I18N::translate('half circle'), 71 3 => /* I18N: layout option for the fan chart */ I18N::translate('three-quarter circle'), 72 4 => /* I18N: layout option for the fan chart */ I18N::translate('full circle'), 73 ); 74 } 75 76 /** 77 * split and center text by lines 78 * 79 * @param string $data input string 80 * @param int $maxlen max length of each line 81 * 82 * @return string $text output string 83 */ 84 public function splitAlignText($data, $maxlen) 85 { 86 $RTLOrd = array(215, 216, 217, 218, 219); 87 88 $lines = explode("\n", $data); 89 // more than 1 line : recursive calls 90 if (count($lines) > 1) { 91 $text = ''; 92 foreach ($lines as $line) { 93 $text .= $this->splitAlignText($line, $maxlen) . "\n"; 94 } 95 96 return $text; 97 } 98 // process current line word by word 99 $split = explode(' ', $data); 100 $text = ''; 101 $line = ''; 102 103 // do not split hebrew line 104 $found = false; 105 foreach ($RTLOrd as $ord) { 106 if (strpos($data, chr($ord)) !== false) { 107 $found = true; 108 } 109 } 110 if ($found) { 111 $line = $data; 112 } else { 113 foreach ($split as $word) { 114 $len = strlen($line); 115 $wlen = strlen($word); 116 if (($len + $wlen) < $maxlen) { 117 if (!empty($line)) { 118 $line .= ' '; 119 } 120 $line .= "$word"; 121 } else { 122 $p = max(0, (int) (($maxlen - $len) / 2)); 123 if (!empty($line)) { 124 $line = str_repeat(' ', $p) . $line; // center alignment using spaces 125 $text .= $line . "\n"; 126 } 127 $line = $word; 128 } 129 } 130 } 131 // last line 132 if (!empty($line)) { 133 $len = strlen($line); 134 if (in_array(ord($line[0]), $RTLOrd)) { 135 $len /= 2; 136 } 137 $p = max(0, (int) (($maxlen - $len) / 2)); 138 $line = str_repeat(' ', $p) . $line; // center alignment using spaces 139 $text .= $line; 140 } 141 142 return $text; 143 } 144 145 /** 146 * Generate both the HTML and PNG components of the fan chart 147 * 148 * The HTML and PNG components both require the same co-ordinate calculations, 149 * so we generate them using the same code, but we send them in separate 150 * HTTP requests. 151 * 152 * @param string $what "png" or "html" 153 * 154 * @return string 155 */ 156 public function generateFanChart($what) 157 { 158 $treeid = $this->sosaAncestors($this->generations); 159 $fanw = 640 * $this->fan_width / 100; 160 $fandeg = 90 * $this->fan_style; 161 $html = ''; 162 163 $treesize = count($treeid) + 1; 164 165 // generations count 166 $gen = log($treesize) / log(2) - 1; 167 $sosa = $treesize - 1; 168 169 // fan size 170 if ($fandeg == 0) { 171 $fandeg = 360; 172 } 173 $fandeg = min($fandeg, 360); 174 $fandeg = max($fandeg, 90); 175 $cx = $fanw / 2 - 1; // center x 176 $cy = $cx; // center y 177 $rx = $fanw - 1; 178 $rw = $fanw / ($gen + 1); 179 $fanh = $fanw; // fan height 180 if ($fandeg == 180) { 181 $fanh = round($fanh * ($gen + 1) / ($gen * 2)); 182 } 183 if ($fandeg == 270) { 184 $fanh = round($fanh * 0.86); 185 } 186 $scale = $fanw / 640; 187 188 // image init 189 $image = imagecreate($fanw, $fanh); 190 $white = imagecolorallocate($image, 0xFF, 0xFF, 0xFF); 191 imagefilledrectangle($image, 0, 0, $fanw, $fanh, $white); 192 imagecolortransparent($image, $white); 193 194 $color = imagecolorallocate( 195 $image, 196 hexdec(substr(Theme::theme()->parameter('chart-font-color'), 0, 2)), 197 hexdec(substr(Theme::theme()->parameter('chart-font-color'), 2, 2)), 198 hexdec(substr(Theme::theme()->parameter('chart-font-color'), 4, 2))); 199 $bgcolor = imagecolorallocate( 200 $image, 201 hexdec(substr(Theme::theme()->parameter('chart-background-u'), 0, 2)), 202 hexdec(substr(Theme::theme()->parameter('chart-background-u'), 2, 2)), 203 hexdec(substr(Theme::theme()->parameter('chart-background-u'), 4, 2)) 204 ); 205 $bgcolorM = imagecolorallocate( 206 $image, 207 hexdec(substr(Theme::theme()->parameter('chart-background-m'), 0, 2)), 208 hexdec(substr(Theme::theme()->parameter('chart-background-m'), 2, 2)), 209 hexdec(substr(Theme::theme()->parameter('chart-background-m'), 4, 2)) 210 ); 211 $bgcolorF = imagecolorallocate( 212 $image, 213 hexdec(substr(Theme::theme()->parameter('chart-background-f'), 0, 2)), 214 hexdec(substr(Theme::theme()->parameter('chart-background-f'), 2, 2)), 215 hexdec(substr(Theme::theme()->parameter('chart-background-f'), 4, 2)) 216 ); 217 218 // imagemap 219 $imagemap = '<map id="fanmap" name="fanmap">'; 220 221 // loop to create fan cells 222 while ($gen >= 0) { 223 // clean current generation area 224 $deg2 = 360 + ($fandeg - 180) / 2; 225 $deg1 = $deg2 - $fandeg; 226 imagefilledarc($image, $cx, $cy, $rx, $rx, $deg1, $deg2, $bgcolor, IMG_ARC_PIE); 227 $rx -= 3; 228 229 // calculate new angle 230 $p2 = pow(2, $gen); 231 $angle = $fandeg / $p2; 232 $deg2 = 360 + ($fandeg - 180) / 2; 233 $deg1 = $deg2 - $angle; 234 // special case for rootid cell 235 if ($gen == 0) { 236 $deg1 = 90; 237 $deg2 = 360 + $deg1; 238 } 239 240 // draw each cell 241 while ($sosa >= $p2) { 242 $person = $treeid[$sosa]; 243 if ($person) { 244 $name = $person->getFullName(); 245 $addname = $person->getAddName(); 246 247 $text = I18N::reverseText($name); 248 if ($addname) { 249 $text .= "\n" . I18N::reverseText($addname); 250 } 251 252 $text .= "\n" . I18N::reverseText($person->getLifeSpan()); 253 254 switch ($person->getSex()) { 255 case 'M': 256 $bg = $bgcolorM; 257 break; 258 case 'F': 259 $bg = $bgcolorF; 260 break; 261 default: 262 $bg = $bgcolor; 263 break; 264 } 265 266 imagefilledarc($image, $cx, $cy, $rx, $rx, $deg1, $deg2, $bg, IMG_ARC_PIE); 267 268 // split and center text by lines 269 $wmax = (int) ($angle * 7 / Theme::theme()->parameter('chart-font-size') * $scale); 270 $wmax = min($wmax, 35 * $scale); 271 if ($gen == 0) { 272 $wmax = min($wmax, 17 * $scale); 273 } 274 $text = $this->splitAlignText($text, $wmax); 275 276 // text angle 277 $tangle = 270 - ($deg1 + $angle / 2); 278 if ($gen == 0) { 279 $tangle = 0; 280 } 281 282 // calculate text position 283 $deg = $deg1 + 0.44; 284 if ($deg2 - $deg1 > 40) { 285 $deg = $deg1 + ($deg2 - $deg1) / 11; 286 } 287 if ($deg2 - $deg1 > 80) { 288 $deg = $deg1 + ($deg2 - $deg1) / 7; 289 } 290 if ($deg2 - $deg1 > 140) { 291 $deg = $deg1 + ($deg2 - $deg1) / 4; 292 } 293 if ($gen == 0) { 294 $deg = 180; 295 } 296 $rad = deg2rad($deg); 297 $mr = ($rx - $rw / 4) / 2; 298 if ($gen > 0 && $deg2 - $deg1 > 80) { 299 $mr = $rx / 2; 300 } 301 $tx = $cx + $mr * cos($rad); 302 $ty = $cy - $mr * -sin($rad); 303 if ($sosa == 1) { 304 $ty -= $mr / 2; 305 } 306 307 // print text 308 imagettftext( 309 $image, 310 Theme::theme()->parameter('chart-font-size'), 311 $tangle, $tx, $ty, 312 $color, Theme::theme()->parameter('chart-font-name'), 313 $text 314 ); 315 316 $imagemap .= '<area shape="poly" coords="'; 317 // plot upper points 318 $mr = $rx / 2; 319 $deg = $deg1; 320 while ($deg <= $deg2) { 321 $rad = deg2rad($deg); 322 $tx = round($cx + $mr * cos($rad)); 323 $ty = round($cy - $mr * -sin($rad)); 324 $imagemap .= "$tx,$ty,"; 325 $deg += ($deg2 - $deg1) / 6; 326 } 327 // plot lower points 328 $mr = ($rx - $rw) / 2; 329 $deg = $deg2; 330 while ($deg >= $deg1) { 331 $rad = deg2rad($deg); 332 $tx = round($cx + $mr * cos($rad)); 333 $ty = round($cy - $mr * -sin($rad)); 334 $imagemap .= "$tx,$ty,"; 335 $deg -= ($deg2 - $deg1) / 6; 336 } 337 // join first point 338 $mr = $rx / 2; 339 $deg = $deg1; 340 $rad = deg2rad($deg); 341 $tx = round($cx + $mr * cos($rad)); 342 $ty = round($cy - $mr * -sin($rad)); 343 $imagemap .= "$tx,$ty"; 344 // add action url 345 $pid = $person->getXref(); 346 $imagemap .= '" href="#' . $pid . '"'; 347 $html .= '<div id="' . $pid . '" class="fan_chart_menu">'; 348 $html .= '<div class="person_box"><div class="details1">'; 349 $html .= '<a href="' . $person->getHtmlUrl() . '" class="name1">' . $name; 350 if ($addname) { 351 $html .= $addname; 352 } 353 $html .= '</a>'; 354 $html .= '<ul class="charts">'; 355 foreach (Theme::theme()->individualBoxMenu($person) as $menu) { 356 $html .= $menu->getMenuAsList(); 357 } 358 $html .= '</ul>'; 359 $html .= '</div></div>'; 360 $html .= '</div>'; 361 $imagemap .= ' alt="' . strip_tags($person->getFullName()) . '" title="' . strip_tags($person->getFullName()) . '">'; 362 } 363 $deg1 -= $angle; 364 $deg2 -= $angle; 365 $sosa--; 366 } 367 $rx -= $rw; 368 $gen--; 369 } 370 371 $imagemap .= '</map>'; 372 373 switch ($what) { 374 case 'html': 375 return $html . $imagemap . '<div id="fan_chart_img"><img src="' . WT_SCRIPT_NAME . '?rootid=' . $this->root->getXref() . '&fan_style=' . $this->fan_style . '&generations=' . $this->generations . '&fan_width=' . $this->fan_width . '&img=1" width="' . $fanw . '" height="' . $fanh . '" alt="' . strip_tags($this->getPageTitle()) . '" usemap="#fanmap"></div>'; 376 377 case 'png': 378 imagestringup($image, 1, $fanw - 10, $fanh / 3, WT_BASE_URL, $color); 379 ob_start(); 380 imagepng($image); 381 imagedestroy($image); 382 383 return ob_get_clean(); 384 385 default: 386 throw new \InvalidArgumentException(__METHOD__ . ' ' . $what); 387 } 388 } 389} 390