Passed
Push — develop ( b53f60...9961ae )
by Greg
14:15
created

MapDataImportAction::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2022 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
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Http\RequestHandlers;
21
22
use Exception;
23
use Fisharebest\Webtrees\Exceptions\FileUploadException;
24
use Fisharebest\Webtrees\FlashMessages;
25
use Fisharebest\Webtrees\Gedcom;
26
use Fisharebest\Webtrees\I18N;
27
use Fisharebest\Webtrees\PlaceLocation;
28
use Fisharebest\Webtrees\Registry;
29
use Fisharebest\Webtrees\Services\MapDataService;
30
use Fisharebest\Webtrees\Validator;
31
use Illuminate\Database\Capsule\Manager as DB;
32
use League\Flysystem\FilesystemException;
33
use League\Flysystem\UnableToCheckFileExistence;
34
use League\Flysystem\UnableToReadFile;
35
use Psr\Http\Message\ResponseInterface;
36
use Psr\Http\Message\ServerRequestInterface;
37
use Psr\Http\Message\StreamFactoryInterface;
38
use Psr\Http\Server\RequestHandlerInterface;
39
40
use function array_filter;
41
use function array_reverse;
42
use function array_slice;
43
use function count;
44
use function fclose;
45
use function fgetcsv;
46
use function implode;
47
use function is_numeric;
48
use function json_decode;
49
use function redirect;
50
use function rewind;
51
use function route;
52
use function str_contains;
53
use function stream_get_contents;
54
55
use const JSON_THROW_ON_ERROR;
56
use const UPLOAD_ERR_NO_FILE;
57
use const UPLOAD_ERR_OK;
58
59
/**
60
 * Import geographic data.
61
 */
62
class MapDataImportAction implements RequestHandlerInterface
63
{
64
    private StreamFactoryInterface $stream_factory;
65
66
    /**
67
     * @param StreamFactoryInterface $stream_factory
68
     */
69
    public function __construct(StreamFactoryInterface $stream_factory)
70
    {
71
        $this->stream_factory = $stream_factory;
72
    }
73
74
    /**
75
     * This function assumes the input file layout is
76
     * level followed by a variable number of placename fields
77
     * followed by Longitude, Latitude, Zoom & Icon
78
     *
79
     * @param ServerRequestInterface $request
80
     *
81
     * @return ResponseInterface
82
     * @throws Exception
83
     */
84
    public function handle(ServerRequestInterface $request): ResponseInterface
85
    {
86
        $source  = Validator::parsedBody($request)->isInArray(['client', 'server'])->string('source');
87
        $options = Validator::parsedBody($request)->isInArray(['add', 'addupdate', 'update'])->string('options');
88
89
        $places = [];
90
        $url    = route(MapDataList::class, ['parent_id' => 0]);
91
        $fp     = null;
92
93
        if ($source === 'client') {
94
            $client_file = $request->getUploadedFiles()['client_file'] ?? null;
95
96
            if ($client_file === null || $client_file->getError() === UPLOAD_ERR_NO_FILE) {
97
                FlashMessages::addMessage(I18N::translate('No file was received.'), 'danger');
98
99
                return redirect(route(MapDataImportPage::class));
100
            }
101
102
            if ($client_file->getError() !== UPLOAD_ERR_OK) {
103
                throw new FileUploadException($client_file);
104
            }
105
106
            $fp = $client_file->getStream()->detach();
107
        }
108
109
        if ($source === 'server') {
110
            $server_file = Validator::parsedBody($request)->string('server_file');
111
112
            if ($server_file === '') {
113
                FlashMessages::addMessage(I18N::translate('No file was received.'), 'danger');
114
115
                return redirect(route(MapDataImportPage::class));
116
            }
117
118
            $resource = Registry::filesystem()->data()->readStream('places/' . $server_file);
119
            $fp       = $this->stream_factory->createStreamFromResource($resource)->detach();
120
        }
121
122
        if ($fp === null) {
123
            return redirect(route(MapDataImportPage::class));
124
        }
125
126
        $string = stream_get_contents($fp);
127
128
        // Check the file type
129
        if (str_contains($string, 'FeatureCollection')) {
130
            $input_array = json_decode($string, false, 512, JSON_THROW_ON_ERROR);
131
132
            foreach ($input_array->features as $feature) {
133
                $places[] = [
134
                    'latitude'  => $feature->geometry->coordinates[1],
135
                    'longitude' => $feature->geometry->coordinates[0],
136
                    'name'      => $feature->properties->name,
137
                ];
138
            }
139
        } else {
140
            rewind($fp);
141
            while (($row = fgetcsv($fp, 0, MapDataService::CSV_SEPARATOR)) !== false) {
142
                // Skip the header
143
                if (!is_numeric($row[0])) {
144
                    continue;
145
                }
146
147
                $level = (int) $row[0];
148
                $count = count($row);
149
                $name  = implode(Gedcom::PLACE_SEPARATOR, array_reverse(array_slice($row, 1, 1 + $level)));
150
151
                $places[] = [
152
                    'latitude'  => (float) strtr($row[$count - 3], ['N' => '', 'S' => '-', ',' => '.']),
153
                    'longitude' => (float) strtr($row[$count - 4], ['E' => '', 'W' => '-', ',' => '.']),
154
                    'name'      => $name
155
                ];
156
            }
157
        }
158
159
        fclose($fp);
160
161
        $added   = 0;
162
        $updated = 0;
163
164
        // Remove places with 0,0 coordinates at lower levels.
165
        $callback = static fn (array $place): bool => !str_contains($place['name'], ',') || $place['longitude'] !== 0.0 || $place['latitude'] !== 0.0;
166
167
        $places = array_filter($places, $callback);
168
169
        foreach ($places as $place) {
170
            $location = new PlaceLocation($place['name']);
171
            $exists   = $location->exists();
172
173
            // Only update existing records
174
            if ($options === 'update' && !$exists) {
175
                continue;
176
            }
177
178
            // Only add new records
179
            if ($options === 'add' && $exists) {
180
                continue;
181
            }
182
183
            if (!$exists) {
184
                $added++;
185
            }
186
187
            $updated += DB::table('place_location')
188
                ->where('id', '=', $location->id())
189
                ->update([
190
                    'latitude'  => $place['latitude'],
191
                    'longitude' => $place['longitude'],
192
                ]);
193
        }
194
195
        FlashMessages::addMessage(
196
            I18N::translate('locations updated: %s, locations added: %s', I18N::number($updated), I18N::number($added)),
197
            'info'
198
        );
199
200
        return Registry::responseFactory()->redirectUrl($url);
201
    }
202
}
203