Passed
Push — develop ( 1125ee...8bca93 )
by Greg
20:39 queued 07:22
created

ChartDistribution::getSurnameChartData()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 30
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 14
nc 12
nop 1
dl 0
loc 30
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2022 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 <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Statistics\Google;
21
22
use Fisharebest\Webtrees\I18N;
23
use Fisharebest\Webtrees\Statistics\Repository\Interfaces\IndividualRepositoryInterface;
24
use Fisharebest\Webtrees\Statistics\Service\CountryService;
25
use Fisharebest\Webtrees\Tree;
26
use Illuminate\Database\Capsule\Manager as DB;
27
use Illuminate\Database\Query\Builder;
28
use Illuminate\Database\Query\Expression;
29
use Illuminate\Database\Query\JoinClause;
30
31
use function preg_match;
32
use function preg_quote;
33
use function view;
34
35
/**
36
 * A chart showing the distribution of different events on a map.
37
 */
38
class ChartDistribution
39
{
40
    private Tree $tree;
41
42
    private CountryService $country_service;
43
44
    private IndividualRepositoryInterface $individual_repository;
45
46
    /**
47
     * @var array<string>
48
     */
49
    private array $country_to_iso3166;
50
51
    /**
52
     * @param Tree                          $tree
53
     * @param CountryService                $country_service
54
     * @param IndividualRepositoryInterface $individual_repository
55
     */
56
    public function __construct(
57
        Tree $tree,
58
        CountryService $country_service,
59
        IndividualRepositoryInterface $individual_repository
60
    ) {
61
        $this->tree                  = $tree;
62
        $this->country_service       = $country_service;
63
        $this->individual_repository = $individual_repository;
64
65
        // Get the country names for each language
66
        $this->country_to_iso3166 = $this->getIso3166Countries();
67
    }
68
69
    /**
70
     * Returns the country names for each language.
71
     *
72
     * @return array<string>
73
     */
74
    private function getIso3166Countries(): array
75
    {
76
        // Get the country names for each language
77
        $country_to_iso3166 = [];
78
79
        $current_language = I18N::languageTag();
80
81
        foreach (I18N::activeLocales() as $locale) {
82
            I18N::init($locale->languageTag());
83
84
            $countries = $this->country_service->getAllCountries();
85
86
            foreach ($this->country_service->iso3166() as $three => $two) {
87
                $country_to_iso3166[$three]             = $two;
88
                $country_to_iso3166[$countries[$three]] = $two;
89
            }
90
        }
91
92
        I18N::init($current_language);
93
94
        return $country_to_iso3166;
95
    }
96
97
    /**
98
     * Returns the data structure required by google geochart.
99
     *
100
     * @param array<int> $places
101
     *
102
     * @return array<int,array<int|string|array<string,string>>>
103
     */
104
    private function createChartData(array $places): array
105
    {
106
        $data = [
107
            [
108
                I18N::translate('Country'),
109
                I18N::translate('Total'),
110
            ],
111
        ];
112
113
        // webtrees uses 3-letter country codes and localised country names, but google uses 2 letter codes.
114
        foreach ($places as $country => $count) {
115
            $data[] = [
116
                [
117
                    'v' => $country,
118
                    'f' => $this->country_service->mapTwoLetterToName($country),
119
                ],
120
                $count
121
            ];
122
        }
123
124
        return $data;
125
    }
126
127
    /**
128
     * @param Tree   $tree
129
     *
130
     * @return array<int>
131
     */
132
    private function countIndividualsByCountry(Tree $tree): array
133
    {
134
        $rows = DB::table('places')
135
            ->where('p_file', '=', $tree->id())
136
            ->where('p_parent_id', '=', 0)
137
            ->join('placelinks', static function (JoinClause $join): void {
138
                $join
139
                    ->on('pl_file', '=', 'p_file')
140
                    ->on('pl_p_id', '=', 'p_id');
141
            })
142
            ->join('individuals', static function (JoinClause $join): void {
143
                $join
144
                    ->on('pl_file', '=', 'i_file')
145
                    ->on('pl_gid', '=', 'i_id');
146
            })
147
            ->groupBy('p_place')
148
            ->pluck(new Expression('COUNT(*)'), 'p_place');
149
150
        $totals = [];
151
152
        foreach ($rows as $country => $count) {
153
            $country_code = $this->country_to_iso3166[$country] ?? null;
154
155
            if ($country_code !== null) {
156
                $totals[$country_code] = $count + ($totals[$country_code] ?? 0);
157
            }
158
        }
159
160
        return $totals;
161
    }
162
163
    /**
164
     * @param Tree   $tree
165
     * @param string $surname
166
     *
167
     * @return array<int>
168
     */
169
    private function countSurnamesByCountry(Tree $tree, string $surname): array
170
    {
171
        $rows =
172
            DB::table('places')
173
                ->where('p_file', '=', $tree->id())
174
                ->where('p_parent_id', '=', 0)
175
                ->join('placelinks', static function (JoinClause $join): void {
176
                    $join
177
                        ->on('pl_file', '=', 'p_file')
178
                        ->on('pl_p_id', '=', 'p_id');
179
                })
180
                ->join('name', static function (JoinClause $join): void {
181
                    $join
182
                        ->on('n_file', '=', 'pl_file')
183
                        ->on('n_id', '=', 'pl_gid');
184
                })
185
                ->where(new Expression('n_surn /*! COLLATE ' . I18N::collation() . ' */'), '=', $surname)
186
                ->groupBy('p_place')
187
                ->pluck(new Expression('COUNT(*)'), 'p_place');
188
189
        $totals = [];
190
191
        foreach ($rows as $country => $count) {
192
            $country_code = $this->country_to_iso3166[$country] ?? null;
193
194
            if ($country_code !== null) {
195
                $totals[$country_code] = $count + ($totals[$country_code] ?? 0);
196
            }
197
        }
198
199
        return $totals;
200
    }
201
202
    /**
203
     * @param Tree   $tree
204
     * @param string $fact
205
     *
206
     * @return array<int>
207
     */
208
    private function countFamilyEventsByCountry(Tree $tree, string $fact): array
209
    {
210
        $query = DB::table('places')
211
            ->where('p_file', '=', $tree->id())
212
            ->where('p_parent_id', '=', 0)
213
            ->join('placelinks', static function (JoinClause $join): void {
214
                $join
215
                    ->on('pl_file', '=', 'p_file')
216
                    ->on('pl_p_id', '=', 'p_id');
217
            })
218
            ->join('families', static function (JoinClause $join): void {
219
                $join
220
                    ->on('pl_file', '=', 'f_file')
221
                    ->on('pl_gid', '=', 'f_id');
222
            })
223
            ->select('p_place AS place', 'f_gedcom AS gedcom');
224
225
        return $this->filterEventPlaces($query, $fact);
226
    }
227
228
    /**
229
     * @param Tree   $tree
230
     * @param string $fact
231
     *
232
     * @return array<int>
233
     */
234
    private function countIndividualEventsByCountry(Tree $tree, string $fact): array
235
    {
236
        $query = DB::table('places')
237
            ->where('p_file', '=', $tree->id())
238
            ->where('p_parent_id', '=', 0)
239
            ->join('placelinks', static function (JoinClause $join): void {
240
                $join
241
                    ->on('pl_file', '=', 'p_file')
242
                    ->on('pl_p_id', '=', 'p_id');
243
            })
244
            ->join('individuals', static function (JoinClause $join): void {
245
                $join
246
                    ->on('pl_file', '=', 'i_file')
247
                    ->on('pl_gid', '=', 'i_id');
248
            })
249
            ->select('p_place AS place', 'i_gedcom AS gedcom');
250
251
        return $this->filterEventPlaces($query, $fact);
252
    }
253
254
    /**
255
     * @param Builder $query
256
     * @param string  $fact
257
     *
258
     * @return array<int>
259
     */
260
    private function filterEventPlaces(Builder $query, string $fact): array
261
    {
262
        $totals = [];
263
264
        foreach ($query->cursor() as $row) {
265
            $country_code = $this->country_to_iso3166[$row->place] ?? null;
266
267
            if ($country_code !== null) {
268
                $place_regex = '/\n1 ' . $fact . '(?:\n[2-9].*)*\n2 PLAC.*[, ]' . preg_quote($row->place, '(?:\n|$)/i') . '\n/';
269
270
                if (preg_match($place_regex, $row->gedcom) === 1) {
271
                    $totals[$country_code] = 1 + ($totals[$country_code] ?? 0);
272
                }
273
            }
274
        }
275
276
        return $totals;
277
    }
278
279
    /**
280
     * Create a chart showing where events occurred.
281
     *
282
     * @param string $chart_shows The type of chart map to show
283
     * @param string $chart_type  The type of chart to show
284
     * @param string $surname     The surname for surname based distribution chart
285
     *
286
     * @return string
287
     */
288
    public function chartDistribution(
289
        string $chart_shows = 'world',
290
        string $chart_type = '',
291
        string $surname = ''
292
    ): string {
293
        switch ($chart_type) {
294
            case 'surname_distribution_chart':
295
                $chart_title = I18N::translate('Surname distribution chart') . ': ' . $surname;
296
                $surname     = $surname ?: $this->individual_repository->getCommonSurname();
297
                $data        = $this->createChartData($this->countSurnamesByCountry($this->tree, $surname));
298
                break;
299
300
            case 'birth_distribution_chart':
301
                $chart_title = I18N::translate('Birth by country');
302
                $data        = $this->createChartData($this->countIndividualEventsByCountry($this->tree, 'BIRT'));
303
                break;
304
305
            case 'death_distribution_chart':
306
                $chart_title = I18N::translate('Death by country');
307
                $data        = $this->createChartData($this->countIndividualEventsByCountry($this->tree, 'DEAT'));
308
                break;
309
310
            case 'marriage_distribution_chart':
311
                $chart_title = I18N::translate('Marriage by country');
312
                $data        = $this->createChartData($this->countFamilyEventsByCountry($this->tree, 'MARR'));
313
                break;
314
315
            case 'indi_distribution_chart':
316
            default:
317
                $chart_title = I18N::translate('Individual distribution chart');
318
                $data        = $this->createChartData($this->countIndividualsByCountry($this->tree));
319
                break;
320
        }
321
322
        return view('statistics/other/charts/geo', [
323
            'chart_title'  => $chart_title,
324
            'chart_color2' => '84beff',
325
            'chart_color3' => 'c3dfff',
326
            'region'       => $chart_shows,
327
            'data'         => $data,
328
            'language'     => I18N::languageTag(),
329
        ]);
330
    }
331
}
332