map: refactor

pull/396/head
Varun Patil 2023-02-08 13:53:38 -08:00
parent 7d90aeacb1
commit 64d4205346
10 changed files with 177 additions and 196 deletions

View File

@ -379,97 +379,6 @@ class ApiBase extends Controller
return \OCA\Memories\Util::placesGISType() > 0; 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. * Helper to get one file or null from a fiolder.
*/ */

View File

@ -172,6 +172,93 @@ class DaysController extends ApiBase
return $this->day($id); 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. * Preload a few "day" at the start of "days" response.
* *

View File

@ -31,8 +31,6 @@ class LocationController extends ApiBase
/** /**
* @NoAdminRequired * @NoAdminRequired
* *
* @PublicPage
*
* @NoCSRFRequired * @NoCSRFRequired
*/ */
public function clusters(): JSONResponse public function clusters(): JSONResponse
@ -46,47 +44,30 @@ class LocationController extends ApiBase
// Get the folder to show // Get the folder to show
$root = null; $root = null;
try { try {
$root = $this->getRequestRoot(); $root = $this->getRequestRoot();
} catch (\Exception $e) { } catch (\Exception $e) {
return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_NOT_FOUND); return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_NOT_FOUND);
} }
// Make sure we have bounds // Make sure we have bounds and zoom level
// $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);
// }
// Zoom level is used to determine the grid length // Zoom level is used to determine the grid length
$bounds = $this->request->getParam('bounds');
$zoomLevel = $this->request->getParam('zoom'); $zoomLevel = $this->request->getParam('zoom');
if (!$zoomLevel || !is_numeric($zoomLevel)) { if (!$bounds || !$zoomLevel || !is_numeric($zoomLevel)) {
return new JSONResponse(['message' => 'Invalid zoom level'], Http::STATUS_PRECONDITION_FAILED); return new JSONResponse(['message' => 'Invalid parameters'], Http::STATUS_PRECONDITION_FAILED);
} }
// A tweakable parameter to determine the number of boxes in the map // A tweakable parameter to determine the number of boxes in the map
$clusterDensity = 2; $clusterDensity = 2;
$gridLength = 180.0 / (2 ** $zoomLevel * $clusterDensity); $gridLen = 180.0 / (2 ** $zoomLevel * $clusterDensity);
try { try {
$clusters = $this->timelineQuery->getMapClusters( $clusters = $this->timelineQuery->getMapClusters($gridLen, $bounds, $root);
$gridLength,
$root,
$uid,
$this->isRecursive(),
$this->isArchive(),
$this->getTransformations(true),
);
// Merge clusters that are close together // Merge clusters that are close together
$distanceThreshold = $gridLength / 3; $distanceThreshold = $gridLen / 3;
$clusters = $this->mergeClusters($clusters, $distanceThreshold); $clusters = $this->mergeClusters($clusters, $distanceThreshold);
return new JSONResponse($clusters); return new JSONResponse($clusters);
@ -125,7 +106,7 @@ class LocationController extends ApiBase
return $updatedClusters; 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]; $deltaX = (float) $cluster1['center'][0] - (float) $cluster2['center'][0];
$deltaY = (float) $cluster1['center'][1] - (float) $cluster2['center'][1]; $deltaY = (float) $cluster1['center'][1] - (float) $cluster2['center'][1];

View File

@ -14,6 +14,7 @@ class TimelineQuery
use TimelineQueryFilters; use TimelineQueryFilters;
use TimelineQueryFolders; use TimelineQueryFolders;
use TimelineQueryLivePhoto; use TimelineQueryLivePhoto;
use TimelineQueryMap;
use TimelineQueryPeopleFaceRecognition; use TimelineQueryPeopleFaceRecognition;
use TimelineQueryPeopleRecognize; use TimelineQueryPeopleRecognize;
use TimelineQueryPlaces; use TimelineQueryPlaces;

View File

@ -195,47 +195,6 @@ trait TimelineQueryDays
return $this->processDay($rows, $uid, $root); 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. * Process the days response.
* *

View File

@ -40,18 +40,6 @@ trait TimelineQueryFilters
$query->setMaxResults($limit); $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 private function applyAllTransforms(array $transforms, IQueryBuilder &$query, string $uid): void
{ {
foreach ($transforms as &$transform) { foreach ($transforms as &$transform) {

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace OCA\Memories\Db;
use OCP\DB\QueryBuilder\IQueryBuilder;
trait TimelineQueryMap
{
public function transformMapBoundsFilter(IQueryBuilder &$query, string $userId, string $bounds)
{
$bounds = explode(',', $bounds);
$bounds = array_map('floatval', $bounds);
if (4 !== \count($bounds)) {
return;
}
$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))
)
);
}
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;
}
}

View File

@ -23,7 +23,6 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { LMap, LTileLayer, LMarker, LPopup } from "vue2-leaflet"; import { LMap, LTileLayer, LMarker, LPopup } from "vue2-leaflet";
import { IMarkerCluster } from "../../types";
import { Icon } from "leaflet"; import { Icon } from "leaflet";
import { API } from "../../services/API"; import { API } from "../../services/API";
@ -36,6 +35,11 @@ const TILE_URL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
const ATTRIBUTION = const ATTRIBUTION =
'&copy; <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors'; '&copy; <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors';
type IMarkerCluster = {
center: [number, number];
count: number;
};
Icon.Default.mergeOptions({ Icon.Default.mergeOptions({
iconRetinaUrl: require("leaflet/dist/images/marker-icon-2x.png"), iconRetinaUrl: require("leaflet/dist/images/marker-icon-2x.png"),
iconUrl: require("leaflet/dist/images/marker-icon.png"), iconUrl: require("leaflet/dist/images/marker-icon.png"),
@ -89,16 +93,9 @@ export default defineComponent({
} }
this.$router.replace({ query: { b: bounds, z: zoom } }); this.$router.replace({ query: { b: bounds, z: zoom } });
// Get query parameters for cluster API
// const mapWidth = maxLat - minLat;
// const mapHeight = maxLon - minLon;
// Show clusters correctly while draging the map // Show clusters correctly while draging the map
const query = new URLSearchParams(); const query = new URLSearchParams();
// query.set("minLat", (minLat - mapWidth).toString()); query.set("bounds", bounds);
// query.set("maxLat", (maxLat + mapWidth).toString());
// query.set("minLon", (minLon - mapHeight).toString());
// query.set("maxLon", (maxLon + mapHeight).toString());
query.set("zoom", zoom); query.set("zoom", zoom);
// Make API call // Make API call

View File

@ -14,7 +14,6 @@ import FolderTopMatter from "./FolderTopMatter.vue";
import TagTopMatter from "./TagTopMatter.vue"; import TagTopMatter from "./TagTopMatter.vue";
import FaceTopMatter from "./FaceTopMatter.vue"; import FaceTopMatter from "./FaceTopMatter.vue";
import AlbumTopMatter from "./AlbumTopMatter.vue"; import AlbumTopMatter from "./AlbumTopMatter.vue";
import LocationTopMatter from "./LocationTopMatter.vue";
import { TopMatterType } from "../../types"; import { TopMatterType } from "../../types";
@ -25,7 +24,6 @@ export default defineComponent({
TagTopMatter, TagTopMatter,
FaceTopMatter, FaceTopMatter,
AlbumTopMatter, AlbumTopMatter,
LocationTopMatter,
}, },
data: () => ({ data: () => ({

View File

@ -217,7 +217,6 @@ export enum TopMatterType {
TAG = 2, TAG = 2,
FACE = 3, FACE = 3,
ALBUM = 4, ALBUM = 4,
LOCATION = 5,
} }
export type TopMatterFolder = TopMatter & { export type TopMatterFolder = TopMatter & {
type: TopMatterType.FOLDER; type: TopMatterType.FOLDER;
@ -239,15 +238,3 @@ export type ISelectionAction = {
/** Allow for public routes (default false) */ /** Allow for public routes (default false) */
allowPublic?: boolean; allowPublic?: boolean;
}; };
export type IMapBoundary = {
minLat: number;
maxLat: number;
minLon: number;
maxLon: number;
};
export type IMarkerCluster = {
center: [number, number];
count: number;
};