#![allow(clippy::unwrap_used)]
mod common;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use common::cli::CliInvoker;
use common::{
TestMember, TestMeta, TestNode, TestRelation, TestWay, read_header, write_indexed_pbf,
};
use pbfhogg::{BlobDecode, BlobReader, Element, MemberId};
use tempfile::TempDir;
fn node(id: i64) -> TestNode {
let k = i32::try_from(id).expect("small id");
TestNode {
id,
lat: 550_000_000 + k * 1_000,
lon: 120_000_000 + k * 1_000,
tags: vec![],
meta: None,
}
}
fn way(id: i64, refs: Vec<i64>) -> TestWay {
TestWay {
id,
refs,
tags: vec![],
meta: None,
}
}
fn mp_relation(id: i64, way_id: i64) -> TestRelation {
TestRelation {
id,
members: vec![TestMember {
id: MemberId::Way(way_id),
role: "outer",
}],
tags: vec![("type", "multipolygon")],
meta: None,
}
}
fn run_inject(input: &Path, output: &Path, backend: &str) {
let out = CliInvoker::new()
.arg("add-locations-to-ways")
.arg(input)
.arg("-o")
.arg(output)
.arg("--index-type")
.arg(backend)
.arg("--inject-prepass")
.run();
assert!(
out.status.success(),
"{backend} inject failed; stderr:\n{}",
out.stderr_str()
);
}
type WayPins = Vec<(i64, Option<Vec<u8>>)>;
type BlobMembers = Vec<(Vec<u8>, u32)>;
fn read_inject_artifacts(path: &Path) -> (WayPins, BlobMembers) {
let mut reader = BlobReader::from_path(path).expect("open output");
reader.set_parse_waymembers(true);
let mut ways: WayPins = Vec::new();
let mut members: BlobMembers = Vec::new();
for blob in reader {
let blob = blob.expect("read blob");
if let Some(wm) = blob.way_members() {
members.push((wm.to_vec(), blob.way_member_count().expect("count")));
}
if let BlobDecode::OsmData(block) = blob.decode().expect("decode") {
for element in block.elements() {
if let Element::Way(w) = element {
ways.push((w.id(), w.shared_node_pins().map(<[u8]>::to_vec)));
}
}
}
}
ways.sort_by_key(|(id, _)| *id);
(ways, members)
}
fn pins_for(ways: &WayPins, id: i64) -> Option<Vec<u8>> {
ways.iter()
.find(|(w, _)| *w == id)
.and_then(|(_, p)| p.clone())
}
#[test]
fn inject_prepass_sparse_emits_feature_flags_and_bitmaps() {
let dir = TempDir::new().expect("tempdir");
let input = dir.path().join("input.osm.pbf");
let output = dir.path().join("output.osm.pbf");
let nodes: Vec<TestNode> = (1..=4).map(node).collect();
let ways = vec![way(10, vec![1, 2, 3]), way(11, vec![2, 4])];
let relations = vec![mp_relation(20, 10)];
write_indexed_pbf(&input, &nodes, &ways, &relations);
run_inject(&input, &output, "sparse");
let header = read_header(&output);
assert!(header.has_way_members_v1());
assert!(header.has_shared_node_pins_v1());
let (ways_out, members) = read_inject_artifacts(&output);
assert_eq!(pins_for(&ways_out, 10), Some(vec![0b0000_0010]));
assert_eq!(pins_for(&ways_out, 11), Some(vec![0b0000_0001]));
assert_eq!(members, vec![(vec![0b0000_0001], 2)]);
}
#[test]
fn inject_prepass_external_emits_feature_flags() {
let dir = TempDir::new().expect("tempdir");
let input = dir.path().join("input.osm.pbf");
let output = dir.path().join("output.osm.pbf");
let nodes: Vec<TestNode> = (1..=3).map(node).collect();
let ways = vec![way(10, vec![1, 2, 3])];
write_indexed_pbf(&input, &nodes, &ways, &[]);
run_inject(&input, &output, "external");
let header = read_header(&output);
assert!(header.has_way_members_v1());
assert!(header.has_shared_node_pins_v1());
let (_ways, members) = read_inject_artifacts(&output);
assert_eq!(
members.len(),
1,
"the single way blob answers way_members()"
);
}
#[test]
fn backend_parity_inject_prepass() {
let dir = TempDir::new().expect("tempdir");
let input = dir.path().join("input.osm.pbf");
let nodes: Vec<TestNode> = (1..=12).map(node).collect();
let ways = vec![
way(10, vec![1, 2, 3]),
way(11, vec![2, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
];
let relations = vec![mp_relation(20, 10)];
write_indexed_pbf(&input, &nodes, &ways, &relations);
let sparse_out = dir.path().join("sparse.osm.pbf");
let external_out = dir.path().join("external.osm.pbf");
run_inject(&input, &sparse_out, "sparse");
run_inject(&input, &external_out, "external");
let sparse = read_inject_artifacts(&sparse_out);
let external = read_inject_artifacts(&external_out);
assert_eq!(pins_for(&sparse.0, 10), Some(vec![0b0000_0010]));
assert_eq!(pins_for(&sparse.0, 11), Some(vec![0b0000_0001, 0]));
assert_eq!(
sparse.0, external.0,
"shared_node_pins differ between backends"
);
assert_eq!(sparse.1, external.1, "way_members differ between backends");
}
fn expected_pins(nodes: &[TestNode], ways: &[TestWay]) -> HashMap<i64, Option<Vec<u8>>> {
let present: HashSet<i64> = nodes.iter().map(|n| n.id).collect();
let mut count: HashMap<i64, u32> = HashMap::new();
for w in ways {
for &id in trimmed_refs(&w.refs) {
if id >= 0 {
*count.entry(id).or_default() += 1;
}
}
}
let mut out = HashMap::new();
for w in ways {
let mut bits = vec![0u8; w.refs.len().div_ceil(8)];
let mut any = false;
for (i, &id) in w.refs.iter().enumerate() {
let shared = id >= 0 && count.get(&id).copied().unwrap_or(0) >= 2;
let resolved = present.contains(&id);
if shared && resolved {
bits[i / 8] |= 1 << (i % 8);
any = true;
}
}
out.insert(w.id, if any { Some(bits) } else { None });
}
out
}
fn trimmed_refs(refs: &[i64]) -> &[i64] {
if refs.len() >= 4 && refs.first() == refs.last() {
&refs[..refs.len() - 1]
} else {
refs
}
}
fn expected_members(ways: &[TestWay], relations: &[TestRelation]) -> Vec<bool> {
let member_ids: HashSet<i64> = relations
.iter()
.filter(|r| {
r.tags
.iter()
.any(|(k, v)| *k == "type" && matches!(*v, "multipolygon" | "boundary"))
})
.flat_map(|r| r.members.iter())
.filter_map(|m| match m.id {
MemberId::Way(id) if id >= 0 => Some(id),
_ => None,
})
.collect();
ways.iter().map(|w| member_ids.contains(&w.id)).collect()
}
fn pack_lsb(bits: &[bool]) -> Vec<u8> {
let mut out = vec![0u8; bits.len().div_ceil(8)];
for (i, &b) in bits.iter().enumerate() {
if b {
out[i / 8] |= 1 << (i % 8);
}
}
out
}
#[test]
fn inject_prepass_oracle_roundtrip() {
let nodes: Vec<TestNode> = (1..=10).map(node).collect();
let ways = vec![
way(10, vec![1, 2, 3, 4]), way(11, vec![3, 5]), way(12, vec![2, 3, 6, 2]), way(13, vec![7, 8, 7]), way(14, vec![9, 99]), way(15, vec![99, 10]), ];
let relations = vec![
mp_relation(20, 10),
TestRelation {
id: 21,
members: vec![TestMember {
id: MemberId::Way(12),
role: "outer",
}],
tags: vec![("type", "boundary")],
meta: None,
},
TestRelation {
id: 22,
members: vec![TestMember {
id: MemberId::Way(11),
role: "",
}],
tags: vec![("type", "route")], meta: None,
},
TestRelation {
id: 23,
members: vec![
TestMember {
id: MemberId::Node(2),
role: "",
},
TestMember {
id: MemberId::Way(99),
role: "",
},
],
tags: vec![("type", "multipolygon")],
meta: None,
},
];
let dir = TempDir::new().expect("tempdir");
let input = dir.path().join("input.osm.pbf");
write_indexed_pbf(&input, &nodes, &ways, &relations);
let want_pins = expected_pins(&nodes, &ways);
let want_members = pack_lsb(&expected_members(&ways, &relations));
for backend in ["sparse", "external"] {
let output = dir.path().join(format!("{backend}.osm.pbf"));
run_inject(&input, &output, backend);
let header = read_header(&output);
assert!(header.has_way_members_v1(), "{backend}: no WayMembers-v1");
assert!(
header.has_shared_node_pins_v1(),
"{backend}: no SharedNodePins-v1"
);
let (ways_out, members) = read_inject_artifacts(&output);
assert_eq!(
ways_out.len(),
ways.len(),
"{backend}: way count round-trips"
);
for (id, pins) in &ways_out {
assert_eq!(
pins,
want_pins.get(id).expect("known way"),
"{backend}: pins mismatch for way {id}"
);
}
assert_eq!(members.len(), 1, "{backend}: single way blob");
let (bitmap, count) = &members[0];
assert_eq!(bitmap, &want_members, "{backend}: field-5 membership");
assert_eq!(
*count as usize,
ways.len(),
"{backend}: way_member_count == decoded way count"
);
}
}
fn assert_flags_dropped(label: &str, path: &Path) {
let h = read_header(path);
assert!(
!h.has_way_members_v1(),
"{label}: WayMembers-v1 leaked into rewritten output"
);
assert!(
!h.has_shared_node_pins_v1(),
"{label}: SharedNodePins-v1 leaked into rewritten output"
);
}
fn assert_cmd_ok(label: &str, cli: CliInvoker) {
let out = cli.run();
assert!(
out.status.success(),
"{label} failed; stderr:\n{}",
out.stderr_str()
);
}
#[test]
#[allow(clippy::too_many_lines)]
fn rewriting_commands_drop_enrichment_flags() {
let dir = TempDir::new().expect("tempdir");
let raw = dir.path().join("raw.osm.pbf");
let enriched = dir.path().join("enriched.osm.pbf");
let nodes: Vec<TestNode> = (1..=5)
.map(|id| TestNode {
tags: vec![("name", "n")],
meta: Some(TestMeta::default()),
..node(id)
})
.collect();
let ways = vec![
TestWay {
tags: vec![("highway", "primary")],
meta: Some(TestMeta::default()),
..way(10, vec![1, 2, 3])
},
TestWay {
tags: vec![("highway", "service")],
meta: Some(TestMeta::default()),
..way(11, vec![3, 4, 5])
},
];
let relations = vec![mp_relation(20, 10)];
write_indexed_pbf(&raw, &nodes, &ways, &relations);
let out = CliInvoker::new()
.arg("add-locations-to-ways")
.arg(&raw)
.arg("-o")
.arg(&enriched)
.arg("--index-type")
.arg("external")
.arg("--keep-untagged-nodes")
.arg("--inject-prepass")
.run();
assert!(
out.status.success(),
"enrich failed; stderr:\n{}",
out.stderr_str()
);
let enriched_header = read_header(&enriched);
assert!(
enriched_header.has_way_members_v1() && enriched_header.has_shared_node_pins_v1(),
"fixture precondition: enriched input must declare both feature strings"
);
let o = |name: &str| dir.path().join(format!("out_{name}.osm.pbf"));
assert_cmd_ok(
"sort",
CliInvoker::new()
.arg("sort")
.arg(&enriched)
.arg("-o")
.arg(o("sort")),
);
assert_flags_dropped("sort", &o("sort"));
assert_cmd_ok(
"renumber",
CliInvoker::new()
.arg("renumber")
.arg(&enriched)
.arg("-o")
.arg(o("renumber")),
);
assert_flags_dropped("renumber", &o("renumber"));
assert_cmd_ok(
"repack",
CliInvoker::new()
.arg("repack")
.arg(&enriched)
.arg("-o")
.arg(o("repack")),
);
assert_flags_dropped("repack", &o("repack"));
assert_cmd_ok(
"cat",
CliInvoker::new()
.arg("cat")
.arg(&enriched)
.arg("-o")
.arg(o("cat")),
);
assert_flags_dropped("cat", &o("cat"));
assert_cmd_ok(
"cat-dedupe",
CliInvoker::new()
.arg("cat")
.arg(&enriched)
.arg(&enriched)
.arg("--dedupe")
.arg("-o")
.arg(o("cat_dedupe")),
);
assert_flags_dropped("cat-dedupe", &o("cat_dedupe"));
assert_cmd_ok(
"getid",
CliInvoker::new()
.arg("getid")
.arg(&enriched)
.arg("w10")
.arg("-o")
.arg(o("getid")),
);
assert_flags_dropped("getid", &o("getid"));
assert_cmd_ok(
"getparents",
CliInvoker::new()
.arg("getparents")
.arg(&enriched)
.arg("n3")
.arg("-o")
.arg(o("getparents")),
);
assert_flags_dropped("getparents", &o("getparents"));
assert_cmd_ok(
"tags-filter",
CliInvoker::new()
.arg("tags-filter")
.arg(&enriched)
.arg("highway")
.arg("-o")
.arg(o("tags_filter")),
);
assert_flags_dropped("tags-filter", &o("tags_filter"));
assert_cmd_ok(
"time-filter",
CliInvoker::new()
.arg("time-filter")
.arg(&enriched)
.arg("4102444800")
.arg("-o")
.arg(o("time_filter")),
);
assert_flags_dropped("time-filter", &o("time_filter"));
assert_cmd_ok(
"degrade",
CliInvoker::new()
.arg("degrade")
.arg(&enriched)
.arg("--strip-locations")
.arg("-o")
.arg(o("degrade")),
);
assert_flags_dropped("degrade", &o("degrade"));
let bbox = "11,54,13,56";
assert_cmd_ok(
"extract-complete",
CliInvoker::new()
.arg("extract")
.arg(&enriched)
.arg("-b")
.arg(bbox)
.arg("-o")
.arg(o("extract_complete")),
);
assert_flags_dropped("extract-complete", &o("extract_complete"));
assert_cmd_ok(
"extract-simple",
CliInvoker::new()
.arg("extract")
.arg(&enriched)
.arg("-s")
.arg("-b")
.arg(bbox)
.arg("-o")
.arg(o("extract_simple")),
);
assert_flags_dropped("extract-simple", &o("extract_simple"));
assert_cmd_ok(
"extract-smart",
CliInvoker::new()
.arg("extract")
.arg(&enriched)
.arg("--smart")
.arg("-b")
.arg(bbox)
.arg("-o")
.arg(o("extract_smart")),
);
assert_flags_dropped("extract-smart", &o("extract_smart"));
}