Issues (2559)

app/Services/MapDataService.php (1 issue)

Labels
Severity
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2025 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
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Services;
21
22
use Fisharebest\Webtrees\DB;
23
use Fisharebest\Webtrees\FlashMessages;
24
use Fisharebest\Webtrees\Gedcom;
25
use Fisharebest\Webtrees\I18N;
26
use Fisharebest\Webtrees\PlaceLocation;
27
use Illuminate\Database\Query\Expression;
28
use Illuminate\Support\Collection;
29
30
use function abs;
31
use function array_filter;
32
use function array_unshift;
33
use function implode;
34
use function mb_strtolower;
35
use function round;
36
37
/**
38
 * Process geographic data.
39
 */
40
class MapDataService
41
{
42
    // Location of files to import
43
    public const string PLACES_FOLDER = 'places/';
0 ignored issues
show
A parse error occurred: Syntax error, unexpected T_STRING, expecting '=' on line 43 at column 24
Loading history...
44
45
    // Format of CSV files
46
    public const string CSV_SEPARATOR = ';';
47
48
    public function findById(int $id): PlaceLocation
49
    {
50
        $hierarchy = [];
51
52
        while (true) {
53
            $row = DB::table('place_location')
54
                ->where('id', '=', $id)
55
                ->select(['place', 'parent_id'])
56
                ->first();
57
58
            if ($row === null) {
59
                return new PlaceLocation(implode(Gedcom::PLACE_SEPARATOR, $hierarchy));
60
            }
61
62
            $hierarchy[] = $row->place;
63
            $id          = $row->parent_id;
64
        }
65
    }
66
67
    /**
68
     * Which trees use a particular location?
69
     *
70
     * @return array<list<object{p_place:string,tree_name:string,tree_title:string,p_id:int}>>
71
     */
72
    public function activePlaces(PlaceLocation $location): array
73
    {
74
        $parents  = $this->placeIdsForLocation($location);
75
        $children = [];
76
77
        $rows = DB::table('places')
78
            ->join('gedcom', 'gedcom.gedcom_id', '=', 'p_file')
79
            ->join('gedcom_setting', 'gedcom_setting.gedcom_id', '=', 'gedcom.gedcom_id')
80
            ->where('setting_name', '=', 'title')
81
            ->whereIn('p_parent_id', $parents)
82
            ->select(['p_place', 'gedcom_name AS tree_name', 'setting_value AS tree_title', 'p_id'])
83
            ->get()
84
            ->map(static fn (object $row): object => (object) [
85
                'p_place'    => $row->p_place,
86
                'tree_name'  => $row->tree_name,
87
                'tree_title' => $row->tree_title,
88
                'p_id'       => (int) $row->p_id,
89
            ]);
90
91
        foreach ($rows as $row) {
92
            $children[mb_strtolower($row->p_place)][] = $row;
93
        }
94
95
        return $children;
96
    }
97
98
    /**
99
     * Make sure that all places in the genealogy data also exist in the location data.
100
     */
101
    public function importMissingLocations(): void
102
    {
103
        $all_places = DB::table('places AS p0')
104
            ->leftJoin('places AS p1', 'p1.p_id', '=', 'p0.p_parent_id')
105
            ->leftJoin('places AS p2', 'p2.p_id', '=', 'p1.p_parent_id')
106
            ->leftJoin('places AS p3', 'p3.p_id', '=', 'p2.p_parent_id')
107
            ->leftJoin('places AS p4', 'p4.p_id', '=', 'p3.p_parent_id')
108
            ->leftJoin('places AS p5', 'p5.p_id', '=', 'p4.p_parent_id')
109
            ->leftJoin('places AS p6', 'p6.p_id', '=', 'p5.p_parent_id')
110
            ->leftJoin('places AS p7', 'p7.p_id', '=', 'p6.p_parent_id')
111
            ->leftJoin('places AS p8', 'p8.p_id', '=', 'p7.p_parent_id')
112
            ->select([
113
                'p0.p_place AS part_0',
114
                'p1.p_place AS part_1',
115
                'p2.p_place AS part_2',
116
                'p3.p_place AS part_3',
117
                'p4.p_place AS part_4',
118
                'p5.p_place AS part_5',
119
                'p6.p_place AS part_6',
120
                'p7.p_place AS part_7',
121
                'p8.p_place AS part_8',
122
            ])
123
            ->get()
124
            ->map(static fn (object $row): string => implode(Gedcom::PLACE_SEPARATOR, array_filter((array) $row)));
125
126
        $all_locations = DB::table('place_location AS p0')
127
            ->leftJoin('place_location AS p1', 'p1.id', '=', 'p0.parent_id')
128
            ->leftJoin('place_location AS p2', 'p2.id', '=', 'p1.parent_id')
129
            ->leftJoin('place_location AS p3', 'p3.id', '=', 'p2.parent_id')
130
            ->leftJoin('place_location AS p4', 'p4.id', '=', 'p3.parent_id')
131
            ->leftJoin('place_location AS p5', 'p5.id', '=', 'p4.parent_id')
132
            ->leftJoin('place_location AS p6', 'p6.id', '=', 'p5.parent_id')
133
            ->leftJoin('place_location AS p7', 'p7.id', '=', 'p6.parent_id')
134
            ->leftJoin('place_location AS p8', 'p8.id', '=', 'p7.parent_id')
135
            ->select([
136
                'p0.place AS part_0',
137
                'p1.place AS part_1',
138
                'p2.place AS part_2',
139
                'p3.place AS part_3',
140
                'p4.place AS part_4',
141
                'p5.place AS part_5',
142
                'p6.place AS part_6',
143
                'p7.place AS part_7',
144
                'p8.place AS part_8',
145
            ])
146
            ->get()
147
            ->map(static fn (object $row): string => implode(Gedcom::PLACE_SEPARATOR, array_filter((array) $row)));
148
149
        $missing = $all_places->diff($all_locations);
150
151
        foreach ($missing as $location) {
152
            (new PlaceLocation($location))->id();
153
        }
154
    }
155
156
    public function deleteRecursively(int $id): void
157
    {
158
        // Uses on-delete-cascade
159
        DB::table('place_location')
160
            ->where('id', '=', $id)
161
            ->delete();
162
    }
163
164
    /**
165
     * @param list<int> $parent_place_ids
166
     */
167
    public function deleteUnusedLocations(int|null $parent_location_id, array $parent_place_ids): void
168
    {
169
        if ($parent_location_id === null) {
170
            $location_query = DB::table('place_location')
171
                ->whereNull('parent_id');
172
        } else {
173
            $location_query = DB::table('place_location')
174
                ->where('parent_id', '=', $parent_location_id);
175
        }
176
177
        foreach ($location_query->get() as $location) {
178
            $places = DB::table('places')
179
                ->whereIn('p_parent_id', $parent_place_ids)
180
                ->where('p_place', '=', $location->place)
181
                ->get();
182
183
            if ($places->isEmpty()) {
184
                FlashMessages::addMessage(I18N::translate('“%s” has been deleted.', e($location->place)));
185
186
                DB::table('place_location')
187
                    ->where('id', '=', $location->id)
188
                    ->delete();
189
            } else {
190
                $place_ids = $places->map(static fn (object $place): int => (int) $place->p_id)->all();
191
                $this->deleteUnusedLocations((int) $location->id, $place_ids);
192
            }
193
        }
194
    }
195
196
    /**
197
     * Find a list of child places.
198
     * How many children does each child place have?  How many have co-ordinates?
199
     *
200
     * @return Collection<int,object{id:int,key:string,place:string,latitude:float|null,longitude:float|null,child_count:int,no_coord:int}>
201
     */
202
    public function getPlaceListLocation(int|null $parent_id): Collection
203
    {
204
        $expression =
205
            DB::prefix('p1') . '.place IS NOT NULL AND ' . DB::prefix('p1') . '.latitude IS NULL OR ' .
206
            DB::prefix('p2') . '.place IS NOT NULL AND ' . DB::prefix('p2') . '.latitude IS NULL OR ' .
207
            DB::prefix('p3') . '.place IS NOT NULL AND ' . DB::prefix('p3') . '.latitude IS NULL OR ' .
208
            DB::prefix('p4') . '.place IS NOT NULL AND ' . DB::prefix('p4') . '.latitude IS NULL OR ' .
209
            DB::prefix('p5') . '.place IS NOT NULL AND ' . DB::prefix('p5') . '.latitude IS NULL OR ' .
210
            DB::prefix('p6') . '.place IS NOT NULL AND ' . DB::prefix('p6') . '.latitude IS NULL OR ' .
211
            DB::prefix('p7') . '.place IS NOT NULL AND ' . DB::prefix('p7') . '.latitude IS NULL OR ' .
212
            DB::prefix('p8') . '.place IS NOT NULL AND ' . DB::prefix('p8') . '.latitude IS NULL OR ' .
213
            DB::prefix('p9') . '.place IS NOT NULL AND ' . DB::prefix('p9') . '.latitude IS NULL';
214
215
        $expression = 'CASE ' . $expression . ' WHEN TRUE THEN 1 ELSE 0 END';
216
217
        $query = DB::table('place_location AS p0')
218
            ->leftJoin('place_location AS p1', 'p1.parent_id', '=', 'p0.id')
219
            ->leftJoin('place_location AS p2', 'p2.parent_id', '=', 'p1.id')
220
            ->leftJoin('place_location AS p3', 'p3.parent_id', '=', 'p2.id')
221
            ->leftJoin('place_location AS p4', 'p4.parent_id', '=', 'p3.id')
222
            ->leftJoin('place_location AS p5', 'p5.parent_id', '=', 'p4.id')
223
            ->leftJoin('place_location AS p6', 'p6.parent_id', '=', 'p5.id')
224
            ->leftJoin('place_location AS p7', 'p7.parent_id', '=', 'p6.id')
225
            ->leftJoin('place_location AS p8', 'p8.parent_id', '=', 'p7.id')
226
            ->leftJoin('place_location AS p9', 'p9.parent_id', '=', 'p8.id');
227
228
        if ($parent_id === null) {
229
            $query->whereNull('p0.parent_id');
230
        } else {
231
            $query->where('p0.parent_id', '=', $parent_id);
232
        }
233
234
        return $query
235
            ->groupBy(['p0.id'])
236
            ->select([
237
                'p0.*',
238
                new Expression('COUNT(' . DB::prefix('p1') . '.id) AS child_count'),
239
                new Expression('SUM(' . $expression . ') AS no_coord'),
240
            ])
241
            ->get()
242
            ->map(static fn (object $row): object => (object) [
243
                'id'          => (int) $row->id,
244
                'place'       => $row->place,
245
                'key'         => mb_strtolower($row->place),
246
                'latitude'    => $row->latitude === null ? null : (float) $row->latitude,
247
                'longitude'   => $row->longitude === null ? null : (float) $row->longitude,
248
                'child_count' => (int) $row->child_count,
249
                'no_coord'    => (int) $row->no_coord,
250
            ])
251
            ->sort(static fn (object $x, object $y): int => I18N::comparator()($x->place, $y->place));
252
    }
253
254
    public function writeLatitude(float $latitude): string
255
    {
256
        return $this->writeDegrees($latitude, Gedcom::LATITUDE_NORTH, Gedcom::LATITUDE_SOUTH);
257
    }
258
259
    public function writeLongitude(float $longitude): string
260
    {
261
        return $this->writeDegrees($longitude, Gedcom::LONGITUDE_EAST, Gedcom::LONGITUDE_WEST);
262
    }
263
264
    /**
265
     * Find all active places that match a location
266
     *
267
     * @return array<string>
268
     */
269
    private function placeIdsForLocation(PlaceLocation $location): array
270
    {
271
        $hierarchy = [];
272
273
        while ($location->id() !== null) {
274
            array_unshift($hierarchy, $location->locationName());
275
            $location = $location->parent();
276
        }
277
278
        $place_ids = ['0'];
279
280
        foreach ($hierarchy as $place_name) {
281
            $place_ids = DB::table('places')
282
                ->whereIn('p_parent_id', $place_ids)
283
                ->where('p_place', '=', $place_name)
284
                ->groupBy(['p_id'])
285
                ->pluck('p_id')
286
                ->all();
287
        }
288
289
        return $place_ids;
290
    }
291
292
    private function writeDegrees(float $degrees, string $positive, string $negative): string
293
    {
294
        $degrees = round($degrees, 5);
295
296
        if ($degrees < 0.0) {
297
            return $negative . abs($degrees);
298
        }
299
300
        return $positive . $degrees;
301
    }
302
}
303