map: refactor
parent
7d90aeacb1
commit
64d4205346
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -14,6 +14,7 @@ class TimelineQuery
|
|||
use TimelineQueryFilters;
|
||||
use TimelineQueryFolders;
|
||||
use TimelineQueryLivePhoto;
|
||||
use TimelineQueryMap;
|
||||
use TimelineQueryPeopleFaceRecognition;
|
||||
use TimelineQueryPeopleRecognize;
|
||||
use TimelineQueryPlaces;
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -23,7 +23,6 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { LMap, LTileLayer, LMarker, LPopup } from "vue2-leaflet";
|
||||
import { IMarkerCluster } from "../../types";
|
||||
import { Icon } from "leaflet";
|
||||
|
||||
import { API } from "../../services/API";
|
||||
|
@ -36,6 +35,11 @@ const TILE_URL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
|
|||
const ATTRIBUTION =
|
||||
'© <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors';
|
||||
|
||||
type IMarkerCluster = {
|
||||
center: [number, number];
|
||||
count: number;
|
||||
};
|
||||
|
||||
Icon.Default.mergeOptions({
|
||||
iconRetinaUrl: require("leaflet/dist/images/marker-icon-2x.png"),
|
||||
iconUrl: require("leaflet/dist/images/marker-icon.png"),
|
||||
|
@ -89,16 +93,9 @@ export default defineComponent({
|
|||
}
|
||||
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
|
||||
const query = new URLSearchParams();
|
||||
// query.set("minLat", (minLat - mapWidth).toString());
|
||||
// query.set("maxLat", (maxLat + mapWidth).toString());
|
||||
// query.set("minLon", (minLon - mapHeight).toString());
|
||||
// query.set("maxLon", (maxLon + mapHeight).toString());
|
||||
query.set("bounds", bounds);
|
||||
query.set("zoom", zoom);
|
||||
|
||||
// Make API call
|
||||
|
|
|
@ -14,7 +14,6 @@ import FolderTopMatter from "./FolderTopMatter.vue";
|
|||
import TagTopMatter from "./TagTopMatter.vue";
|
||||
import FaceTopMatter from "./FaceTopMatter.vue";
|
||||
import AlbumTopMatter from "./AlbumTopMatter.vue";
|
||||
import LocationTopMatter from "./LocationTopMatter.vue";
|
||||
|
||||
import { TopMatterType } from "../../types";
|
||||
|
||||
|
@ -25,7 +24,6 @@ export default defineComponent({
|
|||
TagTopMatter,
|
||||
FaceTopMatter,
|
||||
AlbumTopMatter,
|
||||
LocationTopMatter,
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
|
|
13
src/types.ts
13
src/types.ts
|
@ -217,7 +217,6 @@ export enum TopMatterType {
|
|||
TAG = 2,
|
||||
FACE = 3,
|
||||
ALBUM = 4,
|
||||
LOCATION = 5,
|
||||
}
|
||||
export type TopMatterFolder = TopMatter & {
|
||||
type: TopMatterType.FOLDER;
|
||||
|
@ -239,15 +238,3 @@ export type ISelectionAction = {
|
|||
/** Allow for public routes (default false) */
|
||||
allowPublic?: boolean;
|
||||
};
|
||||
|
||||
export type IMapBoundary = {
|
||||
minLat: number;
|
||||
maxLat: number;
|
||||
minLon: number;
|
||||
maxLon: number;
|
||||
};
|
||||
|
||||
export type IMarkerCluster = {
|
||||
center: [number, number];
|
||||
count: number;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue