diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b9577f1..7809f817 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,12 @@ All notable changes to this project will be documented in this file. ## [v5.1.1] - Unreleased +**Note:** You will need to run `occ memories:places-setup --recalculate` to re-index places (or reindex everything) + - New project home page: https://memories.gallery - New Discord community: https://discord.gg/7Dr9f9vNjJ - Nextcloud 27 compatibility +- **Feature**: Hierarchical places view - **Feature**: Layout improvements especially for mobile. - **Feature**: Allow downloading entire publicly shared albums. - **Feature**: Basic preview generation configuration in admin interface. diff --git a/lib/ClustersBackend/PlacesBackend.php b/lib/ClustersBackend/PlacesBackend.php index 75992f58..c3bd2788 100644 --- a/lib/ClustersBackend/PlacesBackend.php +++ b/lib/ClustersBackend/PlacesBackend.php @@ -65,6 +65,9 @@ class PlacesBackend extends Backend public function getClusters(): array { + $inside = (int) $this->request->getParam('inside', 0); + $marked = (int) $this->request->getParam('mark', 1); + $query = $this->tq->getBuilder(); // SELECT location name and count of photos @@ -75,7 +78,41 @@ class PlacesBackend extends Backend $query->where($query->expr()->gt('e.admin_level', $query->expr()->literal(0, \PDO::PARAM_INT))); // WHERE there are items with this osm_id - $query->innerJoin('e', 'memories_places', 'mp', $query->expr()->eq('mp.osm_id', 'e.osm_id')); + $mpJoinOn = [$query->expr()->eq('mp.osm_id', 'e.osm_id')]; + + // AND these items are inside the requested place + if ($inside > 0) { + $sub = $this->tq->getBuilder(); + $sub->select($query->expr()->literal(1))->from('memories_places', 'mp_sq') + ->where($sub->expr()->eq('mp_sq.osm_id', $query->createNamedParameter($inside, \PDO::PARAM_INT))) + ->andWhere($sub->expr()->eq('mp_sq.fileid', 'mp.fileid')) + ; + $mpJoinOn[] = $query->createFunction("EXISTS ({$sub->getSQL()})"); + + // Add WHERE clauses to main query to filter out admin_levels + $sub = $this->tq->getBuilder(); + $sub->select('e_sq.admin_level') + ->from('memories_planet', 'e_sq') + ->where($sub->expr()->eq('e_sq.osm_id', $query->createNamedParameter($inside, \PDO::PARAM_INT))) + ; + $adminSql = "({$sub->getSQL()})"; + $query->andWhere($query->expr()->gt('e.admin_level', $query->createFunction($adminSql))) + ->andWhere($query->expr()->lte('e.admin_level', $query->createFunction("{$adminSql} + 3"))) + ; + } + + // Else if we are looking for countries + elseif ($inside === -1) { + $query->where($query->expr()->eq('e.admin_level', $query->expr()->literal(2, \PDO::PARAM_INT))); + } + + // AND these items are marked (only if not inside) + elseif ($marked > 0) { + $mpJoinOn[] = $query->expr()->eq('mp.mark', $query->expr()->literal(1, \PDO::PARAM_INT)); + } + + // JOIN on memories_places + $query->innerJoin('e', 'memories_places', 'mp', $query->expr()->andX(...$mpJoinOn)); // WHERE these items are memories indexed photos $query->innerJoin('mp', 'memories', 'm', $query->expr()->eq('m.fileid', 'mp.fileid')); @@ -104,8 +141,14 @@ class PlacesBackend extends Backend // INNER JOIN back on the planet table to get the names $query->innerJoin('sub', 'memories_planet', 'e', $query->expr()->eq('e.osm_id', 'sub.osm_id')); + // WHERE at least 3 photos if want marked clusters + if ($marked) { + $query->andWhere($query->expr()->gte('sub.count', $query->expr()->literal(3, \PDO::PARAM_INT))); + } + // ORDER BY name and osm_id - $query->orderBy($query->createFunction('LOWER(e.name)'), 'ASC'); + $query->orderBy($query->createFunction('sub.count'), 'DESC'); + $query->addOrderBy('e.name'); $query->addOrderBy('e.osm_id'); // tie-breaker // FETCH all tags @@ -115,7 +158,7 @@ class PlacesBackend extends Backend $lang = Util::getUserLang(); foreach ($places as &$row) { $row['osm_id'] = (int) $row['osm_id']; - $row['count'] = (int) $row['count']; + $row['count'] = $marked ? 0 : (int) $row['count']; // the count is incorrect self::choosePlaceLang($row, $lang); } diff --git a/lib/Command/PlacesSetup.php b/lib/Command/PlacesSetup.php index d96a1707..09fdea10 100644 --- a/lib/Command/PlacesSetup.php +++ b/lib/Command/PlacesSetup.php @@ -26,6 +26,7 @@ namespace OCA\Memories\Command; use OCA\Memories\Service\Places; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class PlacesSetup extends Command @@ -45,12 +46,14 @@ class PlacesSetup extends Command $this ->setName('memories:places-setup') ->setDescription('Setup reverse geocoding') + ->addOption('recalculate', 'r', InputOption::VALUE_NONE, 'Only recalculate places for existing files') ; } protected function execute(InputInterface $input, OutputInterface $output): int { $this->output = $output; + $recalculate = $input->getOption('recalculate'); $this->output->writeln('Attempting to set up reverse geocoding'); @@ -63,35 +66,18 @@ class PlacesSetup extends Command $this->output->writeln('Database support was detected'); // Check if database is already set up - if ($this->places->geomCount() > 0) { - $this->output->writeln(''); - $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 '); - - // Ask confirmation - $this->output->writeln(''); - $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'); - - return 1; - } - if ('y' !== trim($line)) { - $this->output->writeln('Aborting'); - - return 1; - } + if ($this->places->geomCount() > 0 && !$recalculate && !$this->warnDownloaded()) { + return 1; } - // Download the planet database - $datafile = $this->places->downloadPlanet(); + // Check if we only need to recalculate + if (!$recalculate) { + // Download the planet database + $datafile = $this->places->downloadPlanet(); - // Import the planet database - $this->places->importPlanet($datafile); + // Import the planet database + $this->places->importPlanet($datafile); + } // Recalculate all places $this->places->recalculateAll(); @@ -100,4 +86,31 @@ class PlacesSetup extends Command return 0; } + + protected function warnDownloaded(): bool + { + $this->output->writeln(''); + $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 '); + + // Ask confirmation + $this->output->writeln(''); + $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'); + + return false; + } + if ('y' !== trim($line)) { + $this->output->writeln('Aborting'); + + return false; + } + + return true; + } } diff --git a/lib/Db/TimelineWritePlaces.php b/lib/Db/TimelineWritePlaces.php index 2c9c113d..8551af5f 100644 --- a/lib/Db/TimelineWritePlaces.php +++ b/lib/Db/TimelineWritePlaces.php @@ -46,42 +46,25 @@ trait TimelineWritePlaces return []; } - // Construct WHERE clause depending on GIS type - $where = null; - if (1 === $gisType) { - $where = "ST_Contains(geometry, ST_GeomFromText('POINT({$lon} {$lat})'))"; - } elseif (2 === $gisType) { - $where = "POINT('{$lon},{$lat}') <@ geometry"; - } else { - return []; - } + // Get places + $rows = \OC::$server->get(\OCA\Memories\Service\Places::class)->queryPoint($lat, $lon); - // Make query to memories_planet table - $query = $this->connection->getQueryBuilder(); - $query->select($query->createFunction('DISTINCT(osm_id)')) - ->from('memories_planet_geometry') - ->where($query->createFunction($where)) - ; - - // Cancel out inner rings - $query->groupBy('poly_id', 'osm_id'); - $query->having($query->createFunction('SUM(type_id) > 0')); - - // memories_planet_geometry has no *PREFIX* - $sql = str_replace('*PREFIX*memories_planet_geometry', 'memories_planet_geometry', $query->getSQL()); - - // Run query - $rows = $this->connection->executeQuery($sql)->fetchAll(); + // Get last ID, i.e. the ID with highest admin_level but <= 8 + $markRow = array_pop(array_filter($rows, fn ($row) => $row['admin_level'] <= 8)); // Insert records in transaction $this->connection->beginTransaction(); foreach ($rows as $row) { + $isMark = $markRow && $row['osm_id'] === $markRow['osm_id']; + + // Insert the place $query = $this->connection->getQueryBuilder(); $query->insert('memories_places') ->values([ 'fileid' => $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT), 'osm_id' => $query->createNamedParameter($row['osm_id'], IQueryBuilder::PARAM_INT), + 'mark' => $query->createNamedParameter($isMark, IQueryBuilder::PARAM_BOOL), ]) ; $query->executeStatement(); diff --git a/lib/Migration/Version502000Date20230530052850.php b/lib/Migration/Version502000Date20230530052850.php new file mode 100644 index 00000000..43290a84 --- /dev/null +++ b/lib/Migration/Version502000Date20230530052850.php @@ -0,0 +1,66 @@ +. + */ + +namespace OCA\Memories\Migration; + +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Auto-generated migration step: Please modify to your needs! + */ +class Version502000Date20230530052850 extends SimpleMigrationStep +{ + /** + * @param \Closure(): ISchemaWrapper $schemaClosure + */ + public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void + { + } + + /** + * @param \Closure(): ISchemaWrapper $schemaClosure + */ + public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $table = $schema->getTable('memories_places'); + $table->addColumn('mark', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => false, + ]); + + $table->addIndex(['osm_id', 'mark'], 'memories_places_id_mk_idx'); + + return $schema; + } + + /** + * @param \Closure(): ISchemaWrapper $schemaClosure + */ + public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void + { + } +} diff --git a/lib/Service/Places.php b/lib/Service/Places.php index 4e4ffd66..d9363046 100644 --- a/lib/Service/Places.php +++ b/lib/Service/Places.php @@ -88,6 +88,50 @@ class Places } } + /** + * Get list of osm IDs for a given point. + */ + public function queryPoint(float $lat, float $lon): array + { + // Get GIS type + $gisType = \OCA\Memories\Util::placesGISType(); + + // Construct WHERE clause depending on GIS type + $where = null; + if (1 === $gisType) { + $where = "ST_Contains(geometry, ST_GeomFromText('POINT({$lon} {$lat})'))"; + } elseif (2 === $gisType) { + $where = "POINT('{$lon},{$lat}') <@ geometry"; + } else { + return []; + } + + // Make query to memories_planet table + $query = $this->connection->getQueryBuilder(); + $query->select($query->createFunction('DISTINCT(osm_id)')) + ->from('memories_planet_geometry') + ->where($query->createFunction($where)) + ; + + // Cancel out inner rings + $query->groupBy('poly_id', 'osm_id'); + $query->having($query->createFunction('SUM(type_id) > 0')); + + // memories_planet_geometry has no *PREFIX* + $sql = str_replace('*PREFIX*memories_planet_geometry', 'memories_planet_geometry', $query->getSQL()); + + // Use as subquery to get admin level + $query = $this->connection->getQueryBuilder(); + $query->select('sub.osm_id', 'mp.admin_level') + ->from($query->createFunction("({$sql})"), 'sub') + ->innerJoin('sub', 'memories_planet', 'mp', $query->expr()->eq('sub.osm_id', 'mp.osm_id')) + ->orderBy('mp.admin_level', 'ASC') + ; + + // Run query + return $query->executeQuery()->fetchAll(); + } + /** * Download planet database file and return path to it. */ diff --git a/src/components/ClusterGrid.vue b/src/components/ClusterGrid.vue index f633940e..b149ff1e 100644 --- a/src/components/ClusterGrid.vue +++ b/src/components/ClusterGrid.vue @@ -12,6 +12,10 @@ :gridItems="gridItems" @resize="resize" > + +