xref: /webtrees/app/Services/MapDataService.php (revision 90949315619051b772509f417394730bd8b1bda0)
194e35917SGreg Roach<?php
294e35917SGreg Roach
394e35917SGreg Roach/**
494e35917SGreg Roach * webtrees: online genealogy
5*90949315SGreg Roach * Copyright (C) 2021 webtrees development team
694e35917SGreg Roach * This program is free software: you can redistribute it and/or modify
794e35917SGreg Roach * it under the terms of the GNU General Public License as published by
894e35917SGreg Roach * the Free Software Foundation, either version 3 of the License, or
994e35917SGreg Roach * (at your option) any later version.
1094e35917SGreg Roach * This program is distributed in the hope that it will be useful,
1194e35917SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
1294e35917SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1394e35917SGreg Roach * GNU General Public License for more details.
1494e35917SGreg Roach * You should have received a copy of the GNU General Public License
1594e35917SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>.
1694e35917SGreg Roach */
1794e35917SGreg Roach
1894e35917SGreg Roachdeclare(strict_types=1);
1994e35917SGreg Roach
2094e35917SGreg Roachnamespace Fisharebest\Webtrees\Services;
2194e35917SGreg Roach
2294e35917SGreg Roachuse Fisharebest\Webtrees\Gedcom;
230e50635cSGreg Roachuse Fisharebest\Webtrees\I18N;
2494e35917SGreg Roachuse Fisharebest\Webtrees\PlaceLocation;
2594e35917SGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
260e50635cSGreg Roachuse Illuminate\Database\Query\Expression;
270e50635cSGreg Roachuse Illuminate\Support\Collection;
2894e35917SGreg Roachuse stdClass;
2994e35917SGreg Roach
30*90949315SGreg Roachuse function abs;
31*90949315SGreg Roachuse function array_filter;
3294e35917SGreg Roachuse function array_unshift;
330e50635cSGreg Roachuse function implode;
34*90949315SGreg Roachuse function mb_strtolower;
35*90949315SGreg Roachuse function round;
3694e35917SGreg Roach
3794e35917SGreg Roach/**
3894e35917SGreg Roach * Process geographic data.
3994e35917SGreg Roach */
4094e35917SGreg Roachclass MapDataService
4194e35917SGreg Roach{
42*90949315SGreg Roach    // Location of files to import
43*90949315SGreg Roach    public const PLACES_FOLDER = 'places/';
44*90949315SGreg Roach
45*90949315SGreg Roach    // Format of CSV files
46*90949315SGreg Roach    public const CSV_SEPARATOR = ';';
47*90949315SGreg Roach
4894e35917SGreg Roach    /**
4994e35917SGreg Roach     * @param int $id
5094e35917SGreg Roach     *
5194e35917SGreg Roach     * @return PlaceLocation
5294e35917SGreg Roach     */
5394e35917SGreg Roach    public function findById(int $id): PlaceLocation
5494e35917SGreg Roach    {
5594e35917SGreg Roach        $hierarchy = [];
5694e35917SGreg Roach
57*90949315SGreg Roach        while (true) {
58*90949315SGreg Roach            $row = DB::table('place_location')
59*90949315SGreg Roach                ->where('id', '=', $id)
60*90949315SGreg Roach                ->select(['place', 'parent_id'])
6194e35917SGreg Roach                ->first();
6294e35917SGreg Roach
6394e35917SGreg Roach            if ($row === null) {
64*90949315SGreg Roach                return new PlaceLocation(implode(Gedcom::PLACE_SEPARATOR, $hierarchy));
6594e35917SGreg Roach            }
6694e35917SGreg Roach
67*90949315SGreg Roach            $hierarchy[] = $row->place;
68*90949315SGreg Roach            $id          = $row->parent_id;
69*90949315SGreg Roach        }
7094e35917SGreg Roach    }
7194e35917SGreg Roach
7294e35917SGreg Roach    /**
7394e35917SGreg Roach     * Which trees use a particular location?
7494e35917SGreg Roach     *
7594e35917SGreg Roach     * @param PlaceLocation $location
7694e35917SGreg Roach     *
7794e35917SGreg Roach     * @return array<string,array<stdClass>>
7894e35917SGreg Roach     */
7994e35917SGreg Roach    public function activePlaces(PlaceLocation $location): array
8094e35917SGreg Roach    {
8194e35917SGreg Roach        $parents  = $this->placeIdsForLocation($location);
8294e35917SGreg Roach        $children = [];
8394e35917SGreg Roach
8494e35917SGreg Roach        $rows = DB::table('places')
8594e35917SGreg Roach            ->join('gedcom', 'gedcom.gedcom_id', '=', 'p_file')
8694e35917SGreg Roach            ->join('gedcom_setting', 'gedcom_setting.gedcom_id', '=', 'gedcom.gedcom_id')
8794e35917SGreg Roach            ->where('setting_name', '=', 'title')
8894e35917SGreg Roach            ->whereIn('p_parent_id', $parents)
8994e35917SGreg Roach            ->select(['p_place', 'gedcom_name AS tree_name', 'setting_value AS tree_title', 'p_id'])
9094e35917SGreg Roach            ->get();
9194e35917SGreg Roach
9294e35917SGreg Roach        foreach ($rows as $row) {
93*90949315SGreg Roach            $children[mb_strtolower($row->p_place)][] = $row;
9494e35917SGreg Roach        }
9594e35917SGreg Roach
9694e35917SGreg Roach        return $children;
9794e35917SGreg Roach    }
9894e35917SGreg Roach
9994e35917SGreg Roach    /**
10094e35917SGreg Roach     * Make sure that all places in the genealogy data also exist in the location data.
10194e35917SGreg Roach     *
10294e35917SGreg Roach     * @return void
10394e35917SGreg Roach     */
1040e50635cSGreg Roach    public function importMissingLocations(): void
10594e35917SGreg Roach    {
1060e50635cSGreg Roach        $all_places = DB::table('places AS p0')
1070e50635cSGreg Roach            ->leftJoin('places AS p1', 'p1.p_id', '=', 'p0.p_parent_id')
1080e50635cSGreg Roach            ->leftJoin('places AS p2', 'p2.p_id', '=', 'p1.p_parent_id')
1090e50635cSGreg Roach            ->leftJoin('places AS p3', 'p3.p_id', '=', 'p2.p_parent_id')
1100e50635cSGreg Roach            ->leftJoin('places AS p4', 'p4.p_id', '=', 'p3.p_parent_id')
1110e50635cSGreg Roach            ->leftJoin('places AS p5', 'p5.p_id', '=', 'p4.p_parent_id')
1120e50635cSGreg Roach            ->leftJoin('places AS p6', 'p6.p_id', '=', 'p5.p_parent_id')
1130e50635cSGreg Roach            ->leftJoin('places AS p7', 'p7.p_id', '=', 'p6.p_parent_id')
1140e50635cSGreg Roach            ->leftJoin('places AS p8', 'p8.p_id', '=', 'p7.p_parent_id')
1150e50635cSGreg Roach            ->select([
1160e50635cSGreg Roach                'p0.p_place AS part_0',
1170e50635cSGreg Roach                'p1.p_place AS part_1',
1180e50635cSGreg Roach                'p2.p_place AS part_2',
1190e50635cSGreg Roach                'p3.p_place AS part_3',
1200e50635cSGreg Roach                'p4.p_place AS part_4',
1210e50635cSGreg Roach                'p5.p_place AS part_5',
1220e50635cSGreg Roach                'p6.p_place AS part_6',
1230e50635cSGreg Roach                'p7.p_place AS part_7',
1240e50635cSGreg Roach                'p8.p_place AS part_8',
1250e50635cSGreg Roach            ])
1260e50635cSGreg Roach            ->get()
1270e50635cSGreg Roach            ->map(static function (stdClass $row): string {
128*90949315SGreg Roach                return implode(Gedcom::PLACE_SEPARATOR, array_filter((array) $row));
1290e50635cSGreg Roach            });
13094e35917SGreg Roach
131*90949315SGreg Roach        $all_locations = DB::table('place_location AS p0')
132*90949315SGreg Roach            ->leftJoin('place_location AS p1', 'p1.id', '=', 'p0.parent_id')
133*90949315SGreg Roach            ->leftJoin('place_location AS p2', 'p2.id', '=', 'p1.parent_id')
134*90949315SGreg Roach            ->leftJoin('place_location AS p3', 'p3.id', '=', 'p2.parent_id')
135*90949315SGreg Roach            ->leftJoin('place_location AS p4', 'p4.id', '=', 'p3.parent_id')
136*90949315SGreg Roach            ->leftJoin('place_location AS p5', 'p5.id', '=', 'p4.parent_id')
137*90949315SGreg Roach            ->leftJoin('place_location AS p6', 'p6.id', '=', 'p5.parent_id')
138*90949315SGreg Roach            ->leftJoin('place_location AS p7', 'p7.id', '=', 'p6.parent_id')
139*90949315SGreg Roach            ->leftJoin('place_location AS p8', 'p8.id', '=', 'p7.parent_id')
1400e50635cSGreg Roach            ->select([
141*90949315SGreg Roach                'p0.place AS part_0',
142*90949315SGreg Roach                'p1.place AS part_1',
143*90949315SGreg Roach                'p2.place AS part_2',
144*90949315SGreg Roach                'p3.place AS part_3',
145*90949315SGreg Roach                'p4.place AS part_4',
146*90949315SGreg Roach                'p5.place AS part_5',
147*90949315SGreg Roach                'p6.place AS part_6',
148*90949315SGreg Roach                'p7.place AS part_7',
149*90949315SGreg Roach                'p8.place AS part_8',
1500e50635cSGreg Roach            ])
1510e50635cSGreg Roach            ->get()
1520e50635cSGreg Roach            ->map(static function (stdClass $row): string {
153*90949315SGreg Roach                return implode(Gedcom::PLACE_SEPARATOR, array_filter((array) $row));
1540e50635cSGreg Roach            });
15594e35917SGreg Roach
1560e50635cSGreg Roach        $missing = $all_places->diff($all_locations);
15794e35917SGreg Roach
1580e50635cSGreg Roach        foreach ($missing as $location) {
1590e50635cSGreg Roach            (new PlaceLocation($location))->id();
16094e35917SGreg Roach        }
16194e35917SGreg Roach    }
16294e35917SGreg Roach
16394e35917SGreg Roach    /**
16494e35917SGreg Roach     * Find all active places that match a location
16594e35917SGreg Roach     *
16694e35917SGreg Roach     * @param PlaceLocation $location
16794e35917SGreg Roach     *
16894e35917SGreg Roach     * @return array<string>
16994e35917SGreg Roach     */
17094e35917SGreg Roach    private function placeIdsForLocation(PlaceLocation $location): array
17194e35917SGreg Roach    {
17294e35917SGreg Roach        $hierarchy = [];
17394e35917SGreg Roach
174*90949315SGreg Roach        while ($location->id() !== null) {
17594e35917SGreg Roach            array_unshift($hierarchy, $location->locationName());
17694e35917SGreg Roach            $location = $location->parent();
17794e35917SGreg Roach        }
17894e35917SGreg Roach
17994e35917SGreg Roach        $place_ids = ['0'];
18094e35917SGreg Roach
18194e35917SGreg Roach        foreach ($hierarchy as $place_name) {
18294e35917SGreg Roach            $place_ids = DB::table('places')
18394e35917SGreg Roach                ->whereIn('p_parent_id', $place_ids)
18494e35917SGreg Roach                ->where('p_place', '=', $place_name)
18594e35917SGreg Roach                ->groupBy(['p_id'])
18694e35917SGreg Roach                ->pluck('p_id')
18794e35917SGreg Roach                ->all();
18894e35917SGreg Roach        }
18994e35917SGreg Roach
19094e35917SGreg Roach        return $place_ids;
19194e35917SGreg Roach    }
19294e35917SGreg Roach
19394e35917SGreg Roach    /**
194*90949315SGreg Roach     * @param int $id
19594e35917SGreg Roach     */
196*90949315SGreg Roach    public function deleteRecursively(int $id): void
19794e35917SGreg Roach    {
198*90949315SGreg Roach        // Uses on-delete-cascade
199*90949315SGreg Roach        DB::table('place_location')
200*90949315SGreg Roach            ->where('id', '=', $id)
20194e35917SGreg Roach            ->delete();
20294e35917SGreg Roach    }
2030e50635cSGreg Roach
2040e50635cSGreg Roach    /**
2050e50635cSGreg Roach     * Find a list of child places.
2060e50635cSGreg Roach     * How many children does each child place have?  How many have co-ordinates?
2070e50635cSGreg Roach     *
208*90949315SGreg Roach     * @param int|null $parent_id
2090e50635cSGreg Roach     *
2100e50635cSGreg Roach     * @return Collection<object>
2110e50635cSGreg Roach     */
212*90949315SGreg Roach    public function getPlaceListLocation(?int $parent_id): Collection
2130e50635cSGreg Roach    {
2140e50635cSGreg Roach        $prefix = DB::connection()->getTablePrefix();
2150e50635cSGreg Roach
2160e50635cSGreg Roach        $expression =
217*90949315SGreg Roach            $prefix . 'p1.place IS NOT NULL AND ' . $prefix . 'p1.latitude IS NULL OR ' .
218*90949315SGreg Roach            $prefix . 'p2.place IS NOT NULL AND ' . $prefix . 'p2.latitude IS NULL OR ' .
219*90949315SGreg Roach            $prefix . 'p3.place IS NOT NULL AND ' . $prefix . 'p3.latitude IS NULL OR ' .
220*90949315SGreg Roach            $prefix . 'p4.place IS NOT NULL AND ' . $prefix . 'p4.latitude IS NULL OR ' .
221*90949315SGreg Roach            $prefix . 'p5.place IS NOT NULL AND ' . $prefix . 'p5.latitude IS NULL OR ' .
222*90949315SGreg Roach            $prefix . 'p6.place IS NOT NULL AND ' . $prefix . 'p6.latitude IS NULL OR ' .
223*90949315SGreg Roach            $prefix . 'p7.place IS NOT NULL AND ' . $prefix . 'p7.latitude IS NULL OR ' .
224*90949315SGreg Roach            $prefix . 'p8.place IS NOT NULL AND ' . $prefix . 'p8.latitude IS NULL OR ' .
225*90949315SGreg Roach            $prefix . 'p9.place IS NOT NULL AND ' . $prefix . 'p9.latitude IS NULL';
2260e50635cSGreg Roach
22779c6313cSGreg Roach        $expression = 'CASE ' . $expression . ' WHEN TRUE THEN 1 ELSE 0 END';
22879c6313cSGreg Roach
229*90949315SGreg Roach        $query = DB::table('place_location AS p0')
230*90949315SGreg Roach            ->leftJoin('place_location AS p1', 'p1.parent_id', '=', 'p0.id')
231*90949315SGreg Roach            ->leftJoin('place_location AS p2', 'p2.parent_id', '=', 'p1.id')
232*90949315SGreg Roach            ->leftJoin('place_location AS p3', 'p3.parent_id', '=', 'p2.id')
233*90949315SGreg Roach            ->leftJoin('place_location AS p4', 'p4.parent_id', '=', 'p3.id')
234*90949315SGreg Roach            ->leftJoin('place_location AS p5', 'p5.parent_id', '=', 'p4.id')
235*90949315SGreg Roach            ->leftJoin('place_location AS p6', 'p6.parent_id', '=', 'p5.id')
236*90949315SGreg Roach            ->leftJoin('place_location AS p7', 'p7.parent_id', '=', 'p6.id')
237*90949315SGreg Roach            ->leftJoin('place_location AS p8', 'p8.parent_id', '=', 'p7.id')
238*90949315SGreg Roach            ->leftJoin('place_location AS p9', 'p9.parent_id', '=', 'p8.id');
239*90949315SGreg Roach
240*90949315SGreg Roach        if ($parent_id === null) {
241*90949315SGreg Roach            $query->whereNull('p0.parent_id');
242*90949315SGreg Roach        } else {
243*90949315SGreg Roach            $query->where('p0.parent_id', '=', $parent_id);
244*90949315SGreg Roach        }
245*90949315SGreg Roach
246*90949315SGreg Roach        return $query
247*90949315SGreg Roach            ->groupBy(['p0.id'])
248*90949315SGreg Roach            ->orderBy(new Expression($prefix . 'p0.place /*! COLLATE ' . I18N::collation() . ' */'))
2490e50635cSGreg Roach            ->select([
2500e50635cSGreg Roach                'p0.*',
251*90949315SGreg Roach                new Expression('COUNT(' . $prefix . 'p1.id) AS child_count'),
2520e50635cSGreg Roach                new Expression('SUM(' . $expression . ') AS no_coord'),
2530e50635cSGreg Roach            ])
2540e50635cSGreg Roach            ->get()
2550e50635cSGreg Roach            ->map(static function (stdClass $row): stdClass {
2560e50635cSGreg Roach                $row->child_count = (int) $row->child_count;
2570e50635cSGreg Roach                $row->no_coord    = (int) $row->no_coord;
258*90949315SGreg Roach                $row->key         = mb_strtolower($row->place);
2590e50635cSGreg Roach
2600e50635cSGreg Roach                return $row;
2610e50635cSGreg Roach            });
2620e50635cSGreg Roach    }
263*90949315SGreg Roach
264*90949315SGreg Roach    /**
265*90949315SGreg Roach     * @param float $latitude
266*90949315SGreg Roach     *
267*90949315SGreg Roach     * @return string
268*90949315SGreg Roach     */
269*90949315SGreg Roach    public function writeLatitude(float $latitude): string
270*90949315SGreg Roach    {
271*90949315SGreg Roach        return $this->writeDegrees($latitude, Gedcom::LATITUDE_NORTH, Gedcom::LATITUDE_SOUTH);
272*90949315SGreg Roach    }
273*90949315SGreg Roach
274*90949315SGreg Roach    /**
275*90949315SGreg Roach     * @param float $longitude
276*90949315SGreg Roach     *
277*90949315SGreg Roach     * @return string
278*90949315SGreg Roach     */
279*90949315SGreg Roach    public function writeLongitude(float $longitude): string
280*90949315SGreg Roach    {
281*90949315SGreg Roach        return $this->writeDegrees($longitude, Gedcom::LONGITUDE_EAST, Gedcom::LONGITUDE_WEST);
282*90949315SGreg Roach    }
283*90949315SGreg Roach
284*90949315SGreg Roach    /**
285*90949315SGreg Roach     * @param float  $degrees
286*90949315SGreg Roach     * @param string $positive
287*90949315SGreg Roach     * @param string $negative
288*90949315SGreg Roach     *
289*90949315SGreg Roach     * @return string
290*90949315SGreg Roach     */
291*90949315SGreg Roach    private function writeDegrees(float $degrees, string $positive, string $negative): string
292*90949315SGreg Roach    {
293*90949315SGreg Roach        $degrees = round($degrees, 5);
294*90949315SGreg Roach
295*90949315SGreg Roach        if ($degrees < 0.0) {
296*90949315SGreg Roach            return $negative . abs($degrees);
297*90949315SGreg Roach        }
298*90949315SGreg Roach
299*90949315SGreg Roach        return $positive . $degrees;
300*90949315SGreg Roach    }
30194e35917SGreg Roach}
302