230 lines
7.5 KiB
Rust
230 lines
7.5 KiB
Rust
use std::str::FromStr;
|
|
use std::sync::Arc;
|
|
|
|
use axum::extract::Query;
|
|
use axum::http::StatusCode;
|
|
use axum::response::Json;
|
|
use rustc_hash::FxHashMap;
|
|
use serde::{Deserialize, Serialize};
|
|
use tracing::{info, warn};
|
|
|
|
use crate::consts::{DEFAULT_PROPERTIES_LIMIT, ENUM_NULL, MAX_PROPERTIES_LIMIT};
|
|
use crate::data::EnumFeatureData;
|
|
use crate::filter::{parse_filters, row_passes_filters};
|
|
use crate::state::AppState;
|
|
|
|
use super::parse::h3_cell_bounds;
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct HexagonPropertiesParams {
|
|
pub h3: String,
|
|
pub resolution: u8,
|
|
pub filters: Option<String>,
|
|
pub limit: Option<usize>,
|
|
pub offset: Option<usize>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct Property {
|
|
// String fields
|
|
pub address: Option<String>,
|
|
pub postcode: Option<String>,
|
|
pub property_type: Option<String>,
|
|
pub built_form: Option<String>,
|
|
pub duration: Option<String>,
|
|
pub current_energy_rating: Option<String>,
|
|
pub potential_energy_rating: Option<String>,
|
|
|
|
// Numeric fields
|
|
pub lat: f64,
|
|
pub lon: f64,
|
|
|
|
pub is_construction_date_approximate: Option<bool>,
|
|
|
|
#[serde(flatten)]
|
|
pub features: FxHashMap<String, f64>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct HexagonPropertiesResponse {
|
|
pub properties: Vec<Property>,
|
|
pub total: usize,
|
|
pub limit: usize,
|
|
pub offset: usize,
|
|
pub truncated: bool,
|
|
}
|
|
|
|
fn non_empty_string(text: &str) -> Option<String> {
|
|
let trimmed = text.trim();
|
|
if trimmed.is_empty() {
|
|
None
|
|
} else {
|
|
Some(trimmed.to_string())
|
|
}
|
|
}
|
|
|
|
fn lookup_enum_value(
|
|
enum_features: &[EnumFeatureData],
|
|
enum_idx: &FxHashMap<String, usize>,
|
|
row: usize,
|
|
names: &[&str],
|
|
) -> Option<String> {
|
|
for name in names {
|
|
if let Some(&feature_index) = enum_idx.get(*name) {
|
|
let enum_feature = &enum_features[feature_index];
|
|
let data_index = enum_feature.data[row];
|
|
if data_index != ENUM_NULL {
|
|
if let Some(value) = enum_feature.values.get(data_index as usize) {
|
|
return Some(value.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
pub async fn get_hexagon_properties(
|
|
state: Arc<AppState>,
|
|
Query(params): Query<HexagonPropertiesParams>,
|
|
) -> 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))
|
|
})?;
|
|
let cell_u64: u64 = cell.into();
|
|
|
|
let resolution = params.resolution as usize;
|
|
if resolution >= state.h3_cells.len() || state.h3_cells[resolution].is_empty() {
|
|
warn!(
|
|
resolution,
|
|
"Invalid or non-precomputed resolution for hexagon-properties"
|
|
);
|
|
return Err((
|
|
StatusCode::BAD_REQUEST,
|
|
"Invalid or non-precomputed resolution".to_string(),
|
|
));
|
|
}
|
|
|
|
let h3_str = params.h3.clone();
|
|
let filters_str = params.filters.clone();
|
|
let (parsed_filters, parsed_enum_filters) = parse_filters(
|
|
params.filters.as_deref(),
|
|
&state.data.feature_names,
|
|
&state.data.enum_features,
|
|
);
|
|
let num_filters = parsed_filters.len() + parsed_enum_filters.len();
|
|
|
|
let result = tokio::task::spawn_blocking(move || {
|
|
let t0 = std::time::Instant::now();
|
|
let h3_data = &state.h3_cells[resolution];
|
|
let num_features = state.data.num_features;
|
|
let feature_data = &state.data.feature_data;
|
|
let enum_features = &state.data.enum_features;
|
|
|
|
let (min_lat, min_lon, max_lat, max_lon) = h3_cell_bounds(cell, 0.001);
|
|
|
|
let mut matching_rows: Vec<usize> = Vec::new();
|
|
state
|
|
.grid
|
|
.for_each_in_bounds(min_lat, min_lon, max_lat, max_lon, |row_idx| {
|
|
let row = row_idx as usize;
|
|
if h3_data[row] == cell_u64
|
|
&& row_passes_filters(
|
|
row,
|
|
&parsed_filters,
|
|
&parsed_enum_filters,
|
|
feature_data,
|
|
num_features,
|
|
enum_features,
|
|
)
|
|
{
|
|
matching_rows.push(row);
|
|
}
|
|
});
|
|
|
|
let total = matching_rows.len();
|
|
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;
|
|
|
|
let properties: Vec<Property> = matching_rows
|
|
.iter()
|
|
.skip(offset)
|
|
.take(limit)
|
|
.map(|&row| {
|
|
let mut features = FxHashMap::default();
|
|
let base = row * num_features;
|
|
for (feat_idx, feat_name) in state.data.feature_names.iter().enumerate() {
|
|
let value = feature_data[base + feat_idx];
|
|
if value.is_finite() {
|
|
features.insert(feat_name.clone(), value);
|
|
}
|
|
}
|
|
|
|
Property {
|
|
address: non_empty_string(&state.data.address[row]),
|
|
postcode: non_empty_string(&state.data.postcode[row]),
|
|
is_construction_date_approximate: Some(state.data.is_approx_build_date[row]),
|
|
property_type: lookup_enum_value(
|
|
enum_features,
|
|
&state.enum_name_to_idx,
|
|
row,
|
|
&["Property type", "epc_property_type", "pp_property_type"],
|
|
),
|
|
built_form: lookup_enum_value(
|
|
enum_features,
|
|
&state.enum_name_to_idx,
|
|
row,
|
|
&["Property type/built form", "built_form"],
|
|
),
|
|
duration: lookup_enum_value(
|
|
enum_features,
|
|
&state.enum_name_to_idx,
|
|
row,
|
|
&["Leashold/Freehold", "duration"],
|
|
),
|
|
current_energy_rating: lookup_enum_value(
|
|
enum_features,
|
|
&state.enum_name_to_idx,
|
|
row,
|
|
&["Current energy rating", "current_energy_rating"],
|
|
),
|
|
potential_energy_rating: lookup_enum_value(
|
|
enum_features,
|
|
&state.enum_name_to_idx,
|
|
row,
|
|
&["Potential energy rating", "potential_energy_rating"],
|
|
),
|
|
lat: state.data.lat[row],
|
|
lon: state.data.lon[row],
|
|
features,
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
let elapsed = t0.elapsed();
|
|
info!(
|
|
h3 = %h3_str,
|
|
resolution,
|
|
total,
|
|
returned = properties.len(),
|
|
offset,
|
|
filters = num_filters,
|
|
filters_raw = filters_str.as_deref().unwrap_or("-"),
|
|
ms = format_args!("{:.1}", elapsed.as_secs_f64() * 1000.0),
|
|
"GET /api/hexagon-properties"
|
|
);
|
|
|
|
HexagonPropertiesResponse {
|
|
properties,
|
|
total,
|
|
limit,
|
|
offset,
|
|
truncated,
|
|
}
|
|
})
|
|
.await
|
|
.map_err(|error| (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()))?;
|
|
|
|
Ok(Json(result))
|
|
}
|