xref: /webtrees/app/Services/MapDataService.php (revision 6f4ec3cadc983f0a7294108c634bef48846b4311)
194e35917SGreg Roach<?php
294e35917SGreg Roach
394e35917SGreg Roach/**
494e35917SGreg Roach * webtrees: online genealogy
5d11be702SGreg Roach * Copyright (C) 2023 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
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
1694e35917SGreg Roach */
1794e35917SGreg Roach
1894e35917SGreg Roachdeclare(strict_types=1);
1994e35917SGreg Roach
2094e35917SGreg Roachnamespace Fisharebest\Webtrees\Services;
2194e35917SGreg Roach
22*6f4ec3caSGreg Roachuse Fisharebest\Webtrees\DB;
237b5ff3baSGreg Roachuse Fisharebest\Webtrees\FlashMessages;
2494e35917SGreg Roachuse Fisharebest\Webtrees\Gedcom;
250e50635cSGreg Roachuse Fisharebest\Webtrees\I18N;
2694e35917SGreg Roachuse Fisharebest\Webtrees\PlaceLocation;
270e50635cSGreg Roachuse Illuminate\Database\Query\Expression;
280e50635cSGreg Roachuse Illuminate\Support\Collection;
2994e35917SGreg Roach
3090949315SGreg Roachuse function abs;
3190949315SGreg Roachuse function array_filter;
3294e35917SGreg Roachuse function array_unshift;
330e50635cSGreg Roachuse function implode;
3490949315SGreg Roachuse function mb_strtolower;
3590949315SGreg Roachuse function round;
3694e35917SGreg Roach
3794e35917SGreg Roach/**
3894e35917SGreg Roach * Process geographic data.
3994e35917SGreg Roach */
4094e35917SGreg Roachclass MapDataService
4194e35917SGreg Roach{
4290949315SGreg Roach    // Location of files to import
4390949315SGreg Roach    public const PLACES_FOLDER = 'places/';
4490949315SGreg Roach
4590949315SGreg Roach    // Format of CSV files
4690949315SGreg Roach    public const CSV_SEPARATOR = ';';
4790949315SGreg 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
5790949315SGreg Roach        while (true) {
5890949315SGreg Roach            $row = DB::table('place_location')
5990949315SGreg Roach                ->where('id', '=', $id)
6090949315SGreg Roach                ->select(['place', 'parent_id'])
6194e35917SGreg Roach                ->first();
6294e35917SGreg Roach
6394e35917SGreg Roach            if ($row === null) {
6490949315SGreg Roach                return new PlaceLocation(implode(Gedcom::PLACE_SEPARATOR, $hierarchy));
6594e35917SGreg Roach            }
6694e35917SGreg Roach
6790949315SGreg Roach            $hierarchy[] = $row->place;
6890949315SGreg Roach            $id          = $row->parent_id;
6990949315SGreg Roach        }
7094e35917SGreg Roach    }
7194e35917SGreg Roach
7294e35917SGreg Roach    /**
7394e35917SGreg Roach     * Which trees use a particular location?
7494e35917SGreg Roach     *
7594e35917SGreg Roach     * @param PlaceLocation $location
7694e35917SGreg Roach     *
777b5ff3baSGreg Roach     * @return array<string,array<object>>
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) {
9390949315SGreg 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()
1277b5ff3baSGreg Roach            ->map(static function (object $row): string {
12890949315SGreg Roach                return implode(Gedcom::PLACE_SEPARATOR, array_filter((array) $row));
1290e50635cSGreg Roach            });
13094e35917SGreg Roach
13190949315SGreg Roach        $all_locations = DB::table('place_location AS p0')
13290949315SGreg Roach            ->leftJoin('place_location AS p1', 'p1.id', '=', 'p0.parent_id')
13390949315SGreg Roach            ->leftJoin('place_location AS p2', 'p2.id', '=', 'p1.parent_id')
13490949315SGreg Roach            ->leftJoin('place_location AS p3', 'p3.id', '=', 'p2.parent_id')
13590949315SGreg Roach            ->leftJoin('place_location AS p4', 'p4.id', '=', 'p3.parent_id')
13690949315SGreg Roach            ->leftJoin('place_location AS p5', 'p5.id', '=', 'p4.parent_id')
13790949315SGreg Roach            ->leftJoin('place_location AS p6', 'p6.id', '=', 'p5.parent_id')
13890949315SGreg Roach            ->leftJoin('place_location AS p7', 'p7.id', '=', 'p6.parent_id')
13990949315SGreg Roach            ->leftJoin('place_location AS p8', 'p8.id', '=', 'p7.parent_id')
1400e50635cSGreg Roach            ->select([
14190949315SGreg Roach                'p0.place AS part_0',
14290949315SGreg Roach                'p1.place AS part_1',
14390949315SGreg Roach                'p2.place AS part_2',
14490949315SGreg Roach                'p3.place AS part_3',
14590949315SGreg Roach                'p4.place AS part_4',
14690949315SGreg Roach                'p5.place AS part_5',
14790949315SGreg Roach                'p6.place AS part_6',
14890949315SGreg Roach                'p7.place AS part_7',
14990949315SGreg Roach                'p8.place AS part_8',
1500e50635cSGreg Roach            ])
1510e50635cSGreg Roach            ->get()
1527b5ff3baSGreg Roach            ->map(static function (object $row): string {
15390949315SGreg 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    /**
16490949315SGreg Roach     * @param int $id
1657b5ff3baSGreg Roach     *
1667b5ff3baSGreg Roach     * @return void
16794e35917SGreg Roach     */
16890949315SGreg Roach    public function deleteRecursively(int $id): void
16994e35917SGreg Roach    {
17090949315SGreg Roach        // Uses on-delete-cascade
17190949315SGreg Roach        DB::table('place_location')
17290949315SGreg Roach            ->where('id', '=', $id)
17394e35917SGreg Roach            ->delete();
17494e35917SGreg Roach    }
1750e50635cSGreg Roach
1760e50635cSGreg Roach    /**
1777b5ff3baSGreg Roach     * @param int|null   $parent_location_id
1787b5ff3baSGreg Roach     * @param array<int> $parent_place_ids
1797b5ff3baSGreg Roach     *
1807b5ff3baSGreg Roach     * @return void
1817b5ff3baSGreg Roach     */
1827b5ff3baSGreg Roach    public function deleteUnusedLocations(?int $parent_location_id, array $parent_place_ids): void
1837b5ff3baSGreg Roach    {
1847b5ff3baSGreg Roach        if ($parent_location_id === null) {
1857b5ff3baSGreg Roach            $location_query = DB::table('place_location')
1867b5ff3baSGreg Roach                ->whereNull('parent_id');
1877b5ff3baSGreg Roach        } else {
1887b5ff3baSGreg Roach            $location_query = DB::table('place_location')
1897b5ff3baSGreg Roach                ->where('parent_id', '=', $parent_location_id);
1907b5ff3baSGreg Roach        }
1917b5ff3baSGreg Roach
1927b5ff3baSGreg Roach        foreach ($location_query->get() as $location) {
1937b5ff3baSGreg Roach            $places = DB::table('places')
1947b5ff3baSGreg Roach                ->whereIn('p_parent_id', $parent_place_ids)
1957b5ff3baSGreg Roach                ->where('p_place', '=', $location->place)
1967b5ff3baSGreg Roach                ->get();
1977b5ff3baSGreg Roach
1987b5ff3baSGreg Roach            if ($places->isEmpty()) {
1997b5ff3baSGreg Roach                FlashMessages::addMessage(I18N::translate('“%s” has been deleted.', e($location->place)));
2007b5ff3baSGreg Roach
2017b5ff3baSGreg Roach                DB::table('place_location')
2027b5ff3baSGreg Roach                    ->where('id', '=', $location->id)
2037b5ff3baSGreg Roach                    ->delete();
2047b5ff3baSGreg Roach            } else {
2057b5ff3baSGreg Roach                $place_ids = $places->map(static fn (object $place): int => (int) $place->p_id)->all();
2067b5ff3baSGreg Roach                $this->deleteUnusedLocations((int) $location->id, $place_ids);
2077b5ff3baSGreg Roach            }
2087b5ff3baSGreg Roach        }
2097b5ff3baSGreg Roach    }
2107b5ff3baSGreg Roach
2117b5ff3baSGreg Roach    /**
2120e50635cSGreg Roach     * Find a list of child places.
2130e50635cSGreg Roach     * How many children does each child place have?  How many have co-ordinates?
2140e50635cSGreg Roach     *
21590949315SGreg Roach     * @param int|null $parent_id
2160e50635cSGreg Roach     *
21736779af1SGreg Roach     * @return Collection<int,object>
2180e50635cSGreg Roach     */
21990949315SGreg Roach    public function getPlaceListLocation(?int $parent_id): Collection
2200e50635cSGreg Roach    {
2210e50635cSGreg Roach        $prefix = DB::connection()->getTablePrefix();
2220e50635cSGreg Roach
2230e50635cSGreg Roach        $expression =
22490949315SGreg Roach            $prefix . 'p1.place IS NOT NULL AND ' . $prefix . 'p1.latitude IS NULL OR ' .
22590949315SGreg Roach            $prefix . 'p2.place IS NOT NULL AND ' . $prefix . 'p2.latitude IS NULL OR ' .
22690949315SGreg Roach            $prefix . 'p3.place IS NOT NULL AND ' . $prefix . 'p3.latitude IS NULL OR ' .
22790949315SGreg Roach            $prefix . 'p4.place IS NOT NULL AND ' . $prefix . 'p4.latitude IS NULL OR ' .
22890949315SGreg Roach            $prefix . 'p5.place IS NOT NULL AND ' . $prefix . 'p5.latitude IS NULL OR ' .
22990949315SGreg Roach            $prefix . 'p6.place IS NOT NULL AND ' . $prefix . 'p6.latitude IS NULL OR ' .
23090949315SGreg Roach            $prefix . 'p7.place IS NOT NULL AND ' . $prefix . 'p7.latitude IS NULL OR ' .
23190949315SGreg Roach            $prefix . 'p8.place IS NOT NULL AND ' . $prefix . 'p8.latitude IS NULL OR ' .
23290949315SGreg Roach            $prefix . 'p9.place IS NOT NULL AND ' . $prefix . 'p9.latitude IS NULL';
2330e50635cSGreg Roach
23479c6313cSGreg Roach        $expression = 'CASE ' . $expression . ' WHEN TRUE THEN 1 ELSE 0 END';
23579c6313cSGreg Roach
23690949315SGreg Roach        $query = DB::table('place_location AS p0')
23790949315SGreg Roach            ->leftJoin('place_location AS p1', 'p1.parent_id', '=', 'p0.id')
23890949315SGreg Roach            ->leftJoin('place_location AS p2', 'p2.parent_id', '=', 'p1.id')
23990949315SGreg Roach            ->leftJoin('place_location AS p3', 'p3.parent_id', '=', 'p2.id')
24090949315SGreg Roach            ->leftJoin('place_location AS p4', 'p4.parent_id', '=', 'p3.id')
24190949315SGreg Roach            ->leftJoin('place_location AS p5', 'p5.parent_id', '=', 'p4.id')
24290949315SGreg Roach            ->leftJoin('place_location AS p6', 'p6.parent_id', '=', 'p5.id')
24390949315SGreg Roach            ->leftJoin('place_location AS p7', 'p7.parent_id', '=', 'p6.id')
24490949315SGreg Roach            ->leftJoin('place_location AS p8', 'p8.parent_id', '=', 'p7.id')
24590949315SGreg Roach            ->leftJoin('place_location AS p9', 'p9.parent_id', '=', 'p8.id');
24690949315SGreg Roach
24790949315SGreg Roach        if ($parent_id === null) {
24890949315SGreg Roach            $query->whereNull('p0.parent_id');
24990949315SGreg Roach        } else {
25090949315SGreg Roach            $query->where('p0.parent_id', '=', $parent_id);
25190949315SGreg Roach        }
25290949315SGreg Roach
25390949315SGreg Roach        return $query
25490949315SGreg Roach            ->groupBy(['p0.id'])
25552288ec7SGreg Roach            ->orderBy('p0.place')
2560e50635cSGreg Roach            ->select([
2570e50635cSGreg Roach                'p0.*',
25890949315SGreg Roach                new Expression('COUNT(' . $prefix . 'p1.id) AS child_count'),
2590e50635cSGreg Roach                new Expression('SUM(' . $expression . ') AS no_coord'),
2600e50635cSGreg Roach            ])
2610e50635cSGreg Roach            ->get()
2627b5ff3baSGreg Roach            ->map(static function (object $row): object {
2630e50635cSGreg Roach                $row->child_count = (int) $row->child_count;
2640e50635cSGreg Roach                $row->no_coord    = (int) $row->no_coord;
26590949315SGreg Roach                $row->key         = mb_strtolower($row->place);
2660e50635cSGreg Roach
2670e50635cSGreg Roach                return $row;
2680e50635cSGreg Roach            });
2690e50635cSGreg Roach    }
27090949315SGreg Roach
27190949315SGreg Roach    /**
27290949315SGreg Roach     * @param float $latitude
27390949315SGreg Roach     *
27490949315SGreg Roach     * @return string
27590949315SGreg Roach     */
27690949315SGreg Roach    public function writeLatitude(float $latitude): string
27790949315SGreg Roach    {
27890949315SGreg Roach        return $this->writeDegrees($latitude, Gedcom::LATITUDE_NORTH, Gedcom::LATITUDE_SOUTH);
27990949315SGreg Roach    }
28090949315SGreg Roach
28190949315SGreg Roach    /**
28290949315SGreg Roach     * @param float $longitude
28390949315SGreg Roach     *
28490949315SGreg Roach     * @return string
28590949315SGreg Roach     */
28690949315SGreg Roach    public function writeLongitude(float $longitude): string
28790949315SGreg Roach    {
28890949315SGreg Roach        return $this->writeDegrees($longitude, Gedcom::LONGITUDE_EAST, Gedcom::LONGITUDE_WEST);
28990949315SGreg Roach    }
29090949315SGreg Roach
29190949315SGreg Roach    /**
2927b5ff3baSGreg Roach     * Find all active places that match a location
2937b5ff3baSGreg Roach     *
2947b5ff3baSGreg Roach     * @param PlaceLocation $location
2957b5ff3baSGreg Roach     *
2967b5ff3baSGreg Roach     * @return array<string>
2977b5ff3baSGreg Roach     */
2987b5ff3baSGreg Roach    private function placeIdsForLocation(PlaceLocation $location): array
2997b5ff3baSGreg Roach    {
3007b5ff3baSGreg Roach        $hierarchy = [];
3017b5ff3baSGreg Roach
3027b5ff3baSGreg Roach        while ($location->id() !== null) {
3037b5ff3baSGreg Roach            array_unshift($hierarchy, $location->locationName());
3047b5ff3baSGreg Roach            $location = $location->parent();
3057b5ff3baSGreg Roach        }
3067b5ff3baSGreg Roach
3077b5ff3baSGreg Roach        $place_ids = ['0'];
3087b5ff3baSGreg Roach
3097b5ff3baSGreg Roach        foreach ($hierarchy as $place_name) {
3107b5ff3baSGreg Roach            $place_ids = DB::table('places')
3117b5ff3baSGreg Roach                ->whereIn('p_parent_id', $place_ids)
3127b5ff3baSGreg Roach                ->where('p_place', '=', $place_name)
3137b5ff3baSGreg Roach                ->groupBy(['p_id'])
3147b5ff3baSGreg Roach                ->pluck('p_id')
3157b5ff3baSGreg Roach                ->all();
3167b5ff3baSGreg Roach        }
3177b5ff3baSGreg Roach
3187b5ff3baSGreg Roach        return $place_ids;
3197b5ff3baSGreg Roach    }
3207b5ff3baSGreg Roach
3217b5ff3baSGreg Roach    /**
32290949315SGreg Roach     * @param float  $degrees
32390949315SGreg Roach     * @param string $positive
32490949315SGreg Roach     * @param string $negative
32590949315SGreg Roach     *
32690949315SGreg Roach     * @return string
32790949315SGreg Roach     */
32890949315SGreg Roach    private function writeDegrees(float $degrees, string $positive, string $negative): string
32990949315SGreg Roach    {
33090949315SGreg Roach        $degrees = round($degrees, 5);
33190949315SGreg Roach
33290949315SGreg Roach        if ($degrees < 0.0) {
33390949315SGreg Roach            return $negative . abs($degrees);
33490949315SGreg Roach        }
33590949315SGreg Roach
33690949315SGreg Roach        return $positive . $degrees;
33790949315SGreg Roach    }
33894e35917SGreg Roach}
339