use std::collections::HashMap;
use bambam_core::model::bambam_typed::BambamOutputRow;
use bambam_core::model::destination::{self, BinInterval, DestinationFilter, DestinationPredicate};
use bambam_core::model::output_plugin::isochrone::{
GeometryModel, IsochroneAlgorithm, IsochroneOutputFormat,
};
use bambam_core::model::output_plugin::opportunity::OpportunityFormat;
use bambam_core::model::{bambam_field as field, bambam_ops, bambam_typed, TimeBin};
use routee_compass::app::{compass::CompassAppError, search::SearchAppResult};
use routee_compass::plugin::output::OutputPlugin;
use routee_compass::plugin::output::OutputPluginError;
use routee_compass_core::algorithm::search::{SearchInstance, SearchResult};
use serde_json::json;
use serde_json::Value;
pub struct IsochroneOutputPlugin {}
impl OutputPlugin for IsochroneOutputPlugin {
fn process(
&self,
output: &mut serde_json::Value,
result: &Result<(SearchAppResult, SearchInstance), CompassAppError>,
) -> Result<(), OutputPluginError> {
let mut row = bambam_typed::BambamOutputRow::new(output);
if !requires_isochrones(&row)? {
return Ok(());
}
match result {
Ok((sr, si)) => run_isochrone(row, sr, si),
Err(_) => empty_isochrones(row),
}
}
}
pub fn requires_isochrones(row: &BambamOutputRow<'_>) -> Result<bool, OutputPluginError> {
let info = row.info_ref()?;
let format = info.get_opportunity_format()?;
let requires_isochrones = matches!(format, Some(OpportunityFormat::Aggregate));
Ok(requires_isochrones)
}
pub fn empty_isochrones(mut row: BambamOutputRow<'_>) -> Result<(), OutputPluginError> {
let get_isochrone_request = GetIsochroneRequest::try_from(&row)?;
let info = row.info_ref()?;
let bin_config = match info.get_bin_range()? {
Some(bc) => bc,
None => {
let msg = String::from("row with aggregate opportunities has no bin range config");
return Err(OutputPluginError::OutputPluginFailed(msg));
}
};
let mut agg = row.aggregate()?;
let bins = bin_config
.build_bins(false)
.map_err(|e| OutputPluginError::OutputPluginFailed(e.to_string()))?;
for bin in bins.into_iter() {
let bin_key = bin.bin_key();
let result = get_isochrone_request.empty()?;
agg.set_isochrone(&bin_key, result.isochrone_value);
agg.set_n_destinations(&bin_key, result.tree_size);
}
Ok(())
}
pub fn run_isochrone(
mut row: BambamOutputRow<'_>,
sr: &SearchAppResult,
si: &SearchInstance,
) -> Result<(), OutputPluginError> {
let get_isochrone_request = GetIsochroneRequest::try_from(&row)?;
let info = row.info_ref()?;
let bin_config = match info.get_bin_range()? {
Some(bc) => bc,
None => {
let msg = String::from("row with aggregate opportunities has no bin range config");
return Err(OutputPluginError::OutputPluginFailed(msg));
}
};
let mut agg = row.aggregate()?;
let bins = bin_config
.build_bins(false)
.map_err(|e| OutputPluginError::OutputPluginFailed(e.to_string()))?;
for bin in bins.into_iter() {
let bin_key = bin.bin_key();
let result = get_isochrone_request.run(&bin, sr, si)?;
agg.set_isochrone(&bin_key, result.isochrone_value);
agg.set_n_destinations(&bin_key, result.tree_size);
}
Ok(())
}
struct GetIsochroneRequest {
filter: Option<DestinationFilter>,
geometry_model: GeometryModel,
isochrone_algorithm: IsochroneAlgorithm,
isochrone_format: IsochroneOutputFormat,
}
impl<'a> TryFrom<&'a BambamOutputRow<'a>> for GetIsochroneRequest {
type Error = OutputPluginError;
fn try_from(value: &'a BambamOutputRow<'a>) -> Result<Self, Self::Error> {
let info = value.info_ref()?;
let format = info.get_opportunity_format()?;
let filter = info.get_destination_filter()?.map(DestinationFilter);
let geometry_model_config = info
.get_geometry_model()?
.ok_or_else(|| missing_expected("info.geometry_model"))?;
let geometry_model = GeometryModel::try_from(&geometry_model_config)?;
let isochrone_algorithm = info
.get_isochrone_algorithm()?
.ok_or_else(|| missing_expected("info.isochrone_algorithm"))?;
let isochrone_format = info
.get_isochrone_format()?
.ok_or_else(|| missing_expected("info.isochrone_format"))?;
Ok(Self {
filter,
geometry_model,
isochrone_algorithm,
isochrone_format,
})
}
}
impl GetIsochroneRequest {
pub fn empty(&self) -> Result<GetIsochroneResult, OutputPluginError> {
let empty = self.isochrone_format.empty_geometry()?;
let result = GetIsochroneResult {
isochrone_value: json![empty],
tree_size: 0,
};
Ok(result)
}
pub fn run(
&self,
bin: &BinInterval,
search_result: &SearchAppResult,
si: &SearchInstance,
) -> Result<GetIsochroneResult, OutputPluginError> {
let tree_destinations: Vec<_> = destination::iter::new_destinations_iterator(
search_result,
Some(bin),
self.filter.as_ref(),
&si.state_model,
)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| {
OutputPluginError::OutputPluginFailed(format!("failure collecting destinations: {e}"))
})?;
let tree_size = tree_destinations.len();
let tree_mp = self
.geometry_model
.generate_destination_points(&tree_destinations, si.map_model.clone())?;
let geometry = self.isochrone_algorithm.run(tree_mp)?;
let isochrone = self.isochrone_format.serialize_geometry(&geometry)?;
let result = GetIsochroneResult {
isochrone_value: json![isochrone],
tree_size,
};
Ok(result)
}
}
struct GetIsochroneResult {
isochrone_value: Value,
tree_size: usize,
}
fn missing_expected(field: &str) -> OutputPluginError {
let msg = format!("output row missing expected field '{field}'");
OutputPluginError::OutputPluginFailed(msg)
}