96 lines
2.9 KiB
Rust
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 }))
|
|
}
|