xref: /webtrees/app/Http/RequestHandlers/SearchAdvancedPage.php (revision c4943cff72f95a28fbb9404e3c20b169ff098e5c)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2021 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
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees\Http\RequestHandlers;
21
22use Fisharebest\Webtrees\GedcomTag;
23use Fisharebest\Webtrees\Http\ViewResponseTrait;
24use Fisharebest\Webtrees\I18N;
25use Fisharebest\Webtrees\Services\SearchService;
26use Fisharebest\Webtrees\Tree;
27use Illuminate\Support\Collection;
28use Psr\Http\Message\ResponseInterface;
29use Psr\Http\Message\ServerRequestInterface;
30use Psr\Http\Server\RequestHandlerInterface;
31
32use function array_fill_keys;
33use function array_filter;
34use function array_key_exists;
35use function assert;
36use function explode;
37
38/**
39 * Search for genealogy data
40 */
41class SearchAdvancedPage implements RequestHandlerInterface
42{
43    use ViewResponseTrait;
44
45    private const DEFAULT_ADVANCED_FIELDS = [
46        'NAME:GIVN',
47        'NAME:SURN',
48        'BIRT:DATE',
49        'BIRT:PLAC',
50        'FAMS:MARR:DATE',
51        'FAMS:MARR:PLAC',
52        'DEAT:DATE',
53        'DEAT:PLAC',
54        'FAMC:HUSB:NAME:GIVN',
55        'FAMC:HUSB:NAME:SURN',
56        'FAMC:WIFE:NAME:GIVN',
57        'FAMC:WIFE:NAME:SURN',
58    ];
59
60    private const OTHER_ADVANCED_FIELDS = [
61        'ADOP:DATE',
62        'ADOP:PLAC',
63        'AFN',
64        'BAPL:DATE',
65        'BAPL:PLAC',
66        'BAPM:DATE',
67        'BAPM:PLAC',
68        'BARM:DATE',
69        'BARM:PLAC',
70        'BASM:DATE',
71        'BASM:PLAC',
72        'BLES:DATE',
73        'BLES:PLAC',
74        'BURI:DATE',
75        'BURI:PLAC',
76        'CENS:DATE',
77        'CENS:PLAC',
78        'CHAN:DATE',
79        'CHAN:_WT_USER',
80        'CHR:DATE',
81        'CHR:PLAC',
82        'CREM:DATE',
83        'CREM:PLAC',
84        'DSCR',
85        'EMIG:DATE',
86        'EMIG:PLAC',
87        'ENDL:DATE',
88        'ENDL:PLAC',
89        'EVEN',
90        'EVEN:TYPE',
91        'EVEN:DATE',
92        'EVEN:PLAC',
93        'FACT',
94        'FACT:TYPE',
95        'FAMS:CENS:DATE',
96        'FAMS:CENS:PLAC',
97        'FAMS:DIV:DATE',
98        'FAMS:NOTE',
99        'FAMS:SLGS:DATE',
100        'FAMS:SLGS:PLAC',
101        'FCOM:DATE',
102        'FCOM:PLAC',
103        'IMMI:DATE',
104        'IMMI:PLAC',
105        'NAME:NICK',
106        'NAME:_MARNM',
107        'NAME:_HEB',
108        'NAME:ROMN',
109        'NATI',
110        'NATU:DATE',
111        'NATU:PLAC',
112        'NOTE',
113        'OCCU',
114        'ORDN:DATE',
115        'ORDN:PLAC',
116        'REFN',
117        'RELI',
118        'RESI:DATE',
119        'RESI:EMAIL',
120        'RESI:PLAC',
121        'SLGC:DATE',
122        'SLGC:PLAC',
123        'TITL',
124    ];
125
126    private SearchService $search_service;
127
128    /**
129     * SearchController constructor.
130     *
131     * @param SearchService $search_service
132     */
133    public function __construct(SearchService $search_service)
134    {
135        $this->search_service = $search_service;
136    }
137
138    /**
139     * A structured search.
140     *
141     * @param ServerRequestInterface $request
142     *
143     * @return ResponseInterface
144     */
145    public function handle(ServerRequestInterface $request): ResponseInterface
146    {
147        $tree = $request->getAttribute('tree');
148        assert($tree instanceof Tree);
149
150        $default_fields = array_fill_keys(self::DEFAULT_ADVANCED_FIELDS, '');
151
152        $params = $request->getQueryParams();
153
154        $fields    = $params['fields'] ?? $default_fields;
155        $modifiers = $params['modifiers'] ?? [];
156
157        $other_fields = $this->otherFields($tree, $fields);
158        $date_options = $this->dateOptions();
159        $name_options = $this->nameOptions();
160
161        if (array_filter($fields) !== []) {
162            $individuals = $this->search_service->searchIndividualsAdvanced([$tree], $fields, $modifiers);
163        } else {
164            $individuals = new Collection();
165        }
166
167        $title = I18N::translate('Advanced search');
168
169        return $this->viewResponse('search-advanced-page', [
170            'date_options' => $date_options,
171            'fields'       => $fields,
172            'field_labels' => $this->customFieldLabels(),
173            'individuals'  => $individuals,
174            'modifiers'    => $modifiers,
175            'name_options' => $name_options,
176            'other_fields' => $other_fields,
177            'title'        => $title,
178            'tree'         => $tree,
179        ]);
180    }
181
182    /**
183     * Extra search fields to add to the advanced search
184     *
185     * @param Tree     $tree
186     * @param string[] $fields
187     *
188     * @return array<string,string>
189     */
190    private function otherFields(Tree $tree, array $fields): array
191    {
192        $default_facts     = new Collection(self::OTHER_ADVANCED_FIELDS);
193        $indi_facts_add    = new Collection(explode(',', $tree->getPreference('INDI_FACTS_ADD')));
194        $indi_facts_unique = new Collection(explode(',', $tree->getPreference('INDI_FACTS_UNIQUE')));
195
196        return $default_facts
197            ->merge($indi_facts_add)
198            ->merge($indi_facts_unique)
199            ->unique()
200            ->reject(static function (string $field) use ($fields): bool {
201                return
202                    array_key_exists($field, $fields) ||
203                    array_key_exists($field . ':DATE', $fields) ||
204                    array_key_exists($field . ':PLAC', $fields);
205            })
206            ->mapWithKeys(static function (string $fact): array {
207                return [$fact => GedcomTag::getLabel($fact)];
208            })
209            ->all();
210    }
211
212
213    /**
214     * We use some pseudo-GEDCOM tags for some of our fields.
215     *
216     * @return array<string,string>
217     */
218    private function customFieldLabels(): array
219    {
220        return [
221            'FAMS:DIV:DATE'       => I18N::translate('Date of divorce'),
222            'FAMS:NOTE'           => I18N::translate('Spouse note'),
223            'FAMS:SLGS:DATE'      => I18N::translate('Date of LDS spouse sealing'),
224            'FAMS:SLGS:PLAC'      => I18N::translate('Place of LDS spouse sealing'),
225            'FAMS:MARR:DATE'      => I18N::translate('Date of marriage'),
226            'FAMS:MARR:PLAC'      => I18N::translate('Place of marriage'),
227            'FAMC:HUSB:NAME:GIVN' => I18N::translate('Given names'),
228            'FAMC:HUSB:NAME:SURN' => I18N::translate('Surname'),
229            'FAMC:WIFE:NAME:GIVN' => I18N::translate('Given names'),
230            'FAMC:WIFE:NAME:SURN' => I18N::translate('Surname'),
231        ];
232    }
233
234    /**
235     * For the advanced search
236     *
237     * @return array<string>
238     */
239    private function dateOptions(): array
240    {
241        return [
242            0  => I18N::translate('Exact date'),
243            1  => I18N::plural('±%s year', '±%s years', 1, I18N::number(1)),
244            2  => I18N::plural('±%s year', '±%s years', 2, I18N::number(2)),
245            5  => I18N::plural('±%s year', '±%s years', 5, I18N::number(5)),
246            10 => I18N::plural('±%s year', '±%s years', 10, I18N::number(10)),
247            20 => I18N::plural('±%s year', '±%s years', 20, I18N::number(20)),
248        ];
249    }
250
251    /**
252     * For the advanced search
253     *
254     * @return array<string>
255     */
256    private function nameOptions(): array
257    {
258        return [
259            'EXACT'    => I18N::translate('Exact'),
260            'BEGINS'   => I18N::translate('Begins with'),
261            'CONTAINS' => I18N::translate('Contains'),
262            'SDX'      => I18N::translate('Sounds like'),
263        ];
264    }
265}
266