Passed
Push — develop ( 5f211d...930448 )
by Greg
13:36 queued 06:57
created

PlaceLocation::icon()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
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 <http://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees;
21
22
use Illuminate\Database\Capsule\Manager as DB;
23
use Illuminate\Database\Query\Builder;
24
use Illuminate\Support\Collection;
25
use stdClass;
26
27
use function max;
28
use function min;
29
use function preg_split;
30
use function trim;
31
32
use const PREG_SPLIT_NO_EMPTY;
33
34
/**
35
 * Class PlaceLocation
36
 */
37
class PlaceLocation
38
{
39
    /** @var string e.g. "Westminster, London, England" */
40
    private $location_name;
41
42
    /** @var Collection<string> The parts of a location name, e.g. ["Westminster", "London", "England"] */
43
    private $parts;
44
45
    /**
46
     * Create a place-location.
47
     *
48
     * @param string $location_name
49
     */
50
    public function __construct(string $location_name)
51
    {
52
        // Ignore any empty parts in location names such as "Village, , , Country".
53
        $location_name = trim($location_name);
54
        $this->parts   = new Collection(preg_split(Gedcom::PLACE_SEPARATOR_REGEX, $location_name, -1, PREG_SPLIT_NO_EMPTY));
55
56
        // Rebuild the location name in the correct format.
57
        $this->location_name = $this->parts->implode(Gedcom::PLACE_SEPARATOR);
58
    }
59
60
    /**
61
     * Get the higher level location.
62
     *
63
     * @return PlaceLocation
64
     */
65
    public function parent(): PlaceLocation
66
    {
67
        return new self($this->parts->slice(1)->implode(Gedcom::PLACE_SEPARATOR));
68
    }
69
70
    /**
71
     * The database row id that contains this location.
72
     * Note that due to database collation, both "Quebec" and "Québec" will share the same row.
73
     *
74
     * @return int|null
75
     */
76
    public function id(): ?int
77
    {
78
        // The "top-level" location won't exist in the database.
79
        if ($this->parts->isEmpty()) {
80
            return null;
81
        }
82
83
        return Registry::cache()->array()->remember('location-' . $this->location_name, function () {
84
            $parent_id = $this->parent()->id();
85
86
            $place = $this->parts->first();
87
            $place = mb_substr($place, 0, 120);
88
89
            if ($parent_id === null) {
90
                $location_id = DB::table('place_location')
91
                    ->where('place', '=', $place)
92
                    ->whereNull('parent_id')
93
                    ->value('id');
94
            } else {
95
                $location_id = DB::table('place_location')
96
                    ->where('place', '=', $place)
97
                    ->where('parent_id', '=', $parent_id)
98
                    ->value('id');
99
            }
100
101
            $location_id = $location_id ?? DB::table('place_location')->insertGetId([
102
                    'parent_id' => $parent_id,
103
                    'place'     => $place,
104
                ]);
105
106
            return (int) $location_id;
107
        });
108
    }
109
110
    /**
111
     * Does this location exist in the database?  Note that calls to PlaceLocation::id() will
112
     * create the row, so this function is only meaningful when called before a call to PlaceLocation::id().
113
     *
114
     * @return bool
115
     */
116
    public function exists(): bool
117
    {
118
        $parent_id = null;
119
120
        foreach ($this->parts->reverse() as $place) {
121
            if ($parent_id === null) {
122
                $parent_id = DB::table('place_location')
123
                    ->whereNull('parent_id')
124
                    ->where('place', '=', mb_substr($place, 0, 120))
125
                    ->value('id');
126
            } else {
127
                $parent_id = DB::table('place_location')
128
                    ->where('parent_id', '=', $parent_id)
129
                    ->where('place', '=', mb_substr($place, 0, 120))
130
                    ->value('id');
131
            }
132
133
            if ($parent_id === null) {
134
                return false;
135
            }
136
        }
137
138
        return true;
139
    }
140
141
    /**
142
     * @return stdClass
143
     */
144
    private function details(): stdClass
145
    {
146
        return Registry::cache()->array()->remember('location-details-' . $this->id(), function () {
147
            // The "top-level" location won't exist in the database.
148
            if ($this->parts->isEmpty()) {
149
                return (object) [
150
                    'latitude'  => null,
151
                    'longitude' => null,
152
                ];
153
            }
154
155
            $row = DB::table('place_location')
156
                ->where('id', '=', $this->id())
157
                ->select(['latitude', 'longitude'])
158
                ->first();
159
160
            if ($row->latitude !== null) {
161
                $row->latitude = (float) $row->latitude;
162
            }
163
164
            if ($row->longitude !== null) {
165
                $row->longitude = (float) $row->longitude;
166
            }
167
168
            return $row;
169
        });
170
    }
171
172
    /**
173
     * Latitude of the location.
174
     *
175
     * @return float|null
176
     */
177
    public function latitude(): ?float
178
    {
179
        return $this->details()->latitude;
180
    }
181
182
    /**
183
     * Longitude of the location.
184
     *
185
     * @return float|null
186
     */
187
    public function longitude(): ?float
188
    {
189
        return $this->details()->longitude;
190
    }
191
192
    /**
193
     * @return string
194
     */
195
    public function locationName(): string
196
    {
197
        return (string) $this->parts->first();
198
    }
199
200
    /**
201
     * Find a rectangle that (approximately) encloses this place.
202
     *
203
     * @return array<array<float>>
204
     */
205
    public function boundingRectangle(): array
206
    {
207
        if ($this->id() === 0) {
208
            return [[-180.0, -90.0], [180.0, 90.0]];
209
        }
210
211
        // Find our own co-ordinates and those of any child places
212
        $latitudes = DB::table('place_location')
213
            ->whereNotNull('latitude')
214
            ->where(function (Builder $query): void {
215
                $query
216
                    ->where('parent_id', '=', $this->id())
217
                    ->orWhere('id', '=', $this->id());
218
            })
219
            ->groupBy(['latitude'])
220
            ->pluck('latitude')
221
            ->map(static function (string $x): float {
222
                return (float) $x;
223
            });
224
225
        $longitudes = DB::table('place_location')
226
            ->whereNotNull('longitude')
227
            ->where(function (Builder $query): void {
228
                $query
229
                    ->where('parent_id', '=', $this->id())
230
                    ->orWhere('id', '=', $this->id());
231
            })
232
            ->groupBy(['longitude'])
233
            ->pluck('longitude')
234
            ->map(static function (string $x): float {
235
                return (float) $x;
236
            });
237
238
        // No co-ordinates?  Use the parent place instead.
239
        if ($latitudes->isEmpty() || $longitudes->isEmpty()) {
240
            return $this->parent()->boundingRectangle();
241
        }
242
243
        // Many co-ordinates?  Generate a bounding rectangle that includes them.
244
        if ($latitudes->count() > 1 || $longitudes->count() > 1) {
245
            return [[$latitudes->min(), $longitudes->min()], [$latitudes->max(), $longitudes->max()]];
246
        }
247
248
        // Just one co-ordinate?  Draw a box around it.
249
        switch ($this->parts->count()) {
250
            case 1:
251
                // Countries
252
                $delta = 5.0;
253
                break;
254
            case 2:
255
                // Regions
256
                $delta = 1.0;
257
                break;
258
            default:
259
                // Cities and districts
260
                $delta = 0.2;
261
                break;
262
        }
263
264
        return [[
265
            max($latitudes->min() - $delta, -90.0),
266
            max($longitudes->min() - $delta, -180.0),
267
        ], [
268
            min($latitudes->max() + $delta, 90.0),
269
            min($longitudes->max() + $delta, 180.0),
270
        ]];
271
    }
272
}
273