3.2 KiB · text History 25c4237
//! 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 tracing::warn;

use super::{internal, render};
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>,
}

pub async fn search(
    State(state): State<AppState>,
    _user: UserContext,
    Query(qs): Query<SearchQuery>,
) -> Result<Html<String>, StatusCode> {
    let q = qs.q.trim();
    let near = qs.lat.zip(qs.lon);
    let food = qs.food.as_deref() == Some("1");
    let (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)
            }
        }
    };
    candidates(&state, places, failed, true).await
}

#[derive(Debug, Deserialize)]
pub struct NearbyQuery {
    pub lat: f64,
    pub lon: f64,
}

pub async fn nearby(
    State(state): State<AppState>,
    _user: UserContext,
    Query(qs): Query<NearbyQuery>,
) -> Result<Html<String>, StatusCode> {
    let (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)
        }
    };
    candidates(&state, places, failed, false).await
}

/// Render the candidate list, marking places that are already restaurants
/// here so nobody adds the same one twice. `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,
) -> Result<Html<String>, StatusCode> {
    let refs: Vec<String> = places.iter().filter_map(|p| p.osm_ref.clone()).collect();
    let existing: HashMap<String, uuid::Uuid> = if refs.is_empty() {
        HashMap::new()
    } else {
        restaurants::Entity::find()
            .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 })
}