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 226f4ec3caSGreg 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() 127f25fc0f9SGreg Roach ->map(static fn (object $row): string => implode(Gedcom::PLACE_SEPARATOR, array_filter((array) $row))); 12894e35917SGreg Roach 12990949315SGreg Roach $all_locations = DB::table('place_location AS p0') 13090949315SGreg Roach ->leftJoin('place_location AS p1', 'p1.id', '=', 'p0.parent_id') 13190949315SGreg Roach ->leftJoin('place_location AS p2', 'p2.id', '=', 'p1.parent_id') 13290949315SGreg Roach ->leftJoin('place_location AS p3', 'p3.id', '=', 'p2.parent_id') 13390949315SGreg Roach ->leftJoin('place_location AS p4', 'p4.id', '=', 'p3.parent_id') 13490949315SGreg Roach ->leftJoin('place_location AS p5', 'p5.id', '=', 'p4.parent_id') 13590949315SGreg Roach ->leftJoin('place_location AS p6', 'p6.id', '=', 'p5.parent_id') 13690949315SGreg Roach ->leftJoin('place_location AS p7', 'p7.id', '=', 'p6.parent_id') 13790949315SGreg Roach ->leftJoin('place_location AS p8', 'p8.id', '=', 'p7.parent_id') 1380e50635cSGreg Roach ->select([ 13990949315SGreg Roach 'p0.place AS part_0', 14090949315SGreg Roach 'p1.place AS part_1', 14190949315SGreg Roach 'p2.place AS part_2', 14290949315SGreg Roach 'p3.place AS part_3', 14390949315SGreg Roach 'p4.place AS part_4', 14490949315SGreg Roach 'p5.place AS part_5', 14590949315SGreg Roach 'p6.place AS part_6', 14690949315SGreg Roach 'p7.place AS part_7', 14790949315SGreg Roach 'p8.place AS part_8', 1480e50635cSGreg Roach ]) 1490e50635cSGreg Roach ->get() 150f25fc0f9SGreg Roach ->map(static fn (object $row): string => implode(Gedcom::PLACE_SEPARATOR, array_filter((array) $row))); 15194e35917SGreg Roach 1520e50635cSGreg Roach $missing = $all_places->diff($all_locations); 15394e35917SGreg Roach 1540e50635cSGreg Roach foreach ($missing as $location) { 1550e50635cSGreg Roach (new PlaceLocation($location))->id(); 15694e35917SGreg Roach } 15794e35917SGreg Roach } 15894e35917SGreg Roach 15994e35917SGreg Roach /** 16090949315SGreg Roach * @param int $id 1617b5ff3baSGreg Roach * 1627b5ff3baSGreg Roach * @return void 16394e35917SGreg Roach */ 16490949315SGreg Roach public function deleteRecursively(int $id): void 16594e35917SGreg Roach { 16690949315SGreg Roach // Uses on-delete-cascade 16790949315SGreg Roach DB::table('place_location') 16890949315SGreg Roach ->where('id', '=', $id) 16994e35917SGreg Roach ->delete(); 17094e35917SGreg Roach } 1710e50635cSGreg Roach 1720e50635cSGreg Roach /** 1737b5ff3baSGreg Roach * @param int|null $parent_location_id 1747b5ff3baSGreg Roach * @param array<int> $parent_place_ids 1757b5ff3baSGreg Roach * 1767b5ff3baSGreg Roach * @return void 1777b5ff3baSGreg Roach */ 1781ff45046SGreg Roach public function deleteUnusedLocations(int|null $parent_location_id, array $parent_place_ids): void 1797b5ff3baSGreg Roach { 1807b5ff3baSGreg Roach if ($parent_location_id === null) { 1817b5ff3baSGreg Roach $location_query = DB::table('place_location') 1827b5ff3baSGreg Roach ->whereNull('parent_id'); 1837b5ff3baSGreg Roach } else { 1847b5ff3baSGreg Roach $location_query = DB::table('place_location') 1857b5ff3baSGreg Roach ->where('parent_id', '=', $parent_location_id); 1867b5ff3baSGreg Roach } 1877b5ff3baSGreg Roach 1887b5ff3baSGreg Roach foreach ($location_query->get() as $location) { 1897b5ff3baSGreg Roach $places = DB::table('places') 1907b5ff3baSGreg Roach ->whereIn('p_parent_id', $parent_place_ids) 1917b5ff3baSGreg Roach ->where('p_place', '=', $location->place) 1927b5ff3baSGreg Roach ->get(); 1937b5ff3baSGreg Roach 1947b5ff3baSGreg Roach if ($places->isEmpty()) { 1957b5ff3baSGreg Roach FlashMessages::addMessage(I18N::translate('“%s” has been deleted.', e($location->place))); 1967b5ff3baSGreg Roach 1977b5ff3baSGreg Roach DB::table('place_location') 1987b5ff3baSGreg Roach ->where('id', '=', $location->id) 1997b5ff3baSGreg Roach ->delete(); 2007b5ff3baSGreg Roach } else { 2017b5ff3baSGreg Roach $place_ids = $places->map(static fn (object $place): int => (int) $place->p_id)->all(); 2027b5ff3baSGreg Roach $this->deleteUnusedLocations((int) $location->id, $place_ids); 2037b5ff3baSGreg Roach } 2047b5ff3baSGreg Roach } 2057b5ff3baSGreg Roach } 2067b5ff3baSGreg Roach 2077b5ff3baSGreg Roach /** 2080e50635cSGreg Roach * Find a list of child places. 2090e50635cSGreg Roach * How many children does each child place have? How many have co-ordinates? 2100e50635cSGreg Roach * 21190949315SGreg Roach * @param int|null $parent_id 2120e50635cSGreg Roach * 21336779af1SGreg Roach * @return Collection<int,object> 2140e50635cSGreg Roach */ 2151ff45046SGreg Roach public function getPlaceListLocation(int|null $parent_id): Collection 2160e50635cSGreg Roach { 217*211018abSGreg Roach $prefix = DB::prefix(); 2180e50635cSGreg Roach 2190e50635cSGreg Roach $expression = 22090949315SGreg Roach $prefix . 'p1.place IS NOT NULL AND ' . $prefix . 'p1.latitude IS NULL OR ' . 22190949315SGreg Roach $prefix . 'p2.place IS NOT NULL AND ' . $prefix . 'p2.latitude IS NULL OR ' . 22290949315SGreg Roach $prefix . 'p3.place IS NOT NULL AND ' . $prefix . 'p3.latitude IS NULL OR ' . 22390949315SGreg Roach $prefix . 'p4.place IS NOT NULL AND ' . $prefix . 'p4.latitude IS NULL OR ' . 22490949315SGreg Roach $prefix . 'p5.place IS NOT NULL AND ' . $prefix . 'p5.latitude IS NULL OR ' . 22590949315SGreg Roach $prefix . 'p6.place IS NOT NULL AND ' . $prefix . 'p6.latitude IS NULL OR ' . 22690949315SGreg Roach $prefix . 'p7.place IS NOT NULL AND ' . $prefix . 'p7.latitude IS NULL OR ' . 22790949315SGreg Roach $prefix . 'p8.place IS NOT NULL AND ' . $prefix . 'p8.latitude IS NULL OR ' . 22890949315SGreg Roach $prefix . 'p9.place IS NOT NULL AND ' . $prefix . 'p9.latitude IS NULL'; 2290e50635cSGreg Roach 23079c6313cSGreg Roach $expression = 'CASE ' . $expression . ' WHEN TRUE THEN 1 ELSE 0 END'; 23179c6313cSGreg Roach 23290949315SGreg Roach $query = DB::table('place_location AS p0') 23390949315SGreg Roach ->leftJoin('place_location AS p1', 'p1.parent_id', '=', 'p0.id') 23490949315SGreg Roach ->leftJoin('place_location AS p2', 'p2.parent_id', '=', 'p1.id') 23590949315SGreg Roach ->leftJoin('place_location AS p3', 'p3.parent_id', '=', 'p2.id') 23690949315SGreg Roach ->leftJoin('place_location AS p4', 'p4.parent_id', '=', 'p3.id') 23790949315SGreg Roach ->leftJoin('place_location AS p5', 'p5.parent_id', '=', 'p4.id') 23890949315SGreg Roach ->leftJoin('place_location AS p6', 'p6.parent_id', '=', 'p5.id') 23990949315SGreg Roach ->leftJoin('place_location AS p7', 'p7.parent_id', '=', 'p6.id') 24090949315SGreg Roach ->leftJoin('place_location AS p8', 'p8.parent_id', '=', 'p7.id') 24190949315SGreg Roach ->leftJoin('place_location AS p9', 'p9.parent_id', '=', 'p8.id'); 24290949315SGreg Roach 24390949315SGreg Roach if ($parent_id === null) { 24490949315SGreg Roach $query->whereNull('p0.parent_id'); 24590949315SGreg Roach } else { 24690949315SGreg Roach $query->where('p0.parent_id', '=', $parent_id); 24790949315SGreg Roach } 24890949315SGreg Roach 24990949315SGreg Roach return $query 25090949315SGreg Roach ->groupBy(['p0.id']) 2510e50635cSGreg Roach ->select([ 2520e50635cSGreg Roach 'p0.*', 25390949315SGreg Roach new Expression('COUNT(' . $prefix . 'p1.id) AS child_count'), 2540e50635cSGreg Roach new Expression('SUM(' . $expression . ') AS no_coord'), 2550e50635cSGreg Roach ]) 2560e50635cSGreg Roach ->get() 2577b5ff3baSGreg Roach ->map(static function (object $row): object { 2580e50635cSGreg Roach $row->child_count = (int) $row->child_count; 2590e50635cSGreg Roach $row->no_coord = (int) $row->no_coord; 26090949315SGreg Roach $row->key = mb_strtolower($row->place); 2610e50635cSGreg Roach 2620e50635cSGreg Roach return $row; 26324dd03f2SGreg Roach }) 26424dd03f2SGreg Roach ->sort(static fn (object $x, object $y): int => I18N::comparator()($x->place, $y->place)); 2650e50635cSGreg Roach } 26690949315SGreg Roach 26790949315SGreg Roach /** 26890949315SGreg Roach * @param float $latitude 26990949315SGreg Roach * 27090949315SGreg Roach * @return string 27190949315SGreg Roach */ 27290949315SGreg Roach public function writeLatitude(float $latitude): string 27390949315SGreg Roach { 27490949315SGreg Roach return $this->writeDegrees($latitude, Gedcom::LATITUDE_NORTH, Gedcom::LATITUDE_SOUTH); 27590949315SGreg Roach } 27690949315SGreg Roach 27790949315SGreg Roach /** 27890949315SGreg Roach * @param float $longitude 27990949315SGreg Roach * 28090949315SGreg Roach * @return string 28190949315SGreg Roach */ 28290949315SGreg Roach public function writeLongitude(float $longitude): string 28390949315SGreg Roach { 28490949315SGreg Roach return $this->writeDegrees($longitude, Gedcom::LONGITUDE_EAST, Gedcom::LONGITUDE_WEST); 28590949315SGreg Roach } 28690949315SGreg Roach 28790949315SGreg Roach /** 2887b5ff3baSGreg Roach * Find all active places that match a location 2897b5ff3baSGreg Roach * 2907b5ff3baSGreg Roach * @param PlaceLocation $location 2917b5ff3baSGreg Roach * 2927b5ff3baSGreg Roach * @return array<string> 2937b5ff3baSGreg Roach */ 2947b5ff3baSGreg Roach private function placeIdsForLocation(PlaceLocation $location): array 2957b5ff3baSGreg Roach { 2967b5ff3baSGreg Roach $hierarchy = []; 2977b5ff3baSGreg Roach 2987b5ff3baSGreg Roach while ($location->id() !== null) { 2997b5ff3baSGreg Roach array_unshift($hierarchy, $location->locationName()); 3007b5ff3baSGreg Roach $location = $location->parent(); 3017b5ff3baSGreg Roach } 3027b5ff3baSGreg Roach 3037b5ff3baSGreg Roach $place_ids = ['0']; 3047b5ff3baSGreg Roach 3057b5ff3baSGreg Roach foreach ($hierarchy as $place_name) { 3067b5ff3baSGreg Roach $place_ids = DB::table('places') 3077b5ff3baSGreg Roach ->whereIn('p_parent_id', $place_ids) 3087b5ff3baSGreg Roach ->where('p_place', '=', $place_name) 3097b5ff3baSGreg Roach ->groupBy(['p_id']) 3107b5ff3baSGreg Roach ->pluck('p_id') 3117b5ff3baSGreg Roach ->all(); 3127b5ff3baSGreg Roach } 3137b5ff3baSGreg Roach 3147b5ff3baSGreg Roach return $place_ids; 3157b5ff3baSGreg Roach } 3167b5ff3baSGreg Roach 3177b5ff3baSGreg Roach /** 31890949315SGreg Roach * @param float $degrees 31990949315SGreg Roach * @param string $positive 32090949315SGreg Roach * @param string $negative 32190949315SGreg Roach * 32290949315SGreg Roach * @return string 32390949315SGreg Roach */ 32490949315SGreg Roach private function writeDegrees(float $degrees, string $positive, string $negative): string 32590949315SGreg Roach { 32690949315SGreg Roach $degrees = round($degrees, 5); 32790949315SGreg Roach 32890949315SGreg Roach if ($degrees < 0.0) { 32990949315SGreg Roach return $negative . abs($degrees); 33090949315SGreg Roach } 33190949315SGreg Roach 33290949315SGreg Roach return $positive . $degrees; 33390949315SGreg Roach } 33494e35917SGreg Roach} 335