xref: /webtrees/app/Http/RequestHandlers/MapDataExportCSV.php (revision 49f828d09aa0a49d064593c8626c56c5dc5354ac)
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($resource, $header, MapDataService::CSV_SEPARATOR);
164
165        foreach ($places as $place) {
166            fputcsv($resource, $place, MapDataService::CSV_SEPARATOR);
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