xref: /webtrees/app/Http/RequestHandlers/MapDataImportAction.php (revision 6f4ec3cadc983f0a7294108c634bef48846b4311)
190949315SGreg Roach<?php
290949315SGreg Roach
390949315SGreg Roach/**
490949315SGreg Roach * webtrees: online genealogy
5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team
690949315SGreg Roach * This program is free software: you can redistribute it and/or modify
790949315SGreg Roach * it under the terms of the GNU General Public License as published by
890949315SGreg Roach * the Free Software Foundation, either version 3 of the License, or
990949315SGreg Roach * (at your option) any later version.
1090949315SGreg Roach * This program is distributed in the hope that it will be useful,
1190949315SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
1290949315SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1390949315SGreg Roach * GNU General Public License for more details.
1490949315SGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
1690949315SGreg Roach */
1790949315SGreg Roach
1890949315SGreg Roachdeclare(strict_types=1);
1990949315SGreg Roach
2090949315SGreg Roachnamespace Fisharebest\Webtrees\Http\RequestHandlers;
2190949315SGreg Roach
2290949315SGreg Roachuse Exception;
23*6f4ec3caSGreg Roachuse Fisharebest\Webtrees\DB;
24b5c53c7fSGreg Roachuse Fisharebest\Webtrees\Exceptions\FileUploadException;
2590949315SGreg Roachuse Fisharebest\Webtrees\FlashMessages;
2690949315SGreg Roachuse Fisharebest\Webtrees\Gedcom;
2790949315SGreg Roachuse Fisharebest\Webtrees\I18N;
2890949315SGreg Roachuse Fisharebest\Webtrees\PlaceLocation;
2990949315SGreg Roachuse Fisharebest\Webtrees\Registry;
3090949315SGreg Roachuse Fisharebest\Webtrees\Services\MapDataService;
316c5ec5a2SGreg Roachuse Fisharebest\Webtrees\Validator;
3290949315SGreg Roachuse Psr\Http\Message\ResponseInterface;
3390949315SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
346c5ec5a2SGreg Roachuse Psr\Http\Message\StreamFactoryInterface;
3590949315SGreg Roachuse Psr\Http\Server\RequestHandlerInterface;
3690949315SGreg Roach
3790949315SGreg Roachuse function array_filter;
3890949315SGreg Roachuse function array_reverse;
3990949315SGreg Roachuse function array_slice;
4090949315SGreg Roachuse function count;
4190949315SGreg Roachuse function fclose;
4290949315SGreg Roachuse function fgetcsv;
4390949315SGreg Roachuse function implode;
4490949315SGreg Roachuse function is_numeric;
4590949315SGreg Roachuse function json_decode;
4690949315SGreg Roachuse function redirect;
4790949315SGreg Roachuse function rewind;
4890949315SGreg Roachuse function route;
4990949315SGreg Roachuse function str_contains;
5090949315SGreg Roachuse function stream_get_contents;
5190949315SGreg Roach
5208b5db2aSGreg Roachuse const JSON_THROW_ON_ERROR;
536c5ec5a2SGreg Roachuse const UPLOAD_ERR_NO_FILE;
5490949315SGreg Roachuse const UPLOAD_ERR_OK;
5590949315SGreg Roach
5690949315SGreg Roach/**
5790949315SGreg Roach * Import geographic data.
5890949315SGreg Roach */
5990949315SGreg Roachclass MapDataImportAction implements RequestHandlerInterface
6090949315SGreg Roach{
616c5ec5a2SGreg Roach    private StreamFactoryInterface $stream_factory;
626c5ec5a2SGreg Roach
636c5ec5a2SGreg Roach    /**
646c5ec5a2SGreg Roach     * @param StreamFactoryInterface $stream_factory
656c5ec5a2SGreg Roach     */
666c5ec5a2SGreg Roach    public function __construct(StreamFactoryInterface $stream_factory)
676c5ec5a2SGreg Roach    {
686c5ec5a2SGreg Roach        $this->stream_factory = $stream_factory;
696c5ec5a2SGreg Roach    }
706c5ec5a2SGreg Roach
7190949315SGreg Roach    /**
7290949315SGreg Roach     * This function assumes the input file layout is
7390949315SGreg Roach     * level followed by a variable number of placename fields
7490949315SGreg Roach     * followed by Longitude, Latitude, Zoom & Icon
7590949315SGreg Roach     *
7690949315SGreg Roach     * @param ServerRequestInterface $request
7790949315SGreg Roach     *
7890949315SGreg Roach     * @return ResponseInterface
7990949315SGreg Roach     * @throws Exception
8090949315SGreg Roach     */
8190949315SGreg Roach    public function handle(ServerRequestInterface $request): ResponseInterface
8290949315SGreg Roach    {
836c5ec5a2SGreg Roach        $source  = Validator::parsedBody($request)->isInArray(['client', 'server'])->string('source');
846c5ec5a2SGreg Roach        $options = Validator::parsedBody($request)->isInArray(['add', 'addupdate', 'update'])->string('options');
8590949315SGreg Roach
8690949315SGreg Roach        $places = [];
8790949315SGreg Roach        $url    = route(MapDataList::class, ['parent_id' => 0]);
886c5ec5a2SGreg Roach        $fp     = null;
8990949315SGreg Roach
906c5ec5a2SGreg Roach        if ($source === 'client') {
916c5ec5a2SGreg Roach            $client_file = $request->getUploadedFiles()['client_file'] ?? null;
926c5ec5a2SGreg Roach
936c5ec5a2SGreg Roach            if ($client_file === null || $client_file->getError() === UPLOAD_ERR_NO_FILE) {
946c5ec5a2SGreg Roach                FlashMessages::addMessage(I18N::translate('No file was received.'), 'danger');
956c5ec5a2SGreg Roach
966c5ec5a2SGreg Roach                return redirect(route(MapDataImportPage::class));
97f0448b68SGreg Roach            }
98f0448b68SGreg Roach
996c5ec5a2SGreg Roach            if ($client_file->getError() !== UPLOAD_ERR_OK) {
1006c5ec5a2SGreg Roach                throw new FileUploadException($client_file);
10190949315SGreg Roach            }
10290949315SGreg Roach
1036c5ec5a2SGreg Roach            $fp = $client_file->getStream()->detach();
1046c5ec5a2SGreg Roach        }
1056c5ec5a2SGreg Roach
1066c5ec5a2SGreg Roach        if ($source === 'server') {
1076c5ec5a2SGreg Roach            $server_file = Validator::parsedBody($request)->string('server_file');
1086c5ec5a2SGreg Roach
1096c5ec5a2SGreg Roach            if ($server_file === '') {
1106c5ec5a2SGreg Roach                FlashMessages::addMessage(I18N::translate('No file was received.'), 'danger');
1116c5ec5a2SGreg Roach
1126c5ec5a2SGreg Roach                return redirect(route(MapDataImportPage::class));
1136c5ec5a2SGreg Roach            }
1146c5ec5a2SGreg Roach
1156c5ec5a2SGreg Roach            $resource = Registry::filesystem()->data()->readStream('places/' . $server_file);
1166c5ec5a2SGreg Roach            $fp       = $this->stream_factory->createStreamFromResource($resource)->detach();
1176c5ec5a2SGreg Roach        }
1186c5ec5a2SGreg Roach
1196c5ec5a2SGreg Roach        if ($fp === null) {
1206c5ec5a2SGreg Roach            return redirect(route(MapDataImportPage::class));
12190949315SGreg Roach        }
12290949315SGreg Roach
12390949315SGreg Roach        $string = stream_get_contents($fp);
12490949315SGreg Roach
12590949315SGreg Roach        // Check the file type
12690949315SGreg Roach        if (str_contains($string, 'FeatureCollection')) {
12708b5db2aSGreg Roach            $input_array = json_decode($string, false, 512, JSON_THROW_ON_ERROR);
12890949315SGreg Roach
12990949315SGreg Roach            foreach ($input_array->features as $feature) {
13090949315SGreg Roach                $places[] = [
13190949315SGreg Roach                    'latitude'  => $feature->geometry->coordinates[1],
13290949315SGreg Roach                    'longitude' => $feature->geometry->coordinates[0],
13390949315SGreg Roach                    'name'      => $feature->properties->name,
13490949315SGreg Roach                ];
13590949315SGreg Roach            }
13690949315SGreg Roach        } else {
13790949315SGreg Roach            rewind($fp);
13890949315SGreg Roach            while (($row = fgetcsv($fp, 0, MapDataService::CSV_SEPARATOR)) !== false) {
13990949315SGreg Roach                // Skip the header
14090949315SGreg Roach                if (!is_numeric($row[0])) {
14190949315SGreg Roach                    continue;
14290949315SGreg Roach                }
14390949315SGreg Roach
14490949315SGreg Roach                $level = (int) $row[0];
14590949315SGreg Roach                $count = count($row);
14690949315SGreg Roach                $name  = implode(Gedcom::PLACE_SEPARATOR, array_reverse(array_slice($row, 1, 1 + $level)));
14790949315SGreg Roach
14890949315SGreg Roach                $places[] = [
14990949315SGreg Roach                    'latitude'  => (float) strtr($row[$count - 3], ['N' => '', 'S' => '-', ',' => '.']),
15090949315SGreg Roach                    'longitude' => (float) strtr($row[$count - 4], ['E' => '', 'W' => '-', ',' => '.']),
15190949315SGreg Roach                    'name'      => $name
15290949315SGreg Roach                ];
15390949315SGreg Roach            }
15490949315SGreg Roach        }
15590949315SGreg Roach
15690949315SGreg Roach        fclose($fp);
15790949315SGreg Roach
15890949315SGreg Roach        $added   = 0;
15990949315SGreg Roach        $updated = 0;
16090949315SGreg Roach
16190949315SGreg Roach        // Remove places with 0,0 coordinates at lower levels.
1624c78e066SGreg Roach        $callback = static fn (array $place): bool => !str_contains($place['name'], ',') || $place['longitude'] !== 0.0 || $place['latitude'] !== 0.0;
1634c78e066SGreg Roach
1644c78e066SGreg Roach        $places = array_filter($places, $callback);
16590949315SGreg Roach
16690949315SGreg Roach        foreach ($places as $place) {
16790949315SGreg Roach            $location = new PlaceLocation($place['name']);
16890949315SGreg Roach            $exists   = $location->exists();
16990949315SGreg Roach
17090949315SGreg Roach            // Only update existing records
17190949315SGreg Roach            if ($options === 'update' && !$exists) {
17290949315SGreg Roach                continue;
17390949315SGreg Roach            }
17490949315SGreg Roach
17590949315SGreg Roach            // Only add new records
17690949315SGreg Roach            if ($options === 'add' && $exists) {
17790949315SGreg Roach                continue;
17890949315SGreg Roach            }
17990949315SGreg Roach
18090949315SGreg Roach            if (!$exists) {
18190949315SGreg Roach                $added++;
18290949315SGreg Roach            }
18390949315SGreg Roach
18490949315SGreg Roach            $updated += DB::table('place_location')
18590949315SGreg Roach                ->where('id', '=', $location->id())
18690949315SGreg Roach                ->update([
18790949315SGreg Roach                    'latitude'  => $place['latitude'],
18890949315SGreg Roach                    'longitude' => $place['longitude'],
18990949315SGreg Roach                ]);
19090949315SGreg Roach        }
19190949315SGreg Roach
19290949315SGreg Roach        FlashMessages::addMessage(
19390949315SGreg Roach            I18N::translate('locations updated: %s, locations added: %s', I18N::number($updated), I18N::number($added)),
19490949315SGreg Roach            'info'
19590949315SGreg Roach        );
19690949315SGreg Roach
19790949315SGreg Roach        return redirect($url);
19890949315SGreg Roach    }
19990949315SGreg Roach}
200