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