1168ff6f3Sric2016<?php 23976b470SGreg Roach 3168ff6f3Sric2016/** 4168ff6f3Sric2016 * webtrees: online genealogy 5d11be702SGreg Roach * Copyright (C) 2023 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 1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>. 16168ff6f3Sric2016 */ 17fcfa147eSGreg Roach 18e7f56f2aSGreg Roachdeclare(strict_types=1); 19e7f56f2aSGreg Roach 20168ff6f3Sric2016namespace Fisharebest\Webtrees\Module; 21168ff6f3Sric2016 2271378461SGreg Roachuse Fig\Http\Message\RequestMethodInterface; 23389266c0SGreg Roachuse Fisharebest\Webtrees\Auth; 24168ff6f3Sric2016use Fisharebest\Webtrees\I18N; 25168ff6f3Sric2016use Fisharebest\Webtrees\Individual; 266664b4a3SGreg Roachuse Fisharebest\Webtrees\Menu; 2791bb35acSGreg Roachuse Fisharebest\Webtrees\Registry; 28389266c0SGreg Roachuse Fisharebest\Webtrees\Services\ChartService; 29b55cbc6bSGreg Roachuse Fisharebest\Webtrees\Validator; 30f397d0fdSGreg Roachuse Fisharebest\Webtrees\Webtrees; 31f117d295SGreg Roachuse GdImage; 326ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface; 336ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface; 3471378461SGreg Roachuse Psr\Http\Server\RequestHandlerInterface; 3571378461SGreg Roach 3691bb35acSGreg Roachuse function array_filter; 3791bb35acSGreg Roachuse function array_map; 3891bb35acSGreg Roachuse function cos; 3991bb35acSGreg Roachuse function deg2rad; 4091bb35acSGreg Roachuse function e; 4191bb35acSGreg Roachuse function gd_info; 4291bb35acSGreg Roachuse function hexdec; 4391bb35acSGreg Roachuse function imagecolorallocate; 4491bb35acSGreg Roachuse function imagecolortransparent; 4591bb35acSGreg Roachuse function imagecreate; 4691bb35acSGreg Roachuse function imagefilledarc; 4791bb35acSGreg Roachuse function imagefilledrectangle; 4891bb35acSGreg Roachuse function imagepng; 4991bb35acSGreg Roachuse function imagettfbbox; 5091bb35acSGreg Roachuse function imagettftext; 5171378461SGreg Roachuse function implode; 52a18dc098SGreg Roachuse function intdiv; 5391bb35acSGreg Roachuse function mb_substr; 5491bb35acSGreg Roachuse function ob_get_clean; 5591bb35acSGreg Roachuse function ob_start; 5671378461SGreg Roachuse function redirect; 5791bb35acSGreg Roachuse function response; 5891bb35acSGreg Roachuse function round; 5971378461SGreg Roachuse function route; 6091bb35acSGreg Roachuse function rtrim; 6191bb35acSGreg Roachuse function sin; 6291bb35acSGreg Roachuse function sqrt; 6391bb35acSGreg Roachuse function strip_tags; 6491bb35acSGreg Roachuse function substr; 6591bb35acSGreg Roachuse function view; 6691bb35acSGreg Roach 6791bb35acSGreg Roachuse const IMG_ARC_PIE; 68168ff6f3Sric2016 69168ff6f3Sric2016/** 70168ff6f3Sric2016 * Class FanChartModule 71168ff6f3Sric2016 */ 7271378461SGreg Roachclass FanChartModule extends AbstractModule implements ModuleChartInterface, RequestHandlerInterface 73c1010edaSGreg Roach{ 7449a243cbSGreg Roach use ModuleChartTrait; 7549a243cbSGreg Roach 7672f04adfSGreg Roach protected const ROUTE_URL = '/tree/{tree}/fan-chart-{style}-{generations}-{width}/{xref}'; 7771378461SGreg Roach 78389266c0SGreg Roach // Chart styles 79d2d056daSGreg Roach private const STYLE_HALF_CIRCLE = 2; 80d2d056daSGreg Roach private const STYLE_THREE_QUARTER_CIRCLE = 3; 81d2d056daSGreg Roach private const STYLE_FULL_CIRCLE = 4; 8271378461SGreg Roach 8371378461SGreg Roach // Defaults 84266e9c61SGreg Roach public const DEFAULT_STYLE = self::STYLE_THREE_QUARTER_CIRCLE; 85266e9c61SGreg Roach public const DEFAULT_GENERATIONS = 4; 86266e9c61SGreg Roach public const DEFAULT_WIDTH = 100; 8771378461SGreg Roach protected const DEFAULT_PARAMETERS = [ 8871378461SGreg Roach 'style' => self::DEFAULT_STYLE, 8971378461SGreg Roach 'generations' => self::DEFAULT_GENERATIONS, 9071378461SGreg Roach 'width' => self::DEFAULT_WIDTH, 9171378461SGreg Roach ]; 92389266c0SGreg Roach 93389266c0SGreg Roach // Limits 94389266c0SGreg Roach private const MINIMUM_GENERATIONS = 2; 95389266c0SGreg Roach private const MAXIMUM_GENERATIONS = 9; 96389266c0SGreg Roach private const MINIMUM_WIDTH = 50; 97389266c0SGreg Roach private const MAXIMUM_WIDTH = 500; 98389266c0SGreg Roach 9991bb35acSGreg Roach // Chart layout parameters 10091bb35acSGreg Roach private const FONT = Webtrees::ROOT_DIR . 'resources/fonts/DejaVuSans.ttf'; 10191bb35acSGreg Roach private const CHART_WIDTH_PIXELS = 800; 10291bb35acSGreg Roach private const TEXT_SIZE_POINTS = self::CHART_WIDTH_PIXELS / 120.0; 10391bb35acSGreg Roach private const GAP_BETWEEN_RINGS = 2; 10491bb35acSGreg Roach 10591bb35acSGreg Roach private ChartService $chart_service; 10657ab2231SGreg Roach 10757ab2231SGreg Roach /** 10857ab2231SGreg Roach * @param ChartService $chart_service 10957ab2231SGreg Roach */ 1103976b470SGreg Roach public function __construct(ChartService $chart_service) 1113976b470SGreg Roach { 11257ab2231SGreg Roach $this->chart_service = $chart_service; 11357ab2231SGreg Roach } 11457ab2231SGreg Roach 115168ff6f3Sric2016 /** 11671378461SGreg Roach * Initialization. 11771378461SGreg Roach * 1189e18e23bSGreg Roach * @return void 11971378461SGreg Roach */ 1209e18e23bSGreg Roach public function boot(): void 12171378461SGreg Roach { 122158900c2SGreg Roach Registry::routeFactory()->routeMap() 12372f04adfSGreg Roach ->get(static::class, static::ROUTE_URL, $this) 124158900c2SGreg Roach ->allows(RequestMethodInterface::METHOD_POST); 12571378461SGreg Roach } 12671378461SGreg Roach 12771378461SGreg Roach /** 1280cfd6963SGreg Roach * How should this module be identified in the control panel, etc.? 129168ff6f3Sric2016 * 130168ff6f3Sric2016 * @return string 131168ff6f3Sric2016 */ 13249a243cbSGreg Roach public function title(): string 133c1010edaSGreg Roach { 134bbb76c12SGreg Roach /* I18N: Name of a module/chart */ 135bbb76c12SGreg Roach return I18N::translate('Fan chart'); 136168ff6f3Sric2016 } 137168ff6f3Sric2016 13849a243cbSGreg Roach public function description(): string 139c1010edaSGreg Roach { 140bbb76c12SGreg Roach /* I18N: Description of the “Fan Chart” module */ 141bbb76c12SGreg Roach return I18N::translate('A fan chart of an individual’s ancestors.'); 142168ff6f3Sric2016 } 143168ff6f3Sric2016 144168ff6f3Sric2016 /** 145377a2979SGreg Roach * CSS class for the URL. 146377a2979SGreg Roach * 147377a2979SGreg Roach * @return string 148377a2979SGreg Roach */ 149377a2979SGreg Roach public function chartMenuClass(): string 150377a2979SGreg Roach { 151377a2979SGreg Roach return 'menu-chart-fanchart'; 152377a2979SGreg Roach } 153377a2979SGreg Roach 154377a2979SGreg Roach /** 1554eb71cfaSGreg Roach * Return a menu item for this chart - for use in individual boxes. 1564eb71cfaSGreg Roach */ 157*1ff45046SGreg Roach public function chartBoxMenu(Individual $individual): Menu|null 158c1010edaSGreg Roach { 159e6562982SGreg Roach return $this->chartMenu($individual); 160e6562982SGreg Roach } 161e6562982SGreg Roach 162e6562982SGreg Roach /** 163e6562982SGreg Roach * The title for a specific instance of this chart. 164e6562982SGreg Roach * 165e6562982SGreg Roach * @param Individual $individual 166e6562982SGreg Roach * 167e6562982SGreg Roach * @return string 168e6562982SGreg Roach */ 169e6562982SGreg Roach public function chartTitle(Individual $individual): string 170e6562982SGreg Roach { 171ad3143ccSGreg Roach /* I18N: https://en.wikipedia.org/wiki/Family_tree#Fan_chart - %s is an individual’s name */ 17239ca88baSGreg Roach return I18N::translate('Fan chart of %s', $individual->fullName()); 173e6562982SGreg Roach } 174e6562982SGreg Roach 175e6562982SGreg Roach /** 176389266c0SGreg Roach * A form to request the chart parameters. 177389266c0SGreg Roach * 17871378461SGreg Roach * @param Individual $individual 17976d39c55SGreg Roach * @param array<bool|int|string|array<string>|null> $parameters 18071378461SGreg Roach * 18171378461SGreg Roach * @return string 18271378461SGreg Roach */ 18371378461SGreg Roach public function chartUrl(Individual $individual, array $parameters = []): string 18471378461SGreg Roach { 18572f04adfSGreg Roach return route(static::class, [ 18671378461SGreg Roach 'xref' => $individual->xref(), 18771378461SGreg Roach 'tree' => $individual->tree()->name(), 18871378461SGreg Roach ] + $parameters + self::DEFAULT_PARAMETERS); 18971378461SGreg Roach } 19071378461SGreg Roach 19171378461SGreg Roach /** 1926ccdf4f0SGreg Roach * @param ServerRequestInterface $request 193389266c0SGreg Roach * 1946ccdf4f0SGreg Roach * @return ResponseInterface 195389266c0SGreg Roach */ 19671378461SGreg Roach public function handle(ServerRequestInterface $request): ResponseInterface 197389266c0SGreg Roach { 198b55cbc6bSGreg Roach $tree = Validator::attributes($request)->tree(); 199b55cbc6bSGreg Roach $user = Validator::attributes($request)->user(); 200b55cbc6bSGreg Roach $xref = Validator::attributes($request)->isXref()->string('xref'); 201d2d056daSGreg Roach $style = Validator::attributes($request)->isInArrayKeys($this->styles())->integer('style'); 202b55cbc6bSGreg Roach $generations = Validator::attributes($request)->isBetween(self::MINIMUM_GENERATIONS, self::MAXIMUM_GENERATIONS)->integer('generations'); 203b55cbc6bSGreg Roach $width = Validator::attributes($request)->isBetween(self::MINIMUM_WIDTH, self::MAXIMUM_WIDTH)->integer('width'); 204b55cbc6bSGreg Roach $ajax = Validator::queryParams($request)->boolean('ajax', false); 205389266c0SGreg Roach 20671378461SGreg Roach // Convert POST requests into GET requests for pretty URLs. 20771378461SGreg Roach if ($request->getMethod() === RequestMethodInterface::METHOD_POST) { 20872f04adfSGreg Roach return redirect(route(static::class, [ 2094ea62551SGreg Roach 'tree' => $tree->name(), 210158900c2SGreg Roach 'generations' => Validator::parsedBody($request)->isBetween(self::MINIMUM_GENERATIONS, self::MAXIMUM_GENERATIONS)->integer('generations'), 211d2d056daSGreg Roach 'style' => Validator::parsedBody($request)->isInArrayKeys($this->styles())->integer('style'), 212158900c2SGreg Roach 'width' => Validator::parsedBody($request)->isBetween(self::MINIMUM_WIDTH, self::MAXIMUM_WIDTH)->integer('width'), 213158900c2SGreg Roach 'xref' => Validator::parsedBody($request)->isXref()->string('xref'), 21471378461SGreg Roach ])); 21571378461SGreg Roach } 21671378461SGreg Roach 217ef483801SGreg Roach Auth::checkComponentAccess($this, ModuleChartInterface::class, $tree, $user); 218389266c0SGreg Roach 219b55cbc6bSGreg Roach $individual = Registry::individualFactory()->make($xref, $tree); 220b55cbc6bSGreg Roach $individual = Auth::checkIndividualAccess($individual, false, true); 221389266c0SGreg Roach 222b55cbc6bSGreg Roach if ($ajax) { 22371378461SGreg Roach return $this->chart($individual, $style, $width, $generations); 224389266c0SGreg Roach } 225389266c0SGreg Roach 226389266c0SGreg Roach $ajax_url = $this->chartUrl($individual, [ 2279b5537c3SGreg Roach 'ajax' => true, 228389266c0SGreg Roach 'generations' => $generations, 22971378461SGreg Roach 'style' => $style, 23071378461SGreg Roach 'width' => $width, 231389266c0SGreg Roach ]); 232389266c0SGreg Roach 2339b5537c3SGreg Roach return $this->viewResponse('modules/fanchart/page', [ 234389266c0SGreg Roach 'ajax_url' => $ajax_url, 235389266c0SGreg Roach 'generations' => $generations, 236389266c0SGreg Roach 'individual' => $individual, 237389266c0SGreg Roach 'maximum_generations' => self::MAXIMUM_GENERATIONS, 238389266c0SGreg Roach 'minimum_generations' => self::MINIMUM_GENERATIONS, 239389266c0SGreg Roach 'maximum_width' => self::MAXIMUM_WIDTH, 240389266c0SGreg Roach 'minimum_width' => self::MINIMUM_WIDTH, 24171378461SGreg Roach 'module' => $this->name(), 24271378461SGreg Roach 'style' => $style, 24371378461SGreg Roach 'styles' => $this->styles(), 244389266c0SGreg Roach 'title' => $this->chartTitle($individual), 245ef5d23f1SGreg Roach 'tree' => $tree, 24671378461SGreg Roach 'width' => $width, 247389266c0SGreg Roach ]); 248389266c0SGreg Roach } 249389266c0SGreg Roach 250389266c0SGreg Roach /** 251389266c0SGreg Roach * Generate both the HTML and PNG components of the fan chart 252e6562982SGreg Roach * 253e6562982SGreg Roach * @param Individual $individual 254d2d056daSGreg Roach * @param int $style 25571378461SGreg Roach * @param int $width 256389266c0SGreg Roach * @param int $generations 257e6562982SGreg Roach * 2586ccdf4f0SGreg Roach * @return ResponseInterface 259e6562982SGreg Roach */ 260d2d056daSGreg Roach protected function chart(Individual $individual, int $style, int $width, int $generations): ResponseInterface 261e6562982SGreg Roach { 26271378461SGreg Roach $ancestors = $this->chart_service->sosaStradonitzAncestors($individual, $generations); 263389266c0SGreg Roach 26491bb35acSGreg Roach $width = intdiv(self::CHART_WIDTH_PIXELS * $width, 100); 265389266c0SGreg Roach 26691bb35acSGreg Roach switch ($style) { 26791bb35acSGreg Roach case self::STYLE_HALF_CIRCLE: 26891bb35acSGreg Roach $chart_start_angle = 180; 26991bb35acSGreg Roach $chart_end_angle = 360; 27091bb35acSGreg Roach $height = intdiv($width, 2); 27191bb35acSGreg Roach break; 27291bb35acSGreg Roach 2730945b150SGreg Roach case self::STYLE_THREE_QUARTER_CIRCLE: 27491bb35acSGreg Roach $chart_start_angle = 135; 27591bb35acSGreg Roach $chart_end_angle = 405; 27691bb35acSGreg Roach $height = intdiv($width * 86, 100); 27791bb35acSGreg Roach break; 27891bb35acSGreg Roach 2790945b150SGreg Roach case self::STYLE_FULL_CIRCLE: 28091bb35acSGreg Roach default: 28191bb35acSGreg Roach $chart_start_angle = 90; 28291bb35acSGreg Roach $chart_end_angle = 450; 28391bb35acSGreg Roach $height = $width; 28491bb35acSGreg Roach break; 285389266c0SGreg Roach } 286389266c0SGreg Roach 28791bb35acSGreg Roach // Start with a transparent image. 28891bb35acSGreg Roach $image = imagecreate($width, $height); 289389266c0SGreg Roach $transparent = imagecolorallocate($image, 0, 0, 0); 290389266c0SGreg Roach imagecolortransparent($image, $transparent); 29191bb35acSGreg Roach imagefilledrectangle($image, 0, 0, $width, $height, $transparent); 292389266c0SGreg Roach 29391bb35acSGreg Roach // Use theme-specified colors. 294d35568b4SGreg Roach $theme = Registry::container()->get(ModuleThemeInterface::class); 29534b20f29SGreg Roach $text_color = $this->imageColor($image, '000000'); 296389266c0SGreg Roach $backgrounds = [ 29734b20f29SGreg Roach 'M' => $this->imageColor($image, 'b1cff0'), 29834b20f29SGreg Roach 'F' => $this->imageColor($image, 'e9daf1'), 29934b20f29SGreg Roach 'U' => $this->imageColor($image, 'eeeeee'), 300389266c0SGreg Roach ]; 301389266c0SGreg Roach 30291bb35acSGreg Roach // Co-ordinates are measured from the top-left corner. 30391bb35acSGreg Roach $center_x = intdiv($width, 2); 30491bb35acSGreg Roach $center_y = $center_x; 30591bb35acSGreg Roach $arc_width = $width / $generations / 2.0; 306389266c0SGreg Roach 30791bb35acSGreg Roach // Popup menus for each ancestor. 308389266c0SGreg Roach $html = ''; 309389266c0SGreg Roach 31091bb35acSGreg Roach // Areas for the image map. 311389266c0SGreg Roach $areas = ''; 312389266c0SGreg Roach 31391bb35acSGreg Roach for ($generation = $generations; $generation >= 1; $generation--) { 31491bb35acSGreg Roach // Which ancestors to include in this ring. 1, 2-3, 4-7, 8-15, 16-31, etc. 31591bb35acSGreg Roach // The end of the range is also the number of ancestors in the ring. 31691bb35acSGreg Roach $sosa_start = 2 ** $generation - 1; 31791bb35acSGreg Roach $sosa_end = 2 ** ($generation - 1); 318a18dc098SGreg Roach 31991bb35acSGreg Roach $arc_diameter = intdiv($width * $generation, $generations); 32091bb35acSGreg Roach $arc_radius = $arc_diameter / 2; 321389266c0SGreg Roach 32291bb35acSGreg Roach // Draw an empty background, for missing ancestors. 32391bb35acSGreg Roach imagefilledarc( 32491bb35acSGreg Roach $image, 32591bb35acSGreg Roach $center_x, 32691bb35acSGreg Roach $center_y, 32791bb35acSGreg Roach $arc_diameter, 32891bb35acSGreg Roach $arc_diameter, 32991bb35acSGreg Roach $chart_start_angle, 33091bb35acSGreg Roach $chart_end_angle, 33191bb35acSGreg Roach $backgrounds['U'], 33291bb35acSGreg Roach IMG_ARC_PIE 33391bb35acSGreg Roach ); 334389266c0SGreg Roach 33591bb35acSGreg Roach $arc_diameter -= 2 * self::GAP_BETWEEN_RINGS; 33691bb35acSGreg Roach 33791bb35acSGreg Roach for ($sosa = $sosa_start; $sosa >= $sosa_end; $sosa--) { 338389266c0SGreg Roach if ($ancestors->has($sosa)) { 33991bb35acSGreg Roach $individual = $ancestors->get($sosa); 340389266c0SGreg Roach 34191bb35acSGreg Roach $chart_angle = $chart_end_angle - $chart_start_angle; 34291bb35acSGreg Roach $start_angle = $chart_start_angle + intdiv($chart_angle * ($sosa - $sosa_end), $sosa_end); 34391bb35acSGreg Roach $end_angle = $chart_start_angle + intdiv($chart_angle * ($sosa - $sosa_end + 1), $sosa_end); 34491bb35acSGreg Roach $angle = $end_angle - $start_angle; 34591bb35acSGreg Roach 34691bb35acSGreg Roach imagefilledarc( 34791bb35acSGreg Roach $image, 34891bb35acSGreg Roach $center_x, 34991bb35acSGreg Roach $center_y, 35091bb35acSGreg Roach $arc_diameter, 35191bb35acSGreg Roach $arc_diameter, 35291bb35acSGreg Roach $start_angle, 35391bb35acSGreg Roach $end_angle, 35423a98013SGreg Roach $backgrounds[$individual->sex()] ?? $backgrounds['U'], 35591bb35acSGreg Roach IMG_ARC_PIE 35691bb35acSGreg Roach ); 35791bb35acSGreg Roach 35891bb35acSGreg Roach // Text is written at a tangent to the arc. 35991bb35acSGreg Roach $text_angle = 270.0 - ($start_angle + $end_angle) / 2.0; 36091bb35acSGreg Roach 36191bb35acSGreg Roach $text_radius = $arc_diameter / 2.0 - $arc_width * 0.25; 36291bb35acSGreg Roach 36391bb35acSGreg Roach // Don't draw text right up to the edge of the arc. 36491bb35acSGreg Roach if ($angle === 360) { 36591bb35acSGreg Roach $delta = 90; 36691bb35acSGreg Roach } elseif ($angle === 180) { 36791bb35acSGreg Roach if ($generation === 1) { 36891bb35acSGreg Roach $delta = 20; 36991bb35acSGreg Roach } else { 37091bb35acSGreg Roach $delta = 60; 37191bb35acSGreg Roach } 37291bb35acSGreg Roach } elseif ($angle > 120) { 37391bb35acSGreg Roach $delta = 45; 37491bb35acSGreg Roach } elseif ($angle > 60) { 37591bb35acSGreg Roach $delta = 15; 37691bb35acSGreg Roach } else { 37791bb35acSGreg Roach $delta = 1; 378389266c0SGreg Roach } 379389266c0SGreg Roach 38091bb35acSGreg Roach $tx_start = $center_x + $text_radius * cos(deg2rad($start_angle + $delta)); 38191bb35acSGreg Roach $ty_start = $center_y + $text_radius * sin(deg2rad($start_angle + $delta)); 38291bb35acSGreg Roach $tx_end = $center_x + $text_radius * cos(deg2rad($end_angle - $delta)); 38391bb35acSGreg Roach $ty_end = $center_y + $text_radius * sin(deg2rad($end_angle - $delta)); 384389266c0SGreg Roach 38591bb35acSGreg Roach $max_text_length = (int) sqrt(($tx_end - $tx_start) ** 2 + ($ty_end - $ty_start) ** 2); 386389266c0SGreg Roach 38791bb35acSGreg Roach $text_lines = array_filter([ 38891bb35acSGreg Roach I18N::reverseText($individual->fullName()), 38991bb35acSGreg Roach I18N::reverseText($individual->alternateName() ?? ''), 39091bb35acSGreg Roach I18N::reverseText($individual->lifespan()), 39191bb35acSGreg Roach ]); 392389266c0SGreg Roach 39391bb35acSGreg Roach $text_lines = array_map( 39491bb35acSGreg Roach fn (string $line): string => $this->fitTextToPixelWidth($line, $max_text_length), 39591bb35acSGreg Roach $text_lines 39691bb35acSGreg Roach ); 397389266c0SGreg Roach 39891bb35acSGreg Roach $text = implode("\n", $text_lines); 399389266c0SGreg Roach 40091bb35acSGreg Roach if ($generation === 1) { 40191bb35acSGreg Roach $ty_start -= $text_radius / 2; 402389266c0SGreg Roach } 403389266c0SGreg Roach 404acd78f72SGreg Roach // If PHP is compiled with --enable-gd-jis-conv, then the function 405acd78f72SGreg Roach // imagettftext() is modified to expect EUC-JP encoding instead of UTF-8. 406acd78f72SGreg Roach // Attempt to detect and convert... 407acd78f72SGreg Roach if (gd_info()['JIS-mapped Japanese Font Support'] ?? false) { 408acd78f72SGreg Roach $text = mb_convert_encoding($text, 'EUC-JP', 'UTF-8'); 409acd78f72SGreg Roach } 410acd78f72SGreg Roach 411389266c0SGreg Roach imagettftext( 412389266c0SGreg Roach $image, 41391bb35acSGreg Roach self::TEXT_SIZE_POINTS, 41491bb35acSGreg Roach $text_angle, 41591bb35acSGreg Roach (int) $tx_start, 41691bb35acSGreg Roach (int) $ty_start, 41791bb35acSGreg Roach $text_color, 4186bd19c8cSGreg Roach self::FONT, 419389266c0SGreg Roach $text 420389266c0SGreg Roach ); 42191bb35acSGreg Roach // Debug text positions by underlining first line of text 42291bb35acSGreg Roach //imageline($image, (int) $tx_start, (int) $ty_start, (int) $tx_end, (int) $ty_end, $backgrounds['U']); 423389266c0SGreg Roach 424389266c0SGreg Roach $areas .= '<area shape="poly" coords="'; 42591bb35acSGreg Roach for ($deg = $start_angle; $deg <= $end_angle; $deg++) { 426389266c0SGreg Roach $rad = deg2rad($deg); 4279b60e0d2SFranz Frese $areas .= round($center_x + $arc_radius * cos($rad), 1) . ','; 4289b60e0d2SFranz Frese $areas .= round($center_y + $arc_radius * sin($rad), 1) . ','; 429389266c0SGreg Roach } 43091bb35acSGreg Roach for ($deg = $end_angle; $deg >= $start_angle; $deg--) { 431389266c0SGreg Roach $rad = deg2rad($deg); 43291bb35acSGreg Roach $areas .= round($center_x + ($arc_radius - $arc_width) * cos($rad), 1) . ','; 43391bb35acSGreg Roach $areas .= round($center_y + ($arc_radius - $arc_width) * sin($rad), 1) . ','; 434389266c0SGreg Roach } 43591bb35acSGreg Roach $rad = deg2rad($start_angle); 43691bb35acSGreg Roach $areas .= round($center_x + $arc_radius * cos($rad), 1) . ','; 43791bb35acSGreg Roach $areas .= round($center_y + $arc_radius * sin($rad), 1) . '"'; 43891bb35acSGreg Roach 43991bb35acSGreg Roach $areas .= ' href="#' . e($individual->xref()) . '"'; 44091bb35acSGreg Roach $areas .= ' alt="' . strip_tags($individual->fullName()) . '"'; 44191bb35acSGreg Roach $areas .= ' title="' . strip_tags($individual->fullName()) . '">'; 44291bb35acSGreg Roach 44391bb35acSGreg Roach $html .= '<div id="' . $individual->xref() . '" class="fan_chart_menu">'; 44491bb35acSGreg Roach $html .= '<a href="' . e($individual->url()) . '" class="dropdown-item p-1">'; 44591bb35acSGreg Roach $html .= $individual->fullName(); 44691bb35acSGreg Roach $html .= '</a>'; 44791bb35acSGreg Roach 44891bb35acSGreg Roach foreach ($theme->individualBoxMenu($individual) as $menu) { 44991bb35acSGreg Roach $link = $menu->getLink(); 45091bb35acSGreg Roach $class = $menu->getClass(); 45191bb35acSGreg Roach $html .= '<a href="' . e($link) . '" class="dropdown-item p-1 ' . e($class) . '">'; 45291bb35acSGreg Roach $html .= $menu->getLabel(); 45391bb35acSGreg Roach $html .= '</a>'; 454389266c0SGreg Roach } 45591bb35acSGreg Roach 456b6c326d8SGreg Roach $html .= '</div>'; 457389266c0SGreg Roach } 458389266c0SGreg Roach } 459389266c0SGreg Roach } 460389266c0SGreg Roach 461389266c0SGreg Roach ob_start(); 462389266c0SGreg Roach imagepng($image); 463389266c0SGreg Roach $png = ob_get_clean(); 464389266c0SGreg Roach 4656ccdf4f0SGreg Roach return response(view('modules/fanchart/chart', [ 46691bb35acSGreg Roach 'fanh' => $height, 46791bb35acSGreg Roach 'fanw' => $width, 468389266c0SGreg Roach 'html' => $html, 469389266c0SGreg Roach 'areas' => $areas, 470389266c0SGreg Roach 'png' => $png, 471389266c0SGreg Roach 'title' => $this->chartTitle($individual), 472389266c0SGreg Roach ])); 473389266c0SGreg Roach } 474389266c0SGreg Roach 475389266c0SGreg Roach /** 476389266c0SGreg Roach * Convert a CSS color into a GD color. 477389266c0SGreg Roach * 478f117d295SGreg Roach * @param GdImage $image 479389266c0SGreg Roach * @param string $css_color 480389266c0SGreg Roach * 481389266c0SGreg Roach * @return int 482389266c0SGreg Roach */ 483f117d295SGreg Roach protected function imageColor(GdImage $image, string $css_color): int 484389266c0SGreg Roach { 485389266c0SGreg Roach return imagecolorallocate( 486389266c0SGreg Roach $image, 487389266c0SGreg Roach (int) hexdec(substr($css_color, 0, 2)), 488389266c0SGreg Roach (int) hexdec(substr($css_color, 2, 2)), 489389266c0SGreg Roach (int) hexdec(substr($css_color, 4, 2)) 490389266c0SGreg Roach ); 491389266c0SGreg Roach } 492389266c0SGreg Roach 493389266c0SGreg Roach /** 494389266c0SGreg Roach * This chart can display its output in a number of styles 495389266c0SGreg Roach * 496fc26b4f6SGreg Roach * @return array<string> 497389266c0SGreg Roach */ 49871378461SGreg Roach protected function styles(): array 499389266c0SGreg Roach { 500389266c0SGreg Roach return [ 501389266c0SGreg Roach /* I18N: layout option for the fan chart */ 502389266c0SGreg Roach self::STYLE_HALF_CIRCLE => I18N::translate('half circle'), 503389266c0SGreg Roach /* I18N: layout option for the fan chart */ 504389266c0SGreg Roach self::STYLE_THREE_QUARTER_CIRCLE => I18N::translate('three-quarter circle'), 505389266c0SGreg Roach /* I18N: layout option for the fan chart */ 506389266c0SGreg Roach self::STYLE_FULL_CIRCLE => I18N::translate('full circle'), 507389266c0SGreg Roach ]; 508e6562982SGreg Roach } 50991bb35acSGreg Roach 51091bb35acSGreg Roach /** 51191bb35acSGreg Roach * Fit text to a given number of pixels by either cropping to fit, 51291bb35acSGreg Roach * or adding spaces to center. 51391bb35acSGreg Roach * 51491bb35acSGreg Roach * @param string $text 51591bb35acSGreg Roach * @param int $pixels 51691bb35acSGreg Roach * 51791bb35acSGreg Roach * @return string 51891bb35acSGreg Roach */ 51991bb35acSGreg Roach protected function fitTextToPixelWidth(string $text, int $pixels): string 52091bb35acSGreg Roach { 52191bb35acSGreg Roach while ($this->textWidthInPixels($text) > $pixels) { 52291bb35acSGreg Roach $text = mb_substr($text, 0, -1); 52391bb35acSGreg Roach } 52491bb35acSGreg Roach 52591bb35acSGreg Roach while ($this->textWidthInPixels(' ' . $text . ' ') < $pixels) { 52691bb35acSGreg Roach $text = ' ' . $text . ' '; 52791bb35acSGreg Roach } 52891bb35acSGreg Roach 52991bb35acSGreg Roach // We only need the leading spaces. 53091bb35acSGreg Roach return rtrim($text); 53191bb35acSGreg Roach } 53291bb35acSGreg Roach 53391bb35acSGreg Roach /** 53491bb35acSGreg Roach * @param string $text 53591bb35acSGreg Roach * 53691bb35acSGreg Roach * @return int 53791bb35acSGreg Roach */ 53891bb35acSGreg Roach protected function textWidthInPixels(string $text): int 53991bb35acSGreg Roach { 54029033689SGreg Roach // If PHP is compiled with --enable-gd-jis-conv, then the function 54129033689SGreg Roach // imagettftext() is modified to expect EUC-JP encoding instead of UTF-8. 54229033689SGreg Roach // Attempt to detect and convert... 54329033689SGreg Roach if (gd_info()['JIS-mapped Japanese Font Support'] ?? false) { 54429033689SGreg Roach $text = mb_convert_encoding($text, 'EUC-JP', 'UTF-8'); 54529033689SGreg Roach } 54629033689SGreg Roach 54791bb35acSGreg Roach $bounding_box = imagettfbbox(self::TEXT_SIZE_POINTS, 0, self::FONT, $text); 54891bb35acSGreg Roach 54991bb35acSGreg Roach return $bounding_box[4] - $bounding_box[0]; 55091bb35acSGreg Roach } 551168ff6f3Sric2016} 552