xref: /webtrees/app/Module/FanChartModule.php (revision acd78f726d6605c5b80bd6bfddc79eb43d197413)
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