draft: eager clustering
parent
74e69d0d6f
commit
e7c8748cc9
|
@ -67,6 +67,7 @@ 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'],
|
||||
|
||||
|
|
|
@ -297,7 +297,7 @@ class ApiBase extends Controller
|
|||
/**
|
||||
* Given a list of file ids, return the first preview image possible.
|
||||
*/
|
||||
protected function getPreviewFromImageList(array &$list)
|
||||
protected function getPreviewFromImageList(array &$list, int $quality=512)
|
||||
{
|
||||
// Get preview manager
|
||||
$previewManager = \OC::$server->get(\OCP\IPreview::class);
|
||||
|
@ -318,7 +318,7 @@ class ApiBase extends Controller
|
|||
|
||||
// Get preview image
|
||||
try {
|
||||
$preview = $previewManager->getPreview($files[0], 512, 512, false);
|
||||
$preview = $previewManager->getPreview($files[0], $quality, $quality, false);
|
||||
$response = new DataDisplayResponse($preview->getContent(), Http::STATUS_OK, [
|
||||
'Content-Type' => $preview->getMimeType(),
|
||||
]);
|
||||
|
|
|
@ -60,7 +60,7 @@ class MapController extends ApiBase
|
|||
$clusters = $this->timelineQuery->getMapClusters($gridLen, $bounds, $root);
|
||||
|
||||
// Merge clusters that are close together
|
||||
$distanceThreshold = $gridLen / 3;
|
||||
$distanceThreshold = $gridLen / 2;
|
||||
$clusters = $this->mergeClusters($clusters, $distanceThreshold);
|
||||
|
||||
return new JSONResponse($clusters);
|
||||
|
@ -69,6 +69,39 @@ class MapController extends ApiBase
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
private function mergeClusters($clusters, $distanceThreshold): array
|
||||
{
|
||||
$valid = array_fill(0, \count($clusters), true);
|
||||
|
@ -117,7 +150,11 @@ class MapController extends ApiBase
|
|||
($cluster1Count * $cluster1Center[0] + $cluster2Count * $cluster2Center[0]) / ($cluster1Count + $cluster2Count),
|
||||
($cluster1Count * $cluster1Center[1] + $cluster2Count * $cluster2Center[1]) / ($cluster1Count + $cluster2Count),
|
||||
];
|
||||
$clusters[] = ['center' => $newCenter, 'count' => $cluster1Count + $cluster2Count];
|
||||
$clusters[] = [
|
||||
'id' => $clusters[$index1]['id'],
|
||||
'center' => $newCenter,
|
||||
'count' => $cluster1Count + $cluster2Count,
|
||||
];
|
||||
$valid[] = true;
|
||||
$valid[$index1] = $valid[$index2] = false;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ trait TimelineQueryMap
|
|||
{
|
||||
protected IDBConnection $connection;
|
||||
|
||||
public function transformMapBoundsFilter(IQueryBuilder &$query, string $userId, string $bounds)
|
||||
public function transformMapBoundsFilter(IQueryBuilder &$query, string $userId, string $bounds, $table = 'm')
|
||||
{
|
||||
$bounds = explode(',', $bounds);
|
||||
$bounds = array_map('floatval', $bounds);
|
||||
|
@ -19,12 +19,14 @@ trait TimelineQueryMap
|
|||
return;
|
||||
}
|
||||
|
||||
$latCol = $table.'.lat';
|
||||
$lonCol = $table.'.lon';
|
||||
$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))
|
||||
$query->expr()->gte($latCol, $query->createNamedParameter($bounds[0], IQueryBuilder::PARAM_STR)),
|
||||
$query->expr()->lte($latCol, $query->createNamedParameter($bounds[1], IQueryBuilder::PARAM_STR)),
|
||||
$query->expr()->gte($lonCol, $query->createNamedParameter($bounds[2], IQueryBuilder::PARAM_STR)),
|
||||
$query->expr()->lte($lonCol, $query->createNamedParameter($bounds[3], IQueryBuilder::PARAM_STR))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -37,22 +39,31 @@ trait TimelineQueryMap
|
|||
$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');
|
||||
$lat = $query->createFunction('AVG(c.lat) AS lat');
|
||||
$lon = $query->createFunction('AVG(c.lon) AS lon');
|
||||
$count = $query->createFunction('COUNT(m.fileid) AS count');
|
||||
$query->select($avgLat, $avgLng, $count)
|
||||
->from('memories', 'm')
|
||||
|
||||
$query->select($lat, $lon, $count)
|
||||
->from('memories_map_clusters', 'c')
|
||||
;
|
||||
|
||||
if ($gridLen > 0.2) {
|
||||
// Coarse grouping
|
||||
$query->addGroupBy($query->createFunction("CAST(c.lat / {$gridLen} AS INT)"));
|
||||
$query->addGroupBy($query->createFunction("CAST(c.lon / {$gridLen} AS INT)"));
|
||||
} else {
|
||||
// Fine grouping
|
||||
$query->addSelect('c.id')->groupBy('c.id');
|
||||
}
|
||||
|
||||
// JOIN with memories for files from the current user
|
||||
$query->innerJoin('c', 'memories', 'm', $query->expr()->eq('c.id', 'm.map_cluster_id'));
|
||||
|
||||
// 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);
|
||||
// Bound the query to the map bounds
|
||||
$this->transformMapBoundsFilter($query, '', $bounds, 'c');
|
||||
|
||||
// Execute query
|
||||
$cursor = $this->executeQueryWithCTEs($query);
|
||||
|
@ -61,17 +72,47 @@ trait TimelineQueryMap
|
|||
|
||||
// Post-process results
|
||||
$clusters = [];
|
||||
foreach ($res as $cluster) {
|
||||
$clusters[] =
|
||||
[
|
||||
foreach ($res as &$cluster) {
|
||||
$c = [
|
||||
'center' => [
|
||||
(float) $cluster['avgLat'],
|
||||
(float) $cluster['avgLon'],
|
||||
(float) $cluster['lat'],
|
||||
(float) $cluster['lon'],
|
||||
],
|
||||
'count' => (float) $cluster['count'],
|
||||
];
|
||||
if (\array_key_exists('id', $cluster)) {
|
||||
$c['id'] = (int) $cluster['id'];
|
||||
}
|
||||
$clusters[] = $c;
|
||||
}
|
||||
|
||||
return $clusters;
|
||||
}
|
||||
|
||||
public function getMapClusterPreviews(int $clusterId, 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.map_cluster_id', $query->createNamedParameter($clusterId, IQueryBuilder::PARAM_INT))
|
||||
);
|
||||
|
||||
// WHERE these photos are in the user's requested folder recursively
|
||||
$query = $this->joinFilecache($query, $root, true, false);
|
||||
|
||||
// MAX 8
|
||||
$query->setMaxResults(8);
|
||||
|
||||
// FETCH tag previews
|
||||
$cursor = $this->executeQueryWithCTEs($query);
|
||||
$ans = $cursor->fetchAll();
|
||||
|
||||
// Post-process
|
||||
foreach ($ans as &$row) {
|
||||
$row['fileid'] = (int) $row['fileid'];
|
||||
}
|
||||
|
||||
return $ans;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,9 @@ const DELETE_TABLES = ['memories', 'memories_livephoto', 'memories_places'];
|
|||
|
||||
class TimelineWrite
|
||||
{
|
||||
use TimelineWriteMap;
|
||||
use TimelineWriteOrphans;
|
||||
use TimelineWritePlaces;
|
||||
protected IDBConnection $connection;
|
||||
protected IPreview $preview;
|
||||
protected LivePhoto $livePhoto;
|
||||
|
@ -79,7 +82,7 @@ class TimelineWrite
|
|||
|
||||
// Check if need to update
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->select('fileid', 'mtime')
|
||||
$query->select('fileid', 'mtime', 'map_cluster_id')
|
||||
->from('memories')
|
||||
->where($query->expr()->eq('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)))
|
||||
;
|
||||
|
@ -160,11 +163,13 @@ class TimelineWrite
|
|||
// Store location data
|
||||
$lat = null;
|
||||
$lon = null;
|
||||
$mapCluster = $prevRow ? (int) $prevRow['map_cluster_id'] : -1;
|
||||
if (\array_key_exists('GPSLatitude', $exif) && \array_key_exists('GPSLongitude', $exif)) {
|
||||
try {
|
||||
$lat = (float) $exif['GPSLatitude'];
|
||||
$lon = (float) $exif['GPSLongitude'];
|
||||
$this->updatePlacesData($file, $lat, $lon);
|
||||
$mapCluster = $this->getMapCluster($fileId, $mapCluster, $lat, $lon);
|
||||
$this->updatePlacesData($fileId, $lat, $lon);
|
||||
} catch (\Error $e) {
|
||||
$logger = \OC::$server->get(LoggerInterface::class);
|
||||
$logger->log(3, 'Error updating geo data: '.$e->getMessage(), ['app' => 'memories']);
|
||||
|
@ -186,6 +191,7 @@ class TimelineWrite
|
|||
'liveid' => $query->createNamedParameter($liveid, IQueryBuilder::PARAM_STR),
|
||||
'lat' => $query->createNamedParameter($lat, IQueryBuilder::PARAM_STR),
|
||||
'lon' => $query->createNamedParameter($lon, IQueryBuilder::PARAM_STR),
|
||||
'map_cluster_id' => $query->createNamedParameter($mapCluster, IQueryBuilder::PARAM_INT),
|
||||
];
|
||||
|
||||
if ($prevRow) {
|
||||
|
@ -239,103 +245,4 @@ class TimelineWrite
|
|||
$this->connection->executeStatement($p->getTruncateTableSQL('*PREFIX*'.$table, false));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a file as not orphaned.
|
||||
*/
|
||||
public function unorphan(File &$file)
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->update('memories')
|
||||
->set('orphan', $query->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))
|
||||
->where($query->expr()->eq('fileid', $query->createNamedParameter($file->getId(), IQueryBuilder::PARAM_INT)))
|
||||
;
|
||||
$query->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all files in the table as orphaned.
|
||||
*
|
||||
* @return int Number of rows affected
|
||||
*/
|
||||
public function orphanAll(): int
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->update('memories')
|
||||
->set('orphan', $query->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))
|
||||
;
|
||||
|
||||
return $query->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all entries that are orphans.
|
||||
*
|
||||
* @return int Number of rows affected
|
||||
*/
|
||||
public function removeOrphans(): int
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->delete('memories')
|
||||
->where($query->expr()->eq('orphan', $query->createNamedParameter(true, IQueryBuilder::PARAM_BOOL)))
|
||||
;
|
||||
|
||||
return $query->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add places data for a file.
|
||||
*/
|
||||
public function updatePlacesData(File &$file, float $lat, float $lon): void
|
||||
{
|
||||
// Get GIS type
|
||||
$gisType = \OCA\Memories\Util::placesGISType();
|
||||
|
||||
// Construct WHERE clause depending on GIS type
|
||||
$where = null;
|
||||
if (1 === $gisType) {
|
||||
$where = "ST_Contains(geometry, ST_GeomFromText('POINT({$lon} {$lat})'))";
|
||||
} elseif (2 === $gisType) {
|
||||
$where = "POINT('{$lon},{$lat}') <@ geometry";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make query to memories_planet table
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->select($query->createFunction('DISTINCT(osm_id)'))
|
||||
->from('memories_planet_geometry')
|
||||
->where($query->createFunction($where))
|
||||
;
|
||||
|
||||
// Cancel out inner rings
|
||||
$query->groupBy('poly_id', 'osm_id');
|
||||
$query->having($query->createFunction('SUM(type_id) > 0'));
|
||||
|
||||
// memories_planet_geometry has no *PREFIX*
|
||||
$sql = str_replace('*PREFIX*memories_planet_geometry', 'memories_planet_geometry', $query->getSQL());
|
||||
|
||||
// Run query
|
||||
$result = $this->connection->executeQuery($sql);
|
||||
$rows = $result->fetchAll();
|
||||
|
||||
// Delete previous records
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->delete('memories_places')
|
||||
->where($query->expr()->eq('fileid', $query->createNamedParameter($file->getId(), IQueryBuilder::PARAM_INT)))
|
||||
;
|
||||
$query->executeStatement();
|
||||
|
||||
// Insert records
|
||||
foreach ($rows as $row) {
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->insert('memories_places')
|
||||
->values([
|
||||
'fileid' => $query->createNamedParameter($file->getId(), IQueryBuilder::PARAM_INT),
|
||||
'osm_id' => $query->createNamedParameter($row['osm_id'], IQueryBuilder::PARAM_INT),
|
||||
])
|
||||
;
|
||||
$query->executeStatement();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Memories\Db;
|
||||
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
trait TimelineWriteMap
|
||||
{
|
||||
protected IDBConnection $connection;
|
||||
|
||||
protected function getMapCluster(int $fileId, int $prevCluster, float $lat, float $lon): int
|
||||
{
|
||||
// get all clusters within 30 metres
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->select('id')
|
||||
->from('memories_map_clusters')
|
||||
->andWhere($query->expr()->gte('lat', $query->createNamedParameter($lat - 0.0003, IQueryBuilder::PARAM_STR)))
|
||||
->andWhere($query->expr()->lte('lat', $query->createNamedParameter($lat + 0.0003, IQueryBuilder::PARAM_STR)))
|
||||
->andWhere($query->expr()->gte('lon', $query->createNamedParameter($lon - 0.0003, IQueryBuilder::PARAM_STR)))
|
||||
->andWhere($query->expr()->lte('lon', $query->createNamedParameter($lon + 0.0003, IQueryBuilder::PARAM_STR)))
|
||||
;
|
||||
|
||||
$result = $query->executeQuery();
|
||||
$rows = $result->fetchAll();
|
||||
|
||||
// Find cluster closest to the point
|
||||
$minDist = 999999999;
|
||||
$minId = -1;
|
||||
foreach ($rows as $r) {
|
||||
$clusterLat = (float) $r['lat'];
|
||||
$clusterLon = (float) $r['lon'];
|
||||
$dist = ($lat - $clusterLat) ** 2 + ($lon - $clusterLon) ** 2;
|
||||
if ($dist < $minDist) {
|
||||
$minDist = $dist;
|
||||
$minId = $r['id'];
|
||||
}
|
||||
}
|
||||
|
||||
// If no cluster found, create a new one
|
||||
if ($minId <= 0) {
|
||||
$this->removeFromCluster($prevCluster, $lat, $lon);
|
||||
|
||||
return $this->createMapCluster($lat, $lon);
|
||||
}
|
||||
|
||||
// If the file was previously in the same cluster, return that
|
||||
if ($prevCluster === $minId) {
|
||||
return $minId;
|
||||
}
|
||||
|
||||
// If the file was previously in a different cluster,
|
||||
// remove it from the first cluster and add it to the second
|
||||
$this->removeFromCluster($prevCluster, $lat, $lon);
|
||||
$this->addToCluster($minId, $lat, $lon);
|
||||
|
||||
return $minId;
|
||||
}
|
||||
|
||||
protected function createMapCluster(float $lat, float $lon): int
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->insert('memories_map_clusters')
|
||||
->values([
|
||||
'point_count' => $query->createNamedParameter(1, IQueryBuilder::PARAM_INT),
|
||||
'lat_sum' => $query->createNamedParameter($lat, IQueryBuilder::PARAM_STR),
|
||||
'lon_sum' => $query->createNamedParameter($lon, IQueryBuilder::PARAM_STR),
|
||||
'lat' => $query->createNamedParameter($lat, IQueryBuilder::PARAM_STR),
|
||||
'lon' => $query->createNamedParameter($lon, IQueryBuilder::PARAM_STR),
|
||||
])
|
||||
;
|
||||
$query->executeStatement();
|
||||
|
||||
return (int) $query->getLastInsertId();
|
||||
}
|
||||
|
||||
protected function removeFromCluster(int $clusterId, float $lat, float $lon): void
|
||||
{
|
||||
if ($clusterId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->update('memories_map_clusters')
|
||||
->set('point_count', $query->createFunction('point_count - 1'))
|
||||
->set('lat_sum', $query->createFunction('lat_sum - '.$lat))
|
||||
->set('lon_sum', $query->createFunction('lon_sum - '.$lon))
|
||||
->set('lat', $query->createFunction('lat_sum / point_count'))
|
||||
->set('lon', $query->createFunction('lon_sum / point_count'))
|
||||
->where($query->expr()->eq('id', $query->createNamedParameter($clusterId, IQueryBuilder::PARAM_INT)))
|
||||
;
|
||||
$query->executeStatement();
|
||||
}
|
||||
|
||||
protected function addToCluster(int $clusterId, float $lat, float $lon): void
|
||||
{
|
||||
if ($clusterId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->update('memories_map_clusters')
|
||||
->set('point_count', $query->createFunction('point_count + 1'))
|
||||
->set('lat_sum', $query->createFunction('lat_sum + '.$lat))
|
||||
->set('lon_sum', $query->createFunction('lon_sum + '.$lon))
|
||||
->set('lat', $query->createFunction('lat_sum / point_count'))
|
||||
->set('lon', $query->createFunction('lon_sum / point_count'))
|
||||
->where($query->expr()->eq('id', $query->createNamedParameter($clusterId, IQueryBuilder::PARAM_INT)))
|
||||
;
|
||||
$query->executeStatement();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Memories\Db;
|
||||
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\Files\File;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
trait TimelineWriteOrphans
|
||||
{
|
||||
protected IDBConnection $connection;
|
||||
|
||||
/**
|
||||
* Mark a file as not orphaned.
|
||||
*/
|
||||
public function unorphan(File &$file)
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->update('memories')
|
||||
->set('orphan', $query->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))
|
||||
->where($query->expr()->eq('fileid', $query->createNamedParameter($file->getId(), IQueryBuilder::PARAM_INT)))
|
||||
;
|
||||
$query->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all files in the table as orphaned.
|
||||
*
|
||||
* @return int Number of rows affected
|
||||
*/
|
||||
public function orphanAll(): int
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->update('memories')
|
||||
->set('orphan', $query->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))
|
||||
;
|
||||
|
||||
return $query->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all entries that are orphans.
|
||||
*
|
||||
* @return int Number of rows affected
|
||||
*/
|
||||
public function removeOrphans(): int
|
||||
{
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->delete('memories')
|
||||
->where($query->expr()->eq('orphan', $query->createNamedParameter(true, IQueryBuilder::PARAM_BOOL)))
|
||||
;
|
||||
|
||||
return $query->executeStatement();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\Memories\Db;
|
||||
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
trait TimelineWritePlaces
|
||||
{
|
||||
protected IDBConnection $connection;
|
||||
|
||||
/**
|
||||
* Add places data for a file.
|
||||
*/
|
||||
protected function updatePlacesData(int $fileId, float $lat, float $lon): void
|
||||
{
|
||||
// Get GIS type
|
||||
$gisType = \OCA\Memories\Util::placesGISType();
|
||||
|
||||
// Construct WHERE clause depending on GIS type
|
||||
$where = null;
|
||||
if (1 === $gisType) {
|
||||
$where = "ST_Contains(geometry, ST_GeomFromText('POINT({$lon} {$lat})'))";
|
||||
} elseif (2 === $gisType) {
|
||||
$where = "POINT('{$lon},{$lat}') <@ geometry";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make query to memories_planet table
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->select($query->createFunction('DISTINCT(osm_id)'))
|
||||
->from('memories_planet_geometry')
|
||||
->where($query->createFunction($where))
|
||||
;
|
||||
|
||||
// Cancel out inner rings
|
||||
$query->groupBy('poly_id', 'osm_id');
|
||||
$query->having($query->createFunction('SUM(type_id) > 0'));
|
||||
|
||||
// memories_planet_geometry has no *PREFIX*
|
||||
$sql = str_replace('*PREFIX*memories_planet_geometry', 'memories_planet_geometry', $query->getSQL());
|
||||
|
||||
// Run query
|
||||
$result = $this->connection->executeQuery($sql);
|
||||
$rows = $result->fetchAll();
|
||||
|
||||
// Delete previous records
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->delete('memories_places')
|
||||
->where($query->expr()->eq('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)))
|
||||
;
|
||||
$query->executeStatement();
|
||||
|
||||
// Insert records
|
||||
foreach ($rows as $row) {
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->insert('memories_places')
|
||||
->values([
|
||||
'fileid' => $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT),
|
||||
'osm_id' => $query->createNamedParameter($row['osm_id'], IQueryBuilder::PARAM_INT),
|
||||
])
|
||||
;
|
||||
$query->executeStatement();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="map-matter">
|
||||
<l-map
|
||||
<LMap
|
||||
class="map"
|
||||
ref="map"
|
||||
:zoom="zoom"
|
||||
|
@ -8,21 +8,26 @@
|
|||
@moveend="refresh"
|
||||
@zoomend="refresh"
|
||||
>
|
||||
<l-tile-layer :url="url" :attribution="attribution" />
|
||||
<l-marker
|
||||
<LTileLayer :url="url" :attribution="attribution" />
|
||||
<LMarker
|
||||
v-for="cluster in clusters"
|
||||
:key="cluster.center.toString()"
|
||||
:lat-lng="cluster.center"
|
||||
>
|
||||
<l-popup :content="cluster.count.toString()" />
|
||||
</l-marker>
|
||||
</l-map>
|
||||
<LIcon v-if="cluster.id" :icon-anchor="[24, 24]">
|
||||
<div class="preview">
|
||||
<div class="count">{{ cluster.count }}</div>
|
||||
<img :src="clusterPreviewUrl(cluster)" />
|
||||
</div>
|
||||
</LIcon>
|
||||
</LMarker>
|
||||
</LMap>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { LMap, LTileLayer, LMarker, LPopup } from "vue2-leaflet";
|
||||
import { LMap, LTileLayer, LMarker, LPopup, LIcon } from "vue2-leaflet";
|
||||
import { Icon } from "leaflet";
|
||||
|
||||
import { API } from "../../services/API";
|
||||
|
@ -35,6 +40,7 @@ const ATTRIBUTION =
|
|||
'© <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors';
|
||||
|
||||
type IMarkerCluster = {
|
||||
id?: number;
|
||||
center: [number, number];
|
||||
count: number;
|
||||
};
|
||||
|
@ -52,6 +58,7 @@ export default defineComponent({
|
|||
LTileLayer,
|
||||
LMarker,
|
||||
LPopup,
|
||||
LIcon,
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
|
@ -101,6 +108,10 @@ export default defineComponent({
|
|||
const res = await axios.get(url);
|
||||
this.clusters = res.data;
|
||||
},
|
||||
|
||||
clusterPreviewUrl(cluster: IMarkerCluster) {
|
||||
return API.MAP_CLUSTER_PREVIEW(cluster.id);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@ -117,4 +128,42 @@ export default defineComponent({
|
|||
margin: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.preview {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background-color: #fff;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.8);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.count {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background-color: var(--color-primary-default);
|
||||
color: var(--color-primary-text);
|
||||
padding: 0 4px;
|
||||
border-radius: 5px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
// Show leaflet marker on top on hover
|
||||
.leaflet-marker-icon:hover {
|
||||
z-index: 100000 !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -126,4 +126,8 @@ export class API {
|
|||
static MAP_CLUSTERS() {
|
||||
return tok(gen(`${BASE}/map/clusters`));
|
||||
}
|
||||
|
||||
static MAP_CLUSTER_PREVIEW(id: number) {
|
||||
return tok(gen(`${BASE}/map/clusters/preview/{id}`, { id }));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue