1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2023 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\DB; 23use Fisharebest\Webtrees\PlaceLocation; 24use Fisharebest\Webtrees\Services\MapDataService; 25use Psr\Http\Message\ResponseInterface; 26use Psr\Http\Message\ServerRequestInterface; 27use Psr\Http\Server\RequestHandlerInterface; 28use RuntimeException; 29 30use function addcslashes; 31use function array_map; 32use function array_merge; 33use function array_pad; 34use function array_reverse; 35use function array_shift; 36use function array_unshift; 37use function count; 38use function fopen; 39use function fputcsv; 40use function max; 41use function preg_replace; 42use function response; 43use function rewind; 44use function stream_get_contents; 45 46/** 47 * Export geographic data. 48 */ 49class MapDataExportCSV implements RequestHandlerInterface 50{ 51 private MapDataService $map_data_service; 52 53 /** 54 * Dependency injection. 55 * 56 * @param MapDataService $map_data_service 57 */ 58 public function __construct(MapDataService $map_data_service) 59 { 60 $this->map_data_service = $map_data_service; 61 } 62 63 /** 64 * @param ServerRequestInterface $request 65 * 66 * @return ResponseInterface 67 */ 68 public function handle(ServerRequestInterface $request): ResponseInterface 69 { 70 $parent_id = $request->getAttribute('parent_id'); 71 72 if ($parent_id === null) { 73 $parent = new PlaceLocation(''); 74 } else { 75 $parent = $this->map_data_service->findById((int) $parent_id); 76 } 77 78 for ($tmp = $parent, $hierarchy = []; $tmp->id() !== null; $tmp = $tmp->parent()) { 79 $hierarchy[] = $tmp->locationName(); 80 } 81 82 // Create the file name 83 $filename = preg_replace('/[^\p{L}]+/u', '-', $hierarchy[0] ?? 'Global') . '.csv'; 84 85 // Recursively search for child places 86 $places = []; 87 $queue = [[ 88 $parent->id(), array_reverse($hierarchy), $parent->latitude(), $parent->longitude() 89 ]]; 90 91 while ($queue !== []) { 92 [$id, $hierarchy, $latitude, $longitude] = array_shift($queue); 93 94 if ($latitude !== null && $longitude !== null) { 95 $places[] = (object) [ 96 'hierarchy' => $hierarchy, 97 'latitude' => $latitude, 98 'longitude' => $longitude, 99 ]; 100 } 101 102 $query = DB::table('place_location'); 103 // Data for the next level. 104 105 if ($id === null) { 106 $query->whereNull('parent_id'); 107 } else { 108 $query->where('parent_id', '=', $id); 109 } 110 111 $rows = $query 112 ->orderBy('place', 'DESC') 113 ->select(['id', 'place', 'latitude', 'longitude']) 114 ->get(); 115 116 $next_level = count($hierarchy); 117 118 foreach ($rows as $row) { 119 $hierarchy[$next_level] = $row->place; 120 array_unshift($queue, [$row->id, $hierarchy, $row->latitude, $row->longitude]); 121 } 122 } 123 124 // Pad all locations to the length of the longest. 125 $max_level = 0; 126 foreach ($places as $place) { 127 $max_level = max($max_level, count($place->hierarchy)); 128 } 129 130 $places = array_map(fn (object $place): array => array_merge( 131 [ 132 count($place->hierarchy) - 1, 133 ], 134 array_pad($place->hierarchy, $max_level, ''), 135 [ 136 $this->map_data_service->writeLongitude((float) $place->longitude), 137 $this->map_data_service->writeLatitude((float) $place->latitude), 138 '', 139 '', 140 ] 141 ), $places); 142 143 // Create the header line for the output file (always English) 144 $header = [ 145 'Level', 146 ]; 147 148 for ($i = 0; $i < $max_level; $i++) { 149 $header[] = 'Place' . $i; 150 } 151 152 $header[] = 'Longitude'; 153 $header[] = 'Latitude'; 154 $header[] = 'Zoom'; 155 $header[] = 'Icon'; 156 157 $resource = fopen('php://memory', 'wb+'); 158 159 if ($resource === false) { 160 throw new RuntimeException('Failed to create temporary stream'); 161 } 162 163 fputcsv(stream: $resource, fields: $header, separator: MapDataService::CSV_SEPARATOR, escape: '\\'); 164 165 foreach ($places as $place) { 166 fputcsv(stream: $resource, fields: $place, separator: MapDataService::CSV_SEPARATOR, escape: '\\'); 167 } 168 169 rewind($resource); 170 171 $filename = addcslashes($filename, '"'); 172 173 return response(stream_get_contents($resource)) 174 ->withHeader('content-type', 'text/csv; charset=UTF-8') 175 ->withHeader('content-disposition', 'attachment; filename="' . $filename . '"'); 176 } 177} 178