xref: /webtrees/app/Module/TopSurnamesModule.php (revision 0c0910bf0f275a14f35d2ccdf698f91f79e269d4)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2019 webtrees development team
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17declare(strict_types=1);
18
19namespace Fisharebest\Webtrees\Module;
20
21use Fisharebest\Webtrees\Auth;
22use Fisharebest\Webtrees\Functions\FunctionsPrintLists;
23use Fisharebest\Webtrees\I18N;
24use Fisharebest\Webtrees\Tree;
25use Fisharebest\Webtrees\Services\ModuleService;
26use Illuminate\Database\Capsule\Manager as DB;
27use Illuminate\Database\Query\Expression;
28use Illuminate\Support\Str;
29use Psr\Http\Message\ServerRequestInterface;
30
31/**
32 * Class TopSurnamesModule
33 */
34class TopSurnamesModule extends AbstractModule implements ModuleBlockInterface
35{
36    use ModuleBlockTrait;
37
38    // Default values for new blocks.
39    private const DEFAULT_NUMBER = '10';
40    private const DEFAULT_STYLE  = 'table';
41
42    /**
43     * How should this module be identified in the control panel, etc.?
44     *
45     * @return string
46     */
47    public function title(): string
48    {
49        /* I18N: Name of a module. Top=Most common */
50        return I18N::translate('Top surnames');
51    }
52
53    /**
54     * A sentence describing what this module does.
55     *
56     * @return string
57     */
58    public function description(): string
59    {
60        /* I18N: Description of the “Top surnames” module */
61        return I18N::translate('A list of the most popular surnames.');
62    }
63
64    /**
65     * Generate the HTML content of this block.
66     *
67     * @param Tree     $tree
68     * @param int      $block_id
69     * @param string   $context
70     * @param string[] $config
71     *
72     * @return string
73     */
74    public function getBlock(Tree $tree, int $block_id, string $context, array $config = []): string
75    {
76        $num       = (int) $this->getBlockSetting($block_id, 'num', self::DEFAULT_NUMBER);
77        $infoStyle = $this->getBlockSetting($block_id, 'infoStyle', self::DEFAULT_STYLE);
78
79        extract($config, EXTR_OVERWRITE);
80
81        // Use the count of base surnames.
82        $top_surnames = DB::table('name')
83            ->where('n_file', '=', $tree->id())
84            ->where('n_type', '<>', '_MARNM')
85            ->whereNotIn('n_surn', ['@N.N.', ''])
86            ->groupBy(['n_surn'])
87            ->orderByDesc(new Expression('COUNT(n_surn)'))
88            ->take($num)
89            ->pluck('n_surn');
90
91        $all_surnames = [];
92
93        foreach ($top_surnames as $top_surname) {
94            $variants = DB::table('name')
95                ->where('n_file', '=', $tree->id())
96                ->where(new Expression('n_surn /*! COLLATE utf8_bin */'), '=', $top_surname)
97                ->groupBy(['surname'])
98                ->select([new Expression('n_surname /*! COLLATE utf8_bin */ AS surname'), new Expression('count(*) AS total')])
99                ->pluck('total', 'surname')
100                ->all();
101
102            $all_surnames[$top_surname] = $variants;
103        }
104
105        //find a module providing individual lists
106        $module = app(ModuleService::class)->findByComponent(ModuleListInterface::class, $tree, Auth::user())->first(static function (ModuleInterface $module): bool {
107            return $module instanceof IndividualListModule;
108        });
109
110        switch ($infoStyle) {
111            case 'tagcloud':
112                uksort($all_surnames, [I18N::class, 'strcasecmp']);
113                $content = FunctionsPrintLists::surnameTagCloud($all_surnames, $module, true, $tree);
114                break;
115            case 'list':
116                uasort($all_surnames, [$this, 'surnameCountSort']);
117                $content = FunctionsPrintLists::surnameList($all_surnames, 1, true, $module, $tree);
118                break;
119            case 'array':
120                uasort($all_surnames, [$this, 'surnameCountSort']);
121                $content = FunctionsPrintLists::surnameList($all_surnames, 2, true, $module, $tree);
122                break;
123            case 'table':
124            default:
125                $content = view('lists/surnames-table', [
126                    'surnames' => $all_surnames,
127                    'module'   => $module,
128                    'families' => false,
129                    'tree'     => $tree,
130                ]);
131                break;
132        }
133
134        if ($context !== self::CONTEXT_EMBED) {
135            $num = count($top_surnames);
136            if ($num === 1) {
137                // I18N: i.e. most popular surname.
138                $title = I18N::translate('Top surname');
139            } else {
140                // I18N: Title for a list of the most common surnames, %s is a number. Note that a separate translation exists when %s is 1
141                $title = I18N::plural('Top %s surname', 'Top %s surnames', $num, I18N::number($num));
142            }
143
144            return view('modules/block-template', [
145                'block'      => Str::kebab($this->name()),
146                'id'         => $block_id,
147                'config_url' => $this->configUrl($tree, $context, $block_id),
148                'title'      => $title,
149                'content'    => $content,
150            ]);
151        }
152
153        return $content;
154    }
155
156    /**
157     * Should this block load asynchronously using AJAX?
158     *
159     * Simple blocks are faster in-line, more complex ones can be loaded later.
160     *
161     * @return bool
162     */
163    public function loadAjax(): bool
164    {
165        return false;
166    }
167
168    /**
169     * Can this block be shown on the user’s home page?
170     *
171     * @return bool
172     */
173    public function isUserBlock(): bool
174    {
175        return true;
176    }
177
178    /**
179     * Can this block be shown on the tree’s home page?
180     *
181     * @return bool
182     */
183    public function isTreeBlock(): bool
184    {
185        return true;
186    }
187
188    /**
189     * Update the configuration for a block.
190     *
191     * @param ServerRequestInterface $request
192     * @param int     $block_id
193     *
194     * @return void
195     */
196    public function saveBlockConfiguration(ServerRequestInterface $request, int $block_id): void
197    {
198        $params = $request->getParsedBody();
199
200        $this->setBlockSetting($block_id, 'num', $params['num']);
201        $this->setBlockSetting($block_id, 'infoStyle', $params['infoStyle']);
202    }
203
204    /**
205     * An HTML form to edit block settings
206     *
207     * @param Tree $tree
208     * @param int  $block_id
209     *
210     * @return string
211     */
212    public function editBlockConfiguration(Tree $tree, int $block_id): string
213    {
214        $num       = $this->getBlockSetting($block_id, 'num', self::DEFAULT_NUMBER);
215        $infoStyle = $this->getBlockSetting($block_id, 'infoStyle', self::DEFAULT_STYLE);
216
217        $info_styles = [
218            /* I18N: An option in a list-box */
219            'list'     => I18N::translate('bullet list'),
220            /* I18N: An option in a list-box */
221            'array'    => I18N::translate('compact list'),
222            /* I18N: An option in a list-box */
223            'table'    => I18N::translate('table'),
224            /* I18N: An option in a list-box */
225            'tagcloud' => I18N::translate('tag cloud'),
226        ];
227
228        return view('modules/top10_surnames/config', [
229            'num'         => $num,
230            'infoStyle'   => $infoStyle,
231            'info_styles' => $info_styles,
232        ]);
233    }
234
235    /**
236     * Sort (lists of counts of similar) surname by total count.
237     *
238     * @param string[] $a
239     * @param string[] $b
240     *
241     * @return int
242     */
243    private function surnameCountSort(array $a, array $b): int
244    {
245        return array_sum($b) - array_sum($a);
246    }
247}
248