perfect-postcode/server-rs/src/routes/travel_destinations.rs
2026-03-17 21:08:32 +00:00

96 lines
2.9 KiB
Rust

use std::sync::Arc;
use axum::extract::{Query, State};
use axum::http::StatusCode;
use axum::response::Json;
use rustc_hash::FxHashSet;
use serde::{Deserialize, Serialize};
use tracing::info;
use crate::data::slugify;
use crate::state::SharedState;
#[derive(Serialize)]
pub struct DestinationResult {
name: String,
slug: String,
place_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
city: Option<String>,
}
#[derive(Serialize)]
pub struct DestinationsResponse {
destinations: Vec<DestinationResult>,
}
#[derive(Deserialize)]
pub struct DestinationsParams {
mode: String,
}
pub async fn get_travel_destinations(
State(shared): State<Arc<SharedState>>,
Query(params): Query<DestinationsParams>,
) -> Result<Json<DestinationsResponse>, (StatusCode, String)> {
let state = shared.load_state();
let mode = params.mode;
let destinations = tokio::task::spawn_blocking(move || {
let t0 = std::time::Instant::now();
let pd = &state.place_data;
let tt_store = &state.travel_time_store;
let slug_set = match tt_store.destinations.get(&mode) {
Some(slugs) => slugs,
None => return Vec::new(),
};
// Find places that have travel time data for this mode
let mut matches: Vec<(usize, String, u8, u32, usize)> = pd
.name
.iter()
.enumerate()
.filter_map(|(idx, name)| {
let slug = slugify(name);
if slug_set.contains(&slug) {
Some((idx, slug, pd.type_rank[idx], pd.population[idx], name.len()))
} else {
None
}
})
.collect();
// Sort: type rank asc, population desc, name length asc
matches.sort_unstable_by(|a, b| a.2.cmp(&b.2).then(b.3.cmp(&a.3)).then(a.4.cmp(&b.4)));
// Deduplicate by slug — multiple places can share a name/slug
// (e.g. "Richmond" as city + suburb), keep the best-ranked one
let mut seen_slugs = FxHashSet::default();
matches.retain(|(_, slug, ..)| seen_slugs.insert(slug.clone()));
let results: Vec<DestinationResult> = matches
.into_iter()
.map(|(idx, slug, ..)| DestinationResult {
name: pd.name[idx].clone(),
slug,
place_type: pd.place_type.get(idx).to_string(),
city: pd.city[idx].clone(),
})
.collect();
let elapsed = t0.elapsed();
info!(
mode = mode.as_str(),
results = results.len(),
ms = format_args!("{:.1}", elapsed.as_secs_f64() * 1000.0),
"GET /api/travel-destinations"
);
results
})
.await
.map_err(|error| (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()))?;
Ok(Json(DestinationsResponse { destinations }))
}