Last night
This commit is contained in:
parent
2906b01734
commit
42ee2d4c51
47 changed files with 848 additions and 478 deletions
|
|
@ -45,12 +45,12 @@ fn build_prompt(req: &AreaSummaryRequest) -> String {
|
|||
|
||||
let area_type = if req.is_postcode { "postcode" } else { "area" };
|
||||
parts.push(format!(
|
||||
"Summarise this {} of England ({}) which contain {} properties matching the filters.",
|
||||
area_type, req.location, req.count
|
||||
"Summarise this {} of England which contains {} properties matching my requirements.\n",
|
||||
area_type, req.count
|
||||
));
|
||||
|
||||
if !req.filters.is_empty() {
|
||||
parts.push(format!("Active filters: {}.", req.filters.join(", ")));
|
||||
parts.push(format!("Active filters: {}.\n", req.filters.join(", ")));
|
||||
}
|
||||
|
||||
if !req.numeric_stats.is_empty() {
|
||||
|
|
@ -59,7 +59,11 @@ fn build_prompt(req: &AreaSummaryRequest) -> String {
|
|||
.iter()
|
||||
.map(|stat| format!("{}: {:.1}", stat.name, stat.mean))
|
||||
.collect();
|
||||
parts.push(format!("Average values: {}.", stats.join(", ")));
|
||||
parts.push(format!(
|
||||
"Average values of the {}: {}.",
|
||||
if req.is_postcode { "postcode" } else { "area" },
|
||||
stats.join(", ")
|
||||
));
|
||||
}
|
||||
|
||||
for es in &req.enum_stats {
|
||||
|
|
|
|||
|
|
@ -93,13 +93,13 @@ fn extract_filter_feature_names(filters_str: Option<&str>) -> Vec<String> {
|
|||
names
|
||||
}
|
||||
|
||||
/// Fetch the OG screenshot image from the sidecar service.
|
||||
async fn fetch_og_image(
|
||||
/// Fetch a screenshot image from the screenshot service for Excel export.
|
||||
async fn fetch_screenshot(
|
||||
state: &AppState,
|
||||
view_param: &str,
|
||||
filters_str: Option<&str>,
|
||||
) -> Option<Vec<u8>> {
|
||||
let sidecar_url = state.og_sidecar_url.as_deref()?;
|
||||
let screenshot_base = &state.screenshot_url;
|
||||
|
||||
let mut params = vec![format!("v={}", urlencoding::encode(view_param))];
|
||||
if let Some(fs) = filters_str {
|
||||
|
|
@ -107,25 +107,25 @@ async fn fetch_og_image(
|
|||
params.push(format!("f={}", urlencoding::encode(fs)));
|
||||
}
|
||||
}
|
||||
let url = format!("{}/screenshot?{}", sidecar_url, params.join("&"));
|
||||
let url = format!("{}/screenshot?{}", screenshot_base, params.join("&"));
|
||||
|
||||
match state.http_client.get(&url).send().await {
|
||||
Ok(resp) if resp.status().is_success() => match resp.bytes().await {
|
||||
Ok(bytes) => {
|
||||
info!(bytes = bytes.len(), "Fetched OG image for export");
|
||||
info!(bytes = bytes.len(), "Fetched screenshot for export");
|
||||
Some(bytes.to_vec())
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Failed to read OG sidecar response for export: {err}");
|
||||
warn!("Failed to read screenshot response for export: {err}");
|
||||
None
|
||||
}
|
||||
},
|
||||
Ok(resp) => {
|
||||
warn!(status = %resp.status(), "OG sidecar returned error for export");
|
||||
warn!(status = %resp.status(), "Screenshot service returned error for export");
|
||||
None
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Failed to reach OG sidecar for export: {err}");
|
||||
warn!("Failed to reach screenshot service for export: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
@ -163,8 +163,8 @@ pub async fn get_export(
|
|||
};
|
||||
let view_param = format!("{:.4},{:.4},{:.1}", center_lat, center_lon, zoom);
|
||||
|
||||
// Fetch OG image from sidecar (async, before spawn_blocking)
|
||||
let og_image_bytes = fetch_og_image(&state, &view_param, filters_str.as_deref()).await;
|
||||
// Fetch screenshot (async, before spawn_blocking)
|
||||
let og_image_bytes = fetch_screenshot(&state, &view_param, filters_str.as_deref()).await;
|
||||
|
||||
// Build feature name → description map from the precomputed features response
|
||||
let feature_descriptions: FxHashMap<String, String> = state
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ struct CellAgg {
|
|||
count: u32,
|
||||
mins: Box<[f32]>,
|
||||
maxs: Box<[f32]>,
|
||||
sums: Box<[f64]>,
|
||||
feat_counts: Box<[u32]>,
|
||||
}
|
||||
|
||||
impl CellAgg {
|
||||
|
|
@ -46,6 +48,8 @@ impl CellAgg {
|
|||
count: 0,
|
||||
mins: vec![f32::INFINITY; num_features].into_boxed_slice(),
|
||||
maxs: vec![f32::NEG_INFINITY; num_features].into_boxed_slice(),
|
||||
sums: vec![0.0f64; num_features].into_boxed_slice(),
|
||||
feat_counts: vec![0u32; num_features].into_boxed_slice(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -65,6 +69,8 @@ impl CellAgg {
|
|||
if value > self.maxs[feat_index] {
|
||||
self.maxs[feat_index] = value;
|
||||
}
|
||||
self.sums[feat_index] += value as f64;
|
||||
self.feat_counts[feat_index] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -89,6 +95,8 @@ impl CellAgg {
|
|||
if value > self.maxs[feat_index] {
|
||||
self.maxs[feat_index] = value;
|
||||
}
|
||||
self.sums[feat_index] += value as f64;
|
||||
self.feat_counts[feat_index] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -99,6 +107,7 @@ fn build_feature_maps(
|
|||
groups: &FxHashMap<u64, CellAgg>,
|
||||
min_keys: &[String],
|
||||
max_keys: &[String],
|
||||
avg_keys: &[String],
|
||||
num_features: usize,
|
||||
indices: Option<&[usize]>,
|
||||
query_bounds: (f64, f64, f64, f64), // (south, west, north, east)
|
||||
|
|
@ -122,6 +131,14 @@ fn build_feature_maps(
|
|||
let mut map = Map::new();
|
||||
map.insert("h3".into(), Value::String(cell.to_string()));
|
||||
map.insert("count".into(), Value::Number(aggregation.count.into()));
|
||||
let center: h3o::LatLng = cell.into();
|
||||
if let (Some(lat), Some(lon)) = (
|
||||
serde_json::Number::from_f64(center.lat()),
|
||||
serde_json::Number::from_f64(center.lng()),
|
||||
) {
|
||||
map.insert("lat".into(), Value::Number(lat));
|
||||
map.insert("lon".into(), Value::Number(lon));
|
||||
}
|
||||
|
||||
let iter: Box<dyn Iterator<Item = usize>> = if let Some(idx) = indices {
|
||||
Box::new(idx.iter().copied())
|
||||
|
|
@ -130,14 +147,16 @@ fn build_feature_maps(
|
|||
};
|
||||
|
||||
for feat_index in iter {
|
||||
if aggregation.mins[feat_index].is_finite() && aggregation.maxs[feat_index].is_finite()
|
||||
{
|
||||
if let (Some(min_num), Some(max_num)) = (
|
||||
if aggregation.feat_counts[feat_index] > 0 {
|
||||
let avg = aggregation.sums[feat_index] / aggregation.feat_counts[feat_index] as f64;
|
||||
if let (Some(min_num), Some(max_num), Some(avg_num)) = (
|
||||
serde_json::Number::from_f64(aggregation.mins[feat_index] as f64),
|
||||
serde_json::Number::from_f64(aggregation.maxs[feat_index] as f64),
|
||||
serde_json::Number::from_f64(avg),
|
||||
) {
|
||||
map.insert(min_keys[feat_index].clone(), Value::Number(min_num));
|
||||
map.insert(max_keys[feat_index].clone(), Value::Number(max_num));
|
||||
map.insert(avg_keys[feat_index].clone(), Value::Number(avg_num));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -208,6 +227,7 @@ pub async fn get_hexagons(
|
|||
let feature_data = &state.data.feature_data;
|
||||
let min_keys = &state.min_keys;
|
||||
let max_keys = &state.max_keys;
|
||||
let avg_keys = &state.avg_keys;
|
||||
|
||||
let h3_res = h3o::Resolution::try_from(resolution)
|
||||
.map_err(|error| format!("Invalid H3 resolution {}: {}", resolution, error))?;
|
||||
|
|
@ -277,6 +297,7 @@ pub async fn get_hexagons(
|
|||
&groups,
|
||||
min_keys,
|
||||
max_keys,
|
||||
avg_keys,
|
||||
num_features,
|
||||
field_indices.as_deref(),
|
||||
(south, west, north, east),
|
||||
|
|
|
|||
|
|
@ -15,20 +15,20 @@ pub struct OgImageQuery {
|
|||
filters: Option<String>,
|
||||
poi: Option<String>,
|
||||
tab: Option<String>,
|
||||
/// When "1", renders the OG heading overlay on the screenshot
|
||||
og: 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 screenshot_base = &state.screenshot_url;
|
||||
|
||||
let mut params = Vec::new();
|
||||
if query.og.as_deref() == Some("1") {
|
||||
params.push("og=1".to_string());
|
||||
}
|
||||
if let Some(ref val) = query.view {
|
||||
params.push(format!("v={}", urlencoding::encode(val)));
|
||||
}
|
||||
|
|
@ -47,9 +47,8 @@ pub async fn get_og_image(
|
|||
} else {
|
||||
format!("?{}", params.join("&"))
|
||||
};
|
||||
|
||||
let url = format!("{}/screenshot{}", sidecar_url, qs);
|
||||
info!("Proxying OG screenshot request to: {}", url);
|
||||
let url = format!("{}/screenshot{}", screenshot_base, qs);
|
||||
info!("Proxying screenshot request to: {}", url);
|
||||
|
||||
match state.http_client.get(&url).send().await {
|
||||
Ok(resp) if resp.status().is_success() => match resp.bytes().await {
|
||||
|
|
@ -63,19 +62,19 @@ pub async fn get_og_image(
|
|||
)
|
||||
.into_response(),
|
||||
Err(err) => {
|
||||
warn!("Failed to read sidecar response: {}", err);
|
||||
warn!("Failed to read screenshot 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();
|
||||
warn!("Sidecar returned status {}: {}", status, body);
|
||||
(StatusCode::BAD_GATEWAY, "Screenshot sidecar error").into_response()
|
||||
warn!("Screenshot service returned status {}: {}", status, body);
|
||||
(StatusCode::BAD_GATEWAY, "Screenshot service error").into_response()
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Failed to reach sidecar: {}", err);
|
||||
(StatusCode::BAD_GATEWAY, "Screenshot sidecar unavailable").into_response()
|
||||
warn!("Failed to reach screenshot service: {}", err);
|
||||
(StatusCode::BAD_GATEWAY, "Screenshot service unavailable").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ struct PostcodeAgg {
|
|||
count: u32,
|
||||
mins: Box<[f32]>,
|
||||
maxs: Box<[f32]>,
|
||||
sums: Box<[f64]>,
|
||||
feat_counts: Box<[u32]>,
|
||||
}
|
||||
|
||||
impl PostcodeAgg {
|
||||
|
|
@ -39,6 +41,8 @@ impl PostcodeAgg {
|
|||
count: 0,
|
||||
mins: vec![f32::INFINITY; num_features].into_boxed_slice(),
|
||||
maxs: vec![f32::NEG_INFINITY; num_features].into_boxed_slice(),
|
||||
sums: vec![0.0f64; num_features].into_boxed_slice(),
|
||||
feat_counts: vec![0u32; num_features].into_boxed_slice(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,6 +59,8 @@ impl PostcodeAgg {
|
|||
if value > self.maxs[feat_index] {
|
||||
self.maxs[feat_index] = value;
|
||||
}
|
||||
self.sums[feat_index] += value as f64;
|
||||
self.feat_counts[feat_index] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -78,6 +84,8 @@ impl PostcodeAgg {
|
|||
if value > self.maxs[feat_index] {
|
||||
self.maxs[feat_index] = value;
|
||||
}
|
||||
self.sums[feat_index] += value as f64;
|
||||
self.feat_counts[feat_index] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -127,6 +135,7 @@ pub async fn get_postcodes(
|
|||
let feature_data = &state.data.feature_data;
|
||||
let min_keys = &state.min_keys;
|
||||
let max_keys = &state.max_keys;
|
||||
let avg_keys = &state.avg_keys;
|
||||
|
||||
let has_selective = field_indices.is_some();
|
||||
let sel_indices = field_indices.as_deref().unwrap_or(&[]);
|
||||
|
|
@ -272,15 +281,16 @@ pub async fn get_postcodes(
|
|||
};
|
||||
|
||||
for feat_index in iter {
|
||||
if aggregation.mins[feat_index].is_finite()
|
||||
&& aggregation.maxs[feat_index].is_finite()
|
||||
{
|
||||
if let (Some(min_num), Some(max_num)) = (
|
||||
if aggregation.feat_counts[feat_index] > 0 {
|
||||
let avg = aggregation.sums[feat_index] / aggregation.feat_counts[feat_index] as f64;
|
||||
if let (Some(min_num), Some(max_num), Some(avg_num)) = (
|
||||
serde_json::Number::from_f64(aggregation.mins[feat_index] as f64),
|
||||
serde_json::Number::from_f64(aggregation.maxs[feat_index] as f64),
|
||||
serde_json::Number::from_f64(avg),
|
||||
) {
|
||||
props.insert(min_keys[feat_index].clone(), Value::Number(min_num));
|
||||
props.insert(max_keys[feat_index].clone(), Value::Number(max_num));
|
||||
props.insert(avg_keys[feat_index].clone(), Value::Number(avg_num));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue