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