Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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());
}