map: open photo on click at high zoom
parent
f5b6f92339
commit
cc2accae54
|
@ -67,7 +67,6 @@ return [
|
|||
['name' => 'Places#preview', 'url' => '/api/places/preview/{id}', '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'],
|
||||
|
||||
|
|
|
@ -57,42 +57,24 @@ class MapController extends ApiBase
|
|||
try {
|
||||
$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);
|
||||
} catch (\Exception $e) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,30 +92,41 @@ trait TimelineQueryMap
|
|||
return $clusters;
|
||||
}
|
||||
|
||||
public function getMapClusterPreviews(int $clusterId, TimelineRoot &$root)
|
||||
public function getMapClusterPreviews(array $clusterIds, TimelineRoot &$root)
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
|
||||
// SELECT all photos with this tag
|
||||
$query->select('f.fileid', 'f.etag')->from('memories', 'm')->where(
|
||||
$query->expr()->eq('m.mapcluster', $query->createNamedParameter($clusterId, IQueryBuilder::PARAM_INT))
|
||||
$fileid = $query->createFunction('MAX(m.fileid) AS fileid');
|
||||
$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
|
||||
$query = $this->joinFilecache($query, $root, true, false);
|
||||
|
||||
// MAX 8
|
||||
$query->setMaxResults(8);
|
||||
// GROUP BY the cluster
|
||||
$query->groupBy('m.mapcluster');
|
||||
|
||||
// FETCH tag previews
|
||||
// Get the fileIds
|
||||
$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
|
||||
foreach ($ans as &$row) {
|
||||
foreach ($files as &$row) {
|
||||
$row['fileid'] = (int) $row['fileid'];
|
||||
$row['mapcluster'] = (int) $row['mapcluster'];
|
||||
$row['dayid'] = (int) $row['dayid'];
|
||||
}
|
||||
|
||||
return $ans;
|
||||
return $files;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -868,11 +868,7 @@ export default defineComponent({
|
|||
|
||||
/** Open viewer with given photo */
|
||||
openViewer(photo: IPhoto) {
|
||||
this.$router.push({
|
||||
path: this.$route.path,
|
||||
query: this.$route.query,
|
||||
hash: utils.getViewerHash(photo),
|
||||
});
|
||||
this.$router.push(utils.getViewerRoute(photo));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
v-for="cluster in clusters"
|
||||
:key="cluster.id"
|
||||
:lat-lng="cluster.center"
|
||||
@click="zoomTo(cluster.center)"
|
||||
@click="zoomTo(cluster)"
|
||||
>
|
||||
<LIcon :icon-anchor="[24, 24]">
|
||||
<div class="preview">
|
||||
|
@ -32,9 +32,12 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { LMap, LTileLayer, LMarker, LPopup, LIcon } from "vue2-leaflet";
|
||||
import { IPhoto } from "../../types";
|
||||
|
||||
import { API } from "../../services/API";
|
||||
import { getPreviewUrl } from "../../services/FileUtils";
|
||||
import axios from "@nextcloud/axios";
|
||||
import * as utils from "../../services/Utils";
|
||||
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
||||
|
@ -49,6 +52,7 @@ type IMarkerCluster = {
|
|||
u?: any;
|
||||
center: [number, number];
|
||||
count: number;
|
||||
preview?: IPhoto;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
|
@ -119,18 +123,26 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
clusterPreviewUrl(cluster: IMarkerCluster) {
|
||||
let url = API.MAP_CLUSTER_PREVIEW(cluster.id);
|
||||
let url = getPreviewUrl(cluster.preview, false, 256);
|
||||
if (cluster.u) {
|
||||
url += `?u=${cluster.u}`;
|
||||
}
|
||||
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 factor = globalThis.innerWidth >= 768 ? 2 : 1;
|
||||
const zoom = map.mapObject.getZoom() + factor;
|
||||
map.mapObject.setView(center, zoom, { animate: true });
|
||||
map.mapObject.setView(cluster.center, zoom, { animate: true });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -287,6 +287,18 @@ export function getViewerHash(photo: IPhoto) {
|
|||
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 */
|
||||
export function setRenewingTimeout(
|
||||
ctx: any,
|
||||
|
|
Loading…
Reference in New Issue