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() . '&amp;fan_style=' . $this->fan_style . '&amp;generations=' . $this->generations . '&amp;fan_width=' . $this->fan_width . '&amp;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