Skip to main content

routee_compass/plugin/input/default/inject/
inject_plugin_config.rs

1//! Configuration for building an instance of an [`super::InjectInputPlugin`].
2
3use super::{inject_plugin::InjectInputPlugin, CoordinateOrientation, WriteMode};
4use crate::plugin::input::InputPluginError;
5use geojson::{Feature, FeatureCollection, GeoJson};
6use routee_compass_core::util::geo::{geo_io_utils, PolygonalRTree};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case", tag = "format")]
12pub enum InjectPluginConfig {
13    SpatialKeyValue(SpatialInjectPlugin),
14    KeyValue(BasicInjectPlugin),
15}
16
17/// injects a value in each query at some key
18#[derive(Serialize, Deserialize, Clone, Debug)]
19pub struct BasicInjectPlugin {
20    pub key: String,
21    pub value: Value,
22    pub write_mode: WriteMode,
23}
24/// injects a value in each query at some key by first intersecting
25/// the query map location with some geospatial dataset in order to
26/// provide spatially-varied injection.
27#[derive(Serialize, Deserialize, Clone, Debug)]
28pub struct SpatialInjectPlugin {
29    pub spatial_input_file: String,
30    pub source_key: Option<String>,
31    pub key: String,
32    pub write_mode: WriteMode,
33    pub orientation: CoordinateOrientation,
34    pub default: Option<Value>,
35}
36
37impl InjectPluginConfig {
38    pub fn build(&self) -> Result<InjectInputPlugin, InputPluginError> {
39        match self {
40            InjectPluginConfig::KeyValue(basic) => Ok(InjectInputPlugin::Basic {
41                key: basic.key.clone(),
42                value: basic.value.clone(),
43                write_mode: basic.write_mode.clone(),
44            }),
45            InjectPluginConfig::SpatialKeyValue(spatial) => {
46                let contents =
47                    std::fs::read_to_string(&spatial.spatial_input_file).map_err(|e| {
48                        InputPluginError::BuildFailed(format!(
49                            "file read failed for file '{}': {}",
50                            spatial.spatial_input_file, e
51                        ))
52                    })?;
53                let geojson = contents.parse::<GeoJson>().map_err(|e| {
54                    InputPluginError::BuildFailed(format!(
55                        "unable to parse file '{}' as GeoJSON: {}",
56                        spatial.spatial_input_file, e
57                    ))
58                })?;
59                let fc = FeatureCollection::try_from(geojson).map_err(|e| {
60                    InputPluginError::BuildFailed(format!(
61                        "failed to unpack GeoJSON in file '{}' as a FeatureCollection: {}",
62                        spatial.spatial_input_file, e
63                    ))
64                })?;
65                let geometries_with_properties = fc
66                    .features
67                    .into_iter()
68                    .map(|row| {
69                        match &row.geometry {
70                            None => Err(InputPluginError::BuildFailed(format!(
71                                "row {} from GeoJSON in file '{}' has no geometry",
72                                get_id(&row),
73                                spatial.spatial_input_file
74                            ))),
75                            Some(g) => {
76                                let geometry_f64: geo::Geometry<f64> = g.try_into().map_err(|e| {
77                                    let msg = format!("row {} from GeoJSON in file '{}' has geometry that cannot be deserialized: {}", get_id(&row), spatial.spatial_input_file, e);
78                                    InputPluginError::BuildFailed(msg)
79                                })?;
80                                // validate the geometry is polygonal
81                                match geometry_f64 {
82                                    geo::Geometry::Polygon(_) | geo::Geometry::MultiPolygon(_) => Ok(()),
83                                    _ => Err(InputPluginError::BuildFailed(format!(
84                                        "row {} from GeoJSON in file '{}' is not polygonal",
85                                        get_id(&row), spatial.spatial_input_file
86                                    ))),
87                                }?;
88                                let geometry = geo_io_utils::downsample_geometry(geometry_f64).map_err(|e| InputPluginError::BuildFailed(format!("row {} from GeoJSON in file '{}' has geometry that cannot be downsampled to f32: {}", get_id(&row), spatial.spatial_input_file, e)))?;
89
90                                // 
91                                let properties = match (&row.properties, spatial.source_key.clone()) {
92                                    (None, _) => Err(InputPluginError::BuildFailed(format!("row {} from GeoJSON in file '{}' has no properties",
93                                        get_id(&row), spatial.spatial_input_file))),
94                                    (Some(props), None) => {
95                                        Ok(serde_json::json!(props))
96                                    },
97                                    (Some(props), Some(props_key)) => {
98                                        let value = props.get(&props_key).ok_or_else(|| InputPluginError::BuildFailed(format!("row {} from GeoJSON in file '{}' has no properties",
99                                        get_id(&row), spatial.spatial_input_file)))?;
100                                        Ok(value.clone())
101                                    },
102                                }?;
103
104                                Ok((geometry, properties))
105                            }
106                        }
107                    })
108                    .collect::<Result<Vec<_>, _>>()?;
109
110                let spatial_index =
111                    PolygonalRTree::new(geometries_with_properties).map_err(|e| {
112                        InputPluginError::BuildFailed(format!(
113                            "failed to build spatial index over GeoJSON input from file '{}': {}",
114                            spatial.spatial_input_file, e
115                        ))
116                    })?;
117
118                Ok(InjectInputPlugin::Spatial {
119                    values: spatial_index,
120                    key: spatial.key.clone(),
121                    write_mode: spatial.write_mode.clone(),
122                    orientation: spatial.orientation,
123                    default: spatial.default.clone(),
124                })
125            }
126        }
127    }
128}
129
130fn get_id(row: &Feature) -> String {
131    row.id
132        .as_ref()
133        .map(|i| format!("{i:?}"))
134        .unwrap_or_default()
135}