1168ff6f3Sric2016<?php 23976b470SGreg Roach 3168ff6f3Sric2016/** 4168ff6f3Sric2016 * webtrees: online genealogy 58fcd0d32SGreg Roach * Copyright (C) 2019 webtrees development team 6168ff6f3Sric2016 * This program is free software: you can redistribute it and/or modify 7168ff6f3Sric2016 * it under the terms of the GNU General Public License as published by 8168ff6f3Sric2016 * the Free Software Foundation, either version 3 of the License, or 9168ff6f3Sric2016 * (at your option) any later version. 10168ff6f3Sric2016 * This program is distributed in the hope that it will be useful, 11168ff6f3Sric2016 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12168ff6f3Sric2016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13168ff6f3Sric2016 * GNU General Public License for more details. 14168ff6f3Sric2016 * You should have received a copy of the GNU General Public License 15168ff6f3Sric2016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16168ff6f3Sric2016 */ 17fcfa147eSGreg Roach 18e7f56f2aSGreg Roachdeclare(strict_types=1); 19e7f56f2aSGreg Roach 20168ff6f3Sric2016namespace Fisharebest\Webtrees\Module; 21168ff6f3Sric2016 2271378461SGreg Roachuse Aura\Router\RouterContainer; 2371378461SGreg Roachuse Fig\Http\Message\RequestMethodInterface; 24389266c0SGreg Roachuse Fisharebest\Webtrees\Auth; 25168ff6f3Sric2016use Fisharebest\Webtrees\I18N; 26168ff6f3Sric2016use Fisharebest\Webtrees\Individual; 276664b4a3SGreg Roachuse Fisharebest\Webtrees\Menu; 28389266c0SGreg Roachuse Fisharebest\Webtrees\Services\ChartService; 294ea62551SGreg Roachuse Fisharebest\Webtrees\Tree; 30f397d0fdSGreg Roachuse Fisharebest\Webtrees\Webtrees; 316ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface; 326ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface; 3371378461SGreg Roachuse Psr\Http\Server\RequestHandlerInterface; 3471378461SGreg Roach 359e18e23bSGreg Roachuse function app; 3671378461SGreg Roachuse function array_keys; 379e18e23bSGreg Roachuse function assert; 3871378461SGreg Roachuse function implode; 39ddeb3354SGreg Roachuse function is_string; 4071378461SGreg Roachuse function max; 4171378461SGreg Roachuse function min; 4271378461SGreg Roachuse function redirect; 4371378461SGreg Roachuse function route; 44168ff6f3Sric2016 45168ff6f3Sric2016/** 46168ff6f3Sric2016 * Class FanChartModule 47168ff6f3Sric2016 */ 4871378461SGreg Roachclass FanChartModule extends AbstractModule implements ModuleChartInterface, RequestHandlerInterface 49c1010edaSGreg Roach{ 5049a243cbSGreg Roach use ModuleChartTrait; 5149a243cbSGreg Roach 5271378461SGreg Roach private const ROUTE_NAME = 'fan-chart'; 5371378461SGreg Roach private const ROUTE_URL = '/tree/{tree}/fan-chart-{style}-{generations}-{width}/{xref}'; 5471378461SGreg Roach 55389266c0SGreg Roach // Chart styles 5671378461SGreg Roach private const STYLE_HALF_CIRCLE = '2'; 5771378461SGreg Roach private const STYLE_THREE_QUARTER_CIRCLE = '3'; 5871378461SGreg Roach private const STYLE_FULL_CIRCLE = '4'; 5971378461SGreg Roach 6071378461SGreg Roach // Defaults 6171378461SGreg Roach private const DEFAULT_STYLE = self::STYLE_THREE_QUARTER_CIRCLE; 6271378461SGreg Roach private const DEFAULT_GENERATIONS = 4; 6371378461SGreg Roach private const DEFAULT_WIDTH = 100; 6471378461SGreg Roach protected const DEFAULT_PARAMETERS = [ 6571378461SGreg Roach 'style' => self::DEFAULT_STYLE, 6671378461SGreg Roach 'generations' => self::DEFAULT_GENERATIONS, 6771378461SGreg Roach 'width' => self::DEFAULT_WIDTH, 6871378461SGreg Roach ]; 69389266c0SGreg Roach 70389266c0SGreg Roach // Limits 71389266c0SGreg Roach private const MINIMUM_GENERATIONS = 2; 72389266c0SGreg Roach private const MAXIMUM_GENERATIONS = 9; 73389266c0SGreg Roach private const MINIMUM_WIDTH = 50; 74389266c0SGreg Roach private const MAXIMUM_WIDTH = 500; 75389266c0SGreg Roach 7657ab2231SGreg Roach /** @var ChartService */ 7757ab2231SGreg Roach private $chart_service; 7857ab2231SGreg Roach 7957ab2231SGreg Roach /** 8057ab2231SGreg Roach * FanChartModule constructor. 8157ab2231SGreg Roach * 8257ab2231SGreg Roach * @param ChartService $chart_service 8357ab2231SGreg Roach */ 843976b470SGreg Roach public function __construct(ChartService $chart_service) 853976b470SGreg Roach { 8657ab2231SGreg Roach $this->chart_service = $chart_service; 8757ab2231SGreg Roach } 8857ab2231SGreg Roach 89168ff6f3Sric2016 /** 9071378461SGreg Roach * Initialization. 9171378461SGreg Roach * 929e18e23bSGreg Roach * @return void 9371378461SGreg Roach */ 949e18e23bSGreg Roach public function boot(): void 9571378461SGreg Roach { 969e18e23bSGreg Roach $router_container = app(RouterContainer::class); 979e18e23bSGreg Roach assert($router_container instanceof RouterContainer); 989e18e23bSGreg Roach 9971378461SGreg Roach $router_container->getMap() 100f7358520SGreg Roach ->get(self::ROUTE_NAME, self::ROUTE_URL, $this) 10171378461SGreg Roach ->allows(RequestMethodInterface::METHOD_POST) 10271378461SGreg Roach ->tokens([ 10371378461SGreg Roach 'generations' => '\d+', 10471378461SGreg Roach 'style' => implode('|', array_keys($this->styles())), 10571378461SGreg Roach 'width' => '\d+', 10671378461SGreg Roach ]); 10771378461SGreg Roach } 10871378461SGreg Roach 10971378461SGreg Roach /** 1100cfd6963SGreg Roach * How should this module be identified in the control panel, etc.? 111168ff6f3Sric2016 * 112168ff6f3Sric2016 * @return string 113168ff6f3Sric2016 */ 11449a243cbSGreg Roach public function title(): string 115c1010edaSGreg Roach { 116bbb76c12SGreg Roach /* I18N: Name of a module/chart */ 117bbb76c12SGreg Roach return I18N::translate('Fan chart'); 118168ff6f3Sric2016 } 119168ff6f3Sric2016 120168ff6f3Sric2016 /** 121168ff6f3Sric2016 * A sentence describing what this module does. 122168ff6f3Sric2016 * 123168ff6f3Sric2016 * @return string 124168ff6f3Sric2016 */ 12549a243cbSGreg Roach public function description(): string 126c1010edaSGreg Roach { 127bbb76c12SGreg Roach /* I18N: Description of the “Fan Chart” module */ 128bbb76c12SGreg Roach return I18N::translate('A fan chart of an individual’s ancestors.'); 129168ff6f3Sric2016 } 130168ff6f3Sric2016 131168ff6f3Sric2016 /** 132377a2979SGreg Roach * CSS class for the URL. 133377a2979SGreg Roach * 134377a2979SGreg Roach * @return string 135377a2979SGreg Roach */ 136377a2979SGreg Roach public function chartMenuClass(): string 137377a2979SGreg Roach { 138377a2979SGreg Roach return 'menu-chart-fanchart'; 139377a2979SGreg Roach } 140377a2979SGreg Roach 141377a2979SGreg Roach /** 1424eb71cfaSGreg Roach * Return a menu item for this chart - for use in individual boxes. 1434eb71cfaSGreg Roach * 14460bc3e3fSGreg Roach * @param Individual $individual 14560bc3e3fSGreg Roach * 1464eb71cfaSGreg Roach * @return Menu|null 1474eb71cfaSGreg Roach */ 148377a2979SGreg Roach public function chartBoxMenu(Individual $individual): ?Menu 149c1010edaSGreg Roach { 150e6562982SGreg Roach return $this->chartMenu($individual); 151e6562982SGreg Roach } 152e6562982SGreg Roach 153e6562982SGreg Roach /** 154e6562982SGreg Roach * The title for a specific instance of this chart. 155e6562982SGreg Roach * 156e6562982SGreg Roach * @param Individual $individual 157e6562982SGreg Roach * 158e6562982SGreg Roach * @return string 159e6562982SGreg Roach */ 160e6562982SGreg Roach public function chartTitle(Individual $individual): string 161e6562982SGreg Roach { 162389266c0SGreg Roach /* I18N: http://en.wikipedia.org/wiki/Family_tree#Fan_chart - %s is an individual’s name */ 16339ca88baSGreg Roach return I18N::translate('Fan chart of %s', $individual->fullName()); 164e6562982SGreg Roach } 165e6562982SGreg Roach 166e6562982SGreg Roach /** 167389266c0SGreg Roach * A form to request the chart parameters. 168389266c0SGreg Roach * 16971378461SGreg Roach * @param Individual $individual 17059597b37SGreg Roach * @param mixed[] $parameters 17171378461SGreg Roach * 17271378461SGreg Roach * @return string 17371378461SGreg Roach */ 17471378461SGreg Roach public function chartUrl(Individual $individual, array $parameters = []): string 17571378461SGreg Roach { 17671378461SGreg Roach return route(self::ROUTE_NAME, [ 17771378461SGreg Roach 'xref' => $individual->xref(), 17871378461SGreg Roach 'tree' => $individual->tree()->name(), 17971378461SGreg Roach ] + $parameters + self::DEFAULT_PARAMETERS); 18071378461SGreg Roach } 18171378461SGreg Roach 18271378461SGreg Roach /** 1836ccdf4f0SGreg Roach * @param ServerRequestInterface $request 184389266c0SGreg Roach * 1856ccdf4f0SGreg Roach * @return ResponseInterface 186389266c0SGreg Roach */ 18771378461SGreg Roach public function handle(ServerRequestInterface $request): ResponseInterface 188389266c0SGreg Roach { 18957ab2231SGreg Roach $tree = $request->getAttribute('tree'); 1904ea62551SGreg Roach assert($tree instanceof Tree); 1914ea62551SGreg Roach 19257ab2231SGreg Roach $user = $request->getAttribute('user'); 193ddeb3354SGreg Roach 19471378461SGreg Roach $xref = $request->getAttribute('xref'); 195ddeb3354SGreg Roach assert(is_string($xref)); 196ddeb3354SGreg Roach 197ddeb3354SGreg Roach $individual = Individual::getInstance($xref, $tree); 198ddeb3354SGreg Roach $individual = Auth::checkIndividualAccess($individual); 199ddeb3354SGreg Roach 20071378461SGreg Roach $style = $request->getAttribute('style'); 20171378461SGreg Roach $generations = (int) $request->getAttribute('generations'); 20271378461SGreg Roach $width = (int) $request->getAttribute('width'); 2030b93976aSGreg Roach $ajax = $request->getQueryParams()['ajax'] ?? ''; 204389266c0SGreg Roach 20571378461SGreg Roach // Convert POST requests into GET requests for pretty URLs. 20671378461SGreg Roach if ($request->getMethod() === RequestMethodInterface::METHOD_POST) { 20771378461SGreg Roach return redirect(route(self::ROUTE_NAME, [ 2084ea62551SGreg Roach 'tree' => $tree->name(), 20971378461SGreg Roach 'xref' => $request->getParsedBody()['xref'], 21071378461SGreg Roach 'style' => $request->getParsedBody()['style'], 21171378461SGreg Roach 'generations' => $request->getParsedBody()['generations'], 21271378461SGreg Roach 'width' => $request->getParsedBody()['width'], 21371378461SGreg Roach ])); 21471378461SGreg Roach } 21571378461SGreg Roach 2169867b2f0SGreg Roach Auth::checkComponentAccess($this, 'chart', $tree, $user); 217389266c0SGreg Roach 21871378461SGreg Roach $width = min($width, self::MAXIMUM_WIDTH); 21971378461SGreg Roach $width = max($width, self::MINIMUM_WIDTH); 220389266c0SGreg Roach 221389266c0SGreg Roach $generations = min($generations, self::MAXIMUM_GENERATIONS); 222389266c0SGreg Roach $generations = max($generations, self::MINIMUM_GENERATIONS); 223389266c0SGreg Roach 2240b93976aSGreg Roach if ($ajax === '1') { 22571378461SGreg Roach return $this->chart($individual, $style, $width, $generations); 226389266c0SGreg Roach } 227389266c0SGreg Roach 228389266c0SGreg Roach $ajax_url = $this->chartUrl($individual, [ 2299b5537c3SGreg Roach 'ajax' => true, 230389266c0SGreg Roach 'generations' => $generations, 23171378461SGreg Roach 'style' => $style, 23271378461SGreg Roach 'width' => $width, 233389266c0SGreg Roach ]); 234389266c0SGreg Roach 2359b5537c3SGreg Roach return $this->viewResponse('modules/fanchart/page', [ 236389266c0SGreg Roach 'ajax_url' => $ajax_url, 237389266c0SGreg Roach 'generations' => $generations, 238389266c0SGreg Roach 'individual' => $individual, 239389266c0SGreg Roach 'maximum_generations' => self::MAXIMUM_GENERATIONS, 240389266c0SGreg Roach 'minimum_generations' => self::MINIMUM_GENERATIONS, 241389266c0SGreg Roach 'maximum_width' => self::MAXIMUM_WIDTH, 242389266c0SGreg Roach 'minimum_width' => self::MINIMUM_WIDTH, 24371378461SGreg Roach 'module' => $this->name(), 24471378461SGreg Roach 'style' => $style, 24571378461SGreg Roach 'styles' => $this->styles(), 246389266c0SGreg Roach 'title' => $this->chartTitle($individual), 247ef5d23f1SGreg Roach 'tree' => $tree, 24871378461SGreg Roach 'width' => $width, 249389266c0SGreg Roach ]); 250389266c0SGreg Roach } 251389266c0SGreg Roach 252389266c0SGreg Roach /** 253389266c0SGreg Roach * Generate both the HTML and PNG components of the fan chart 254e6562982SGreg Roach * 255e6562982SGreg Roach * @param Individual $individual 25671378461SGreg Roach * @param string $style 25771378461SGreg Roach * @param int $width 258389266c0SGreg Roach * @param int $generations 259e6562982SGreg Roach * 2606ccdf4f0SGreg Roach * @return ResponseInterface 261e6562982SGreg Roach */ 26271378461SGreg Roach protected function chart(Individual $individual, string $style, int $width, int $generations): ResponseInterface 263e6562982SGreg Roach { 26471378461SGreg Roach $ancestors = $this->chart_service->sosaStradonitzAncestors($individual, $generations); 265389266c0SGreg Roach 266389266c0SGreg Roach $gen = $generations - 1; 267e364afe4SGreg Roach $sosa = 2 ** $generations - 1; 268389266c0SGreg Roach 269389266c0SGreg Roach // fan size 27071378461SGreg Roach $fanw = 640 * $width / 100; 271389266c0SGreg Roach $cx = $fanw / 2 - 1; // center x 272389266c0SGreg Roach $cy = $cx; // center y 273389266c0SGreg Roach $rx = $fanw - 1; 274389266c0SGreg Roach $rw = $fanw / ($gen + 1); 275389266c0SGreg Roach $fanh = $fanw; // fan height 27671378461SGreg Roach if ($style === self::STYLE_HALF_CIRCLE) { 277389266c0SGreg Roach $fanh = $fanh * ($gen + 1) / ($gen * 2); 278389266c0SGreg Roach } 27971378461SGreg Roach if ($style === self::STYLE_THREE_QUARTER_CIRCLE) { 280e364afe4SGreg Roach $fanh *= 0.86; 281389266c0SGreg Roach } 282389266c0SGreg Roach $scale = $fanw / 640; 283389266c0SGreg Roach 284389266c0SGreg Roach // Create the image 285389266c0SGreg Roach $image = imagecreate((int) $fanw, (int) $fanh); 286389266c0SGreg Roach 287389266c0SGreg Roach // Create colors 288389266c0SGreg Roach $transparent = imagecolorallocate($image, 0, 0, 0); 289389266c0SGreg Roach imagecolortransparent($image, $transparent); 290389266c0SGreg Roach 291a91af26aSGreg Roach /** @var ModuleThemeInterface $theme */ 292cab242e7SGreg Roach $theme = app(ModuleThemeInterface::class); 293389266c0SGreg Roach 294389266c0SGreg Roach $foreground = $this->imageColor($image, $theme->parameter('chart-font-color')); 295389266c0SGreg Roach 296389266c0SGreg Roach $backgrounds = [ 297389266c0SGreg Roach 'M' => $this->imageColor($image, $theme->parameter('chart-background-m')), 298389266c0SGreg Roach 'F' => $this->imageColor($image, $theme->parameter('chart-background-f')), 299389266c0SGreg Roach 'U' => $this->imageColor($image, $theme->parameter('chart-background-u')), 300389266c0SGreg Roach ]; 301389266c0SGreg Roach 302389266c0SGreg Roach imagefilledrectangle($image, 0, 0, (int) $fanw, (int) $fanh, $transparent); 303389266c0SGreg Roach 30471378461SGreg Roach $fandeg = 90 * $style; 305389266c0SGreg Roach 306389266c0SGreg Roach // Popup menus for each ancestor 307389266c0SGreg Roach $html = ''; 308389266c0SGreg Roach 309389266c0SGreg Roach // Areas for the imagemap 310389266c0SGreg Roach $areas = ''; 311389266c0SGreg Roach 312389266c0SGreg Roach // loop to create fan cells 313389266c0SGreg Roach while ($gen >= 0) { 314389266c0SGreg Roach // clean current generation area 315389266c0SGreg Roach $deg2 = 360 + ($fandeg - 180) / 2; 316389266c0SGreg Roach $deg1 = $deg2 - $fandeg; 317389266c0SGreg Roach imagefilledarc($image, (int) $cx, (int) $cy, (int) $rx, (int) $rx, (int) $deg1, (int) $deg2, $backgrounds['U'], IMG_ARC_PIE); 318389266c0SGreg Roach $rx -= 3; 319389266c0SGreg Roach 320389266c0SGreg Roach // calculate new angle 321389266c0SGreg Roach $p2 = 2 ** $gen; 322389266c0SGreg Roach $angle = $fandeg / $p2; 323389266c0SGreg Roach $deg2 = 360 + ($fandeg - 180) / 2; 324389266c0SGreg Roach $deg1 = $deg2 - $angle; 325389266c0SGreg Roach // special case for rootid cell 326389266c0SGreg Roach if ($gen == 0) { 327389266c0SGreg Roach $deg1 = 90; 328389266c0SGreg Roach $deg2 = 360 + $deg1; 329389266c0SGreg Roach } 330389266c0SGreg Roach 331389266c0SGreg Roach // draw each cell 332389266c0SGreg Roach while ($sosa >= $p2) { 333389266c0SGreg Roach if ($ancestors->has($sosa)) { 334389266c0SGreg Roach $person = $ancestors->get($sosa); 33539ca88baSGreg Roach $name = $person->fullName(); 33639ca88baSGreg Roach $addname = $person->alternateName(); 337389266c0SGreg Roach 338389266c0SGreg Roach $text = I18N::reverseText($name); 339389266c0SGreg Roach if ($addname) { 340389266c0SGreg Roach $text .= "\n" . I18N::reverseText($addname); 341389266c0SGreg Roach } 342389266c0SGreg Roach 3435e6816beSGreg Roach $text .= "\n" . I18N::reverseText($person->lifespan()); 344389266c0SGreg Roach 34539ca88baSGreg Roach $background = $backgrounds[$person->sex()]; 346389266c0SGreg Roach 347389266c0SGreg Roach imagefilledarc($image, (int) $cx, (int) $cy, (int) $rx, (int) $rx, (int) $deg1, (int) $deg2, $background, IMG_ARC_PIE); 348389266c0SGreg Roach 349389266c0SGreg Roach // split and center text by lines 350389266c0SGreg Roach $wmax = (int) ($angle * 7 / 7 * $scale); 351389266c0SGreg Roach $wmax = min($wmax, 35 * $scale); 35271378461SGreg Roach if ($gen === 0) { 353389266c0SGreg Roach $wmax = min($wmax, 17 * $scale); 354389266c0SGreg Roach } 355389266c0SGreg Roach $text = $this->splitAlignText($text, (int) $wmax); 356389266c0SGreg Roach 357389266c0SGreg Roach // text angle 358389266c0SGreg Roach $tangle = 270 - ($deg1 + $angle / 2); 35971378461SGreg Roach if ($gen === 0) { 360389266c0SGreg Roach $tangle = 0; 361389266c0SGreg Roach } 362389266c0SGreg Roach 363389266c0SGreg Roach // calculate text position 364389266c0SGreg Roach $deg = $deg1 + 0.44; 365389266c0SGreg Roach if ($deg2 - $deg1 > 40) { 366389266c0SGreg Roach $deg = $deg1 + ($deg2 - $deg1) / 11; 367389266c0SGreg Roach } 368389266c0SGreg Roach if ($deg2 - $deg1 > 80) { 369389266c0SGreg Roach $deg = $deg1 + ($deg2 - $deg1) / 7; 370389266c0SGreg Roach } 371389266c0SGreg Roach if ($deg2 - $deg1 > 140) { 372389266c0SGreg Roach $deg = $deg1 + ($deg2 - $deg1) / 4; 373389266c0SGreg Roach } 37471378461SGreg Roach if ($gen === 0) { 375389266c0SGreg Roach $deg = 180; 376389266c0SGreg Roach } 377389266c0SGreg Roach $rad = deg2rad($deg); 378389266c0SGreg Roach $mr = ($rx - $rw / 4) / 2; 379389266c0SGreg Roach if ($gen > 0 && $deg2 - $deg1 > 80) { 380389266c0SGreg Roach $mr = $rx / 2; 381389266c0SGreg Roach } 382389266c0SGreg Roach $tx = $cx + $mr * cos($rad); 383389266c0SGreg Roach $ty = $cy + $mr * sin($rad); 38471378461SGreg Roach if ($sosa === 1) { 385389266c0SGreg Roach $ty -= $mr / 2; 386389266c0SGreg Roach } 387389266c0SGreg Roach 388*acd78f72SGreg Roach // If PHP is compiled with --enable-gd-jis-conv, then the function 389*acd78f72SGreg Roach // imagettftext() is modified to expect EUC-JP encoding instead of UTF-8. 390*acd78f72SGreg Roach // Attempt to detect and convert... 391*acd78f72SGreg Roach if (gd_info()['JIS-mapped Japanese Font Support'] ?? false) { 392*acd78f72SGreg Roach $text = mb_convert_encoding($text, 'EUC-JP', 'UTF-8'); 393*acd78f72SGreg Roach } 394*acd78f72SGreg Roach 395389266c0SGreg Roach // print text 396389266c0SGreg Roach imagettftext( 397389266c0SGreg Roach $image, 398389266c0SGreg Roach 7, 399389266c0SGreg Roach $tangle, 400389266c0SGreg Roach (int) $tx, 401389266c0SGreg Roach (int) $ty, 402389266c0SGreg Roach $foreground, 403f397d0fdSGreg Roach Webtrees::ROOT_DIR . 'resources/fonts/DejaVuSans.ttf', 404389266c0SGreg Roach $text 405389266c0SGreg Roach ); 406389266c0SGreg Roach 407389266c0SGreg Roach $areas .= '<area shape="poly" coords="'; 408389266c0SGreg Roach // plot upper points 409389266c0SGreg Roach $mr = $rx / 2; 410389266c0SGreg Roach $deg = $deg1; 411389266c0SGreg Roach while ($deg <= $deg2) { 412389266c0SGreg Roach $rad = deg2rad($deg); 413389266c0SGreg Roach $tx = round($cx + $mr * cos($rad)); 414389266c0SGreg Roach $ty = round($cy + $mr * sin($rad)); 415389266c0SGreg Roach $areas .= "$tx,$ty,"; 416389266c0SGreg Roach $deg += ($deg2 - $deg1) / 6; 417389266c0SGreg Roach } 418389266c0SGreg Roach // plot lower points 419389266c0SGreg Roach $mr = ($rx - $rw) / 2; 420389266c0SGreg Roach $deg = $deg2; 421389266c0SGreg Roach while ($deg >= $deg1) { 422389266c0SGreg Roach $rad = deg2rad($deg); 423389266c0SGreg Roach $tx = round($cx + $mr * cos($rad)); 424389266c0SGreg Roach $ty = round($cy + $mr * sin($rad)); 425389266c0SGreg Roach $areas .= "$tx,$ty,"; 426389266c0SGreg Roach $deg -= ($deg2 - $deg1) / 6; 427389266c0SGreg Roach } 428389266c0SGreg Roach // join first point 429389266c0SGreg Roach $mr = $rx / 2; 430389266c0SGreg Roach $deg = $deg1; 431389266c0SGreg Roach $rad = deg2rad($deg); 432389266c0SGreg Roach $tx = round($cx + $mr * cos($rad)); 433389266c0SGreg Roach $ty = round($cy + $mr * sin($rad)); 434389266c0SGreg Roach $areas .= "$tx,$ty"; 435389266c0SGreg Roach // add action url 436389266c0SGreg Roach $areas .= '" href="#' . $person->xref() . '"'; 437389266c0SGreg Roach $html .= '<div id="' . $person->xref() . '" class="fan_chart_menu">'; 4385e6816beSGreg Roach $html .= '<div class="person_box"><div class="small">'; 439b6c326d8SGreg Roach $html .= '<div class="charts">'; 440b6c326d8SGreg Roach $html .= '<a href="' . e($person->url()) . '" class="dropdown-item">' . $name . '</a>'; 441389266c0SGreg Roach foreach ($theme->individualBoxMenu($person) as $menu) { 442b6c326d8SGreg Roach $html .= '<a href="' . e($menu->getLink()) . '" class="dropdown-item p-1 ' . e($menu->getClass()) . '">' . $menu->getLabel() . '</a>'; 443389266c0SGreg Roach } 444b6c326d8SGreg Roach $html .= '</div>'; 445389266c0SGreg Roach $html .= '</div></div>'; 446389266c0SGreg Roach $html .= '</div>'; 44739ca88baSGreg Roach $areas .= ' alt="' . strip_tags($person->fullName()) . '" title="' . strip_tags($person->fullName()) . '">'; 448389266c0SGreg Roach } 449389266c0SGreg Roach $deg1 -= $angle; 450389266c0SGreg Roach $deg2 -= $angle; 451389266c0SGreg Roach $sosa--; 452389266c0SGreg Roach } 453389266c0SGreg Roach $rx -= $rw; 454389266c0SGreg Roach $gen--; 455389266c0SGreg Roach } 456389266c0SGreg Roach 457389266c0SGreg Roach ob_start(); 458389266c0SGreg Roach imagepng($image); 459389266c0SGreg Roach imagedestroy($image); 460389266c0SGreg Roach $png = ob_get_clean(); 461389266c0SGreg Roach 4626ccdf4f0SGreg Roach return response(view('modules/fanchart/chart', [ 463389266c0SGreg Roach 'fanh' => $fanh, 464389266c0SGreg Roach 'fanw' => $fanw, 465389266c0SGreg Roach 'html' => $html, 466389266c0SGreg Roach 'areas' => $areas, 467389266c0SGreg Roach 'png' => $png, 468389266c0SGreg Roach 'title' => $this->chartTitle($individual), 469389266c0SGreg Roach ])); 470389266c0SGreg Roach } 471389266c0SGreg Roach 472389266c0SGreg Roach /** 473389266c0SGreg Roach * split and center text by lines 474389266c0SGreg Roach * 475389266c0SGreg Roach * @param string $data input string 476389266c0SGreg Roach * @param int $maxlen max length of each line 477389266c0SGreg Roach * 478389266c0SGreg Roach * @return string $text output string 479389266c0SGreg Roach */ 480389266c0SGreg Roach protected function splitAlignText(string $data, int $maxlen): string 481389266c0SGreg Roach { 482389266c0SGreg Roach $RTLOrd = [ 483389266c0SGreg Roach 215, 484389266c0SGreg Roach 216, 485389266c0SGreg Roach 217, 486389266c0SGreg Roach 218, 487389266c0SGreg Roach 219, 488389266c0SGreg Roach ]; 489389266c0SGreg Roach 490389266c0SGreg Roach $lines = explode("\n", $data); 491389266c0SGreg Roach // more than 1 line : recursive calls 492389266c0SGreg Roach if (count($lines) > 1) { 493389266c0SGreg Roach $text = ''; 494389266c0SGreg Roach foreach ($lines as $line) { 495389266c0SGreg Roach $text .= $this->splitAlignText($line, $maxlen) . "\n"; 496389266c0SGreg Roach } 497389266c0SGreg Roach 498389266c0SGreg Roach return $text; 499389266c0SGreg Roach } 500389266c0SGreg Roach // process current line word by word 501389266c0SGreg Roach $split = explode(' ', $data); 502389266c0SGreg Roach $text = ''; 503389266c0SGreg Roach $line = ''; 504389266c0SGreg Roach 505389266c0SGreg Roach // do not split hebrew line 506389266c0SGreg Roach $found = false; 507389266c0SGreg Roach foreach ($RTLOrd as $ord) { 508389266c0SGreg Roach if (strpos($data, chr($ord)) !== false) { 509389266c0SGreg Roach $found = true; 510389266c0SGreg Roach } 511389266c0SGreg Roach } 512389266c0SGreg Roach if ($found) { 513389266c0SGreg Roach $line = $data; 514389266c0SGreg Roach } else { 515389266c0SGreg Roach foreach ($split as $word) { 516389266c0SGreg Roach $len = strlen($line); 517389266c0SGreg Roach $wlen = strlen($word); 518389266c0SGreg Roach if (($len + $wlen) < $maxlen) { 519a91af26aSGreg Roach if ($line !== '') { 520389266c0SGreg Roach $line .= ' '; 521389266c0SGreg Roach } 522e364afe4SGreg Roach $line .= $word; 523389266c0SGreg Roach } else { 524389266c0SGreg Roach $p = max(0, (int) (($maxlen - $len) / 2)); 525a91af26aSGreg Roach if ($line !== '') { 526389266c0SGreg Roach $line = str_repeat(' ', $p) . $line; // center alignment using spaces 527389266c0SGreg Roach $text .= $line . "\n"; 528389266c0SGreg Roach } 529389266c0SGreg Roach $line = $word; 530389266c0SGreg Roach } 531389266c0SGreg Roach } 532389266c0SGreg Roach } 533389266c0SGreg Roach // last line 53496949fc5SGreg Roach if ($line !== '') { 535389266c0SGreg Roach $len = strlen($line); 536d3e2fc58SGreg Roach if (in_array(ord($line[0]), $RTLOrd, true)) { 537389266c0SGreg Roach $len /= 2; 538389266c0SGreg Roach } 539389266c0SGreg Roach $p = max(0, (int) (($maxlen - $len) / 2)); 540389266c0SGreg Roach $line = str_repeat(' ', $p) . $line; // center alignment using spaces 541389266c0SGreg Roach $text .= $line; 542389266c0SGreg Roach } 543389266c0SGreg Roach 544389266c0SGreg Roach return $text; 545389266c0SGreg Roach } 546389266c0SGreg Roach 547389266c0SGreg Roach /** 548389266c0SGreg Roach * Convert a CSS color into a GD color. 549389266c0SGreg Roach * 550389266c0SGreg Roach * @param resource $image 551389266c0SGreg Roach * @param string $css_color 552389266c0SGreg Roach * 553389266c0SGreg Roach * @return int 554389266c0SGreg Roach */ 555389266c0SGreg Roach protected function imageColor($image, string $css_color): int 556389266c0SGreg Roach { 557389266c0SGreg Roach return imagecolorallocate( 558389266c0SGreg Roach $image, 559389266c0SGreg Roach (int) hexdec(substr($css_color, 0, 2)), 560389266c0SGreg Roach (int) hexdec(substr($css_color, 2, 2)), 561389266c0SGreg Roach (int) hexdec(substr($css_color, 4, 2)) 562389266c0SGreg Roach ); 563389266c0SGreg Roach } 564389266c0SGreg Roach 565389266c0SGreg Roach /** 566389266c0SGreg Roach * This chart can display its output in a number of styles 567389266c0SGreg Roach * 568389266c0SGreg Roach * @return array 569389266c0SGreg Roach */ 57071378461SGreg Roach protected function styles(): array 571389266c0SGreg Roach { 572389266c0SGreg Roach return [ 573389266c0SGreg Roach /* I18N: layout option for the fan chart */ 574389266c0SGreg Roach self::STYLE_HALF_CIRCLE => I18N::translate('half circle'), 575389266c0SGreg Roach /* I18N: layout option for the fan chart */ 576389266c0SGreg Roach self::STYLE_THREE_QUARTER_CIRCLE => I18N::translate('three-quarter circle'), 577389266c0SGreg Roach /* I18N: layout option for the fan chart */ 578389266c0SGreg Roach self::STYLE_FULL_CIRCLE => I18N::translate('full circle'), 579389266c0SGreg Roach ]; 580e6562982SGreg Roach } 581168ff6f3Sric2016} 582