facett-geomap/tests/osm_pin_labels.rs
//! **The other half of the clustering lesson: PIN LABEL collision.**
//!
//! The markers learned density first. `osm2d::cluster` collapses co-located pins into
//! one count badge and splits them back apart on zoom-in, so a dense city reads as
//! groups instead of a pile of teardrops. Their **labels** were left exactly as they
//! were: every single pin printed its name at its own anchor, on top of the next one.
//!
//! That is half a cure. Zoomed to where the pins separate into singles — the zoom at
//! which the marker clustering has *finished its job* — the names all appear at once
//! and the pane is unreadable in a different way. The pixels still say "there are pins
//! here"; they no longer say which pin is which.
//!
//! Text cannot be merged the way markers can (two overlapping names are strictly less
//! readable than one), so the honest move is to rank and drop, and that is what
//! `facett_core::legibility::place_labels` does — the SAME placer the graph panes use.
//!
//! What this asserts, through the public surface (`place_pin_labels` + `state_json`):
//!
//! 1. **the pixel oracle** — no two drawn pin labels share a pixel, while the pre-fix
//! behaviour (every label at its anchor) is measured in the same test and piles up;
//! 2. a lone pin in open space still gets its label (thinning must not be a blanket);
//! 3. the **selected** pin's label is never dropped — it is the answer to the click;
//! 4. a group badge never competes for a label (its count lives inside its own disc);
//! 5. `state_json` publishes `pin_labels` + `pin_labels_suppressed`, so the compression
//! is a matrix oracle rather than a claim.
//!
//! Run: `cargo test -p facett-geomap --test osm_pin_labels`.
use facett_core::legibility::max_label_overlap;
use facett_geomap::osm2d::{Msg, OsmView, Pin};
const RECT: egui::Rect =
egui::Rect { min: egui::pos2(0.0, 0.0), max: egui::pos2(900.0, 700.0) };
/// A named-address BLOCK: a 20 × 15 lattice of pins framed so consecutive markers land
/// ~47 px apart — just past the 44 px clustering bin, so **every pin stays its own
/// single** and the marker cure has nothing left to do. Their names are real address
/// labels ("Norra Stationsgatan 118" ≈ 130 px wide), so 300 of them want ~546 000 px²
/// of text in a 630 000 px² pane. No placement can fit that, which is the point: this
/// is the density at which the pre-fix painter printed all 300 on top of each other and
/// called it done.
fn named_street() -> Vec<Pin> {
(0..300u32)
.map(|i| {
let lon = 17.7100 + (i % 20) as f64 * 0.000562;
let lat = 59.6150 + (i / 20) as f64 * 0.000300;
Pin::new(lon, lat, format!("Norra Stationsgatan {}", i + 1))
})
.collect()
}
fn view_with(pins: Vec<Pin>) -> OsmView {
let (points, ways) = facett_geomap::osm2d::sample_dataset(500);
let mut v = OsmView::new("navigator", points, ways).with_pins(pins);
v.fit_to(RECT);
v
}
/// Frame the pins' own bbox so the test is about labels, not framing.
fn center_on(v: &mut OsmView, pins: &[Pin]) {
let (mut lo, mut hi) = (pins[0].lonlat, pins[0].lonlat);
for p in pins {
lo.0 = lo.0.min(p.lonlat.0);
lo.1 = lo.1.min(p.lonlat.1);
hi.0 = hi.0.max(p.lonlat.0);
hi.1 = hi.1.max(p.lonlat.1);
}
let (x0, y0) = facett_geomap::mercator(lo.0, hi.1);
let (x1, y1) = facett_geomap::mercator(hi.0, lo.1);
v.camera_mut().fit_merc_bbox([x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)], RECT);
}
/// A real egui painter, so the label boxes come from measured galleys (the sizes the
/// pane actually paints), not from a guess about character widths.
fn with_painter<R>(f: impl FnOnce(&egui::Painter) -> R) -> R {
let ctx = egui::Context::default();
let mut f = Some(f);
let mut out = None;
ctx.run_ui(egui::RawInput { screen_rect: Some(RECT), ..Default::default() }, |_ui| {
let painter = ctx.layer_painter(egui::LayerId::background());
if let Some(f) = f.take() {
out = Some(f(&painter));
}
}).textures_delta.clear();
out.expect("the painter closure ran")
}
/// **(1) THE PIXEL ORACLE.** The labels that get drawn are pixel-disjoint; the pre-fix
/// behaviour — every single pin's name printed at its own anchor — is rasterized in the
/// same test and piles several deep. So "the label pile is gone" is a comparison
/// against the measured old behaviour, not a promise.
#[test]
fn drawn_pin_labels_never_share_a_pixel_and_the_control_is_a_pile() {
let pins = named_street();
let mut v = view_with(pins.clone());
center_on(&mut v, &pins);
let (boxes, suppressed, clusters) = with_painter(|p| {
let c = v.clusters_for(RECT);
let (b, s) = v.place_pin_labels(p, &c, RECT);
(b, s, c)
});
let singles: Vec<_> = clusters.iter().filter(|c| c.is_single() && !c.label.is_empty()).collect();
assert!(singles.len() > 20, "the street must resolve to many single pins, got {}", singles.len());
let drawn: Vec<egui::Rect> = boxes.values().copied().collect();
assert!(!drawn.is_empty(), "some labels must survive — thinning is not a blanket ban");
assert_eq!(
max_label_overlap(&drawn, RECT, 1.0),
1,
"no pixel may carry two pin labels"
);
assert!(
suppressed > 0,
"a street of {} named pins in one pane must drop some labels; {} drawn, 0 suppressed",
singles.len(),
drawn.len()
);
assert_eq!(
drawn.len() + suppressed,
singles.len(),
"every labelled single is either drawn or suppressed — none vanish unaccounted"
);
// THE CONTROL — the pre-fix painter: every single's name at its own anchor.
let control: Vec<egui::Rect> = with_painter(|p| {
singles
.iter()
.map(|c| {
let g = p.layout_no_wrap(
c.label.clone(),
egui::FontId::proportional(11.0),
egui::Color32::WHITE,
);
egui::Rect::from_center_size(c.pos + egui::vec2(0.0, -9.0), g.size())
})
.collect()
});
let peak = max_label_overlap(&control, RECT, 1.0);
assert!(
peak >= 2,
"the pre-fix behaviour must really overplot for this test to mean anything, peak {peak}"
);
// Peak depth alone under-reports a wide, even pile (a regular lattice of names each
// overlapping only its immediate neighbours never stacks deep, but every single one
// of them is unreadable). So also count how many control labels collide with at
// least one other: that is the number of names the pre-fix painter made illegible.
let colliding = control
.iter()
.enumerate()
.filter(|(i, a)| control.iter().enumerate().any(|(j, b)| j != *i && a.intersects(*b)))
.count();
assert!(
colliding * 2 > control.len(),
"the control must leave most names unreadable: {colliding} of {} collide",
control.len()
);
assert!(
drawn.len() < colliding,
"and the cure must draw fewer than the pile it dissolved ({} vs {colliding})",
drawn.len()
);
}
/// **(2)** A lone pin in open space keeps its label. Collision avoidance must bite only
/// where there is a collision.
#[test]
fn a_lone_pin_still_gets_its_label() {
let solo = vec![Pin::new(17.0, 59.0, "Ensam gata 1")];
let mut v = view_with(solo.clone());
v.camera_mut().fit_merc_bbox(
{
let (x, y) = facett_geomap::mercator(17.0, 59.0);
[x - 0.001, y - 0.001, x + 0.001, y + 0.001]
},
RECT,
);
let (boxes, suppressed) = with_painter(|p| {
let c = v.clusters_for(RECT);
assert_eq!(c.len(), 1, "one pin ⇒ one marker");
assert!(c[0].is_single());
v.place_pin_labels(p, &c, RECT)
});
assert_eq!(boxes.len(), 1, "the lone label is drawn");
assert_eq!(suppressed, 0);
}
/// **(3)** The SELECTED pin's label is never suppressed, however crowded it is. It is
/// the answer to the user's click; dropping it would make selection invisible.
#[test]
fn the_selected_pins_label_is_never_dropped() {
let pins = named_street();
let mut v = view_with(pins.clone());
center_on(&mut v, &pins);
// Find a pin whose label the placer WOULD drop unselected...
let clusters = with_painter(|_| v.clusters_for(RECT));
let (dropped_idx, dropped_id) = {
let boxes = with_painter(|p| v.place_pin_labels(p, &clusters, RECT).0);
clusters
.iter()
.enumerate()
.find(|(i, c)| c.is_single() && !c.label.is_empty() && !boxes.contains_key(i))
.map(|(i, c)| (i, c.id.clone()))
.expect("the crowd must suppress at least one label for this test to mean anything")
};
// ...then select it and watch it come back.
v.update(Msg::SelectPin(dropped_id.clone()));
let boxes = with_painter(|p| v.place_pin_labels(p, &clusters, RECT).0);
assert!(
boxes.contains_key(&dropped_idx),
"selecting pin {dropped_id} must bring its label back (it was suppressed before)"
);
}
/// **(4)** A GROUP badge never enters the label competition — its count is painted
/// inside its own disc, so letting it reserve a text box would starve real names.
#[test]
fn group_badges_do_not_compete_for_label_space() {
// Pins piled tight enough to cluster, at a wide zoom.
let pins: Vec<Pin> = (0..200u32)
.map(|i| {
Pin::new(
17.7000 + (i % 10) as f64 * 0.00002,
59.6000 + (i / 10) as f64 * 0.00001,
format!("Tät gata {i}"),
)
})
.collect();
let mut v = view_with(pins.clone());
center_on(&mut v, &pins);
v.zoom_to_cursor(0.05, RECT.center(), RECT);
let (boxes, suppressed, clusters) = with_painter(|p| {
let c = v.clusters_for(RECT);
let (b, s) = v.place_pin_labels(p, &c, RECT);
(b, s, c)
});
let groups: Vec<usize> = clusters
.iter()
.enumerate()
.filter(|(_, c)| !c.is_single())
.map(|(i, _)| i)
.collect();
assert!(!groups.is_empty(), "the zoom must actually produce group badges");
for g in groups {
assert!(!boxes.contains_key(&g), "group badge {g} must not reserve a text label");
}
let labelled_singles =
clusters.iter().filter(|c| c.is_single() && !c.label.is_empty()).count();
assert_eq!(boxes.len() + suppressed, labelled_singles, "only singles are accounted");
}
/// **(5)** `state_json` publishes the numbers, so the matrix (and a robot) can assert
/// on the compression instead of on a doc sentence.
#[test]
fn state_json_publishes_the_pin_label_compression() {
let pins = named_street();
let mut v = view_with(pins.clone());
center_on(&mut v, &pins);
// Render one real frame so the pane's own paint path fills the counters — this is
// the pane's behaviour, not a helper called directly.
let r = facett_core::harness::render_sized(&mut v, (900.0, 700.0));
assert!(r.drew(), "the navigator drew a frame");
let drawn = r.state["pin_labels"].as_u64().expect("pin_labels is published");
let dropped = r.state["pin_labels_suppressed"].as_u64().expect("pin_labels_suppressed too");
assert!(drawn > 0, "some labels drew: {}", r.state["pin_labels"]);
assert!(
dropped > 0,
"a crowded street must report suppressed labels; drawn={drawn} suppressed={dropped}"
);
// The markers still cluster — the two cures are independent and both live.
assert!(r.state["visible_markers"].as_u64().unwrap() > 0);
assert!(r.state["visible_pins"].as_u64().unwrap() >= r.state["visible_markers"].as_u64().unwrap());
}