map: open photo on click at high zoom

pull/417/head
Varun Patil 2023-02-09 17:27:03 -08:00
parent f5b6f92339
commit cc2accae54
6 changed files with 64 additions and 52 deletions

View File

@ -67,7 +67,6 @@ return [
['name' => 'Places#preview', 'url' => '/api/places/preview/{id}', 'verb' => 'GET'], ['name' => 'Places#preview', 'url' => '/api/places/preview/{id}', 'verb' => 'GET'],
['name' => 'Map#clusters', 'url' => '/api/map/clusters', 'verb' => 'GET'], ['name' => 'Map#clusters', 'url' => '/api/map/clusters', 'verb' => 'GET'],
['name' => 'Map#clusterPreview', 'url' => '/api/map/clusters/preview/{id}', 'verb' => 'GET'],
['name' => 'Archive#archive', 'url' => '/api/archive/{id}', 'verb' => 'PATCH'], ['name' => 'Archive#archive', 'url' => '/api/archive/{id}', 'verb' => 'PATCH'],

View File

@ -57,42 +57,24 @@ class MapController extends ApiBase
try { try {
$clusters = $this->timelineQuery->getMapClusters($gridLen, $bounds, $root); $clusters = $this->timelineQuery->getMapClusters($gridLen, $bounds, $root);
// Get previews for each cluster
$clusterIds = array_map(function ($cluster) {
return (int) $cluster['id'];
}, $clusters);
$previews = $this->timelineQuery->getMapClusterPreviews($clusterIds, $root);
// Merge the responses
$fileMap = [];
foreach ($previews as &$preview) {
$fileMap[$preview['mapcluster']] = $preview;
}
foreach ($clusters as &$cluster) {
$cluster['preview'] = $fileMap[$cluster['id']] ?? null;
}
return new JSONResponse($clusters); return new JSONResponse($clusters);
} catch (\Exception $e) { } catch (\Exception $e) {
return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR); return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
} }
} }
/**
* @NoAdminRequired
*
* @NoCSRFRequired
*
* Get preview for a cluster
*/
public function clusterPreview(int $id)
{
$user = $this->userSession->getUser();
if (null === $user) {
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
}
// If this isn't the timeline folder then things aren't going to work
$root = $this->getRequestRoot();
if ($root->isEmpty()) {
return new JSONResponse([], Http::STATUS_NOT_FOUND);
}
// Run actual query
$list = $this->timelineQuery->getMapClusterPreviews($id, $root);
if (null === $list || 0 === \count($list)) {
return new JSONResponse([], Http::STATUS_NOT_FOUND);
}
shuffle($list);
// Get preview from image list
return $this->getPreviewFromImageList(array_map(static function (&$item) {
return (int) $item['fileid'];
}, $list), 256);
}
} }

View File

@ -92,30 +92,41 @@ trait TimelineQueryMap
return $clusters; return $clusters;
} }
public function getMapClusterPreviews(int $clusterId, TimelineRoot &$root) public function getMapClusterPreviews(array $clusterIds, TimelineRoot &$root)
{ {
$query = $this->connection->getQueryBuilder(); $query = $this->connection->getQueryBuilder();
// SELECT all photos with this tag // SELECT all photos with this tag
$query->select('f.fileid', 'f.etag')->from('memories', 'm')->where( $fileid = $query->createFunction('MAX(m.fileid) AS fileid');
$query->expr()->eq('m.mapcluster', $query->createNamedParameter($clusterId, IQueryBuilder::PARAM_INT)) $query->select($fileid)->from('memories', 'm')->where(
$query->expr()->in('m.mapcluster', $query->createNamedParameter($clusterIds, IQueryBuilder::PARAM_INT_ARRAY))
); );
// WHERE these photos are in the user's requested folder recursively // WHERE these photos are in the user's requested folder recursively
$query = $this->joinFilecache($query, $root, true, false); $query = $this->joinFilecache($query, $root, true, false);
// MAX 8 // GROUP BY the cluster
$query->setMaxResults(8); $query->groupBy('m.mapcluster');
// FETCH tag previews // Get the fileIds
$cursor = $this->executeQueryWithCTEs($query); $cursor = $this->executeQueryWithCTEs($query);
$ans = $cursor->fetchAll(); $fileIds = $cursor->fetchAll(\PDO::FETCH_COLUMN);
// SELECT these files from the filecache
$query = $this->connection->getQueryBuilder();
$query->select('m.fileid', 'm.dayid', 'm.mapcluster', 'f.etag')
->from('memories', 'm')
->innerJoin('m', 'filecache', 'f', $query->expr()->eq('m.fileid', 'f.fileid'))
->where($query->expr()->in('m.fileid', $query->createNamedParameter($fileIds, IQueryBuilder::PARAM_INT_ARRAY)));
$files = $query->executeQuery()->fetchAll();
// Post-process // Post-process
foreach ($ans as &$row) { foreach ($files as &$row) {
$row['fileid'] = (int) $row['fileid']; $row['fileid'] = (int) $row['fileid'];
$row['mapcluster'] = (int) $row['mapcluster'];
$row['dayid'] = (int) $row['dayid'];
} }
return $ans; return $files;
} }
} }

