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;
}
}