Passed
Push — master ( 64c83a...f96f51 )
by Greg
09:17
created

PlaceLocation::details()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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