diff --git a/appinfo/routes.php b/appinfo/routes.php
index ce6bc98c..68496841 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -86,6 +86,7 @@ return [
['name' => 'Other#getSystemStatus', 'url' => '/api/system-status', 'verb' => 'GET'],
['name' => 'Other#getSystemConfig', 'url' => '/api/system-config', 'verb' => 'GET'],
['name' => 'Other#setSystemConfig', 'url' => '/api/system-config/{key}', 'verb' => 'PUT'],
+ ['name' => 'Other#placesSetup', 'url' => '/api/occ/places-setup', 'verb' => 'POST'],
// Service worker
['name' => 'Other#serviceWorker', 'url' => '/service-worker.js', 'verb' => 'GET'],
diff --git a/lib/BinExt.php b/lib/BinExt.php
index b25475c6..913c8025 100644
--- a/lib/BinExt.php
+++ b/lib/BinExt.php
@@ -243,7 +243,10 @@ class BinExt
try {
$client = new \GuzzleHttp\Client();
- $res = $client->request('GET', $url);
+ $res = $client->request('GET', $url, [
+ 'timeout' => 1,
+ 'connect_timeout' => 1,
+ ]);
} catch (\Exception $e) {
throw new \Exception('failed to connect to go-vod: '.$e->getMessage());
}
@@ -277,6 +280,8 @@ class BinExt
$client = new \GuzzleHttp\Client();
$client->request('POST', $url, [
'json' => $config,
+ 'timeout' => 1,
+ 'connect_timeout' => 1,
]);
} catch (\Exception $e) {
throw new \Exception('failed to connect to go-vod: '.$e->getMessage());
diff --git a/lib/Command/PlacesSetup.php b/lib/Command/PlacesSetup.php
index 84542fa4..0196588a 100644
--- a/lib/Command/PlacesSetup.php
+++ b/lib/Command/PlacesSetup.php
@@ -23,8 +23,7 @@ declare(strict_types=1);
namespace OCA\Memories\Command;
-use OCP\IConfig;
-use OCP\IDBConnection;
+use OCA\Memories\Service\Places;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@@ -38,19 +37,14 @@ const PLANET_URL = 'https://github.com/pulsejet/memories-assets/releases/downloa
class PlacesSetup extends Command
{
- protected IConfig $config;
protected OutputInterface $output;
- protected IDBConnection $connection;
-
- protected int $gisType = GIS_TYPE_NONE;
+ protected Places $places;
public function __construct(
- IConfig $config,
- IDBConnection $connection
+ Places $places
) {
parent::__construct();
- $this->config = $config;
- $this->connection = $connection;
+ $this->places = $places;
}
protected function configure(): void
@@ -68,392 +62,43 @@ class PlacesSetup extends Command
$this->output->writeln('Attempting to set up reverse geocoding');
// Detect the GIS type
- $this->detectGisType();
- $this->config->setSystemValue('memories.gis_type', $this->gisType);
-
- // Make sure we support something
- if (GIS_TYPE_NONE === $this->gisType) {
+ if ($this->places->detectGisType() <= 0) {
$this->output->writeln('No supported GIS type detected');
return 1;
}
+ $this->output->writeln('Database support was detected');
- // Check if the database is already set up
- $hasDb = false;
-
- try {
+ // Check if database is already set up
+ if ($this->places->geomCount() > 0) {
$this->output->writeln('');
- $this->connection->executeQuery('SELECT osm_id FROM memories_planet_geometry LIMIT 1')->fetch();
$this->output->writeln('Database is already set up');
$this->output->writeln('This will drop and re-download the planet database');
$this->output->writeln('This is generally not necessary to do frequently ');
- $hasDb = true;
- } catch (\Exception $e) {
- }
- // Ask confirmation
- $tempdir = sys_get_temp_dir();
- $this->output->writeln('');
- $this->output->writeln('Are you sure you want to download the planet database?');
- $this->output->writeln("This will take a very long time and use some disk space in {$tempdir}");
- $this->output->write('Proceed? [y/N] ');
- $handle = fopen('php://stdin', 'r');
- $line = fgets($handle);
- if ('y' !== trim($line)) {
- $this->output->writeln('Aborting');
-
- return 1;
- }
-
- // Drop the table
- $p = $this->connection->getDatabasePlatform();
- if ($hasDb) {
+ // Ask confirmation
$this->output->writeln('');
- $this->output->write('Dropping table ... ');
- $this->connection->executeStatement($p->getDropTableSQL('memories_planet_geometry'));
- $this->output->writeln('OK');
- }
+ $this->output->writeln('Are you sure you want to download the planet database?');
+ $this->output->write('Proceed? [y/N] ');
+ $handle = fopen('php://stdin', 'r');
+ $line = fgets($handle);
+ if (false === $line) {
+ $this->output->writeln('You need an interactive terminal to run this command');
- // Setup the database
- $this->output->write('Setting up database ... ');
- $this->setupDatabase();
- $this->output->writeln('OK');
-
- // Truncate tables
- $this->output->write('Truncating tables ... ');
- $this->connection->executeStatement($p->getTruncateTableSQL('*PREFIX*memories_planet', false));
- $this->connection->executeStatement($p->getTruncateTableSQL('memories_planet_geometry', false));
- $this->output->writeln('OK');
-
- // Download the data
- $this->output->write('Downloading data ... ');
- $datafile = $this->downloadPlanet();
- $this->output->writeln('OK');
-
- // Start importing
- $this->output->writeln('');
- $this->output->writeln('Importing data (this will take a while) ...');
-
- // Start time
- $start = time();
-
- // Create place insertion statement
- $query = $this->connection->getQueryBuilder();
- $query->insert('memories_planet')
- ->values([
- 'osm_id' => $query->createParameter('osm_id'),
- 'admin_level' => $query->createParameter('admin_level'),
- 'name' => $query->createParameter('name'),
- 'other_names' => $query->createParameter('other_names'),
- ])
- ;
- $insertPlace = $this->connection->prepare($query->getSQL());
-
- // Create geometry insertion statement
- $query = $this->connection->getQueryBuilder();
- $geomParam = $query->createParameter('geometry');
- if (GIS_TYPE_MYSQL === $this->gisType) {
- $geomParam = "ST_GeomFromText({$geomParam})";
- } elseif (GIS_TYPE_POSTGRES === $this->gisType) {
- $geomParam = "POLYGON({$geomParam}::text)";
- }
- $query->insert('memories_planet_geometry')
- ->values([
- 'id' => $query->createParameter('id'),
- 'poly_id' => $query->createParameter('poly_id'),
- 'type_id' => $query->createParameter('type_id'),
- 'osm_id' => $query->createParameter('osm_id'),
- 'geometry' => $query->createFunction($geomParam),
- ])
- ;
- $sql = str_replace('*PREFIX*memories_planet_geometry', 'memories_planet_geometry', $query->getSQL());
- $insertGeometry = $this->connection->prepare($sql);
-
- // The number of places in the current transaction
- $txnCount = 0;
-
- // Iterate over the data file
- $handle = fopen($datafile, 'r');
- if ($handle) {
- $count = 0;
- while (($line = fgets($handle)) !== false) {
- // Skip empty lines
- if ('' === trim($line)) {
- continue;
- }
-
- // Begin transaction
- if (0 === $txnCount++) {
- $this->connection->beginTransaction();
- }
- ++$count;
-
- // Decode JSON
- $data = json_decode($line, true);
- if (null === $data) {
- $this->output->writeln('Failed to decode JSON');
-
- continue;
- }
-
- // Extract data
- $osmId = $data['osm_id'];
- $adminLevel = $data['admin_level'];
- $name = $data['name'];
- $boundaries = $data['geometry'];
- $otherNames = json_encode($data['other_names'] ?? []);
-
- // Skip some places
- if ($adminLevel > -2 && ($adminLevel <= 1 || $adminLevel >= 10)) {
- // <=1: These are too general, e.g. "Earth"? or invalid
- // >=10: These are too specific, e.g. "Community Board"
- // <-1: These are special, e.g. "Timezone" = -7
- continue;
- }
-
- // Insert place into database
- $insertPlace->bindValue('osm_id', $osmId);
- $insertPlace->bindValue('admin_level', $adminLevel);
- $insertPlace->bindValue('name', $name);
- $insertPlace->bindValue('other_names', $otherNames);
- $insertPlace->execute();
-
- // Insert polygons into database
- $idx = 0;
- foreach ($boundaries as &$polygon) {
- // $polygon is a struct as
- // [ "t" => "e", "c" => [lon, lat], [lon, lat], ... ] ]
-
- $polyid = $polygon['i'];
- $typeid = $polygon['t'];
- $pkey = $polygon['k'];
- $coords = $polygon['c'];
-
- // Create parameters
- ++$idx;
- $geometry = '';
-
- if (\count($coords) < 3) {
- $this->output->writeln('Invalid polygon');
-
- continue;
- }
-
- if (GIS_TYPE_MYSQL === $this->gisType) {
- $points = implode(',', array_map(function ($point) {
- $x = $point[0];
- $y = $point[1];
-
- return "{$x} {$y}";
- }, $coords));
-
- $geometry = "POLYGON(({$points}))";
- } elseif (GIS_TYPE_POSTGRES === $this->gisType) {
- $geometry = implode(',', array_map(function ($point) {
- $x = $point[0];
- $y = $point[1];
-
- return "({$x},{$y})";
- }, $coords));
- }
-
- try {
- $insertGeometry->bindValue('id', $pkey);
- $insertGeometry->bindValue('poly_id', $polyid);
- $insertGeometry->bindValue('type_id', $typeid);
- $insertGeometry->bindValue('osm_id', $osmId);
- $insertGeometry->bindValue('geometry', $geometry);
- $insertGeometry->execute();
- } catch (\Exception $e) {
- $this->output->writeln('Failed to insert into database');
- $this->output->writeln($e->getMessage());
-
- continue;
- }
- }
-
- // Commit transaction every once in a while
- if (0 === $count % 100) {
- $this->connection->commit();
- $txnCount = 0;
-
- // Print progress
- $end = time();
- $elapsed = ($end - $start) ?: 1;
- $rate = $count / $elapsed;
- $remaining = APPROX_PLACES - $count;
- $eta = round($remaining / $rate);
- $rate = round($rate, 1);
- $this->output->writeln("Inserted {$count} places, {$rate}/s, ETA: {$eta}s, Last: {$name}");
- }
+ return 1;
}
+ if ('y' !== trim($line)) {
+ $this->output->writeln('Aborting');
- fclose($handle);
- }
-
- // Commit final transaction
- if ($txnCount > 0) {
- $this->connection->commit();
- }
-
- // Delete file
- unlink($datafile);
-
- // Done
- $this->output->writeln('');
- $this->output->writeln('Planet database imported successfully!');
- $this->output->writeln('If this is the first time you did this, you should now run:');
- $this->output->writeln('occ memories:index -f');
-
- // Mark success
- $this->config->setSystemValue('memories.gis_type', $this->gisType);
-
- return 0;
- }
-
- protected function detectGisType()
- {
- // Make sure database prefix is set
- $prefix = $this->config->getSystemValue('dbtableprefix', '') ?: '';
- if ('' === $prefix) {
- $this->output->writeln('Database table prefix is not set');
- $this->output->writeln('Custom database extensions cannot be used without a prefix');
- $this->output->writeln('Reverse geocoding will not work and is disabled');
- $this->gisType = GIS_TYPE_NONE;
-
- return;
- }
-
- // Warn the admin about the database prefix not being used
- $this->output->writeln('');
- $this->output->writeln("Database table prefix is set to '{$prefix}'");
- $this->output->writeln('If the planet can be imported, it will not use this prefix');
- $this->output->writeln('The table will be named "memories_planet_geometry"');
- $this->output->writeln('This is necessary for using custom database extensions');
- $this->output->writeln('');
-
- // Detect database type
- $platform = strtolower(\get_class($this->connection->getDatabasePlatform()));
-
- // Test MySQL-like support in databse
- if (str_contains($platform, 'mysql') || str_contains($platform, 'mariadb')) {
- try {
- $res = $this->connection->executeQuery("SELECT ST_GeomFromText('POINT(1 1)')")->fetch();
- if (0 === \count($res)) {
- throw new \Exception('Invalid result');
- }
- $this->output->writeln('MySQL-like support detected!');
- $this->gisType = GIS_TYPE_MYSQL;
-
- return;
- } catch (\Exception $e) {
- $this->output->writeln('No MySQL-like support detected');
+ return 1;
}
}
- // Test Postgres native geometry like support in database
- if (str_contains($platform, 'postgres')) {
- try {
- $res = $this->connection->executeQuery("SELECT POINT('1,1')")->fetch();
- if (0 === \count($res)) {
- throw new \Exception('Invalid result');
- }
- $this->output->writeln('Postgres native geometry support detected!');
- $this->gisType = GIS_TYPE_POSTGRES;
+ // Download the planet database
+ $this->output->writeln('Downloading planet database');
+ $datafile = $this->places->downloadPlanet();
- return;
- } catch (\Exception $e) {
- $this->output->writeln('No Postgres native geometry support detected');
- }
- }
- }
-
- protected function setupDatabase(): void
- {
- try {
- $sql = 'CREATE TABLE memories_planet_geometry (
- id varchar(255) NOT NULL PRIMARY KEY,
- poly_id varchar(255) NOT NULL,
- type_id int NOT NULL,
- osm_id int NOT NULL,
- geometry polygon NOT NULL
- );';
- $this->connection->executeQuery($sql);
-
- // Add indexes
- $this->connection->executeQuery('CREATE INDEX planet_osm_id_idx ON memories_planet_geometry (osm_id);');
-
- // Add spatial index
- if (GIS_TYPE_MYSQL === $this->gisType) {
- $this->connection->executeQuery('CREATE SPATIAL INDEX planet_osm_polygon_geometry_idx ON memories_planet_geometry (geometry);');
- } elseif (GIS_TYPE_POSTGRES === $this->gisType) {
- $this->connection->executeQuery('CREATE INDEX planet_osm_polygon_geometry_idx ON memories_planet_geometry USING GIST (geometry);');
- }
- } catch (\Exception $e) {
- $this->output->writeln('Failed to create planet table');
- $this->output->writeln($e->getMessage());
-
- exit;
- }
- }
-
- protected function ensureDeleted(string $filename)
- {
- if (!file_exists($filename)) {
- return;
- }
-
- unlink($filename);
- if (file_exists($filename)) {
- $this->output->writeln('Failed to delete data file');
- $this->output->writeln("Please delete {$filename} manually");
-
- exit;
- }
- }
-
- protected function downloadPlanet(): string
- {
- $filename = sys_get_temp_dir().'/planet_coarse_boundaries.zip';
- $this->ensureDeleted($filename);
-
- $txtfile = sys_get_temp_dir().'/planet_coarse_boundaries.txt';
- $this->ensureDeleted($txtfile);
-
- $fp = fopen($filename, 'w+');
-
- $ch = curl_init();
- curl_setopt($ch, CURLOPT_URL, PLANET_URL);
- curl_setopt($ch, CURLOPT_FILE, $fp);
- curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
- curl_setopt($ch, CURLOPT_TIMEOUT, 60000);
- curl_exec($ch);
- curl_close($ch);
-
- fclose($fp);
-
- // Unzip
- $zip = new \ZipArchive();
- $res = $zip->open($filename);
- if (true === $res) {
- $zip->extractTo(sys_get_temp_dir());
- $zip->close();
- } else {
- $this->output->writeln('Failed to unzip planet file');
-
- exit;
- }
-
- // Check if file exists
- if (!file_exists($txtfile)) {
- $this->output->writeln('Failed to find planet data file after unzip');
-
- exit;
- }
-
- // Delete zip file
- unlink($filename);
-
- return $txtfile;
+ // Import the planet database
+ $this->places->importPlanet($datafile);
}
}
diff --git a/lib/Controller/OtherController.php b/lib/Controller/OtherController.php
index 8539decc..3e3696ed 100644
--- a/lib/Controller/OtherController.php
+++ b/lib/Controller/OtherController.php
@@ -128,6 +128,16 @@ class OtherController extends GenericApiController
// Check for system perl
$status['perl'] = $this->getExecutableStatus(exec('which perl'));
+ // Get GIS status
+ $places = \OC::$server->get(\OCA\Memories\Service\Places::class);
+
+ try {
+ $status['gis_type'] = $places->detectGisType();
+ $status['gis_count'] = $places->geomCount();
+ } catch (\Exception $e) {
+ $status['gis_type'] = $e->getMessage();
+ }
+
// Check ffmpeg and ffprobe binaries
$status['ffmpeg'] = $this->getExecutableStatus(Util::getSystemConfig('memories.vod.ffmpeg'));
$status['ffprobe'] = $this->getExecutableStatus(Util::getSystemConfig('memories.vod.ffprobe'));
@@ -158,6 +168,37 @@ class OtherController extends GenericApiController
});
}
+ /**
+ * @AdminRequired
+ */
+ public function placesSetup(): Http\Response
+ {
+ try {
+ // Set PHP timeout to infinite
+ set_time_limit(0);
+
+ // Send headers for long-running request
+ header('Content-Type: text/plain');
+ header('X-Accel-Buffering: no');
+ header('Cache-Control: no-cache');
+ header('Connection: keep-alive');
+ header('Content-Length: 0');
+
+ $places = \OC::$server->get(\OCA\Memories\Service\Places::class);
+
+ echo "Downloading planet file...\n";
+ flush();
+ $datafile = $places->downloadPlanet();
+ $places->importPlanet($datafile);
+
+ echo "Done.\n";
+ } catch (\Exception $e) {
+ echo 'Failed: '.$e->getMessage()."\n";
+ }
+
+ exit;
+ }
+
/**
* @NoAdminRequired
*
diff --git a/lib/Service/Places.php b/lib/Service/Places.php
new file mode 100644
index 00000000..e0eb7ee2
--- /dev/null
+++ b/lib/Service/Places.php
@@ -0,0 +1,357 @@
+config = $config;
+ $this->connection = $connection;
+ }
+
+ /**
+ * Make SQL query to detect GIS type.
+ */
+ public function detectGisType()
+ {
+ // Make sure database prefix is set
+ $prefix = $this->config->getSystemValue('dbtableprefix', '') ?: '';
+ if ('' === $prefix) {
+ throw new \Exception('Database table prefix is not set. Cannot use database exceptions (dbtableprefix).');
+ }
+
+ // Detect database type
+ $platform = strtolower(\get_class($this->connection->getDatabasePlatform()));
+
+ // Test MySQL-like support in databse
+ if (str_contains($platform, 'mysql') || str_contains($platform, 'mariadb')) {
+ try {
+ $res = $this->connection->executeQuery("SELECT ST_GeomFromText('POINT(1 1)')")->fetch();
+ if (0 === \count($res)) {
+ throw new \Exception('Invalid result');
+ }
+
+ return GIS_TYPE_MYSQL;
+ } catch (\Exception $e) {
+ throw new \Exception('No MySQL-like geometry support detected');
+ }
+ }
+
+ // Test Postgres native geometry like support in database
+ if (str_contains($platform, 'postgres')) {
+ try {
+ $res = $this->connection->executeQuery("SELECT POINT('1,1')")->fetch();
+ if (0 === \count($res)) {
+ throw new \Exception('Invalid result');
+ }
+
+ return GIS_TYPE_POSTGRES;
+ } catch (\Exception $e) {
+ throw new \Exception('No Postgres native geometry support detected');
+ }
+ }
+
+ return GIS_TYPE_NONE;
+ }
+
+ /**
+ * Check if DB is already setup and return number of entries.
+ */
+ public function geomCount(): int
+ {
+ try {
+ return $this->connection->executeQuery('SELECT COUNT(osm_id) as c FROM memories_planet_geometry')->fetchOne();
+ } catch (\Exception $e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Download planet database file and return path to it.
+ */
+ public function downloadPlanet(): string
+ {
+ $filename = sys_get_temp_dir().'/planet_coarse_boundaries.zip';
+ unlink($filename);
+
+ $txtfile = sys_get_temp_dir().'/planet_coarse_boundaries.txt';
+ unlink($txtfile);
+
+ $fp = fopen($filename, 'w+');
+
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, PLANET_URL);
+ curl_setopt($ch, CURLOPT_FILE, $fp);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 3600);
+ curl_exec($ch);
+ curl_close($ch);
+
+ fclose($fp);
+
+ // Unzip
+ $zip = new \ZipArchive();
+ $res = $zip->open($filename);
+ if (true === $res) {
+ $zip->extractTo(sys_get_temp_dir());
+ $zip->close();
+ } else {
+ throw new \Exception('Failed to unzip planet data file');
+ }
+
+ // Check if file exists
+ if (!file_exists($txtfile)) {
+ throw new \Exception('Failed to find planet data file after unzip');
+ }
+
+ // Delete zip file
+ unlink($filename);
+
+ return $txtfile;
+ }
+
+ /**
+ * Insert planet into database from file.
+ */
+ public function importPlanet(string $datafile): void
+ {
+ echo "Inserting planet data into database...\n";
+
+ // Detect the GIS type
+ $gis = $this->detectGisType();
+
+ // Make sure we support something
+ if (GIS_TYPE_NONE === $gis) {
+ throw new \Exception('No GIS support detected');
+ }
+
+ // Drop the table if it exists
+ $p = $this->connection->getDatabasePlatform();
+ if ($this->geomCount() > 0) {
+ $this->connection->executeStatement($p->getDropTableSQL('memories_planet_geometry'));
+ }
+
+ // Setup the database
+ $this->setupDatabase();
+
+ // Truncate tables
+ $this->connection->executeStatement($p->getTruncateTableSQL('*PREFIX*memories_planet', false));
+ $this->connection->executeStatement($p->getTruncateTableSQL('memories_planet_geometry', false));
+
+ // Create place insertion statement
+ $query = $this->connection->getQueryBuilder();
+ $query->insert('memories_planet')
+ ->values([
+ 'osm_id' => $query->createParameter('osm_id'),
+ 'admin_level' => $query->createParameter('admin_level'),
+ 'name' => $query->createParameter('name'),
+ 'other_names' => $query->createParameter('other_names'),
+ ])
+ ;
+ $insertPlace = $this->connection->prepare($query->getSQL());
+
+ // Create geometry insertion statement
+ $query = $this->connection->getQueryBuilder();
+ $geomParam = $query->createParameter('geometry');
+ if (GIS_TYPE_MYSQL === $gis) {
+ $geomParam = "ST_GeomFromText({$geomParam})";
+ } elseif (GIS_TYPE_POSTGRES === $gis) {
+ $geomParam = "POLYGON({$geomParam}::text)";
+ }
+ $query->insert('memories_planet_geometry')
+ ->values([
+ 'id' => $query->createParameter('id'),
+ 'poly_id' => $query->createParameter('poly_id'),
+ 'type_id' => $query->createParameter('type_id'),
+ 'osm_id' => $query->createParameter('osm_id'),
+ 'geometry' => $query->createFunction($geomParam),
+ ])
+ ;
+ $sql = str_replace('*PREFIX*memories_planet_geometry', 'memories_planet_geometry', $query->getSQL());
+ $insertGeometry = $this->connection->prepare($sql);
+
+ // The number of places in the current transaction
+ $txnCount = 0;
+
+ // Iterate over the data file
+ $handle = fopen($datafile, 'r');
+ if ($handle) {
+ $count = 0;
+ while (($line = fgets($handle)) !== false) {
+ // Skip empty lines
+ if ('' === trim($line)) {
+ continue;
+ }
+
+ // Begin transaction
+ if (0 === $txnCount++) {
+ $this->connection->beginTransaction();
+ }
+ ++$count;
+
+ // Decode JSON
+ $data = json_decode($line, true);
+ if (null === $data) {
+ echo "ERROR: Failed to decode JSON\n";
+
+ continue;
+ }
+
+ // Extract data
+ $osmId = $data['osm_id'];
+ $adminLevel = $data['admin_level'];
+ $name = $data['name'];
+ $boundaries = $data['geometry'];
+ $otherNames = json_encode($data['other_names'] ?? []);
+
+ // Skip some places
+ if ($adminLevel > -2 && ($adminLevel <= 1 || $adminLevel >= 10)) {
+ // <=1: These are too general, e.g. "Earth"? or invalid
+ // >=10: These are too specific, e.g. "Community Board"
+ // <-1: These are special, e.g. "Timezone" = -7
+ continue;
+ }
+
+ // Insert place into database
+ $insertPlace->bindValue('osm_id', $osmId);
+ $insertPlace->bindValue('admin_level', $adminLevel);
+ $insertPlace->bindValue('name', $name);
+ $insertPlace->bindValue('other_names', $otherNames);
+ $insertPlace->execute();
+
+ // Insert polygons into database
+ $idx = 0;
+ foreach ($boundaries as &$polygon) {
+ // $polygon is a struct as
+ // [ "t" => "e", "c" => [lon, lat], [lon, lat], ... ] ]
+
+ $polyid = $polygon['i'];
+ $typeid = $polygon['t'];
+ $pkey = $polygon['k'];
+ $coords = $polygon['c'];
+
+ // Create parameters
+ ++$idx;
+ $geometry = '';
+
+ if (\count($coords) < 3) {
+ echo "ERROR: Invalid polygon {$polyid}\n";
+
+ continue;
+ }
+
+ if (GIS_TYPE_MYSQL === $gis) {
+ $points = implode(',', array_map(function ($point) {
+ $x = $point[0];
+ $y = $point[1];
+
+ return "{$x} {$y}";
+ }, $coords));
+
+ $geometry = "POLYGON(({$points}))";
+ } elseif (GIS_TYPE_POSTGRES === $gis) {
+ $geometry = implode(',', array_map(function ($point) {
+ $x = $point[0];
+ $y = $point[1];
+
+ return "({$x},{$y})";
+ }, $coords));
+ }
+
+ try {
+ $insertGeometry->bindValue('id', $pkey);
+ $insertGeometry->bindValue('poly_id', $polyid);
+ $insertGeometry->bindValue('type_id', $typeid);
+ $insertGeometry->bindValue('osm_id', $osmId);
+ $insertGeometry->bindValue('geometry', $geometry);
+ $insertGeometry->execute();
+ } catch (\Exception $e) {
+ echo "ERROR: Failed to insert polygon {$polyid} ({$e->getMessage()} \n";
+
+ continue;
+ }
+ }
+
+ // Commit transaction every once in a while
+ if (0 === $count % 100) {
+ $this->connection->commit();
+ $txnCount = 0;
+
+ // Print progress
+ $total = APPROX_PLACES;
+ $pct = round($count / $total * 100, 1);
+ }
+
+ if (0 === $count % 500) {
+ echo "Inserted {$count} / {$total} places ({$pct}%), Last: {$name}\n";
+ flush();
+ }
+ }
+
+ fclose($handle);
+ }
+
+ // Commit final transaction
+ if ($txnCount > 0) {
+ $this->connection->commit();
+ }
+
+ // Mark success
+ echo "Planet database imported successfully!\n";
+ $this->config->setSystemValue('memories.gis_type', $gis);
+
+ // Delete data file
+ unlink($datafile);
+ }
+
+ /**
+ * Create database tables and indices.
+ */
+ private function setupDatabase(): void
+ {
+ try {
+ // Get Gis type
+ $gis = $this->detectGisType();
+
+ // Create table
+ $sql = 'CREATE TABLE memories_planet_geometry (
+ id varchar(255) NOT NULL PRIMARY KEY,
+ poly_id varchar(255) NOT NULL,
+ type_id int NOT NULL,
+ osm_id int NOT NULL,
+ geometry polygon NOT NULL
+ );';
+ $this->connection->executeQuery($sql);
+
+ // Add indexes
+ $this->connection->executeQuery('CREATE INDEX planet_osm_id_idx ON memories_planet_geometry (osm_id);');
+
+ // Add spatial index
+ if (GIS_TYPE_MYSQL === $gis) {
+ $this->connection->executeQuery('CREATE SPATIAL INDEX planet_osm_polygon_geometry_idx ON memories_planet_geometry (geometry);');
+ } elseif (GIS_TYPE_POSTGRES === $gis) {
+ $this->connection->executeQuery('CREATE INDEX planet_osm_polygon_geometry_idx ON memories_planet_geometry USING GIST (geometry);');
+ }
+ } catch (\Exception $e) {
+ throw new \Exception('Failed to create database tables: '.$e->getMessage());
+ }
+ }
+}
diff --git a/src/Admin.vue b/src/Admin.vue
index b296ce8a..82ffadb4 100644
--- a/src/Admin.vue
+++ b/src/Admin.vue
@@ -2,6 +2,7 @@
+
{{ t("memories", "EXIF Extraction") }}
@@ -34,6 +35,70 @@
}}
+
+ {{ t("memories", "Reverse Geocoding") }}
+
+
+
+
+ {{ gisStatus }}
+
+
+ {{
+ status.gis_count > 0
+ ? t("memories", "Database is populated with {n} geometries", {
+ n: status.gis_count,
+ })
+ : t("memories", "Geometry table has not been created")
+ }}
+
+
+
+ {{
+ t(
+ "memories",
+ "Memories supports offline reverse geocoding using the OpenStreetMaps data on MySQL and Postgres."
+ )
+ }}
+
+ {{
+ t(
+ "memories",
+ "You need to download the planet data into your database. This is highly recommended and has low overhead."
+ )
+ }}
+
+ {{
+ t(
+ "memories",
+ "If the button below does not work for importing the planet data, use 'occ memories:places-setup'."
+ )
+ }}
+
+ {{
+ t(
+ "memories",
+ "Note: the geometry data is stored in the 'memories_planet_geometry' table, with no prefix."
+ )
+ }}
+
+
+
+
+
{{ t("memories", "Video Streaming") }}
@@ -207,8 +272,8 @@
VA-API configuration
-
- {{ vaapiStatusText() }}
+
+ {{ vaapiStatusText }}
const NcNoteCard = () => import("@nextcloud/vue/dist/Components/NcNoteCard");
const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField");
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon";
+import NcButton from "@nextcloud/vue/dist/Components/NcButton";
/** Map from UI to backend settings */
const settings = {
@@ -328,6 +394,8 @@ const invertedBooleans = ["enableTranscoding"];
type BinaryStatus = "ok" | "not_found" | "not_executable" | "test_ok" | string;
type IStatus = {
+ gis_type: number;
+ gis_count?: number;
exiftool: BinaryStatus;
perl: BinaryStatus;
ffmpeg: BinaryStatus;
@@ -343,6 +411,7 @@ export default defineComponent({
NcNoteCard,
NcTextField,
NcLoadingIcon,
+ NcButton,
},
data: () => ({
@@ -441,6 +510,20 @@ export default defineComponent({
});
},
+ placesSetup(event: Event) {
+ const msg =
+ "Looks like the database is already setup. Are you sure you want to drop the table and redownload OSM data?";
+ if (this.status.gis_count && !confirm(msg)) {
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ } else {
+ alert(
+ "Please wait for the download and insertion to complete. This may take a while."
+ );
+ }
+ },
+
binaryStatus(name: string, status: BinaryStatus): string {
if (status === "ok") {
return this.t("memories", "{name} binary exists and is executable", {
@@ -491,6 +574,43 @@ export default defineComponent({
return "warning";
}
},
+ },
+
+ computed: {
+ requestToken() {
+ return (axios.defaults.headers).requesttoken;
+ },
+
+ gisStatus() {
+ if (typeof this.status.gis_type !== "number") {
+ return this.status.gis_type;
+ }
+
+ if (this.status.gis_type <= 0) {
+ return this.t(
+ "memories",
+ "Geometry support was not detected in your database"
+ );
+ } else if (this.status.gis_type === 1) {
+ return this.t("memories", "MySQL-like geometry support was detected ");
+ } else if (this.status.gis_type === 2) {
+ return this.t(
+ "memories",
+ "Postgres native geometry support was detected"
+ );
+ }
+ },
+
+ gisStatusType() {
+ return typeof this.status.gis_type !== "number" ||
+ this.status.gis_type <= 0
+ ? "error"
+ : "success";
+ },
+
+ placesSetupUrl() {
+ return API.OCC_PLACES_SETUP();
+ },
vaapiStatusText(): string {
const dev = "/dev/dri/renderD128";
@@ -536,6 +656,10 @@ export default defineComponent({
}
}
+ form {
+ margin-top: 1em;
+ }
+
.checkbox-radio-switch {
margin: 2px 8px;
}
diff --git a/src/services/API.ts b/src/services/API.ts
index fce521be..910f9216 100644
--- a/src/services/API.ts
+++ b/src/services/API.ts
@@ -194,6 +194,10 @@ export class API {
return gen(`${BASE}/system-status`);
}
+ static OCC_PLACES_SETUP() {
+ return gen(`${BASE}/occ/places-setup`);
+ }
+
static MAP_CLUSTERS() {
return tok(gen(`${BASE}/map/clusters`));
}