Passed
Push — master ( 12c7ed...dbe534 )
by Greg
06:17
created

Place::tree()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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