xref: /webtrees/app/Place.php (revision 8d897fd11a9a8845d8456cb9eaad33b9c9d5a225)
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;
20
21use Fisharebest\Webtrees\Module\ModuleInterface;
22use Fisharebest\Webtrees\Module\ModuleListInterface;
23use Fisharebest\Webtrees\Module\PlaceHierarchyListModule;
24use Fisharebest\Webtrees\Services\ModuleService;
25use Illuminate\Database\Capsule\Manager as DB;
26use Illuminate\Database\Query\Expression;
27use Illuminate\Support\Collection;
28
29/**
30 * A GEDCOM place (PLAC) object.
31 */
32class Place
33{
34    /** @var string e.g. "Westminster, London, England" */
35    private $place_name;
36
37    /** @var Collection The parts of a place name, e.g. ["Westminster", "London", "England"] */
38    private $parts;
39
40    /** @var Tree We may have the same place name in different trees. */
41    private $tree;
42
43    /**
44     * Create a place.
45     *
46     * @param string $place_name
47     * @param Tree   $tree
48     */
49    public function __construct(string $place_name, Tree $tree)
50    {
51        // Ignore any empty parts in place names such as "Village, , , Country".
52        $this->parts = Collection::make(preg_split(Gedcom::PLACE_SEPARATOR_REGEX, $place_name))
53            ->filter();
54
55        // Rebuild the placename in the correct format.
56        $this->place_name = $this->parts->implode(Gedcom::PLACE_SEPARATOR);
57
58        $this->tree = $tree;
59    }
60
61    /**
62     * Get the higher level place.
63     *
64     * @return Place
65     */
66    public function parent(): Place
67    {
68        return new self($this->parts->slice(1)->implode(Gedcom::PLACE_SEPARATOR), $this->tree);
69    }
70
71    /**
72     * The database row that contains this place.
73     * Note that due to database collation, both "Quebec" and "Québec" will share the same row.
74     *
75     * @return int
76     */
77    public function id(): int
78    {
79        return app('cache.array')->rememberForever(__CLASS__ . __METHOD__ . $this->place_name, function (): int {
80            // The "top-level" place won't exist in the database.
81            if ($this->parts->isEmpty()) {
82                return 0;
83            }
84
85            $parent_place_id = $this->parent()->id();
86
87            $place_id = (int) DB::table('places')
88                ->where('p_file', '=', $this->tree->id())
89                ->where('p_place', '=', $this->parts->first())
90                ->where('p_parent_id', '=', $parent_place_id)
91                ->value('p_id');
92
93            if ($place_id === 0) {
94                $place = $this->parts->first();
95
96                DB::table('places')->insert([
97                    'p_file'        => $this->tree->id(),
98                    'p_place'       => $place,
99                    'p_parent_id'   => $parent_place_id,
100                    'p_std_soundex' => Soundex::russell($place),
101                    'p_dm_soundex'  => Soundex::daitchMokotoff($place),
102                ]);
103
104                $place_id = (int) DB::connection()->getPdo()->lastInsertId();
105            }
106
107            return $place_id;
108        });
109    }
110
111    /**
112     * Extract the locality (first parts) of a place name.
113     *
114     * @param int $n
115     *
116     * @return Collection
117     */
118    public function firstParts(int $n): Collection
119    {
120        return $this->parts->slice(0, $n);
121    }
122
123    /**
124     * Extract the country (last parts) of a place name.
125     *
126     * @param int $n
127     *
128     * @return Collection
129     */
130    public function lastParts(int $n): Collection
131    {
132        return $this->parts->slice(-$n);
133    }
134
135    /**
136     * Get the lower level places.
137     *
138     * @return Place[]
139     */
140    public function getChildPlaces(): array
141    {
142        if ($this->place_name !== '') {
143            $parent_text = Gedcom::PLACE_SEPARATOR . $this->place_name;
144        } else {
145            $parent_text = '';
146        }
147
148        return DB::table('places')
149            ->where('p_file', '=', $this->tree->id())
150            ->where('p_parent_id', '=', $this->id())
151            ->orderBy(new Expression('p_place /*! COLLATE ' . I18N::collation() . ' */'))
152            ->pluck('p_place')
153            ->map(function (string $place) use ($parent_text): Place {
154                return new self($place . $parent_text, $this->tree);
155            })
156            ->all();
157    }
158
159    /**
160     * Create a URL to the place-hierarchy page.
161     *
162     * @return string
163     */
164    public function url(): string
165    {
166        //find a module providing the place hierarchy
167        $module = app(ModuleService::class)
168            ->findByComponent(ModuleListInterface::class, $this->tree, Auth::user())
169            ->first(static function (ModuleInterface $module): bool {
170                return $module instanceof PlaceHierarchyListModule;
171            });
172
173        if ($module instanceof PlaceHierarchyListModule) {
174            return $module->listUrl($this->tree, [
175                'parent' => $this->parts->reverse()->all(),
176                'ged'    => $this->tree->name(),
177            ]);
178        }
179
180        // The place-list module is disabled...
181        return '#';
182    }
183
184    /**
185     * Format this place for GEDCOM data.
186     *
187     * @return string
188     */
189    public function gedcomName(): string
190    {
191        return $this->place_name;
192    }
193
194    /**
195     * Format this place for display on screen.
196     *
197     * @return string
198     */
199    public function placeName(): string
200    {
201        $place_name = $this->parts->first() ?? I18N::translate('unknown');
202
203        return '<span dir="auto">' . e($place_name) . '</span>';
204    }
205
206    /**
207     * Generate the place name for display, including the full hierarchy.
208     *
209     * @param bool $link
210     *
211     * @return string
212     */
213    public function fullName(bool $link = false): string
214    {
215        if ($this->parts->isEmpty()) {
216            return '';
217        }
218
219        $full_name = $this->parts->implode(I18N::$list_separator);
220
221        if ($link) {
222            return '<a dir="auto" href="' . e($this->url()) . '">' . e($full_name) . '</a>';
223        }
224
225        return '<span dir="auto">' . e($full_name) . '</span>';
226    }
227
228    /**
229     * For lists and charts, where the full name won’t fit.
230     *
231     * @param bool $link
232     *
233     * @return string
234     */
235    public function shortName(bool $link = false): string
236    {
237        $SHOW_PEDIGREE_PLACES = (int) $this->tree->getPreference('SHOW_PEDIGREE_PLACES');
238
239        // Abbreviate the place name, for lists
240        if ($this->tree->getPreference('SHOW_PEDIGREE_PLACES_SUFFIX')) {
241            $parts = $this->lastParts($SHOW_PEDIGREE_PLACES);
242        } else {
243            $parts = $this->firstParts($SHOW_PEDIGREE_PLACES);
244        }
245
246        $short_name = $parts->implode(I18N::$list_separator);
247
248        // Add a tool-tip showing the full name
249        $title = strip_tags($this->fullName());
250
251        if ($link) {
252            return '<a dir="auto" href="' . e($this->url()) . '" title="' . $title . '">' . e($short_name) . '</a>';
253        }
254
255        return '<span dir="auto">' . e($short_name) . '</span>';
256    }
257}
258