facett-map3d/tests/no_building_spikes.rs
//! **The five measured OSM height defects, proved on the EXTRUDED MESH.**
//!
//! `facett-map/src/height.rs` decides the metres; this file proves the decision reaches
//! the geometry, because that is the thing that was visibly wrong. Until 2026-08-31 the
//! parser took the leading run of digits off `height` and believed it, so korp's 3D view
//! drew — MEASURED against korp's live Sweden cut:
//!
//! | tag as written | it drew | it means |
//! |---|---|---|
//! | `height=99999` | a **99 999 m** spike | junk |
//! | `height=1;12` | a **1 m** building | a semicolon list |
//! | `height=25'` | **25 m** | 25 feet = 7.62 m |
//! | `height=102,5` | **102 m** | 102.5 m (Swedish decimal comma) |
//! | `building:levels=3-4` | 3 levels | a range |
//!
//! Each case below is asserted as a rooftop Y on the mesh `extrude::build` returns, and
//! each failure message names the tag string. The comparison is against the mesh built
//! from the value's CORRECT reading — same osm id, same ring, so the extruder's
//! deterministic per-building jitter is identical on both sides and the only difference
//! left is the height.
//!
//! Run: `cargo test -p facett-map3d --test no_building_spikes`
use facett_core::Theme;
use facett_map::{DEFAULT_BUILDING_HEIGHT_M, MAX_PLAUSIBLE_HEIGHT_M, Way};
use facett_map3d::extrude;
/// A ~100 m square footprint at a FIXED place with a FIXED id, tagged as given. Both
/// are fixed on purpose: `building_design_height`'s untagged jitter is keyed on the id
/// and the first ring vertex, so two cases built this way differ only by their tags.
fn building(lon: f64, tags: &[(&str, &str)]) -> Way {
let d = 0.0010; // ~100 m
let pts = [(lon, 47.100), (lon + d, 47.100), (lon + d, 47.101), (lon, 47.101), (lon, 47.100)];
let mut kv: Vec<(String, String)> = vec![("building".into(), "yes".into())];
kv.extend(tags.iter().map(|(k, v)| (k.to_string(), v.to_string())));
Way::from_lonlat(2, &pts, &kv, &Theme::default()).expect("valid building footprint")
}
/// Rooftop Y of the test building, in design units. The layout is a fixed 3 m
/// scale-anchor plus the test building, so the world span (and therefore
/// design-units-per-metre) is identical for every case in this file.
fn top_y(tags: &[(&str, &str)]) -> f32 {
let anchor = {
let d = 0.0010;
let pts = [(9.500, 47.100), (9.500 + d, 47.100), (9.500 + d, 47.101), (9.500, 47.101), (9.500, 47.100)];
let kv: Vec<(String, String)> =
vec![("building".into(), "yes".into()), ("height".into(), "3".into())];
Way::from_lonlat(1, &pts, &kv, &Theme::default()).expect("anchor")
};
let ways = vec![anchor, building(9.520, tags)];
let (mesh, _) = extrude::build(&ways, 0.02);
mesh.positions.iter().map(|p| p[1]).fold(f32::MIN, f32::max)
}
/// Design units per metre for this fixture, read off the mesh rather than guessed: two
/// tagged heights 90 m apart give the scale directly.
fn du_per_m() -> f32 {
(top_y(&[("height", "100")]) - top_y(&[("height", "10")])) / 90.0
}
/// ★ **RED-when-broken: each of the five measured defects extrudes correctly, by name.**
///
/// Revert `facett-map/src/height.rs` and all five fail, each naming its own tag string.
#[test]
fn the_five_measured_defects_extrude_correctly() {
// (defect tag, the tags that spell the CORRECT reading, what it means)
type Tags = &'static [(&'static str, &'static str)];
let cases: &[(Tags, Tags, &str)] = &[
(
&[("height", "99999")],
&[],
"junk: must fall back to the untagged default box, not a 99 999 m spike",
),
(
&[("height", "1;12")],
&[],
"a semicolon list is two statements about one building; taking the first drew a 1 m house",
),
(&[("height", "25'")], &[("height", "7.62")], "25 FEET is 7.62 m, not 25 m"),
(
&[("height", "102,5")],
&[("height", "102.5")],
"a Swedish decimal comma is 102.5 m; the old parser stopped at the comma and drew 102 m",
),
(
&[("building:levels", "3-4")],
&[],
"a level RANGE is not a level count; the old parser read the leading `3`",
),
];
// Collected, not asserted one at a time: reverting the parser breaks ALL FIVE, and
// a failure that names only the first hides four.
let mut wrong: Vec<String> = Vec::new();
for (defect, correct, why) in cases {
let (got, want) = (top_y(defect), top_y(correct));
if (got - want).abs() >= 1e-4 {
wrong.push(format!(
" `{defect:?}` extruded a rooftop at {got:.6} du, but its correct reading \
`{correct:?}` extrudes to {want:.6} du — {why}"
));
}
}
assert!(
wrong.is_empty(),
"{} of the 5 measured OSM height defects still reach the mesh:\n{}",
wrong.len(),
wrong.join("\n")
);
// The all-`None` guard. Every "correct" arm above but two is the DEFAULT box, so a
// parser that returned `None` for everything would pass the loop. A real tagged
// height must still come through taller than the default.
let tagged = top_y(&[("height", "40")]);
let default = top_y(&[]);
assert!(
tagged > default * 3.0,
"a 40 m tagged building ({tagged:.6} du) is barely above the ~6 m default \
({default:.6} du) — the tagged path is not reaching the mesh at all"
);
}
/// ★ **RED-when-broken: no tag value can put a spike through the sky.**
///
/// The bound is on the APPLIED output — the rooftop Y of the mesh — expressed in metres
/// via the fixture's own scale, so it cannot be satisfied by a parser that merely
/// returns a small number and an extruder that then multiplies it back up.
#[test]
fn no_tag_value_extrudes_above_the_plausibility_ceiling() {
let scale = du_per_m();
assert!(scale > 0.0, "the fixture's design-units-per-metre came out {scale}");
let ceiling_du = MAX_PLAUSIBLE_HEIGHT_M * scale;
for tags in [
vec![("height", "99999")],
vec![("height", "999999999")],
vec![("height", "12000")],
vec![("height", "50000 m")],
vec![("height", "1e9")],
vec![("height", "99999'")],
vec![("building:levels", "9999")],
vec![("building:levels", "99999")],
] {
let y = top_y(&tags);
assert!(
y <= ceiling_du,
"`{tags:?}` extruded a rooftop at {:.1} m ({y:.4} du), above the \
{MAX_PLAUSIBLE_HEIGHT_M} m ceiling — that is the 99 999 m spike this bound \
exists to stop",
y / scale
);
}
// The ceiling is not vacuous: a real 828 m tower must still reach the mesh.
let burj = top_y(&[("height", "828")]) / scale;
assert!(
(burj - 828.0).abs() < 1.0,
"the Burj Khalifa extruded to {burj:.1} m — the ceiling must refuse junk, not \
real buildings"
);
}
/// ★ **RED-when-broken: the 95.68 % path is EXACTLY the box it was.**
///
/// 3 675 499 of korp's 3 841 483 Swedish buildings (95.6792 %, MEASURED 2026-08-31) have
/// no height tag at all, so this is the branch nearly the whole 3D view is drawn on.
/// Correcting `METRES_PER_LEVEL` from 3.0 to the measured 3.2 must NOT drag it from 6.0 m
/// to 6.4 m as a side effect — which is why the default is now its own constant.
///
/// Change `DEFAULT_BUILDING_HEIGHT_M` and this names the number in metres.
#[test]
fn the_untagged_default_box_did_not_move() {
let scale = du_per_m();
let default_m = top_y(&[]) / scale;
// PINNED to the metre, as a LITERAL. The untagged path is 6.0 m carrying a
// DETERMINISTIC ±25 % jitter keyed on the osm id + first ring vertex, both fixed by
// this fixture — so this building has exactly one height, MEASURED at 5.6119 m on
// origin/main before this branch touched anything. A band would have tolerated the
// 6.4 m that correcting METRES_PER_LEVEL to 3.2 would have caused; a literal does
// not. Change the default, the jitter or the footprint cap and this names the metres.
assert!(
(default_m - 5.6119).abs() < 5e-3,
"an untagged building now extrudes to {default_m:.4} m; on origin/main it was \
5.6119 m (6.0 m x the fixture's fixed jitter). That branch is 95.68 % of every \
town in the 3D view — 3 675 499 of korp's 3 841 483 Swedish buildings — and this \
fix is about junk tags, not about the look."
);
assert!(
(DEFAULT_BUILDING_HEIGHT_M - 6.0).abs() < 1e-6,
"DEFAULT_BUILDING_HEIGHT_M is {DEFAULT_BUILDING_HEIGHT_M} m, not 6.0"
);
// A building whose height tag was REJECTED gets that same box — not a guess, and
// not a special case either.
for junk in ["99999", "1;12", "B", "0", "Österbybruks hembygdsgård"] {
let y = top_y(&[("height", junk)]);
assert!(
(y - top_y(&[])).abs() < 1e-6,
"`height={junk}` extruded to {y:.6} du instead of the untagged default box"
);
}
}