%PDF- %PDF-
| Direktori : /www/varak.net/nextcloud.varak.net/nextcloud/apps/photos/lib/Service/ |
| Current File : //www/varak.net/nextcloud.varak.net/nextcloud/apps/photos/lib/Service/ReverseGeoCoderService.php |
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Photos\Service;
use Hexogen\KDTree\FSKDTree;
use Hexogen\KDTree\FSTreePersister;
use Hexogen\KDTree\Item;
use Hexogen\KDTree\ItemFactory;
use Hexogen\KDTree\ItemList;
use Hexogen\KDTree\KDTree;
use Hexogen\KDTree\NearestSearch;
use Hexogen\KDTree\Point;
use OCA\Photos\AppInfo\Application;
use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFolder;
use OCP\Http\Client\IClientService;
use OCP\IConfig;
class ReverseGeoCoderService {
public const CONFIG_DISABLE_PLACES = 'disable_places';
private ?ISimpleFolder $geoNameFolderCache = null;
private ?NearestSearch $fsSearcher = null;
/** @var array<int, string> */
private ?array $citiesMapping = null;
public function __construct(
private IAppData $appData,
private IClientService $clientService,
private IConfig $config,
) {
}
public function getPlaceForCoordinates(float $latitude, float $longitude): string {
$this->loadKdTree();
$result = $this->fsSearcher->search(new Point([$latitude, $longitude]), 1);
return $this->getPlaceNameForPlaceId($result[0]->getId());
}
private function geoNameFolder(): ISimpleFolder {
if ($this->geoNameFolderCache === null) {
try {
$this->geoNameFolderCache = $this->appData->getFolder("geonames");
} catch (NotFoundException $ex) {
$this->geoNameFolderCache = $this->appData->newFolder("geonames");
}
}
return $this->geoNameFolderCache;
}
private function getPlaceNameForPlaceId(int $placeId): string {
if ($this->citiesMapping === null) {
$this->downloadCities1000();
$cities1000 = $this->loadCities1000();
$this->citiesMapping = [];
foreach ($cities1000 as $city) {
$this->citiesMapping[$city['id']] = $city['name'];
}
}
return $this->citiesMapping[$placeId];
}
public function arePlacesEnabled(): bool {
return ($this->config->getAppValue(Application::APP_ID, self::CONFIG_DISABLE_PLACES, '0') !== '1');
}
private function downloadCities1000(bool $force = false): void {
if (!$this->arePlacesEnabled() || ($this->geoNameFolder()->fileExists('cities1000.csv') && !$force)) {
return;
}
// Download zip file to a tmp file.
$response = $this->clientService->newClient()->get("https://download.nextcloud.com/server/apps/photos/cities1000.zip");
$tmpFile = tmpfile();
$cities1000ZipTmpFileName = stream_get_meta_data($tmpFile)['uri'];
fclose($tmpFile);
file_put_contents($cities1000ZipTmpFileName, $response->getBody());
// Unzip the txt file into a stream.
$zip = new \ZipArchive;
$res = $zip->open($cities1000ZipTmpFileName);
if ($res !== true) {
throw new \Exception("Fail to unzip place file: $res", $res);
}
$cities1000TxtSteam = $zip->getStream('cities1000.txt');
// Dump the txt file info into a smaller csv file.
$destinationStream = $this->geoNameFolder()->newFile('cities1000.csv')->write();
while (($fields = fgetcsv($cities1000TxtSteam, 0, " ")) !== false) {
$result = fputcsv(
$destinationStream,
[
'id' => (int)$fields[0],
'name' => $fields[1],
'latitude' => (float)$fields[4],
'longitude' => (float)$fields[5],
]
);
if ($result === false) {
throw new \Exception('Failed to write csv line to tmp stream');
}
}
$zip->close();
}
private function loadCities1000(): array {
$csvStream = $this->geoNameFolder()->getFile('cities1000.csv')->read();
$cities = [];
while (($fields = fgetcsv($csvStream)) !== false) {
$cities[] = [
'id' => (int)$fields[0],
'name' => $fields[1],
'latitude' => (float)$fields[2],
'longitude' => (float)$fields[3],
];
}
return $cities;
}
public function buildKDTree($force = false): void {
if ($this->geoNameFolder()->fileExists('cities1000.bin') && !$force) {
return;
}
$this->downloadCities1000($force);
$cities1000 = $this->loadCities1000();
$itemList = new ItemList(2);
foreach ($cities1000 as $city) {
$itemList->addItem(new Item($city['id'], [$city['latitude'], $city['longitude']]));
}
$tree = new KDTree($itemList);
// Persiste KDTree in app data.
$persister = new FSTreePersister('/');
$kdTreeTmpFileName = tempnam(sys_get_temp_dir(), "nextcloud_photos_");
$persister->convert($tree, $kdTreeTmpFileName);
$kdTreeString = file_get_contents($kdTreeTmpFileName);
$this->geoNameFolder()->newFile('cities1000.bin', $kdTreeString);
unlink($kdTreeTmpFileName);
}
private function loadKdTree(): void {
if ($this->fsSearcher !== null) {
return;
}
$this->buildKDTree();
$kdTreeFileContent = $this->geoNameFolder()->getFile("cities1000.bin")->getContent();
$kdTreeTmpFileName = tempnam(sys_get_temp_dir(), "nextcloud_photos_");
file_put_contents($kdTreeTmpFileName, $kdTreeFileContent);
$fsTree = new FSKDTree($kdTreeTmpFileName, new ItemFactory());
$this->fsSearcher = new NearestSearch($fsTree);
unlink($kdTreeTmpFileName);
}
}