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 /** @var MapDataService */ 53 private $map_data_service; 54 55 /** 56 * Dependency injection. 57 * 58 * @param MapDataService $map_data_service 59 */ 60 public function __construct(MapDataService $map_data_service) 61 { 62 $this->map_data_service = $map_data_service; 63 } 64 65 /** 66 * @param ServerRequestInterface $request 67 * 68 * @return ResponseInterface 69 */ 70 public function handle(ServerRequestInterface $request): ResponseInterface 71 { 72 $parent_id = $request->getAttribute('parent_id'); 73 74 if ($parent_id === null) { 75 $parent = new PlaceLocation(''); 76 } else { 77 $parent = $this->map_data_service->findById((int) $parent_id); 78 } 79 80 for ($tmp = $parent, $hierarchy = []; $tmp->id() !== null; $tmp = $tmp->parent()) { 81 $hierarchy[] = $tmp->locationName(); 82 } 83 84 // Create the file name 85 $filename = preg_replace('/[^\p{L}]+/u', '-', $hierarchy[0] ?? 'Global') . '.csv'; 86 87 // Recursively search for child places 88 $places = []; 89 $queue = [[ 90 $parent->id(), array_reverse($hierarchy), $parent->latitude(), $parent->longitude() 91 ]]; 92 93 while ($queue !== []) { 94 [$id, $hierarchy, $latitude, $longitude] = array_shift($queue); 95 96 if ($latitude !== null && !$longitude !== null) { 97 $places[] = (object) [ 98 'hierarchy' => $hierarchy, 99 'latitude' => $latitude, 100 'longitude' => $longitude, 101 ]; 102 } 103 104 $query = DB::table('place_location'); 105 // Data for the next level. 106 107 if ($id === null) { 108 $query->whereNull('parent_id'); 109 } else { 110 $query->where('parent_id', '=', $id); 111 } 112 113 $rows = $query 114 ->orderBy('place', 'DESC') 115 ->select(['id', 'place', 'latitude', 'longitude']) 116 ->get(); 117 118 $next_level = count($hierarchy); 119 120 foreach ($rows as $row) { 121 $hierarchy[$next_level] = $row->place; 122 array_unshift($queue, [$row->id, $hierarchy, $row->latitude, $row->longitude]); 123 } 124 } 125 126 // Pad all locations to the length of the longest. 127 $max_level = 0; 128 foreach ($places as $place) { 129 $max_level = max($max_level, count($place->hierarchy)); 130 } 131 132 $places = array_map(function (stdClass $place) use ($max_level): array { 133 return array_merge( 134 [ 135 count($place->hierarchy) - 1, 136 ], 137 array_pad($place->hierarchy, $max_level, ''), 138 [ 139 $this->map_data_service->writeLongitude((float) $place->longitude), 140 $this->map_data_service->writeLatitude((float) $place->latitude), 141 '', 142 '', 143 ] 144 ); 145 }, $places); 146 147 // Create the header line for the output file (always English) 148 $header = [ 149 'Level', 150 ]; 151 152 for ($i = 0; $i < $max_level; $i++) { 153 $header[] = 'Place' . $i; 154 } 155 156 $header[] = 'Longitude'; 157 $header[] = 'Latitude'; 158 $header[] = 'Zoom'; 159 $header[] = 'Icon'; 160 161 $resource = fopen('php://temp', 'wb+'); 162 163 if ($resource === false) { 164 throw new RuntimeException('Failed to create temporary stream'); 165 } 166 167 fputcsv($resource, $header, MapDataService::CSV_SEPARATOR); 168 169 foreach ($places as $place) { 170 fputcsv($resource, $place, MapDataService::CSV_SEPARATOR); 171 } 172 173 rewind($resource); 174 175 $filename = addcslashes($filename, '"'); 176 177 return response(stream_get_contents($resource)) 178 ->withHeader('Content-Type', 'text/csv; charset=UTF-8') 179 ->withHeader('Content-Disposition', 'attachment; filename="' . $filename . '"'); 180 } 181} 182