xref: /webtrees/app/Http/RequestHandlers/MapDataImportAction.php (revision 4c78e066e3ba48f4c71bb900fac88b9e85e97474)
190949315SGreg Roach<?php
290949315SGreg Roach
390949315SGreg Roach/**
490949315SGreg Roach * webtrees: online genealogy
590949315SGreg Roach * Copyright (C) 2021 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;
2390949315SGreg Roachuse Fisharebest\Webtrees\FlashMessages;
2490949315SGreg Roachuse Fisharebest\Webtrees\Gedcom;
2590949315SGreg Roachuse Fisharebest\Webtrees\I18N;
2690949315SGreg Roachuse Fisharebest\Webtrees\PlaceLocation;
2790949315SGreg Roachuse Fisharebest\Webtrees\Registry;
2890949315SGreg Roachuse Fisharebest\Webtrees\Services\MapDataService;
2990949315SGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
30f0448b68SGreg Roachuse League\Flysystem\FilesystemException;
31f0448b68SGreg Roachuse League\Flysystem\UnableToCheckFileExistence;
32f0448b68SGreg Roachuse League\Flysystem\UnableToReadFile;
3390949315SGreg Roachuse Psr\Http\Message\ResponseInterface;
3490949315SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
3590949315SGreg Roachuse Psr\Http\Message\UploadedFileInterface;
3690949315SGreg Roachuse Psr\Http\Server\RequestHandlerInterface;
3790949315SGreg Roach
3890949315SGreg Roachuse function array_filter;
3990949315SGreg Roachuse function array_reverse;
4090949315SGreg Roachuse function array_slice;
4190949315SGreg Roachuse function count;
4290949315SGreg Roachuse function fclose;
4390949315SGreg Roachuse function fgetcsv;
4490949315SGreg Roachuse function implode;
4590949315SGreg Roachuse function is_numeric;
4690949315SGreg Roachuse function json_decode;
4790949315SGreg Roachuse function redirect;
4890949315SGreg Roachuse function rewind;
4990949315SGreg Roachuse function route;
5090949315SGreg Roachuse function str_contains;
5190949315SGreg Roachuse function stream_get_contents;
5290949315SGreg Roach
5308b5db2aSGreg Roachuse const JSON_THROW_ON_ERROR;
5490949315SGreg Roachuse const UPLOAD_ERR_OK;
5590949315SGreg Roach
5690949315SGreg Roach/**
5790949315SGreg Roach * Import geographic data.
5890949315SGreg Roach */
5990949315SGreg Roachclass MapDataImportAction implements RequestHandlerInterface
6090949315SGreg Roach{
61598bb5b5SGreg Roach    private MapDataService $map_data_service;
62598bb5b5SGreg Roach
63598bb5b5SGreg Roach    /**
64598bb5b5SGreg Roach     * MapDataImportAction constructor.
65598bb5b5SGreg Roach     *
66598bb5b5SGreg Roach     * @param MapDataService $map_data_service
67598bb5b5SGreg Roach     */
68598bb5b5SGreg Roach    public function __construct(MapDataService $map_data_service)
69598bb5b5SGreg Roach    {
70598bb5b5SGreg Roach        $this->map_data_service = $map_data_service;
71598bb5b5SGreg Roach    }
72598bb5b5SGreg Roach
7390949315SGreg Roach    /**
7490949315SGreg Roach     * This function assumes the input file layout is
7590949315SGreg Roach     * level followed by a variable number of placename fields
7690949315SGreg Roach     * followed by Longitude, Latitude, Zoom & Icon
7790949315SGreg Roach     *
7890949315SGreg Roach     * @param ServerRequestInterface $request
7990949315SGreg Roach     *
8090949315SGreg Roach     * @return ResponseInterface
8190949315SGreg Roach     * @throws Exception
8290949315SGreg Roach     */
8390949315SGreg Roach    public function handle(ServerRequestInterface $request): ResponseInterface
8490949315SGreg Roach    {
8590949315SGreg Roach        $data_filesystem = Registry::filesystem()->data();
8690949315SGreg Roach
8790949315SGreg Roach        $params = (array) $request->getParsedBody();
8890949315SGreg Roach
8990949315SGreg Roach        $serverfile     = $params['serverfile'] ?? '';
9090949315SGreg Roach        $options        = $params['import-options'] ?? '';
9190949315SGreg Roach        $local_file     = $request->getUploadedFiles()['localfile'] ?? null;
9290949315SGreg Roach
9390949315SGreg Roach        $places = [];
9490949315SGreg Roach
9590949315SGreg Roach        $url = route(MapDataList::class, ['parent_id' => 0]);
9690949315SGreg Roach
9790949315SGreg Roach        $fp = false;
9890949315SGreg Roach
99f0448b68SGreg Roach        try {
100f0448b68SGreg Roach            $file_exists = $data_filesystem->fileExists(MapDataService::PLACES_FOLDER . $serverfile);
101f0448b68SGreg Roach        } catch (FilesystemException | UnableToCheckFileExistence $ex) {
102f0448b68SGreg Roach            $file_exists = false;
103f0448b68SGreg Roach        }
104f0448b68SGreg Roach
105f0448b68SGreg Roach
106f0448b68SGreg Roach        if ($serverfile !== '' && $file_exists) {
10790949315SGreg Roach            // first choice is file on server
108f0448b68SGreg Roach            try {
10990949315SGreg Roach                $fp = $data_filesystem->readStream(MapDataService::PLACES_FOLDER . $serverfile);
110f0448b68SGreg Roach            } catch (FilesystemException | UnableToReadFile $ex) {
111f0448b68SGreg Roach                $fp = false;
112f0448b68SGreg Roach            }
11390949315SGreg Roach        } elseif ($local_file instanceof UploadedFileInterface && $local_file->getError() === UPLOAD_ERR_OK) {
11490949315SGreg Roach            // 2nd choice is local file
11590949315SGreg Roach            $fp = $local_file->getStream()->detach();
11690949315SGreg Roach        }
11790949315SGreg Roach
11890949315SGreg Roach        if ($fp === false || $fp === null) {
11990949315SGreg Roach            return redirect($url);
12090949315SGreg Roach        }
12190949315SGreg Roach
12290949315SGreg Roach        $string = stream_get_contents($fp);
12390949315SGreg Roach
12490949315SGreg Roach        // Check the file type
12590949315SGreg Roach        if (str_contains($string, 'FeatureCollection')) {
12608b5db2aSGreg Roach            $input_array = json_decode($string, false, 512, JSON_THROW_ON_ERROR);
12790949315SGreg Roach
12890949315SGreg Roach            foreach ($input_array->features as $feature) {
12990949315SGreg Roach                $places[] = [
13090949315SGreg Roach                    'latitude'  => $feature->geometry->coordinates[1],
13190949315SGreg Roach                    'longitude' => $feature->geometry->coordinates[0],
13290949315SGreg Roach                    'name'      => $feature->properties->name,
13390949315SGreg Roach                ];
13490949315SGreg Roach            }
13590949315SGreg Roach        } else {
13690949315SGreg Roach            rewind($fp);
13790949315SGreg Roach            while (($row = fgetcsv($fp, 0, MapDataService::CSV_SEPARATOR)) !== false) {
13890949315SGreg Roach                // Skip the header
13990949315SGreg Roach                if (!is_numeric($row[0])) {
14090949315SGreg Roach                    continue;
14190949315SGreg Roach                }
14290949315SGreg Roach
14390949315SGreg Roach                $level = (int) $row[0];
14490949315SGreg Roach                $count = count($row);
14590949315SGreg Roach                $name  = implode(Gedcom::PLACE_SEPARATOR, array_reverse(array_slice($row, 1, 1 + $level)));
14690949315SGreg Roach
14790949315SGreg Roach                $places[] = [
14890949315SGreg Roach                    'latitude'  => (float) strtr($row[$count - 3], ['N' => '', 'S' => '-', ',' => '.']),
14990949315SGreg Roach                    'longitude' => (float) strtr($row[$count - 4], ['E' => '', 'W' => '-', ',' => '.']),
15090949315SGreg Roach                    'name'      => $name
15190949315SGreg Roach                ];
15290949315SGreg Roach            }
15390949315SGreg Roach        }
15490949315SGreg Roach
15590949315SGreg Roach        fclose($fp);
15690949315SGreg Roach
15790949315SGreg Roach        $added   = 0;
15890949315SGreg Roach        $updated = 0;
15990949315SGreg Roach
16090949315SGreg Roach        // Remove places with 0,0 coordinates at lower levels.
161*4c78e066SGreg Roach        $callback = static fn (array $place): bool => !str_contains($place['name'], ',') || $place['longitude'] !== 0.0 || $place['latitude'] !== 0.0;
162*4c78e066SGreg Roach
163*4c78e066SGreg Roach        $places = array_filter($places, $callback);
16490949315SGreg Roach
16590949315SGreg Roach        foreach ($places as $place) {
16690949315SGreg Roach            $location = new PlaceLocation($place['name']);
16790949315SGreg Roach            $exists   = $location->exists();
16890949315SGreg Roach
16990949315SGreg Roach            // Only update existing records
17090949315SGreg Roach            if ($options === 'update' && !$exists) {
17190949315SGreg Roach                continue;
17290949315SGreg Roach            }
17390949315SGreg Roach
17490949315SGreg Roach            // Only add new records
17590949315SGreg Roach            if ($options === 'add' && $exists) {
17690949315SGreg Roach                continue;
17790949315SGreg Roach            }
17890949315SGreg Roach
17990949315SGreg Roach            if (!$exists) {
18090949315SGreg Roach                $added++;
18190949315SGreg Roach            }
18290949315SGreg Roach
18390949315SGreg Roach            $updated += DB::table('place_location')
18490949315SGreg Roach                ->where('id', '=', $location->id())
18590949315SGreg Roach                ->update([
18690949315SGreg Roach                    'latitude'  => $place['latitude'],
18790949315SGreg Roach                    'longitude' => $place['longitude'],
18890949315SGreg Roach                ]);
18990949315SGreg Roach        }
19090949315SGreg Roach
19190949315SGreg Roach        FlashMessages::addMessage(
19290949315SGreg Roach            I18N::translate('locations updated: %s, locations added: %s', I18N::number($updated), I18N::number($added)),
19390949315SGreg Roach            'info'
19490949315SGreg Roach        );
19590949315SGreg Roach
19690949315SGreg Roach        return redirect($url);
19790949315SGreg Roach    }
19890949315SGreg Roach}
199