MapDataImportAction::handle()   D
last analyzed

Complexity

Conditions 20
Paths 88

Size

Total Lines 117
Code Lines 65

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 20
eloc 65
nc 88
nop 1
dl 0
loc 117
rs 4.1666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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