diff --git a/lib/Controller/ApiBase.php b/lib/Controller/ApiBase.php index 7ea87198..865c2d5b 100644 --- a/lib/Controller/ApiBase.php +++ b/lib/Controller/ApiBase.php @@ -379,97 +379,6 @@ class ApiBase extends Controller return \OCA\Memories\Util::placesGISType() > 0; } - /** - * Get transformations depending on the request. - * - * @param bool $aggregateOnly Only apply transformations for aggregation (days call) - */ - protected function getTransformations(bool $aggregateOnly) - { - $transforms = []; - - // Add extra information, basename and mimetype - if (!$aggregateOnly && ($fields = $this->request->getParam('fields'))) { - $fields = explode(',', $fields); - $transforms[] = [$this->timelineQuery, 'transformExtraFields', $fields]; - } - - // Filter for one album - if ($this->albumsIsEnabled()) { - if ($albumId = $this->request->getParam('album')) { - $transforms[] = [$this->timelineQuery, 'transformAlbumFilter', $albumId]; - } - } - - // Other transforms not allowed for public shares - if (null === $this->userSession->getUser()) { - return $transforms; - } - - // Filter only favorites - if ($this->request->getParam('fav')) { - $transforms[] = [$this->timelineQuery, 'transformFavoriteFilter']; - } - - // Filter only videos - if ($this->request->getParam('vid')) { - $transforms[] = [$this->timelineQuery, 'transformVideoFilter']; - } - - // Filter only for one face on Recognize - if (($recognize = $this->request->getParam('recognize')) && $this->recognizeIsEnabled()) { - $transforms[] = [$this->timelineQuery, 'transformPeopleRecognitionFilter', $recognize]; - - $faceRect = $this->request->getParam('facerect'); - if ($faceRect && !$aggregateOnly) { - $transforms[] = [$this->timelineQuery, 'transformPeopleRecognizeRect', $recognize]; - } - } - - // Filter only for one face on Face Recognition - if (($face = $this->request->getParam('facerecognition')) && $this->facerecognitionIsEnabled()) { - $currentModel = (int) $this->config->getAppValue('facerecognition', 'model', -1); - $transforms[] = [$this->timelineQuery, 'transformPeopleFaceRecognitionFilter', $currentModel, $face]; - - $faceRect = $this->request->getParam('facerect'); - if ($faceRect && !$aggregateOnly) { - $transforms[] = [$this->timelineQuery, 'transformPeopleFaceRecognitionRect', $face]; - } - } - - // Filter only for one tag - if ($this->tagsIsEnabled()) { - if ($tagName = $this->request->getParam('tag')) { - $transforms[] = [$this->timelineQuery, 'transformTagFilter', $tagName]; - } - } - - // Filter only for one place - if ($this->placesIsEnabled()) { - if ($locationId = $this->request->getParam('place')) { - $transforms[] = [$this->timelineQuery, 'transformPlaceFilter', (int) $locationId]; - } - } - - // Limit number of responses for day query - $limit = $this->request->getParam('limit'); - if ($limit) { - $transforms[] = [$this->timelineQuery, 'transformLimitDay', (int) $limit]; - } - - // Filter geological bounds - $bounds = $this->request->getParam('mapbounds'); - if ($bounds) { - $bounds = explode(',', $bounds); - $bounds = array_map('floatval', $bounds); - if (4 === \count($bounds)) { - $transforms[] = [$this->timelineQuery, 'transformBoundFilter', $bounds]; - } - } - - return $transforms; - } - /** * Helper to get one file or null from a fiolder. */ diff --git a/lib/Controller/DaysController.php b/lib/Controller/DaysController.php index ba596693..4937d6a1 100644 --- a/lib/Controller/DaysController.php +++ b/lib/Controller/DaysController.php @@ -172,6 +172,93 @@ class DaysController extends ApiBase return $this->day($id); } + /** + * Get transformations depending on the request. + * + * @param bool $aggregateOnly Only apply transformations for aggregation (days call) + */ + private function getTransformations(bool $aggregateOnly) + { + $transforms = []; + + // Add extra information, basename and mimetype + if (!$aggregateOnly && ($fields = $this->request->getParam('fields'))) { + $fields = explode(',', $fields); + $transforms[] = [$this->timelineQuery, 'transformExtraFields', $fields]; + } + + // Filter for one album + if ($this->albumsIsEnabled()) { + if ($albumId = $this->request->getParam('album')) { + $transforms[] = [$this->timelineQuery, 'transformAlbumFilter', $albumId]; + } + } + + // Other transforms not allowed for public shares + if (null === $this->userSession->getUser()) { + return $transforms; + } + + // Filter only favorites + if ($this->request->getParam('fav')) { + $transforms[] = [$this->timelineQuery, 'transformFavoriteFilter']; + } + + // Filter only videos + if ($this->request->getParam('vid')) { + $transforms[] = [$this->timelineQuery, 'transformVideoFilter']; + } + + // Filter only for one face on Recognize + if (($recognize = $this->request->getParam('recognize')) && $this->recognizeIsEnabled()) { + $transforms[] = [$this->timelineQuery, 'transformPeopleRecognitionFilter', $recognize]; + + $faceRect = $this->request->getParam('facerect'); + if ($faceRect && !$aggregateOnly) { + $transforms[] = [$this->timelineQuery, 'transformPeopleRecognizeRect', $recognize]; + } + } + + // Filter only for one face on Face Recognition + if (($face = $this->request->getParam('facerecognition')) && $this->facerecognitionIsEnabled()) { + $currentModel = (int) $this->config->getAppValue('facerecognition', 'model', -1); + $transforms[] = [$this->timelineQuery, 'transformPeopleFaceRecognitionFilter', $currentModel, $face]; + + $faceRect = $this->request->getParam('facerect'); + if ($faceRect && !$aggregateOnly) { + $transforms[] = [$this->timelineQuery, 'transformPeopleFaceRecognitionRect', $face]; + } + } + + // Filter only for one tag + if ($this->tagsIsEnabled()) { + if ($tagName = $this->request->getParam('tag')) { + $transforms[] = [$this->timelineQuery, 'transformTagFilter', $tagName]; + } + } + + // Filter only for one place + if ($this->placesIsEnabled()) { + if ($locationId = $this->request->getParam('place')) { + $transforms[] = [$this->timelineQuery, 'transformPlaceFilter', (int) $locationId]; + } + } + + // Filter geological bounds + $bounds = $this->request->getParam('mapbounds'); + if ($bounds) { + $transforms[] = [$this->timelineQuery, 'transformMapBoundsFilter', $bounds]; + } + + // Limit number of responses for day query + $limit = $this->request->getParam('limit'); + if ($limit) { + $transforms[] = [$this->timelineQuery, 'transformLimitDay', (int) $limit]; + } + + return $transforms; + } + /** * Preload a few "day" at the start of "days" response. * diff --git a/lib/Controller/LocationController.php b/lib/Controller/LocationController.php index 3f017a62..ea674afa 100644 --- a/lib/Controller/LocationController.php +++ b/lib/Controller/LocationController.php @@ -31,8 +31,6 @@ class LocationController extends ApiBase /** * @NoAdminRequired * - * @PublicPage - * * @NoCSRFRequired */ public function clusters(): JSONResponse @@ -46,47 +44,30 @@ class LocationController extends ApiBase // Get the folder to show $root = null; + try { $root = $this->getRequestRoot(); } catch (\Exception $e) { return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_NOT_FOUND); } - // Make sure we have bounds - // $bounds = $this->request->getParam('bounds'); - // if (!$bounds) { - // return new JSONResponse(['message' => 'Invalid perameters'], Http::STATUS_PRECONDITION_FAILED); - // } - - // // Make sure we have 4 bounds - // $bounds = explode(',', $bounds); - // $bounds = array_map('floatval', $bounds); - // if (4 !== \count($bounds)) { - // return new JSONResponse(['message' => 'Invalid perameters'], Http::STATUS_PRECONDITION_FAILED); - // } - + // Make sure we have bounds and zoom level // Zoom level is used to determine the grid length + $bounds = $this->request->getParam('bounds'); $zoomLevel = $this->request->getParam('zoom'); - if (!$zoomLevel || !is_numeric($zoomLevel)) { - return new JSONResponse(['message' => 'Invalid zoom level'], Http::STATUS_PRECONDITION_FAILED); + if (!$bounds || !$zoomLevel || !is_numeric($zoomLevel)) { + return new JSONResponse(['message' => 'Invalid parameters'], Http::STATUS_PRECONDITION_FAILED); } // A tweakable parameter to determine the number of boxes in the map $clusterDensity = 2; - $gridLength = 180.0 / (2 ** $zoomLevel * $clusterDensity); + $gridLen = 180.0 / (2 ** $zoomLevel * $clusterDensity); try { - $clusters = $this->timelineQuery->getMapClusters( - $gridLength, - $root, - $uid, - $this->isRecursive(), - $this->isArchive(), - $this->getTransformations(true), - ); + $clusters = $this->timelineQuery->getMapClusters($gridLen, $bounds, $root); // Merge clusters that are close together - $distanceThreshold = $gridLength / 3; + $distanceThreshold = $gridLen / 3; $clusters = $this->mergeClusters($clusters, $distanceThreshold); return new JSONResponse($clusters); @@ -125,7 +106,7 @@ class LocationController extends ApiBase return $updatedClusters; } - private function isCLose(array $cluster1, array $cluster2, float $threshold): bool + private function isClose(array $cluster1, array $cluster2, float $threshold): bool { $deltaX = (float) $cluster1['center'][0] - (float) $cluster2['center'][0]; $deltaY = (float) $cluster1['center'][1] - (float) $cluster2['center'][1]; diff --git a/lib/Db/TimelineQuery.php b/lib/Db/TimelineQuery.php index f281cdb9..acd91f9d 100644 --- a/lib/Db/TimelineQuery.php +++ b/lib/Db/TimelineQuery.php @@ -14,6 +14,7 @@ class TimelineQuery use TimelineQueryFilters; use TimelineQueryFolders; use TimelineQueryLivePhoto; + use TimelineQueryMap; use TimelineQueryPeopleFaceRecognition; use TimelineQueryPeopleRecognize; use TimelineQueryPlaces; diff --git a/lib/Db/TimelineQueryDays.php b/lib/Db/TimelineQueryDays.php index d2af7a52..1e238863 100644 --- a/lib/Db/TimelineQueryDays.php +++ b/lib/Db/TimelineQueryDays.php @@ -195,47 +195,6 @@ trait TimelineQueryDays return $this->processDay($rows, $uid, $root); } - public function getMapClusters( - float $gridLength, - TimelineRoot &$root, - string $uid, - bool $recursive, - bool $archive, - array $queryTransforms = [] - ): array { - $query = $this->connection->getQueryBuilder(); - - // Get the average location of each cluster - $avgLat = $query->createFunction('AVG(lat) AS avgLat'); - $avgLng = $query->createFunction('AVG(lon) AS avgLng'); - $count = $query->createFunction('COUNT(*) AS count'); - $query->select($avgLat, $avgLng, $count) - ->from('memories', 'm') - ; - - // JOIN with filecache for existing files - $query = $this->joinFilecache($query, $root, $recursive, $archive); - - // Group by cluster - $groupFunction = $query->createFunction('lat DIV '.$gridLength.', lon DIV '.$gridLength); - $query->groupBy($groupFunction); - - // Apply all transformations (including map bounds) - $this->applyAllTransforms($queryTransforms, $query, $uid); - - $cursor = $this->executeQueryWithCTEs($query); - $res = $cursor->fetchAll(); - $cursor->closeCursor(); - - $clusters = []; - foreach ($res as $cluster) { - $clusters[] = - ['center' => [(float) $cluster['avgLat'], (float) $cluster['avgLng']], 'count' => (float) $cluster['count']]; - } - - return $clusters; - } - /** * Process the days response. * diff --git a/lib/Db/TimelineQueryFilters.php b/lib/Db/TimelineQueryFilters.php index 83ad7c6f..bd57180c 100644 --- a/lib/Db/TimelineQueryFilters.php +++ b/lib/Db/TimelineQueryFilters.php @@ -40,18 +40,6 @@ trait TimelineQueryFilters $query->setMaxResults($limit); } - public function transformBoundFilter(IQueryBuilder &$query, string $userId, array $bounds) - { - $query->andWhere( - $query->expr()->andX( - $query->expr()->gte('m.lat', $query->createNamedParameter($bounds[0], IQueryBuilder::PARAM_STR)), - $query->expr()->lte('m.lat', $query->createNamedParameter($bounds[1], IQueryBuilder::PARAM_STR)), - $query->expr()->gte('m.lon', $query->createNamedParameter($bounds[2], IQueryBuilder::PARAM_STR)), - $query->expr()->lte('m.lon', $query->createNamedParameter($bounds[3], IQueryBuilder::PARAM_STR)) - ) - ); - } - private function applyAllTransforms(array $transforms, IQueryBuilder &$query, string $uid): void { foreach ($transforms as &$transform) { diff --git a/lib/Db/TimelineQueryMap.php b/lib/Db/TimelineQueryMap.php new file mode 100644 index 00000000..00edd5e3 --- /dev/null +++ b/lib/Db/TimelineQueryMap.php @@ -0,0 +1,74 @@ +andWhere( + $query->expr()->andX( + $query->expr()->gte('m.lat', $query->createNamedParameter($bounds[0], IQueryBuilder::PARAM_STR)), + $query->expr()->lte('m.lat', $query->createNamedParameter($bounds[1], IQueryBuilder::PARAM_STR)), + $query->expr()->gte('m.lon', $query->createNamedParameter($bounds[2], IQueryBuilder::PARAM_STR)), + $query->expr()->lte('m.lon', $query->createNamedParameter($bounds[3], IQueryBuilder::PARAM_STR)) + ) + ); + } + + public function getMapClusters( + float $gridLen, + string $bounds, + TimelineRoot &$root + ): array { + $query = $this->connection->getQueryBuilder(); + + // Get the average location of each cluster + $avgLat = $query->createFunction('AVG(m.lat) AS avgLat'); + $avgLng = $query->createFunction('AVG(m.lon) AS avgLon'); + $count = $query->createFunction('COUNT(m.fileid) AS count'); + $query->select($avgLat, $avgLng, $count) + ->from('memories', 'm') + ; + + // JOIN with filecache for existing files + $query = $this->joinFilecache($query, $root, true, false); + + // Group by cluster + $query->addGroupBy($query->createFunction("m.lat DIV {$gridLen}")); + $query->addGroupBy($query->createFunction("m.lon DIV {$gridLen}")); + + // Apply all transformations (including map bounds) + $this->transformMapBoundsFilter($query, '', $bounds); + + // Execute query + $cursor = $this->executeQueryWithCTEs($query); + $res = $cursor->fetchAll(); + $cursor->closeCursor(); + + // Post-process results + $clusters = []; + foreach ($res as $cluster) { + $clusters[] = + [ + 'center' => [ + (float) $cluster['avgLat'], + (float) $cluster['avgLon'], + ], + 'count' => (float) $cluster['count'], + ]; + } + + return $clusters; + } +} diff --git a/src/components/top-matter/LocationTopMatter.vue b/src/components/top-matter/LocationTopMatter.vue index b79f944a..17dfc572 100644 --- a/src/components/top-matter/LocationTopMatter.vue +++ b/src/components/top-matter/LocationTopMatter.vue @@ -23,7 +23,6 @@