mod common;
use common::render_with_features;
use ezu_features::{Feature, FeatureLayer, Geometry, Value};
use ezu_graph::TileId;
use std::collections::HashMap;
fn line_feature(class: &str, y: i32) -> Feature {
let mut properties = HashMap::new();
properties.insert("class".to_string(), Value::String(class.to_string()));
let mut geometry = Geometry::default();
geometry.lines.push(vec![(0, y), (4095, y)]);
Feature {
id: None,
geometry,
properties,
}
}
fn one_line_layer(y: i32) -> FeatureLayer {
FeatureLayer {
name: "roads".to_string(),
extent: 4096,
features: vec![line_feature("x", y)],
}
}
fn recipe(extra: &str) -> String {
format!(
r##"{{
"name": "gap",
"tile-size": 64,
"sources": {{ "src": {{ "type": "mvt", "url": "http://example.invalid/{{z}}/{{x}}/{{y}}" }} }},
"nodes": {{
"feats": {{ "op": "features", "source": "src", "layer": "roads" }},
"out": {{ "op": "stroke", "features": "@feats", "color": "#ff0000", "width-px": 4{extra} }}
}},
"output": "@out"
}}"##
)
}
#[test]
fn gap_width_renders_an_annulus_of_the_expected_footprint() {
let r = render_with_features(
&recipe(r#", "gap-width-px": 8"#),
64,
0,
TileId { z: 0, x: 0, y: 0 },
&[("src.roads", one_line_layer(2048))],
);
let opaque = |y: u32| r.pixel(32, y)[3] > 200;
for y in [22, 23, 40, 41] {
assert!(!opaque(y), "row {y} is outside the footprint");
}
for y in [25, 26, 37, 38] {
assert!(opaque(y), "row {y} is inside a casing band");
}
for y in [29, 32, 34] {
assert!(!opaque(y), "row {y} is inside the gap");
}
let covered = (0..64).filter(|&y| opaque(y)).count();
assert_eq!(covered, 8, "footprint should be two 4 px bands");
}
#[test]
fn zero_gap_is_byte_identical_to_a_plain_stroke() {
let tile = TileId { z: 0, x: 0, y: 0 };
let plain = render_with_features(
&recipe(""),
64,
0,
tile,
&[("src.roads", one_line_layer(2048))],
);
for extra in [
r#", "gap-width-px": 0"#,
r#", "gap-width-expr": ["match", ["get","class"], "nope", 8, 0]"#,
] {
let gapped = render_with_features(
&recipe(extra),
64,
0,
tile,
&[("src.roads", one_line_layer(2048))],
);
assert_eq!(
plain.pixels, gapped.pixels,
"a zero gap must paint exactly like a plain stroke ({extra})"
);
}
let covered = (0..64).filter(|&y| plain.pixel(32, y)[3] > 200).count();
assert_eq!(covered, 4);
}
#[test]
fn a_casing_lands_where_the_line_is_not_where_the_tile_starts() {
let mut geometry = Geometry::default();
geometry.lines.push(vec![(2560, 3072), (4095, 3072)]);
let layer = FeatureLayer {
name: "roads".to_string(),
extent: 4096,
features: vec![Feature {
id: None,
geometry,
properties: HashMap::new(),
}],
};
let r = render_with_features(
&recipe(r#", "gap-width-px": 8"#),
64,
0,
TileId { z: 0, x: 0, y: 0 },
&[("src.roads", layer)],
);
let opaque = |x: u32, y: u32| r.pixel(x, y)[3] > 200;
assert!(opaque(50, 41) && opaque(50, 54), "both bands are drawn");
assert!(!opaque(50, 48), "the corridor is knocked out");
assert_eq!(
(0..64).filter(|&y| opaque(50, y)).count(),
8,
"two 4 px bands, and nothing shifted onto other rows"
);
assert_eq!(
(0..64).filter(|&y| opaque(20, y)).count(),
0,
"nothing before the line starts"
);
}
#[test]
fn gap_width_expr_is_data_driven() {
let layer = FeatureLayer {
name: "roads".to_string(),
extent: 4096,
features: vec![line_feature("narrow", 1024), line_feature("wide", 3072)],
};
let r = render_with_features(
&recipe(r#", "gap-width-expr": ["match", ["get","class"], "wide", 16, 0]"#),
64,
0,
TileId { z: 0, x: 0, y: 0 },
&[("src.roads", layer)],
);
let opaque = |y: u32| r.pixel(32, y)[3] > 200;
assert!(opaque(16), "narrow line should be solid at its centre");
assert_eq!((8..24).filter(|&y| opaque(y)).count(), 4);
assert!(!opaque(48), "wide line should be hollow at its centre");
assert!(
opaque(37) && opaque(57),
"wide line keeps both casing bands"
);
assert_eq!((32..64).filter(|&y| opaque(y)).count(), 8);
}