2026-06-19 10:57:01 +00:00
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
|
|
use crate::application::dtos::geo_dto::{GeoBounds, GeoCluster};
|
|
|
|
|
use crate::common::errors::DomainError;
|
2026-07-01 21:54:29 +02:00
|
|
|
use crate::domain::services::authorization::Subject;
|
2026-06-19 10:57:01 +00:00
|
|
|
use crate::infrastructure::repositories::pg::FileBlobReadRepository;
|
2026-07-01 21:54:29 +02:00
|
|
|
use crate::infrastructure::services::pg_acl_engine::PgAclEngine;
|
2026-06-19 10:57:01 +00:00
|
|
|
|
|
|
|
|
/// "Places" use case: the caller's geotagged photos aggregated into map
|
|
|
|
|
/// clusters.
|
|
|
|
|
///
|
2026-07-01 21:54:29 +02:00
|
|
|
/// Post-§15 the surface follows the Photos scope: default personal drive
|
|
|
|
|
/// + drives where `policies.include_in_photo_index = true` AND caller
|
|
|
|
|
/// has Read. The repository query joins `role_grants` on the drive
|
|
|
|
|
/// resource type; group-mediated grants are honoured via the caller
|
|
|
|
|
/// expansion done here.
|
2026-06-19 10:57:01 +00:00
|
|
|
pub struct PlacesService {
|
|
|
|
|
file_read: Arc<FileBlobReadRepository>,
|
2026-07-01 21:54:29 +02:00
|
|
|
authorization: Arc<PgAclEngine>,
|
2026-06-19 10:57:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PlacesService {
|
2026-07-01 21:54:29 +02:00
|
|
|
pub fn new(file_read: Arc<FileBlobReadRepository>, authorization: Arc<PgAclEngine>) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
file_read,
|
|
|
|
|
authorization,
|
|
|
|
|
}
|
2026-06-19 10:57:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Aggregation cell side, in degrees, for a slippy-map zoom level. The
|
|
|
|
|
/// world (360°) is split into `2^zoom` tiles; we use ~4 cells per tile so
|
|
|
|
|
/// clusters refine as the user zooms in. Clamped to a sane range.
|
|
|
|
|
fn cell_for_zoom(zoom: u8) -> f64 {
|
|
|
|
|
let z = i32::from(zoom.min(20));
|
|
|
|
|
360.0 / (2_f64.powi(z) * 4.0)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-01 21:54:29 +02:00
|
|
|
/// Clustered geotagged photos in the caller's Photos-scope drive set,
|
|
|
|
|
/// within `bounds`.
|
2026-06-19 10:57:01 +00:00
|
|
|
pub async fn clusters(
|
|
|
|
|
&self,
|
|
|
|
|
caller_id: Uuid,
|
|
|
|
|
bounds: GeoBounds,
|
|
|
|
|
zoom: u8,
|
|
|
|
|
) -> Result<Vec<GeoCluster>, DomainError> {
|
|
|
|
|
let cell = Self::cell_for_zoom(zoom);
|
2026-07-01 21:54:29 +02:00
|
|
|
let (subject_types, subject_ids) = self
|
|
|
|
|
.authorization
|
|
|
|
|
.expand_subject_for_listing(Subject::User(caller_id))
|
|
|
|
|
.await?;
|
2026-06-19 10:57:01 +00:00
|
|
|
self.file_read
|
2026-07-01 21:54:29 +02:00
|
|
|
.list_geo_clusters(&subject_types, &subject_ids, bounds, cell)
|
2026-06-19 10:57:01 +00:00
|
|
|
.await
|
|
|
|
|
}
|
|
|
|
|
}
|