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' => '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'],
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue