Various changes
This commit is contained in:
parent
a42591c701
commit
c388059f68
19 changed files with 1373 additions and 87 deletions
|
|
@ -66,8 +66,11 @@ pub async fn get_features(state: Arc<AppState>) -> Json<FeaturesResponse> {
|
|||
for feature_group in FEATURE_GROUPS {
|
||||
if feature_group.name == group_name {
|
||||
for feature_config in feature_group.features {
|
||||
if let Some(feat_idx) =
|
||||
state.data.feature_names.iter().position(|feat_name| feat_name == feature_config.name)
|
||||
if let Some(feat_idx) = state
|
||||
.data
|
||||
.feature_names
|
||||
.iter()
|
||||
.position(|feat_name| feat_name == feature_config.name)
|
||||
{
|
||||
let stats = &state.data.feature_stats[feat_idx];
|
||||
features.push(FeatureInfo::Numeric {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,10 @@ pub async fn get_hexagon_stats(
|
|||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let cell = h3o::CellIndex::from_str(¶ms.h3).map_err(|error| {
|
||||
warn!(h3 = %params.h3, error = %error, "Invalid H3 cell index");
|
||||
(StatusCode::BAD_REQUEST, format!("Invalid H3 cell: {}", error))
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("Invalid H3 cell: {}", error),
|
||||
)
|
||||
})?;
|
||||
let cell_u64: u64 = cell.into();
|
||||
|
||||
|
|
@ -60,13 +63,14 @@ pub async fn get_hexagon_stats(
|
|||
|
||||
// Parse optional `fields` param into sets of feature names.
|
||||
// None = include all, Some = only include listed features.
|
||||
let field_set: Option<std::collections::HashSet<String>> = params.fields.as_ref().map(|fields_str| {
|
||||
fields_str
|
||||
.split(',')
|
||||
.map(|field| field.trim().to_string())
|
||||
.filter(|field| !field.is_empty())
|
||||
.collect()
|
||||
});
|
||||
let field_set: Option<std::collections::HashSet<String>> =
|
||||
params.fields.as_ref().map(|fields_str| {
|
||||
fields_str
|
||||
.split(',')
|
||||
.map(|field| field.trim().to_string())
|
||||
.filter(|field| !field.is_empty())
|
||||
.collect()
|
||||
});
|
||||
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
let start_time = std::time::Instant::now();
|
||||
|
|
@ -158,9 +162,10 @@ pub async fn get_hexagon_stats(
|
|||
|
||||
// Bin into histogram using global edges (cast to f64 for bin index math)
|
||||
if bin_width > 0.0 {
|
||||
let bin_index =
|
||||
((value as f64 - histogram_min as f64) / bin_width as f64).floor() as isize;
|
||||
let clamped_index = bin_index.max(0).min((HISTOGRAM_BINS - 1) as isize) as usize;
|
||||
let bin_index = ((value as f64 - histogram_min as f64) / bin_width as f64)
|
||||
.floor() as isize;
|
||||
let clamped_index =
|
||||
bin_index.max(0).min((HISTOGRAM_BINS - 1) as isize) as usize;
|
||||
bins[clamped_index] += 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,13 @@ impl CellAgg {
|
|||
|
||||
/// Add a row, only aggregating the features at the given indices.
|
||||
#[inline]
|
||||
fn add_row_selective(&mut self, feature_data: &[f32], row: usize, num_features: usize, indices: &[usize]) {
|
||||
fn add_row_selective(
|
||||
&mut self,
|
||||
feature_data: &[f32],
|
||||
row: usize,
|
||||
num_features: usize,
|
||||
indices: &[usize],
|
||||
) {
|
||||
self.count += 1;
|
||||
let base = row * num_features;
|
||||
for &feat_index in indices {
|
||||
|
|
@ -133,7 +139,13 @@ impl CellAgg {
|
|||
|
||||
/// Track min/max ordinal index for selected enum features only.
|
||||
#[inline]
|
||||
fn add_enums_selective(&mut self, enum_data: &[u8], row: usize, num_enums: usize, indices: &[usize]) {
|
||||
fn add_enums_selective(
|
||||
&mut self,
|
||||
enum_data: &[u8],
|
||||
row: usize,
|
||||
num_enums: usize,
|
||||
indices: &[usize],
|
||||
) {
|
||||
let base = row * num_enums;
|
||||
for &enum_index in indices {
|
||||
let value = enum_data[base + enum_index];
|
||||
|
|
@ -175,7 +187,9 @@ pub(crate) fn write_json_escaped(buf: &mut String, text: &str) {
|
|||
'\n' => buf.push_str("\\n"),
|
||||
'\r' => buf.push_str("\\r"),
|
||||
'\t' => buf.push_str("\\t"),
|
||||
ctrl if ctrl < '\x20' => { let _ = write!(buf, "\\u{:04x}", ctrl as u32); }
|
||||
ctrl if ctrl < '\x20' => {
|
||||
let _ = write!(buf, "\\u{:04x}", ctrl as u32);
|
||||
}
|
||||
other => buf.push(other),
|
||||
}
|
||||
}
|
||||
|
|
@ -214,21 +228,31 @@ fn write_hexagons_json(
|
|||
|
||||
if let Some(indices) = numeric_indices {
|
||||
for &feat_index in indices {
|
||||
if aggregation.mins[feat_index].is_finite() && aggregation.maxs[feat_index].is_finite() {
|
||||
if aggregation.mins[feat_index].is_finite()
|
||||
&& aggregation.maxs[feat_index].is_finite()
|
||||
{
|
||||
let _ = write!(
|
||||
buf,
|
||||
",\"{}\":{},\"{}\":{}",
|
||||
min_keys[feat_index], aggregation.mins[feat_index], max_keys[feat_index], aggregation.maxs[feat_index]
|
||||
min_keys[feat_index],
|
||||
aggregation.mins[feat_index],
|
||||
max_keys[feat_index],
|
||||
aggregation.maxs[feat_index]
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for feat_index in 0..num_features {
|
||||
if aggregation.mins[feat_index].is_finite() && aggregation.maxs[feat_index].is_finite() {
|
||||
if aggregation.mins[feat_index].is_finite()
|
||||
&& aggregation.maxs[feat_index].is_finite()
|
||||
{
|
||||
let _ = write!(
|
||||
buf,
|
||||
",\"{}\":{},\"{}\":{}",
|
||||
min_keys[feat_index], aggregation.mins[feat_index], max_keys[feat_index], aggregation.maxs[feat_index]
|
||||
min_keys[feat_index],
|
||||
aggregation.mins[feat_index],
|
||||
max_keys[feat_index],
|
||||
aggregation.maxs[feat_index]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -240,8 +264,10 @@ fn write_hexagons_json(
|
|||
let _ = write!(
|
||||
buf,
|
||||
",\"{}\":{},\"{}\":{}",
|
||||
enum_min_keys[enum_index], aggregation.enum_mins[enum_index],
|
||||
enum_max_keys[enum_index], aggregation.enum_maxs[enum_index]
|
||||
enum_min_keys[enum_index],
|
||||
aggregation.enum_mins[enum_index],
|
||||
enum_max_keys[enum_index],
|
||||
aggregation.enum_maxs[enum_index]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -251,8 +277,10 @@ fn write_hexagons_json(
|
|||
let _ = write!(
|
||||
buf,
|
||||
",\"{}\":{},\"{}\":{}",
|
||||
enum_min_keys[enum_index], aggregation.enum_mins[enum_index],
|
||||
enum_max_keys[enum_index], aggregation.enum_maxs[enum_index]
|
||||
enum_min_keys[enum_index],
|
||||
aggregation.enum_mins[enum_index],
|
||||
enum_max_keys[enum_index],
|
||||
aggregation.enum_maxs[enum_index]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -325,24 +353,30 @@ pub async fn get_hexagons(
|
|||
// Parse optional `fields` param into numeric and enum index sets.
|
||||
// If `fields` is absent (None), all features are included.
|
||||
// If `fields` is present (even empty string), only listed features are included.
|
||||
let field_indices: Option<(Vec<usize>, Vec<usize>)> = params.fields.as_ref().map(|fields_str| {
|
||||
let mut numeric_indices = Vec::new();
|
||||
let mut enum_indices = Vec::new();
|
||||
if !fields_str.is_empty() {
|
||||
for name in fields_str.split(',') {
|
||||
let name = name.trim();
|
||||
if name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Some(idx) = state.data.feature_names.iter().position(|feat| feat == name) {
|
||||
numeric_indices.push(idx);
|
||||
} else if let Some(&idx) = state.enum_name_to_idx.get(name) {
|
||||
enum_indices.push(idx);
|
||||
let field_indices: Option<(Vec<usize>, Vec<usize>)> =
|
||||
params.fields.as_ref().map(|fields_str| {
|
||||
let mut numeric_indices = Vec::new();
|
||||
let mut enum_indices = Vec::new();
|
||||
if !fields_str.is_empty() {
|
||||
for name in fields_str.split(',') {
|
||||
let name = name.trim();
|
||||
if name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Some(idx) = state
|
||||
.data
|
||||
.feature_names
|
||||
.iter()
|
||||
.position(|feat| feat == name)
|
||||
{
|
||||
numeric_indices.push(idx);
|
||||
} else if let Some(&idx) = state.enum_name_to_idx.get(name) {
|
||||
enum_indices.push(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(numeric_indices, enum_indices)
|
||||
});
|
||||
(numeric_indices, enum_indices)
|
||||
});
|
||||
|
||||
let json_body = tokio::task::spawn_blocking(move || -> Result<String, String> {
|
||||
let t0 = std::time::Instant::now();
|
||||
|
|
@ -380,7 +414,11 @@ pub async fn get_hexagons(
|
|||
|
||||
// Choose aggregation strategy based on whether fields are specified
|
||||
let has_selective = field_indices.is_some();
|
||||
let (sel_numeric, sel_enum) = field_indices.as_ref().map_or((&[][..], &[][..]), |(ni, ei)| (ni.as_slice(), ei.as_slice()));
|
||||
let (sel_numeric, sel_enum) = field_indices
|
||||
.as_ref()
|
||||
.map_or((&[][..], &[][..]), |(ni, ei)| {
|
||||
(ni.as_slice(), ei.as_slice())
|
||||
});
|
||||
|
||||
let aggregate_row = |groups: &mut FxHashMap<u64, CellAgg>, cell_id: u64, row: usize| {
|
||||
let aggregation = groups
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
mod features;
|
||||
pub(crate) mod hexagons;
|
||||
mod hexagon_stats;
|
||||
pub(crate) mod hexagons;
|
||||
mod og_image;
|
||||
pub(crate) mod parse;
|
||||
mod pois;
|
||||
pub(crate) mod properties;
|
||||
|
|
@ -8,5 +9,6 @@ pub(crate) mod properties;
|
|||
pub use features::get_features;
|
||||
pub use hexagon_stats::get_hexagon_stats;
|
||||
pub use hexagons::get_hexagons;
|
||||
pub use og_image::get_og_image;
|
||||
pub use pois::{get_poi_categories, get_pois};
|
||||
pub use properties::get_hexagon_properties;
|
||||
|
|
|
|||
80
server-rs/src/routes/og_image.rs
Normal file
80
server-rs/src/routes/og_image.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::extract::Query;
|
||||
use axum::http::{header, StatusCode};
|
||||
use axum::response::IntoResponse;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct OgImageQuery {
|
||||
#[serde(rename = "v")]
|
||||
view: Option<String>,
|
||||
#[serde(rename = "f")]
|
||||
filters: Option<String>,
|
||||
poi: Option<String>,
|
||||
tab: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn get_og_image(
|
||||
state: Arc<AppState>,
|
||||
Query(query): Query<OgImageQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let sidecar_url = match &state.og_sidecar_url {
|
||||
Some(url) => url,
|
||||
None => {
|
||||
return (StatusCode::SERVICE_UNAVAILABLE, "OG sidecar not configured").into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let mut params = Vec::new();
|
||||
if let Some(ref val) = query.view {
|
||||
params.push(format!("v={}", urlencoding::encode(val)));
|
||||
}
|
||||
if let Some(ref val) = query.filters {
|
||||
params.push(format!("f={}", urlencoding::encode(val)));
|
||||
}
|
||||
if let Some(ref val) = query.poi {
|
||||
params.push(format!("poi={}", urlencoding::encode(val)));
|
||||
}
|
||||
if let Some(ref val) = query.tab {
|
||||
params.push(format!("tab={}", urlencoding::encode(val)));
|
||||
}
|
||||
|
||||
let qs = if params.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("?{}", params.join("&"))
|
||||
};
|
||||
|
||||
let url = format!("{}/screenshot{}", sidecar_url, qs);
|
||||
tracing::info!("Proxying OG screenshot request to: {}", url);
|
||||
|
||||
match state.http_client.get(&url).send().await {
|
||||
Ok(resp) if resp.status().is_success() => match resp.bytes().await {
|
||||
Ok(bytes) => (
|
||||
StatusCode::OK,
|
||||
[
|
||||
(header::CONTENT_TYPE, "image/png"),
|
||||
(header::CACHE_CONTROL, "public, max-age=86400"),
|
||||
],
|
||||
bytes,
|
||||
)
|
||||
.into_response(),
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to read sidecar response: {}", err);
|
||||
(StatusCode::BAD_GATEWAY, "Failed to read screenshot").into_response()
|
||||
}
|
||||
},
|
||||
Ok(resp) => {
|
||||
let status = resp.status();
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
tracing::warn!("Sidecar returned status {}: {}", status, body);
|
||||
(StatusCode::BAD_GATEWAY, "Screenshot sidecar error").into_response()
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to reach sidecar: {}", err);
|
||||
(StatusCode::BAD_GATEWAY, "Screenshot sidecar unavailable").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -35,7 +35,11 @@ pub async fn get_pois(
|
|||
.categories
|
||||
.as_deref()
|
||||
.filter(|text| !text.is_empty())
|
||||
.map(|text| text.split(',').map(|part| part.trim().to_string()).collect());
|
||||
.map(|text| {
|
||||
text.split(',')
|
||||
.map(|part| part.trim().to_string())
|
||||
.collect()
|
||||
});
|
||||
|
||||
let num_categories = category_filter.as_ref().map(|cats| cats.len()).unwrap_or(0);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ use rustc_hash::FxHashMap;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::consts::{DEFAULT_PROPERTIES_LIMIT, ENUM_NULL, H3_PRECOMPUTE_MAX, H3_REQUEST_MAX, H3_REQUEST_MIN, MAX_PROPERTIES_LIMIT};
|
||||
use crate::consts::{
|
||||
DEFAULT_PROPERTIES_LIMIT, ENUM_NULL, H3_PRECOMPUTE_MAX, H3_REQUEST_MAX, H3_REQUEST_MIN,
|
||||
MAX_PROPERTIES_LIMIT,
|
||||
};
|
||||
use crate::data::EnumFeatureData;
|
||||
use crate::filter::{parse_filters, row_passes_filters};
|
||||
use crate::state::AppState;
|
||||
|
|
@ -91,7 +94,10 @@ pub async fn get_hexagon_properties(
|
|||
) -> Result<Json<HexagonPropertiesResponse>, (StatusCode, String)> {
|
||||
let cell = h3o::CellIndex::from_str(¶ms.h3).map_err(|error| {
|
||||
warn!(h3 = %params.h3, error = %error, "Invalid H3 cell index");
|
||||
(StatusCode::BAD_REQUEST, format!("Invalid H3 cell: {}", error))
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("Invalid H3 cell: {}", error),
|
||||
)
|
||||
})?;
|
||||
let cell_u64: u64 = cell.into();
|
||||
|
||||
|
|
@ -165,7 +171,10 @@ pub async fn get_hexagon_properties(
|
|||
});
|
||||
|
||||
let total = matching_rows.len();
|
||||
let limit = params.limit.unwrap_or(DEFAULT_PROPERTIES_LIMIT).min(MAX_PROPERTIES_LIMIT);
|
||||
let limit = params
|
||||
.limit
|
||||
.unwrap_or(DEFAULT_PROPERTIES_LIMIT)
|
||||
.min(MAX_PROPERTIES_LIMIT);
|
||||
let offset = params.offset.unwrap_or(0);
|
||||
let truncated = total > offset + limit;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue