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() 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']) 2550e50635cSGreg Roach ->select([ 2560e50635cSGreg Roach 'p0.*', 25790949315SGreg Roach new Expression('COUNT(' . $prefix . 'p1.id) AS child_count'), 2580e50635cSGreg Roach new Expression('SUM(' . $expression . ') AS no_coord'), 2590e50635cSGreg Roach ]) 2600e50635cSGreg Roach ->get() 2617b5ff3baSGreg Roach ->map(static function (object $row): object { 2620e50635cSGreg Roach $row->child_count = (int) $row->child_count; 2630e50635cSGreg Roach $row->no_coord = (int) $row->no_coord; 26490949315SGreg Roach $row->key = mb_strtolower($row->place); 2650e50635cSGreg Roach 2660e50635cSGreg Roach return $row; 267*24dd03f2SGreg Roach }) 268*24dd03f2SGreg Roach ->sort(static fn (object $x, object $y): int => I18N::comparator()($x->place, $y->place)); 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