//! Map lookups, proxied to the osm-tiles APIs and returned as an htmx
//! fragment of candidates. The page's map plots them and a click on one fills
//! the page's `#place-form`.
use std::collections::HashMap;
use axum::extract::{Query, State};
use axum::http::StatusCode;
use axum::response::Html;
use minijinja::context;
use sea_orm::*;
use serde::Deserialize;
use uuid::Uuid;
use tracing::warn;
use super::{internal, render};
use crate::access;
use crate::entities::area_members::Role;
use crate::entities::restaurants;
use crate::middleware::UserContext;
use crate::osm::Place;
use crate::AppState;
#[derive(Debug, Deserialize)]
pub struct SearchQuery {
pub q: String,
pub lat: Option<f64>,
pub lon: Option<f64>,
/// "1" limits to eateries (adding a restaurant); anything else searches
/// everything (picking an area's centre).
pub food: Option<String>,
/// The area being added to: marks candidates it already has.
pub area: Option<Uuid>,
}
pub async fn search(
State(state): State<AppState>,
user: UserContext,
Query(qs): Query<SearchQuery>,
) -> Result<Html<String>, StatusCode> {
if let Some(area) = qs.area {
access::require(state.db.as_ref(), area, user.user.id, Role::Voter).await?;
}
let q = qs.q.trim();
let near = qs.lat.zip(qs.lon);
let food = qs.food.as_deref() == Some("1");
let (mut places, failed) = if q.is_empty() {
(vec![], false)
} else {
match state.osm.search(q, near, food).await {
Ok(p) => (p, false),
Err(e) => {
warn!(error = %e, "photon search failed");
(vec![], true)
}
}
};
// Area searches ("Austin") have no cuisine to find.
if food {
state.osm.enrich(&mut places).await;
}
candidates(&state, places, failed, true, qs.area).await
}
#[derive(Debug, Deserialize)]
pub struct NearbyQuery {
pub lat: f64,
pub lon: f64,
pub area: Option<Uuid>,
}
pub async fn nearby(
State(state): State<AppState>,
user: UserContext,
Query(qs): Query<NearbyQuery>,
) -> Result<Html<String>, StatusCode> {
if let Some(area) = qs.area {
access::require(state.db.as_ref(), area, user.user.id, Role::Voter).await?;
}
let (mut places, failed) = match state.osm.nearby(qs.lat, qs.lon, 150).await {
Ok(p) => (p, false),
Err(e) => {
warn!(error = %e, "nearby lookup failed");
(vec![], true)
}
};
state.osm.enrich(&mut places).await;
candidates(&state, places, failed, false, qs.area).await
}
/// Render the candidate list, marking places that are already restaurants
/// in `area` so nobody adds the same one twice. Only that area is checked:
/// other areas may be private, and the same place can be in several. `fit` zooms the map to the results:
/// wanted for a search, which can land anywhere, not for a click's nearby.
async fn candidates(
state: &AppState,
places: Vec<Place>,
failed: bool,
fit: bool,
area: Option<Uuid>,
) -> Result<Html<String>, StatusCode> {
let refs: Vec<String> = places.iter().filter_map(|p| p.osm_ref.clone()).collect();
let existing: HashMap<String, uuid::Uuid> = match area {
None => HashMap::new(),
Some(_) if refs.is_empty() => HashMap::new(),
Some(area) => restaurants::Entity::find()
.filter(restaurants::Column::AreaId.eq(area))
.filter(restaurants::Column::OsmRef.is_in(refs))
.all(state.db.as_ref())
.await
.map_err(internal("load existing restaurants failed"))?
.into_iter()
.filter_map(|r| r.osm_ref.map(|o| (o, r.id)))
.collect(),
};
let rows: Vec<minijinja::Value> = places
.into_iter()
.map(|p| {
let existing_id = p.osm_ref.as_ref().and_then(|r| existing.get(r)).copied();
context! { place => p.clone(), json => p, existing_id => existing_id }
})
.collect();
render(state, "places/_candidates.html", context! { candidates => rows, failed => failed, fit => fit })
}
neiam
/fieri-forever
the flavortown lunch engine
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
4.0 KiB · text
History
aecd85b