HEX
Server: nginx/1.18.0
System: Linux test-ipsremont 5.4.0-214-generic #234-Ubuntu SMP Fri Mar 14 23:50:27 UTC 2025 x86_64
User: ips (1000)
PHP: 8.0.30
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: /var/www/elite/coordParser/CoordinateParserHelper.php
<?php

namespace coordParser;

use Exception;
use Google\Client;
use Google_Service_Sheets;
use Google_Service_Sheets_BatchUpdateValuesRequest;
use Google_Service_Sheets_ValueRange;

class CoordinateParserHelper
{

    // Необходимо указать так же как в ключе в Яндексе иначе не работает
    const REFERER = 'http://limestate-elite.ru';

    // Ключ Яндекс Геокодера
    const YA_API_KEY = 'e1c5022a-67f4-4f75-b9db-6caf3086d693';

    // ID документа
    const SPREADSHEET_ID = '1kyRI9uBu0Ljr3V1CT3oWog1n1bMq6POPkc7qqckKnnQ';

    // Название листа
    const RANGE = 'Лист22';

    private Google_Service_Sheets $service;

    private string $cacheFilename = __DIR__ . '/data.json';
    private array $coordinatesCache = [];

    private string $jsMapFilename = __DIR__ . '/../public/map.json';

    private array $table;

    private int $rowsCount = 0;
    private int $rowsUpdated = 0;

    /** Поля, которые пишутся в JSON с сопоставлением таблицы в Google */
    public array $fields = [
        'rightholder' => 'Правообладатель',
        'cadastral_number' => 'Кадастровый номер',
        'address' => 'Адрес',
        'area' => 'Площадь',
        'type' => 'Назначение',
        'name' => 'Наименование',
        'conservation_status' => 'Охранный статус',
        'cadastral_value' => 'Кадастровая стоимость',
        'cadastral_number_of_the_plot' => 'Кадастровый номер участка',
        'comment' => 'Комментарий',
        'choice' => 'Выбор',
        'longitude' => 'Долгота',
        'latitude' => 'Широта',
        'images' => 'Фото',
    ];

    public array $indices = [];

    private function getIndex(string $field): ?int
    {
        return $this->indices[$field] ?? null;
    }

    private function errorExit(string $message): void
    {
        echo $message . PHP_EOL;

        exit;
    }

    private function getJsonFieldName(int $colNum): string
    {
        $flippedIndices = array_flip($this->indices);
        $flippedFields = array_flip($this->fields);

        return $flippedFields[$flippedIndices[$colNum]] ?? '';
    }

    /** Создаёт класс работы с Google сервисами */
    private function createService(): Google_Service_Sheets
    {
        $client = new Client();
        $client->addScope(Google_Service_Sheets::SPREADSHEETS);
        $client->setAccessType('offline');
        $client->setAuthConfig(__DIR__ . '/credentials.json');

        return new Google_Service_Sheets($client);
    }

    /** Получает координаты по адресу */
    private function getCoordinatesFromGeocoder(string $address): array
    {
        if (empty($address)) {
            return ['longitude' => '', 'latitude' => ''];
        }

        print_r('Получаем координаты для адреса: «' . $address . '»' . PHP_EOL);

        $url = 'https://geocode-maps.yandex.ru/1.x/';
        $query = [
            'apikey' => self::YA_API_KEY,
            'geocode' => str_replace(' ', '+', mb_strtolower($address)),
            'format' => 'json',
        ];

        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $url . '?' . http_build_query($query));
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_REFERER, self::REFERER);
        curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);

        $response = curl_exec($curl);
        curl_close($curl);

        $response = json_decode($response, true);
        if (JSON_ERROR_NONE != json_last_error()) {
            $this->errorExit('Произошла ошибка получения данных');
        }

        if (isset($response['statusCode']) && 200 != $response['statusCode']) {
            $this->errorExit('Ошибка: ' . ($response['error'] ?? 'Неизвестная ошибка') . ' ' . ($response['message'] ?? ''));
        }

        $pos = $response['response']['GeoObjectCollection']['featureMember'][0]['GeoObject']['Point']['pos'] ?? null;
        if (empty($pos)) {
            $this->errorExit('В ответе нет координат');
        }

        [$longitude, $latitude] = explode(' ', $pos);

        return ['longitude' => $longitude, 'latitude' => $latitude];
    }

    public function __construct()
    {
        if (empty(self::REFERER)) {
            $this->errorExit('Необходимо заполнить REFERER');
        }
        if (empty(self::YA_API_KEY)) {
            $this->errorExit('Необходимо заполнить YA_API_KEY');
        }
        if (empty(self::SPREADSHEET_ID)) {
            $this->errorExit('Необходимо заполнить SPREADSHEET_ID');
        }
        if (empty(self::RANGE)) {
            $this->errorExit('Необходимо заполнить RANGE');
        }

        $this->service = $this->createService();
    }

    /** Собираем кеш координат */
    public function checkCoordinatesCache(): self
    {
        $this->coordinatesCache = [];
        if (file_exists($this->cacheFilename)) {
            $json = file_get_contents($this->cacheFilename);
            $json = json_decode($json, true);
            if (JSON_ERROR_NONE != json_last_error()) {
                $this->errorExit('Произошла ошибка чтения файла data.json данные не являются json');
            }

            foreach ($json as $item) {
                $this->coordinatesCache[mb_strtolower($item['address'])] = [
                    'longitude' => $item['longitude'],
                    'latitude' => $item['latitude'],
                ];
            }
        }

        return $this;
    }

    public function getTable(): array
    {
        return $this->table;
    }

    public function getRowsCount(): int
    {
        return $this->rowsCount;
    }

    public function getUpdatedCount(): int
    {
        return $this->rowsUpdated;
    }

    /** Получение данных из Google таблицы */
    public function get(): array
    {
        try {
            return $this->service->spreadsheets_values->get(self::SPREADSHEET_ID, self::RANGE)->getValues();
        } catch (Exception $exception) {
            $this->errorExit('Ошибка: ' . $exception->getMessage());
        }

        return [];
    }

    /** Парсинг таблицы для дельнейшего редактирования */
    public function parseTable(): self
    {
        $table = $this->get();

        $header = [];
        $this->table = [];
        $this->rowsCount = 0;

        // Формируем новые данные с координатами
        foreach ($table as $index => $row) {
            // Пропускаем первую строчку в таблице, она пустая (переделать если будем менять формат документа)
            if (0 === $index) {
                $this->table
                [] = [''];
                continue;
            }

            // Формируем переменную с заголовком и вычисляем индексы нужных колонок
            if (1 === $index) {
                $this->table[] = $row;
                $header = $row;
                foreach ($row as $colIndex => $col) {
                    $this->indices[$col] = $colIndex;
                }
                continue;
            }
            $this->rowsCount++;
            // Собираем новую строку с пустыми колонками если надо, иначе Google не хочет работать
            $newRow = [];
            foreach ($header as $colIndex => $col) {
                $newRow[$colIndex] = $row[$colIndex] ?? '';
            }
            $this->table[] = $newRow;
        }

        return $this;
    }

    /** Обновление координат */
    public function updateCoordinates(): self
    {
        foreach ($this->table as $rowNum => &$row) {
            if ($rowNum < 2) {
                continue;
            }

            // Проверяем есть ли в документе координаты, если нет то пытаемся получить из Геокодера Яндекс
            $address = mb_strtolower($row[$this->getIndex('Адрес')]);
            $lonIndex = $this->getIndex('Долгота');
            $latIndex = $this->getIndex('Широта');
            $lon = $row[$lonIndex];
            $lat = $row[$latIndex];
            if (empty($lon) || empty($lat)) {
                if (empty($this->coordinatesCache[$address])) {
                    $this->coordinatesCache[$address] = $this->getCoordinatesFromGeocoder($address);
                }
                if ($lon != $this->coordinatesCache[$address]['longitude'] || $lat != $this->coordinatesCache[$address]['latitude']) {
                    $this->rowsUpdated++;
                }

                $row[$lonIndex] = $this->coordinatesCache[$address]['longitude'];
                $row[$latIndex] = $this->coordinatesCache[$address]['latitude'];
            } elseif (floatval($lon) != $row[$lonIndex] || floatval($lat) != $row[$latIndex]) {
                $this->rowsUpdated++;
                $row[$lonIndex] = str_replace(',', '.', $lon);
                $row[$latIndex] = str_replace(',', '.', $lat);
            }
        }

        return $this;
    }

    /** Проверка на наличие записи в колонке $column по значению $value */
    private function hasField(string $value, string $column): bool
    {
        foreach ($this->table as $rowNum => $row) {
            if ($rowNum < 2) {
                continue;
            }
            if ($value !== $row[$this->indices[$column]]) {
                continue;
            }

            return true;
        }

        return false;
    }

    /** Проверка на наличие «Кадастрового номера» */
    public function hasCadastralNumber(string $cadastralNumber): bool
    {
        return $this->hasField($cadastralNumber, 'Кадастровый номер');
    }

    /** Проверка на наличие «Кадастрового номера участка» */
    public function hasCadastralNumberOfThePlot(string $cadastralNumberOfThePlot): bool
    {
        return $this->hasField($cadastralNumberOfThePlot, 'Кадастровый номер участка');
    }

    /** Меняем значение поля «Выбор» с указанием значения другого поля */
    private function setChoiceByField(string $value, string $column, bool $choice): void
    {
        foreach ($this->table as $rowNum => &$row) {
            if ($rowNum < 2) {
                continue;
            }
            if ($value !== $row[$this->indices[$column]]) {
                continue;
            }

            $choiceCol = $this->indices['Выбор'];
            $oldChoice = $row[$choiceCol];
            $newChoice = $choice ? 'да' : '';
            if ($oldChoice !== $newChoice) {
                $row[$this->indices['Выбор']] = $newChoice;
                $this->rowsUpdated++;
            }
        }
    }

    public function setChoiceByCadastralNumber(string $cadastralNumber, bool $choice): void
    {
        $this->setChoiceByField($cadastralNumber, 'Кадастровый номер', $choice);
    }

    public function setChoiceByCadastralNumberOfThePlot(string $cadastralNumberOfThePlot, bool $choice): void
    {
        $this->setChoiceByField($cadastralNumberOfThePlot, 'Кадастровый номер участка', $choice);
    }

    /** Отправка изменений в Google таблицу */
    public function commitUpdate(): self
    {
        if (empty($this->rowsUpdated) || empty($this->table)) {
            return $this;
        }

        $data[] = new Google_Service_Sheets_ValueRange(['range' => self::RANGE, 'values' => $this->table]);
        $body = new Google_Service_Sheets_BatchUpdateValuesRequest(['valueInputOption' => 'RAW', 'data' => $data]);

        $this->service->spreadsheets_values->batchUpdate(self::SPREADSHEET_ID, $body);
        $this->rowsUpdated = 0;

        return $this;
    }

    /** Создания кеш файла JSON и файла для карты */
    public function createJson(): self
    {
        $newTable = $this->table;
        array_shift($newTable);
        array_shift($newTable);
        $json = [];
        foreach ($newTable as $rowNum => $row) {
            foreach ($row as $colNum => $cell) {
                $fieldName = $this->getJsonFieldName($colNum);
                switch ($fieldName) {
                    case 'area':
                        $cell = empty($cell) ? null : floatval(str_replace(',', '.', $cell));
                        break;
                    case 'choice':
                        $cell = 'да' === mb_strtolower($cell);
                        break;
                    case 'longitude':
                    case 'latitude':
                        $cell = empty($cell) ? null : floatval($cell);
                        break;
                    case 'images':
                        $images = array_filter(explode(PHP_EOL, $cell));
                        $cell = [];
                        foreach ($images as $image) {
                            $cell[] = ['src' => $image];
                        }
                        break;
                    case '':
                        continue 2;
                }
                $json[$rowNum][$fieldName] = $cell;
            }
        }

        $json = json_encode(array_values($json), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
        file_put_contents($this->cacheFilename, $json);
        file_put_contents($this->jsMapFilename, $json);

        return $this;
    }

}