facett-map3d/tests/wall_roof_ratio.rs
//! **WALL vs FOOTPRINT — a building whose walls swallow its roof is a NUMBER.**
//!
//! Reported: *"the houses' walls have been enlarged and the roofs look like they
//! lean directly on the ground, but I believe it's the walls that have been
//! enlarged, so it looks like ground."*
//!
//! That is a proportion, and a proportion is measurable. A building's wall (its
//! eave height) is bounded by its own plan: you do not build a 6 m wall on a 4 m
//! shed. When the wall grows past the footprint, two things happen at once and they
//! are exactly what was reported — the facade dominates every oblique view (a broad
//! flat surface that reads as ground), and the roof, whose rise scales with the
//! footprint and not with the wall, shrinks to a lid perched on top.
//!
//! ## What was actually wrong
//!
//! Only **69 of the embedded Sigtuna extract's 10373 buildings** carry a `height` or
//! `building:levels` tag — 0.7%. The other 99.3% fell back to a CONSTANT
//! `DEFAULT_BUILDING_HEIGHT_M` = 6 m (spelled `DEFAULT_BUILDING_LEVELS ×
//! METRES_PER_LEVEL` until 2026-08-31, when the level converter was corrected to the
//! measured 3.2 m and the default was pinned as its own literal so it did NOT move with
//! it), a number with no knowledge of the footprint it was lifting. On the demo's Sigtuna town core (182 buildings,
//! footprint long side: p05 4.41 m, median 7.52 m) that produced:
//!
//! | quantity | before | after |
//! |---|---:|---:|
//! | buildings TALLER than they are LONG | **77 of 182** | **0** |
//! | worst wall / long side | 1.77 | 0.90 |
//! | median wall / long side | 0.83 | 0.72 |
//! | median roof rise / wall | 0.36 | 0.41 |
//!
//! The fix caps an **untagged** default against the footprint's short side
//! ([`MAX_DEFAULT_WALL_OVER_FOOTPRINT`]). A TAGGED height is real data and is never
//! capped — a genuine tower on a small plot stays a tower, asserted below.
//!
//! ## Why the assertion is shaped like this
//!
//! `taller_than_long == 0` is a *structural* consequence of the cap (`wall ≤ 0.9 ×
//! short ≤ 0.9 × long`), not a threshold tuned to today's dataset — which is what
//! makes it a regression guard rather than a golden. Remove the cap and it fails by
//! 77 counts on real data; weaken the cap and it fails continuously.
use facett_map3d::extrude::{
building_height_stats, footprint_plan_metres, DESIGN_FILL, MAX_DEFAULT_WALL_OVER_FOOTPRINT,
};
use facett_map::Way;
use facett_map3d::extrude::M_PER_DEG_LAT;
/// A square-ish building footprint of `side_m` metres near Sigtuna, optionally with
/// a real `height` tag. Two of them `sep_m` apart give the scene a world span, so
/// the extruder has a metric scale to work in.
fn building(id: i64, lon: f64, lat: f64, side_m: f64, height_m: Option<&str>) -> Way {
let dlat = side_m / M_PER_DEG_LAT;
let dlon = side_m / (M_PER_DEG_LAT * lat.to_radians().cos());
let ring = [
(lon, lat),
(lon + dlon, lat),
(lon + dlon, lat + dlat),
(lon, lat + dlat),
(lon, lat),
];
let mut tags: Vec<(String, String)> = vec![("building".into(), "yes".into())];
if let Some(h) = height_m {
tags.push(("height".into(), h.into()));
}
Way::from_lonlat(id, &ring, &tags, &facett_core::Theme::default()).expect("a building way")
}
/// A town of `n` small untagged footprints spread over ~`span_m`, the shape of a
/// real OSM extract: almost nothing carries a height tag. Deterministic.
fn untagged_town(n: usize, span_m: f64) -> Vec<Way> {
let (lon0, lat0) = (17.72_f64, 59.62_f64);
let cols = (n as f64).sqrt().ceil() as usize;
(0..n)
.map(|i| {
let (gx, gy) = (i % cols, i / cols);
let step = span_m / cols as f64;
let lon = lon0 + (gx as f64 * step) / (M_PER_DEG_LAT * lat0.to_radians().cos());
let lat = lat0 + (gy as f64 * step) / M_PER_DEG_LAT;
// Footprints 3.5–11 m — the measured Sigtuna distribution, where the
// constant 6 m default did its damage.
let side = 3.5 + (i % 8) as f64;
building(i as i64 + 1, lon, lat, side, None)
})
.collect()
}
/// **THE RED-WHEN-BROKEN ASSERTION.** No building may be taller than its own
/// footprint is long. Under the uncapped constant default this fails by dozens of
/// counts on a realistic untagged town.
#[test]
fn no_building_is_taller_than_its_own_footprint_is_long() {
let ways = untagged_town(200, 800.0);
let s = building_height_stats(&ways, 3.0);
assert!(s.buildings >= 150, "the town extruded, got {} buildings", s.buildings);
assert_eq!(s.tagged, 0, "this town is untagged — the case the default height serves");
assert_eq!(
s.taller_than_long, 0,
"{} of {} buildings are TALLER THAN THEY ARE LONG — walls that swallow their roofs \
(worst ratio {:.2})",
s.taller_than_long, s.buildings, s.wall_over_footprint_max
);
// The cap is the structural bound, so the worst case lands exactly on it and
// never past it.
assert!(
s.wall_over_footprint_max <= MAX_DEFAULT_WALL_OVER_FOOTPRINT + 1e-4,
"worst wall/long-side {:.3} exceeds the cap {MAX_DEFAULT_WALL_OVER_FOOTPRINT}",
s.wall_over_footprint_max
);
// …and the TYPICAL building is comfortably wider than it is tall, which is what
// "a house, not a chimney" means.
assert!(
s.wall_over_footprint_median < 0.85,
"the median house is wider than it is tall, got {:.3}",
s.wall_over_footprint_median
);
}
/// The roof must remain a real part of the silhouette. When the wall runs away the
/// roof rise (which scales with the FOOTPRINT) becomes a vanishing fraction of the
/// building — the "roofs look like they lean directly on the ground" half of the
/// report. Pin a floor under it.
#[test]
fn the_roof_is_a_real_share_of_the_building_not_a_lid() {
let ways = untagged_town(200, 800.0);
let s = building_height_stats(&ways, 3.0);
assert!(
s.roof_over_wall_median > 0.30,
"the median roof rise is {:.3} of its wall — a lid on a box, not a roof",
s.roof_over_wall_median
);
}
/// **Tagged heights are REAL DATA and are never capped.** A 40 m tower on a 12 m
/// plot is a legitimate OSM building; the footprint cap exists to bound a *guess*,
/// not to overrule a measurement. This is the guard that stops the fix becoming a
/// new bug in the other direction.
#[test]
fn a_tagged_height_is_never_capped_by_its_footprint() {
// One tagged tower + one untagged shed of the same footprint, in one scene so
// they share a world scale.
let ways = vec![
building(1, 17.7200, 59.6200, 12.0, Some("40")),
building(2, 17.7250, 59.6200, 12.0, None),
// A third, far away, so the scene has a real span to scale by.
building(3, 17.7400, 59.6260, 10.0, None),
];
let s = building_height_stats(&ways, 3.0);
assert_eq!(s.tagged, 1, "one building carries a real height tag");
assert!(
s.wall_over_footprint_max > 3.0,
"the TAGGED 40 m tower on a 12 m plot survives uncapped (max ratio {:.2})",
s.wall_over_footprint_max
);
assert_eq!(s.taller_than_long, 1, "…and it is the only building over the line");
}
/// `footprint_plan_metres` is the one measurement both the cap and the oracle read,
/// so pin it directly: a ring of a known metric size must measure back to that size.
#[test]
fn footprint_plan_metres_measures_the_real_plan() {
let ways = vec![
building(1, 17.7200, 59.6200, 10.0, None),
building(2, 17.7400, 59.6260, 10.0, None),
];
let span = facett_map3d::extrude::world_span_metres(&ways).expect("a metric span");
let du_per_m = DESIGN_FILL / span;
// Rebuild the first ring in design units exactly as `build` does.
let stats = building_height_stats(&ways, 3.0);
assert!(stats.buildings >= 2);
// A 10 m square footprint in design units.
let side_du = 10.0 * du_per_m;
let ring = [(0.0, 0.0), (side_du, 0.0), (side_du, side_du), (0.0, side_du), (0.0, 0.0)];
let (short, long) = footprint_plan_metres(&ring, du_per_m).expect("a measurable plan");
assert!((short - 10.0).abs() < 0.1, "short side measures 10 m, got {short}");
assert!((long - 10.0).abs() < 0.1, "long side measures 10 m, got {long}");
// Degenerate rings are rejected rather than producing a bogus cap.
assert!(footprint_plan_metres(&[], du_per_m).is_none());
assert!(footprint_plan_metres(&ring, 0.0).is_none());
}