xref: /webtrees/app/Http/RequestHandlers/MapDataImportAction.php (revision 5bfc689774bb9a6401271c4ed15a6d50652c991b)
190949315SGreg Roach<?php
290949315SGreg Roach
390949315SGreg Roach/**
490949315SGreg Roach * webtrees: online genealogy
5*5bfc6897SGreg Roach * Copyright (C) 2022 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;
23b5c53c7fSGreg Roachuse Fisharebest\Webtrees\Exceptions\FileUploadException;
2490949315SGreg Roachuse Fisharebest\Webtrees\FlashMessages;
2590949315SGreg Roachuse Fisharebest\Webtrees\Gedcom;
2690949315SGreg Roachuse Fisharebest\Webtrees\I18N;
2790949315SGreg Roachuse Fisharebest\Webtrees\PlaceLocation;
2890949315SGreg Roachuse Fisharebest\Webtrees\Registry;
2990949315SGreg Roachuse Fisharebest\Webtrees\Services\MapDataService;
3090949315SGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
31f0448b68SGreg Roachuse League\Flysystem\FilesystemException;
32f0448b68SGreg Roachuse League\Flysystem\UnableToCheckFileExistence;
33f0448b68SGreg Roachuse League\Flysystem\UnableToReadFile;
3490949315SGreg Roachuse Psr\Http\Message\ResponseInterface;
3590949315SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
3690949315SGreg Roachuse Psr\Http\Message\UploadedFileInterface;
3790949315SGreg Roachuse Psr\Http\Server\RequestHandlerInterface;
3890949315SGreg Roach
3990949315SGreg Roachuse function array_filter;
4090949315SGreg Roachuse function array_reverse;
4190949315SGreg Roachuse function array_slice;
4290949315SGreg Roachuse function count;
4390949315SGreg Roachuse function fclose;
4490949315SGreg Roachuse function fgetcsv;
4590949315SGreg Roachuse function implode;
4690949315SGreg Roachuse function is_numeric;
4790949315SGreg Roachuse function json_decode;
4890949315SGreg Roachuse function redirect;
4990949315SGreg Roachuse function rewind;
5090949315SGreg Roachuse function route;
5190949315SGreg Roachuse function str_contains;
5290949315SGreg Roachuse function stream_get_contents;
5390949315SGreg Roach
5408b5db2aSGreg Roachuse const JSON_THROW_ON_ERROR;
5590949315SGreg Roachuse const UPLOAD_ERR_OK;
5690949315SGreg Roach
5790949315SGreg Roach/**
5890949315SGreg Roach * Import geographic data.
5990949315SGreg Roach */
6090949315SGreg Roachclass MapDataImportAction implements RequestHandlerInterface
6190949315SGreg Roach{
6290949315SGreg Roach    /**
6390949315SGreg Roach     * This function assumes the input file layout is
6490949315SGreg Roach     * level followed by a variable number of placename fields
6590949315SGreg Roach     * followed by Longitude, Latitude, Zoom & Icon
6690949315SGreg Roach     *
6790949315SGreg Roach     * @param ServerRequestInterface $request
6890949315SGreg Roach     *
6990949315SGreg Roach     * @return ResponseInterface
7090949315SGreg Roach     * @throws Exception
7190949315SGreg Roach     */
7290949315SGreg Roach    public function handle(ServerRequestInterface $request): ResponseInterface
7390949315SGreg Roach    {
7490949315SGreg Roach        $data_filesystem = Registry::filesystem()->data();
7590949315SGreg Roach
7690949315SGreg Roach        $params = (array) $request->getParsedBody();
7790949315SGreg Roach
7890949315SGreg Roach        $serverfile     = $params['serverfile'] ?? '';
7990949315SGreg Roach        $options        = $params['import-options'] ?? '';
8090949315SGreg Roach        $local_file     = $request->getUploadedFiles()['localfile'] ?? null;
8190949315SGreg Roach
8290949315SGreg Roach        $places = [];
8390949315SGreg Roach
8490949315SGreg Roach        $url = route(MapDataList::class, ['parent_id' => 0]);
8590949315SGreg Roach
8690949315SGreg Roach        $fp = false;
8790949315SGreg Roach
88f0448b68SGreg Roach        try {
89f0448b68SGreg Roach            $file_exists = $data_filesystem->fileExists(MapDataService::PLACES_FOLDER . $serverfile);
90f0448b68SGreg Roach        } catch (FilesystemException | UnableToCheckFileExistence $ex) {
91f0448b68SGreg Roach            $file_exists = false;
92f0448b68SGreg Roach        }
93f0448b68SGreg Roach
94f0448b68SGreg Roach
95f0448b68SGreg Roach        if ($serverfile !== '' && $file_exists) {
9690949315SGreg Roach            // first choice is file on server
97f0448b68SGreg Roach            try {
9890949315SGreg Roach                $fp = $data_filesystem->readStream(MapDataService::PLACES_FOLDER . $serverfile);
99f0448b68SGreg Roach            } catch (FilesystemException | UnableToReadFile $ex) {
100f0448b68SGreg Roach                $fp = false;
101f0448b68SGreg Roach            }
10290949315SGreg Roach        } elseif ($local_file instanceof UploadedFileInterface && $local_file->getError() === UPLOAD_ERR_OK) {
10390949315SGreg Roach            // 2nd choice is local file
10490949315SGreg Roach            $fp = $local_file->getStream()->detach();
105b5c53c7fSGreg Roach        } else {
106b5c53c7fSGreg Roach            throw new FileUploadException($local_file);
10790949315SGreg Roach        }
10890949315SGreg Roach
10990949315SGreg Roach        if ($fp === false || $fp === null) {
11090949315SGreg Roach            return redirect($url);
11190949315SGreg Roach        }
11290949315SGreg Roach
11390949315SGreg Roach        $string = stream_get_contents($fp);
11490949315SGreg Roach
11590949315SGreg Roach        // Check the file type
11690949315SGreg Roach        if (str_contains($string, 'FeatureCollection')) {
11708b5db2aSGreg Roach            $input_array = json_decode($string, false, 512, JSON_THROW_ON_ERROR);
11890949315SGreg Roach
11990949315SGreg Roach            foreach ($input_array->features as $feature) {
12090949315SGreg Roach                $places[] = [
12190949315SGreg Roach                    'latitude'  => $feature->geometry->coordinates[1],
12290949315SGreg Roach                    'longitude' => $feature->geometry->coordinates[0],
12390949315SGreg Roach                    'name'      => $feature->properties->name,
12490949315SGreg Roach                ];
12590949315SGreg Roach            }
12690949315SGreg Roach        } else {
12790949315SGreg Roach            rewind($fp);
12890949315SGreg Roach            while (($row = fgetcsv($fp, 0, MapDataService::CSV_SEPARATOR)) !== false) {
12990949315SGreg Roach                // Skip the header
13090949315SGreg Roach                if (!is_numeric($row[0])) {
13190949315SGreg Roach                    continue;
13290949315SGreg Roach                }
13390949315SGreg Roach
13490949315SGreg Roach                $level = (int) $row[0];
13590949315SGreg Roach                $count = count($row);
13690949315SGreg Roach                $name  = implode(Gedcom::PLACE_SEPARATOR, array_reverse(array_slice($row, 1, 1 + $level)));
13790949315SGreg Roach
13890949315SGreg Roach                $places[] = [
13990949315SGreg Roach                    'latitude'  => (float) strtr($row[$count - 3], ['N' => '', 'S' => '-', ',' => '.']),
14090949315SGreg Roach                    'longitude' => (float) strtr($row[$count - 4], ['E' => '', 'W' => '-', ',' => '.']),
14190949315SGreg Roach                    'name'      => $name
14290949315SGreg Roach                ];
14390949315SGreg Roach            }
14490949315SGreg Roach        }
14590949315SGreg Roach
14690949315SGreg Roach        fclose($fp);
14790949315SGreg Roach
14890949315SGreg Roach        $added   = 0;
14990949315SGreg Roach        $updated = 0;
15090949315SGreg Roach
15190949315SGreg Roach        // Remove places with 0,0 coordinates at lower levels.
1524c78e066SGreg Roach        $callback = static fn (array $place): bool => !str_contains($place['name'], ',') || $place['longitude'] !== 0.0 || $place['latitude'] !== 0.0;
1534c78e066SGreg Roach
1544c78e066SGreg Roach        $places = array_filter($places, $callback);
15590949315SGreg Roach
15690949315SGreg Roach        foreach ($places as $place) {
15790949315SGreg Roach            $location = new PlaceLocation($place['name']);
15890949315SGreg Roach            $exists   = $location->exists();
15990949315SGreg Roach
16090949315SGreg Roach            // Only update existing records
16190949315SGreg Roach            if ($options === 'update' && !$exists) {
16290949315SGreg Roach                continue;
16390949315SGreg Roach            }
16490949315SGreg Roach
16590949315SGreg Roach            // Only add new records
16690949315SGreg Roach            if ($options === 'add' && $exists) {
16790949315SGreg Roach                continue;
16890949315SGreg Roach            }
16990949315SGreg Roach
17090949315SGreg Roach            if (!$exists) {
17190949315SGreg Roach                $added++;
17290949315SGreg Roach            }
17390949315SGreg Roach
17490949315SGreg Roach            $updated += DB::table('place_location')
17590949315SGreg Roach                ->where('id', '=', $location->id())
17690949315SGreg Roach                ->update([
17790949315SGreg Roach                    'latitude'  => $place['latitude'],
17890949315SGreg Roach                    'longitude' => $place['longitude'],
17990949315SGreg Roach                ]);
18090949315SGreg Roach        }
18190949315SGreg Roach
18290949315SGreg Roach        FlashMessages::addMessage(
18390949315SGreg Roach            I18N::translate('locations updated: %s, locations added: %s', I18N::number($updated), I18N::number($added)),
18490949315SGreg Roach            'info'
18590949315SGreg Roach        );
18690949315SGreg Roach
18790949315SGreg Roach        return redirect($url);
18890949315SGreg Roach    }
18990949315SGreg Roach}
190