1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2021 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Http\RequestHandlers; 21 22use Fisharebest\Webtrees\PlaceLocation; 23use Fisharebest\Webtrees\Services\MapDataService; 24use Illuminate\Database\Capsule\Manager as DB; 25use Psr\Http\Message\ResponseInterface; 26use Psr\Http\Message\ServerRequestInterface; 27use Psr\Http\Server\RequestHandlerInterface; 28use RuntimeException; 29use stdClass; 30 31use function addcslashes; 32use function array_map; 33use function array_merge; 34use function array_pad; 35use function array_reverse; 36use function array_shift; 37use function array_unshift; 38use function count; 39use function fopen; 40use function fputcsv; 41use function max; 42use function preg_replace; 43use function response; 44use function rewind; 45use function stream_get_contents; 46 47/** 48 * Export geographic data. 49 */ 50class MapDataExportCSV implements RequestHandlerInterface 51{ 52 private MapDataService $map_data_service; 53 54 /** 55 * Dependency injection. 56 * 57 * @param MapDataService $map_data_service 58 */ 59 public function __construct(MapDataService $map_data_service) 60 { 61 $this->map_data_service = $map_data_service; 62 } 63 64 /** 65 * @param ServerRequestInterface $request 66 * 67 * @return ResponseInterface 68 */ 69 public function handle(ServerRequestInterface $request): ResponseInterface 70 { 71 $parent_id = $request->getAttribute('parent_id'); 72 73 if ($parent_id === null) { 74 $parent = new PlaceLocation(''); 75 } else { 76 $parent = $this->map_data_service->findById((int) $parent_id); 77 } 78 79 for ($tmp = $parent, $hierarchy = []; $tmp->id() !== null; $tmp = $tmp->parent()) { 80 $hierarchy[] = $tmp->locationName(); 81 } 82 83 // Create the file name 84 $filename = preg_replace('/[^\p{L}]+/u', '-', $hierarchy[0] ?? 'Global') . '.csv'; 85 86 // Recursively search for child places 87 $places = []; 88 $queue = [[ 89 $parent->id(), array_reverse($hierarchy), $parent->latitude(), $parent->longitude() 90 ]]; 91 92 while ($queue !== []) { 93 [$id, $hierarchy, $latitude, $longitude] = array_shift($queue); 94 95 if ($latitude !== null && !$longitude !== null) { 96 $places[] = (object) [ 97 'hierarchy' => $hierarchy, 98 'latitude' => $latitude, 99 'longitude' => $longitude, 100 ]; 101 } 102 103 $query = DB::table('place_location'); 104 // Data for the next level. 105 106 if ($id === null) { 107 $query->whereNull('parent_id'); 108 } else { 109 $query->where('parent_id', '=', $id); 110 } 111 112 $rows = $query 113 ->orderBy('place', 'DESC') 114 ->select(['id', 'place', 'latitude', 'longitude']) 115 ->get(); 116 117 $next_level = count($hierarchy); 118 119 foreach ($rows as $row) { 120 $hierarchy[$next_level] = $row->place; 121 array_unshift($queue, [$row->id, $hierarchy, $row->latitude, $row->longitude]); 122 } 123 } 124 125 // Pad all locations to the length of the longest. 126 $max_level = 0; 127 foreach ($places as $place) { 128 $max_level = max($max_level, count($place->hierarchy)); 129 } 130 131 $places = array_map(function (stdClass $place) use ($max_level): array { 132 return array_merge( 133 [ 134 count($place->hierarchy) - 1, 135 ], 136 array_pad($place->hierarchy, $max_level, ''), 137 [ 138 $this->map_data_service->writeLongitude((float) $place->longitude), 139 $this->map_data_service->writeLatitude((float) $place->latitude), 140 '', 141 '', 142 ] 143 ); 144 }, $places); 145 146 // Create the header line for the output file (always English) 147 $header = [ 148 'Level', 149 ]; 150 151 for ($i = 0; $i < $max_level; $i++) { 152 $header[] = 'Place' . $i; 153 } 154 155 $header[] = 'Longitude'; 156 $header[] = 'Latitude'; 157 $header[] = 'Zoom'; 158 $header[] = 'Icon'; 159 160 $resource = fopen('php://memory', 'wb+'); 161 162 if ($resource === false) { 163 throw new RuntimeException('Failed to create temporary stream'); 164 } 165 166 fputcsv($resource, $header, MapDataService::CSV_SEPARATOR); 167 168 foreach ($places as $place) { 169 fputcsv($resource, $place, MapDataService::CSV_SEPARATOR); 170 } 171 172 rewind($resource); 173 174 $filename = addcslashes($filename, '"'); 175 176 return response(stream_get_contents($resource)) 177 ->withHeader('Content-Type', 'text/csv; charset=UTF-8') 178 ->withHeader('Content-Disposition', 'attachment; filename="' . $filename . '"'); 179 } 180} 181