Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add map #337

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft

feat: Add map #337

wants to merge 2 commits into from

Conversation

RmStorm
Copy link

@RmStorm RmStorm commented Dec 5, 2024

Started work on a map. I considered spatialite or running clustering in Rust but ultimately you want the map to be responsive. So clusters must appear and disappear based on zoomstate and doing backend roundtrips on those just didn't seem like a simple or sane starting point.

What this code does now is basically load every image with a location and send their url's and positions over to the frontend. Mapbox is then responsible for clustering them. When enabling clustering on a geojson source mapbox adds some properties to points that represent clusters. This can be used to render clusters in one layer with simple circles. Points that represent individual images can be displayed in another layer (a symbol layer).

One clever thingy here is that the thumbnails are not loaded up front! So I send over urls for every single image with a position but they are not loaded untill needed. And this is done by listening to mapbox's styleimagemissing event. When that happens we add that specific thumbnail to the map and 🎉 it works with almost no latency.

Work left to do:

  • Test performance with many more images. Off my couple 1000 images only 700 actually have location data in the exif.
  • When clicking on an image display it in a preview. My idea is to make a preview screen with all the images that were under the cursor when clicked or something.

Really naive way of doing it, no clustering. just load a 1000 thumbnails haha
@RmStorm RmStorm changed the title Add map feat: Add map Dec 5, 2024
Copy link
Owner

@gbbirkisson gbbirkisson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really love this PR. I think it will be an awesome feature, just needs a bit more work 😄

Comment on lines +43 to +72
Some(data) => match data.pos {
Some(p) => {
sqlx::query!(
r#"
INSERT OR REPLACE INTO media ( id, path, taken_at, type, latitude, longitude, walked )
VALUES ( ?1, ?2, ?3, ?4, ?5, ?6, 1 )
"#,
processed_media.uuid,
processed_media.path,
data.taken_at,
media_type,
p.0,
p.1,
).execute(pool).await?
}
None => {
sqlx::query!(
r#"
INSERT OR REPLACE INTO media ( id, path, taken_at, type, walked )
VALUES ( ?1, ?2, ?3, ?4, 1 )
"#,
processed_media.uuid,
processed_media.path,
data.taken_at,
media_type,
)
.execute(pool)
.await?
}
},
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Some(data) => match data.pos {
Some(p) => {
sqlx::query!(
r#"
INSERT OR REPLACE INTO media ( id, path, taken_at, type, latitude, longitude, walked )
VALUES ( ?1, ?2, ?3, ?4, ?5, ?6, 1 )
"#,
processed_media.uuid,
processed_media.path,
data.taken_at,
media_type,
p.0,
p.1,
).execute(pool).await?
}
None => {
sqlx::query!(
r#"
INSERT OR REPLACE INTO media ( id, path, taken_at, type, walked )
VALUES ( ?1, ?2, ?3, ?4, 1 )
"#,
processed_media.uuid,
processed_media.path,
data.taken_at,
media_type,
)
.execute(pool)
.await?
}
},
Some(data) => {
let lat = data.pos.map(|p| p.0);
let lon = data.pos.map(|p| p.1);
sqlx::query!(
r#"
INSERT OR REPLACE INTO media ( id, path, taken_at, type, latitude, longitude, walked )
VALUES ( ?1, ?2, ?3, ?4, ?5, ?6, 1 )
"#,
processed_media.uuid,
processed_media.path,
data.taken_at,
media_type,
lat,
lon
).execute(pool).await?
}

Comment on lines +284 to +294
#[allow(clippy::future_not_send)]
pub async fn media_with_pos(pool: &SqlitePool) -> Result<Vec<MediaRow>> {
let query = r"
SELECT id, path, taken_at, type as media_type, archived, favorite, latitude, longitude FROM media
WHERE latitude IS NOT NULL;
"
.to_string();
let query = sqlx::query_as::<Sqlite, MediaRow>(&query);
query.fetch_all(pool).await.wrap_err("Failed to fetch rows")
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[allow(clippy::future_not_send)]
pub async fn media_with_pos(pool: &SqlitePool) -> Result<Vec<MediaRow>> {
let query = r"
SELECT id, path, taken_at, type as media_type, archived, favorite, latitude, longitude FROM media
WHERE latitude IS NOT NULL;
"
.to_string();
let query = sqlx::query_as::<Sqlite, MediaRow>(&query);
query.fetch_all(pool).await.wrap_err("Failed to fetch rows")
}
#[allow(clippy::future_not_send)]
pub async fn media_with_pos(pool: &SqlitePool, filter: impl Into<Filter>) -> Result<Vec<MediaRow>> {
let filter = filter.into();
let query = format!(
r"
SELECT id, path, taken_at, type as media_type, archived, favorite, latitude, longitude FROM media
{filter} AND latitude IS NOT NULL;
"
);
let mut query = sqlx::query_as::<Sqlite, MediaRow>(&query);
query = filter.bind(query);
query.fetch_all(pool).await.wrap_err("Failed to fetch rows")
}

Allow filtering, more on that below ....

While you are developing you can just add this implementation to call your method:

impl From<()> for Filter {
    fn from(_value: ()) -> Self {
        Self {
            archived: false,
            favorite: None,
            taken_after: None,
            taken_before: None,
        }
    }
}

...
db::media_with_pos(pool, ())
....

@@ -3,7 +3,7 @@ use color_eyre::{
eyre::{eyre, Context},
Result,
};
use exif::{Exif, In, Tag, Value};
use exif::{Exif, In, Rational, Tag, Value};
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
use exif::{Exif, In, Rational, Tag, Value};
use exif::{Exif, In, Tag, Value};

}

pub(super) async fn render(pool: &Pool<Sqlite>, config: &Config, state: State) -> Response {
let media = db::media_with_pos(pool)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let media = db::media_with_pos(pool)
let media = db::media_with_pos(pool, &state)

Pass in state to filter ....

pub fn run(listener: Listener, pool: Pool<Sqlite>, config: Config) -> Result<Server> {
let pool = web::Data::new(pool);
let config = web::Data::new(config);

let server = HttpServer::new(move || {
let mut app = App::new();
let mut app = App::new().wrap(from_fn(log_errors));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, didn't realize this was a problem 🙈

Comment on lines +18 to +20
<link rel="stylesheet" href="https://api.mapbox.com/mapbox-gl-js/v3.8.0/mapbox-gl.css">

<script src="https://api.mapbox.com/mapbox-gl-js/v3.8.0/mapbox-gl.js"></script>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we pull in the dependencies and bake them into the binary, similar to what I do here. I was very much trying to make an effort to have the server no depend on any external dependencies during runtime.

Comment on lines +34 to +35
<!-- <div hx-get="/hx/gallery" -->
<div hx-get="/hx/map"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of doing this, my idea is to keep the filter bar in the regular view, and just replace the <div class="media-gallery"> with the map. That way we can still use the filters while using the map ...

@@ -0,0 +1,139 @@
<div id="map"></div>
<script>
mapboxgl.accessToken = 'pk.eyJ1Ijoicm9hbGRzdG9ybSIsImEiOiJjbHdkaDhlN2QxNGFvMnFwcDB6YmFjd3RsIn0.kZAP04DN43268ORUH5bJsw';
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be parameterized, and maybe just disable the map functionality all together if the token is not supplied.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants