. */ declare(strict_types=1); namespace Fisharebest\Webtrees\Http\RequestHandlers; use Fisharebest\Webtrees\DB; use Fisharebest\Webtrees\PlaceLocation; use Fisharebest\Webtrees\Services\MapDataService; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use RuntimeException; use function addcslashes; use function array_map; use function array_merge; use function array_pad; use function array_reverse; use function array_shift; use function array_unshift; use function count; use function fopen; use function fputcsv; use function max; use function preg_replace; use function response; use function rewind; use function stream_get_contents; /** * Export geographic data. */ class MapDataExportCSV implements RequestHandlerInterface { private MapDataService $map_data_service; /** * Dependency injection. * * @param MapDataService $map_data_service */ public function __construct(MapDataService $map_data_service) { $this->map_data_service = $map_data_service; } /** * @param ServerRequestInterface $request * * @return ResponseInterface */ public function handle(ServerRequestInterface $request): ResponseInterface { $parent_id = $request->getAttribute('parent_id'); if ($parent_id === null) { $parent = new PlaceLocation(''); } else { $parent = $this->map_data_service->findById((int) $parent_id); } for ($tmp = $parent, $hierarchy = []; $tmp->id() !== null; $tmp = $tmp->parent()) { $hierarchy[] = $tmp->locationName(); } // Create the file name $filename = preg_replace('/[^\p{L}]+/u', '-', $hierarchy[0] ?? 'Global') . '.csv'; // Recursively search for child places $places = []; $queue = [[ $parent->id(), array_reverse($hierarchy), $parent->latitude(), $parent->longitude() ]]; while ($queue !== []) { [$id, $hierarchy, $latitude, $longitude] = array_shift($queue); if ($latitude !== null && $longitude !== null) { $places[] = (object) [ 'hierarchy' => $hierarchy, 'latitude' => $latitude, 'longitude' => $longitude, ]; } $query = DB::table('place_location'); // Data for the next level. if ($id === null) { $query->whereNull('parent_id'); } else { $query->where('parent_id', '=', $id); } $rows = $query ->orderBy('place', 'DESC') ->select(['id', 'place', 'latitude', 'longitude']) ->get(); $next_level = count($hierarchy); foreach ($rows as $row) { $hierarchy[$next_level] = $row->place; array_unshift($queue, [$row->id, $hierarchy, $row->latitude, $row->longitude]); } } // Pad all locations to the length of the longest. $max_level = 0; foreach ($places as $place) { $max_level = max($max_level, count($place->hierarchy)); } $places = array_map(fn (object $place): array => array_merge( [ count($place->hierarchy) - 1, ], array_pad($place->hierarchy, $max_level, ''), [ $this->map_data_service->writeLongitude((float) $place->longitude), $this->map_data_service->writeLatitude((float) $place->latitude), '', '', ] ), $places); // Create the header line for the output file (always English) $header = [ 'Level', ]; for ($i = 0; $i < $max_level; $i++) { $header[] = 'Place' . $i; } $header[] = 'Longitude'; $header[] = 'Latitude'; $header[] = 'Zoom'; $header[] = 'Icon'; $resource = fopen('php://memory', 'wb+'); if ($resource === false) { throw new RuntimeException('Failed to create temporary stream'); } fputcsv(stream: $resource, fields: $header, separator: MapDataService::CSV_SEPARATOR, escape: '\\'); foreach ($places as $place) { fputcsv(stream: $resource, fields: $place, separator: MapDataService::CSV_SEPARATOR, escape: '\\'); } rewind($resource); $filename = addcslashes($filename, '"'); return response(stream_get_contents($resource)) ->withHeader('content-type', 'text/csv; charset=UTF-8') ->withHeader('content-disposition', 'attachment; filename="' . $filename . '"'); } }