View File

@ -868,11 +868,7 @@ export default defineComponent({
/** Open viewer with given photo */ /** Open viewer with given photo */
openViewer(photo: IPhoto) { openViewer(photo: IPhoto) {
this.$router.push({ this.$router.push(utils.getViewerRoute(photo));
path: this.$route.path,
query: this.$route.query,
hash: utils.getViewerHash(photo),
});
}, },
}, },
}); });

View File

@ -14,7 +14,7 @@
v-for="cluster in clusters" v-for="cluster in clusters"
:key="cluster.id" :key="cluster.id"
:lat-lng="cluster.center" :lat-lng="cluster.center"
@click="zoomTo(cluster.center)" @click="zoomTo(cluster)"
> >
<LIcon :icon-anchor="[24, 24]"> <LIcon :icon-anchor="[24, 24]">
<div class="preview"> <div class="preview">
@ -32,9 +32,12 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { LMap, LTileLayer, LMarker, LPopup, LIcon } from "vue2-leaflet"; import { LMap, LTileLayer, LMarker, LPopup, LIcon } from "vue2-leaflet";
import { IPhoto } from "../../types";
import { API } from "../../services/API"; import { API } from "../../services/API";
import { getPreviewUrl } from "../../services/FileUtils";
import axios from "@nextcloud/axios"; import axios from "@nextcloud/axios";
import * as utils from "../../services/Utils";
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
@ -49,6 +52,7 @@ type IMarkerCluster = {
u?: any; u?: any;
center: [number, number]; center: [number, number];
count: number; count: number;
preview?: IPhoto;
}; };
export default defineComponent({ export default defineComponent({
@ -119,18 +123,26 @@ export default defineComponent({
}, },
clusterPreviewUrl(cluster: IMarkerCluster) { clusterPreviewUrl(cluster: IMarkerCluster) {
let url = API.MAP_CLUSTER_PREVIEW(cluster.id); let url = getPreviewUrl(cluster.preview, false, 256);
if (cluster.u) { if (cluster.u) {
url += `?u=${cluster.u}`; url += `?u=${cluster.u}`;
} }
return url; return url;
}, },
zoomTo(center: [number, number]) { zoomTo(cluster: IMarkerCluster) {
// At high zoom levels, open the photo
if (this.zoom >= 14 && cluster.preview) {
cluster.preview.key = cluster.preview.fileid.toString();
this.$router.push(utils.getViewerRoute(cluster.preview));
return;
}
// Zoom in
const map = this.$refs.map as LMap; const map = this.$refs.map as LMap;
const factor = globalThis.innerWidth >= 768 ? 2 : 1; const factor = globalThis.innerWidth >= 768 ? 2 : 1;
const zoom = map.mapObject.getZoom() + factor; const zoom = map.mapObject.getZoom() + factor;
map.mapObject.setView(center, zoom, { animate: true }); map.mapObject.setView(cluster.center, zoom, { animate: true });
}, },
}, },
}); });

View File

@ -287,6 +287,18 @@ export function getViewerHash(photo: IPhoto) {
return `#v/${photo.dayid}/${photo.key}`; return `#v/${photo.dayid}/${photo.key}`;
} }
/**
* Get route for viewer for photo
*/
export function getViewerRoute(photo: IPhoto) {
const $route = globalThis.vueroute();
return {
path: $route.path,
query: $route.query,
hash: getViewerHash(photo),
};
}
/** Set a timer that renews if existing */ /** Set a timer that renews if existing */
export function setRenewingTimeout( export function setRenewingTimeout(
ctx: any, ctx: any,