use crate::app::compass::CompassApp;
use std::path::PathBuf;
const GRID_COLS: usize = 10;
const GRID_ROWS: usize = 10;
const BASE_X: f64 = -105.0;
const BASE_Y: f64 = 40.0;
const SPACING: f64 = 0.01;
fn horizontal_edge_id(row: usize, col: usize) -> Option<i64> {
if col >= GRID_COLS - 1 {
return None; }
let edges_before_row = edges_per_row() * row;
let edge_in_row = 2 * col;
Some((edges_before_row + edge_in_row) as i64)
}
fn vertical_edge_id(row: usize, col: usize) -> Option<i64> {
if row >= GRID_ROWS - 1 {
return None; }
let edges_before_row = edges_per_row() * row;
let edge_in_row = if col < GRID_COLS - 1 {
2 * col + 1
} else {
2 * (GRID_COLS - 1) };
Some((edges_before_row + edge_in_row) as i64)
}
fn edges_per_row() -> usize {
(GRID_COLS - 1) + GRID_COLS
}
fn col_x(col: usize) -> f64 {
BASE_X + (col as f64 * SPACING)
}
fn row_y(row: usize) -> f64 {
BASE_Y + (row as f64 * SPACING)
}
fn horizontal_edge_midpoint_x(col: usize) -> f64 {
col_x(col) + SPACING / 2.0
}
fn vertical_edge_midpoint_y(row: usize) -> f64 {
row_y(row) + SPACING / 2.0
}
struct TestTrace {
points: Vec<serde_json::Value>,
expected_edges: Vec<i64>,
}
impl TestTrace {
fn eastward_horizontal(row: usize, count: usize) -> Self {
let points = (0..count)
.map(|col| {
let x = if col == 0 {
col_x(col) + SPACING * 0.25
} else if col == count - 1 {
col_x(col) + SPACING * 0.75
} else {
horizontal_edge_midpoint_x(col)
};
let y = row_y(row);
serde_json::json!({"x": x, "y": y})
})
.collect();
let expected_edges = (0..count)
.map(|col| horizontal_edge_id(row, col).unwrap())
.collect();
Self {
points,
expected_edges,
}
}
fn northward_vertical(col: usize, count: usize) -> Self {
let points = (0..count)
.map(|row| {
let y = if row == 0 {
row_y(row) + SPACING * 0.25
} else if row == count - 1 {
row_y(row) + SPACING * 0.75
} else {
vertical_edge_midpoint_y(row)
};
let x = col_x(col);
serde_json::json!({"x": x, "y": y})
})
.collect();
let expected_edges = (0..count)
.map(|row| vertical_edge_id(row, col).unwrap())
.collect();
Self {
points,
expected_edges,
}
}
fn l_shaped() -> Self {
let points = vec![
serde_json::json!({"x": col_x(0) + SPACING * 0.25, "y": row_y(0)}),
serde_json::json!({"x": col_x(1) + SPACING * 0.25, "y": row_y(0)}),
serde_json::json!({"x": col_x(2), "y": row_y(0) + SPACING * 0.25}),
serde_json::json!({"x": col_x(2), "y": row_y(1) + SPACING * 0.25}),
serde_json::json!({"x": col_x(2), "y": row_y(2) + SPACING * 0.75}),
];
let expected_edges = vec![
horizontal_edge_id(0, 0).unwrap(),
horizontal_edge_id(0, 1).unwrap(),
vertical_edge_id(0, 2).unwrap(),
vertical_edge_id(1, 2).unwrap(),
vertical_edge_id(2, 2).unwrap(),
];
Self {
points,
expected_edges,
}
}
fn noisy_eastward_horizontal(row: usize, count: usize) -> Self {
let points = (0..count)
.map(|col| {
let noise = if col % 2 == 0 { 0.0003 } else { -0.0003 };
let x = if col == 0 {
col_x(col) + SPACING * 0.25
} else if col == count - 1 {
col_x(col) + SPACING * 0.75
} else {
horizontal_edge_midpoint_x(col)
};
let y = row_y(row) + noise;
serde_json::json!({"x": x, "y": y})
})
.collect();
let expected_edges = (0..count)
.map(|col| horizontal_edge_id(row, col).unwrap())
.collect();
Self {
points,
expected_edges,
}
}
}
fn run_map_match_test(app: &CompassApp, trace: TestTrace, label: &str) {
let query = serde_json::json!({
"trace": trace.points
});
let queries = vec![query];
let result = app.map_match(&queries, None).unwrap();
assert_eq!(result.len(), 1, "{}: Expected 1 result", label);
let point_matches = result[0]
.get("point_matches")
.expect("result has point_matches")
.as_array()
.expect("point_matches is array");
assert_eq!(
point_matches.len(),
trace.expected_edges.len(),
"{}: point_matches length mismatch",
label
);
for (i, (matched, &expected)) in point_matches
.iter()
.zip(trace.expected_edges.iter())
.enumerate()
{
let actual = matched.get("edge_id").unwrap().as_i64().unwrap();
assert_eq!(
actual, expected,
"{}: mismatch at point {}, expected edge {}, got {}",
label, i, expected, actual
);
}
}
fn load_lcss_app() -> CompassApp {
let conf_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("app")
.join("compass")
.join("test")
.join("map_matching_test")
.join("compass_lcss.toml");
CompassApp::try_from(conf_file.as_path()).expect("failed to load LCSS map matching config")
}
#[test]
fn test_grid_helper_horizontal_edges_row0() {
assert_eq!(horizontal_edge_id(0, 0), Some(0));
assert_eq!(horizontal_edge_id(0, 1), Some(2));
assert_eq!(horizontal_edge_id(0, 2), Some(4));
assert_eq!(horizontal_edge_id(0, 3), Some(6));
assert_eq!(horizontal_edge_id(0, 4), Some(8));
assert_eq!(horizontal_edge_id(0, 5), Some(10));
assert_eq!(horizontal_edge_id(0, 6), Some(12));
assert_eq!(horizontal_edge_id(0, 7), Some(14));
assert_eq!(horizontal_edge_id(0, 8), Some(16));
assert_eq!(horizontal_edge_id(0, 9), None); }
#[test]
fn test_grid_helper_vertical_edges_row0() {
assert_eq!(vertical_edge_id(0, 0), Some(1));
assert_eq!(vertical_edge_id(0, 1), Some(3));
assert_eq!(vertical_edge_id(0, 2), Some(5));
assert_eq!(vertical_edge_id(0, 3), Some(7));
assert_eq!(vertical_edge_id(0, 4), Some(9));
assert_eq!(vertical_edge_id(0, 5), Some(11));
assert_eq!(vertical_edge_id(0, 6), Some(13));
assert_eq!(vertical_edge_id(0, 7), Some(15));
assert_eq!(vertical_edge_id(0, 8), Some(17));
assert_eq!(vertical_edge_id(0, 9), Some(18)); }
#[test]
fn test_grid_helper_row1_edges() {
assert_eq!(horizontal_edge_id(1, 0), Some(19));
assert_eq!(horizontal_edge_id(1, 1), Some(21));
assert_eq!(vertical_edge_id(1, 0), Some(20));
assert_eq!(vertical_edge_id(1, 9), Some(37));
}
#[test]
fn test_map_match_json() {
let conf_file_test = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("app")
.join("compass")
.join("test")
.join("speeds_test")
.join("speeds_test.toml");
let conf_str = std::fs::read_to_string(&conf_file_test).unwrap();
let conf_str_with_mm = format!(
"{}\n[map_matching]\ntype = \"lcss\"\n[mapping]\nspatial_index_type = \"edge\"",
conf_str
);
let config = crate::app::compass::CompassAppConfig::from_str(
&conf_str_with_mm,
conf_file_test.to_str().unwrap(),
config::FileFormat::Toml,
)
.unwrap();
let builder = crate::app::compass::CompassBuilderInventory::new().unwrap();
let app = CompassApp::new(&config, &builder).unwrap();
let query = serde_json::json!({
"trace": [
{"x": -105.1683, "y": 39.7379},
{"x": -105.1683, "y": 39.7379}
]
});
let queries = vec![query];
let result = app.map_match(&queries, None).unwrap();
assert_eq!(result.len(), 1);
let first_result = &result[0];
assert!(first_result.get("point_matches").is_some());
assert!(first_result.get("matched_path").is_some());
}
#[test]
fn test_map_matching_simple_single_point() {
let app = load_lcss_app();
let query = serde_json::json!({
"trace": [
{"x": -104.995, "y": 40.0}
]
});
let queries = vec![query];
let result = app.map_match(&queries, None).unwrap();
assert_eq!(result.len(), 1);
let first_result = &result[0];
let point_matches = first_result
.get("point_matches")
.expect("result has point_matches");
let first_match = &point_matches[0];
let edge_id = first_match
.get("edge_id")
.expect("match has edge_id")
.as_i64()
.expect("edge_id is i64");
assert_eq!(edge_id, 0);
let matched_path = first_result.get("matched_path").unwrap();
assert_eq!(
matched_path.get("type").unwrap().as_str().unwrap(),
"FeatureCollection"
);
assert!(matched_path
.get("features")
.unwrap()
.as_array()
.unwrap()
.is_empty());
}
#[test]
fn test_map_matching_simple_long_trace() {
let app = load_lcss_app();
let trace_points: Vec<serde_json::Value> = (0..9)
.map(|i| {
let x = if i == 0 {
-105.0 + 0.0025
} else if i == 8 {
-105.0 + (i as f64 * 0.01) + 0.0075
} else {
-105.0 + (i as f64 * 0.01) + 0.005
};
serde_json::json!({"x": x, "y": 40.0})
})
.collect();
let query = serde_json::json!({
"trace": trace_points
});
let queries = vec![query];
let result = app.map_match(&queries, None).unwrap();
assert_eq!(result.len(), 1);
let point_matches = result[0]
.get("point_matches")
.expect("result has point_matches")
.as_array()
.expect("point_matches is array");
assert_eq!(point_matches.len(), 9);
for (i, matched) in point_matches.iter().enumerate() {
let edge_id = matched.get("edge_id").unwrap().as_i64().unwrap();
assert_eq!(edge_id, (i * 2) as i64, "Mismatch at index {}", i);
}
}
#[test]
fn test_lcss_eastward_horizontal_trace() {
let app = load_lcss_app();
let trace = TestTrace::eastward_horizontal(0, 5);
run_map_match_test(&app, trace, "LCSS eastward horizontal");
}
#[test]
fn test_lcss_northward_vertical_trace() {
let app = load_lcss_app();
let trace = TestTrace::northward_vertical(0, 5);
run_map_match_test(&app, trace, "LCSS northward vertical");
}
#[test]
fn test_lcss_l_shaped_path() {
let app = load_lcss_app();
let trace = TestTrace::l_shaped();
run_map_match_test(&app, trace, "LCSS L-shaped");
}
#[test]
fn test_lcss_noisy_trace() {
let app = load_lcss_app();
let trace = TestTrace::noisy_eastward_horizontal(0, 5);
run_map_match_test(&app, trace, "LCSS noisy horizontal");
}
#[test]
fn test_map_matching_with_geometry() {
let app = load_lcss_app();
let query = serde_json::json!({
"trace": [
{"x": col_x(0) + SPACING * 0.25, "y": row_y(0)},
{"x": col_x(1) + SPACING * 0.75, "y": row_y(0)}
],
"output_format": "json"
});
let queries = vec![query];
let result = app.map_match(&queries, None).unwrap();
assert_eq!(result.len(), 1);
let first_result = &result[0];
let point_matches = first_result
.get("point_matches")
.unwrap()
.as_array()
.unwrap();
assert_eq!(point_matches.len(), 2);
assert_eq!(
point_matches[0].get("edge_id").unwrap().as_i64().unwrap(),
0
);
assert_eq!(
point_matches[1].get("edge_id").unwrap().as_i64().unwrap(),
2
);
let matched_path = first_result
.get("matched_path")
.unwrap()
.as_array()
.unwrap();
assert_eq!(matched_path.len(), 2);
let matched_edge = &matched_path[0];
assert_eq!(matched_edge.get("edge_id").unwrap().as_i64().unwrap(), 0);
for edge in matched_path {
assert!(
edge.get("geometry").is_some(),
"geometry should be present by default"
);
let geometry = edge
.get("geometry")
.unwrap()
.as_array()
.expect("geometry should be an array");
assert!(!geometry.is_empty(), "geometry should not be empty");
}
let query_no_geom = serde_json::json!({
"trace": [
{"x": col_x(0) + SPACING * 0.25, "y": row_y(0)},
{"x": col_x(1) + SPACING * 0.75, "y": row_y(0)}
],
"output_format": "edge_id"
});
let result_no_geom = app.map_match(&[query_no_geom], None).unwrap();
let matched_path_no_geom = result_no_geom[0]
.get("matched_path")
.unwrap()
.as_array()
.unwrap();
assert_eq!(matched_path_no_geom[0].as_i64().unwrap(), 0);
}
#[test]
fn test_map_matching_formats_and_summaries() {
let app = load_lcss_app();
let query = serde_json::json!({
"trace": [
{"x": col_x(0) + SPACING * 0.25, "y": row_y(0)},
{"x": col_x(1) + SPACING * 0.75, "y": row_y(0)}
],
"output_format": "wkt",
"summary_ops": {
"trip_distance": "sum"
}
});
let queries = vec![query];
let result = app.map_match(&queries, None).unwrap();
assert_eq!(result.len(), 1);
let first_result = &result[0];
let matched_path = first_result
.get("matched_path")
.expect("should have matched_path")
.as_str()
.expect("matched_path should be a string (WKT)");
assert!(matched_path.starts_with("LINESTRING"));
let summary = first_result
.get("traversal_summary")
.expect("should have traversal_summary")
.as_object()
.expect("traversal_summary should be an object");
let trip_distance = summary
.get("trip_distance")
.expect("should have trip_distance in summary")
.as_object()
.expect("trip_distance summary should be an object");
assert!(trip_distance.contains_key("value"));
assert!(trip_distance.contains_key("unit"));
assert_eq!(trip_distance.get("op").unwrap().as_str().unwrap(), "sum");
let query_geojson = serde_json::json!({
"trace": [
{"x": col_x(0) + SPACING * 0.25, "y": row_y(0)},
{"x": col_x(1) + SPACING * 0.75, "y": row_y(0)}
],
"output_format": "geo_json"
});
let result_geojson = app.map_match(&[query_geojson], None).unwrap();
let matched_path_geojson = result_geojson[0]
.get("matched_path")
.expect("should have matched_path")
.as_object()
.expect("matched_path should be a GeoJSON object");
assert_eq!(
matched_path_geojson.get("type").unwrap().as_str().unwrap(),
"FeatureCollection"
);
}