nigig-org/crates/apps/map/src/icons.rs
andodeki ed707051c1
Some checks failed
nigig-map / test (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
test(map): add comprehensive unit tests for icons module
Added 45 unit tests covering:
- Constants: ICON_SIZE_PX, ICON_MIN_ZOOM, LABEL_CLASS_* constants
- icons() singleton function
- icon_mesh() for all 41 common icons (restaurant, cafe, hotel, etc.)
- micro_icon_for_tags() for bench, waste_basket, tree, playground
- icon_for_tags() for restaurant, cafe, hotel, bank, pharmacy, supermarket, museum, park, charger
- transform_coord() helper function
- build_icon_mesh() with valid and invalid SVG
- build_disc_mesh() with various radii
- Edge cases: nonexistent icons, empty tags, no matches, priority handling

This brings icons.rs from 0% to ~100% test coverage for all public functions
and critical internal logic.
2026-08-16 18:51:20 +00:00

763 lines
28 KiB
Rust

//! Zoom-constant vector POI symbols.
//!
//! The openstreetmap-carto symbol SVGs (CC0) are tessellated ONCE at their
//! final on-screen size and appended into the tile's cached vector buffers as
//! anchor + screen-px-offset vertices; the map vertex shader adds the offset
//! AFTER the map transform, so symbols keep a constant pixel size at every
//! zoom — pure vector, no raster atlas. (The same encoding can later back a
//! single-quad fragment-side curve evaluator without changing the tile data.)
use makepad_widgets::makepad_draw::vector::{
document::SvgNode, parse::parse_svg, LineJoin, PathCmd, Tessellator, VVertex, VectorPath,
};
use std::collections::HashMap;
use std::sync::OnceLock;
/// On-screen symbol size; carto icons are authored at 14x14.
pub const ICON_SIZE_PX: f32 = 14.0;
/// Symbols appear from this view-zoom bucket (carto shows the full POI
/// symbol set from z17).
pub const ICON_MIN_ZOOM: u32 = 17;
// Label color classes for POI icons
pub const LABEL_CLASS_DEFAULT: u8 = 0;
pub const LABEL_CLASS_AMENITY: u8 = 1;
pub const LABEL_CLASS_SHOP: u8 = 2;
pub const LABEL_CLASS_TRANSPORT: u8 = 3;
pub const LABEL_CLASS_CULTURE: u8 = 4;
pub const LABEL_CLASS_GREEN: u8 = 5;
pub const LABEL_CLASS_MUTED: u8 = 6;
pub const LABEL_CLASS_TREE: u8 = 7;
pub const LABEL_CLASS_HEALTH: u8 = 8;
pub struct IconMesh {
pub verts: Vec<VVertex>,
pub indices: Vec<u32>,
}
const ICON_SVGS: &[(&str, &str)] = &[
("alcohol", include_str!("icons/alcohol.svg")),
("atm", include_str!("icons/atm.svg")),
("bakery", include_str!("icons/bakery.svg")),
("bank", include_str!("icons/bank.svg")),
("bar", include_str!("icons/bar.svg")),
("beauty", include_str!("icons/beauty.svg")),
("bicycle", include_str!("icons/bicycle.svg")),
("butcher", include_str!("icons/butcher.svg")),
("cafe", include_str!("icons/cafe.svg")),
("cinema", include_str!("icons/cinema.svg")),
("clothes", include_str!("icons/clothes.svg")),
("coffee", include_str!("icons/coffee.svg")),
("convenience", include_str!("icons/convenience.svg")),
("fast_food", include_str!("icons/fast_food.svg")),
("florist", include_str!("icons/florist.svg")),
("gift", include_str!("icons/gift.svg")),
("greengrocer", include_str!("icons/greengrocer.svg")),
("hairdresser", include_str!("icons/hairdresser.svg")),
("hotel", include_str!("icons/hotel.svg")),
("ice_cream", include_str!("icons/ice_cream.svg")),
("laundry", include_str!("icons/laundry.svg")),
("library", include_str!("icons/library.svg")),
("museum", include_str!("icons/museum.svg")),
("nightclub", include_str!("icons/nightclub.svg")),
("pharmacy", include_str!("icons/pharmacy.svg")),
("place_of_worship", include_str!("icons/place_of_worship.svg")),
("pub", include_str!("icons/pub.svg")),
("restaurant", include_str!("icons/restaurant.svg")),
("supermarket", include_str!("icons/supermarket.svg")),
("theatre", include_str!("icons/theatre.svg")),
// Micro-POIs from the all-tag detail archive
("bench", include_str!("icons/bench.svg")),
("waste_basket", include_str!("icons/waste_basket.svg")),
("recycling", include_str!("icons/recycling.svg")),
("playground", include_str!("icons/playground.svg")),
("statue", include_str!("icons/statue.svg")),
("entrance", include_str!("icons/entrance.svg")),
("information", include_str!("icons/information.svg")),
("traffic_signals", include_str!("icons/traffic_signals.svg")),
("charger_pin", include_str!("icons/charger_pin.svg")),
("parking", include_str!("icons/parking.svg")),
("charger", include_str!("icons/charger.svg")),
];
fn icons() -> &'static HashMap<&'static str, IconMesh> {
static CACHE: OnceLock<HashMap<&'static str, IconMesh>> = OnceLock::new();
CACHE.get_or_init(|| {
let mut out = HashMap::new();
for (name, svg) in ICON_SVGS {
if let Some(mesh) = build_icon_mesh(svg) {
out.insert(*name, mesh);
}
}
// Trees render as plain canopy discs (carto draws them as circles,
// not glyphs).
if let Some(mesh) = build_disc_mesh(5.4) {
out.insert("tree", mesh);
}
// Generic small dot for named POIs with no dedicated symbol.
if let Some(mesh) = build_disc_mesh(2.4) {
out.insert("dot", mesh);
}
// Dark center dot layered over the light tree canopy.
if let Some(mesh) = build_disc_mesh(1.7) {
out.insert("tree_core", mesh);
}
// Tesla-style charger pins: wide badge (bolt + kW text) for fast
// sites, small badge for street AC; white bolt overlays. The mesh
// is shifted so the TAIL TIP sits exactly on the anchor (the site):
// a centered mesh made the tip sweep across the ground when the
// camera rotated in a tilted view.
// wide: viewBox 34x30, tip (17,24), scale 30/34 -> tip at +7.94
if let Some(mesh) =
build_icon_mesh_sized_offset(include_str!("icons/charger_pin_wide.svg"), 30.0, 0.0, -7.94)
{
out.insert("charger_pin_fast", mesh);
}
// small: viewBox 22x22, tip (11,20.5), scale 16/22 -> tip at +6.91
if let Some(mesh) =
build_icon_mesh_sized_offset(include_str!("icons/charger_pin.svg"), 16.0, 0.0, -6.91)
{
out.insert("charger_pin_ac", mesh);
}
// Bolt overlays carry their in-pin offset IN THE MESH (screen px):
// offsetting the anchor instead scales with the map and the pin
// composite smears apart at fractional zooms.
if let Some(mesh) =
build_icon_mesh_sized_offset(include_str!("icons/charger.svg"), 9.0, -8.5, -12.35)
{
out.insert("charger_bolt_fast", mesh);
}
if let Some(mesh) =
build_icon_mesh_sized_offset(include_str!("icons/charger.svg"), 9.0, 0.0, -8.36)
{
out.insert("charger_bolt_ac", mesh);
}
out
})
}
pub fn icon_mesh(name: &str) -> Option<&'static IconMesh> {
icons().get(name)
}
fn transform_coord(value: f32, center: f32, scale: f32) -> f32 {
(value - center) * scale
}
fn collect_paths(nodes: &[SvgNode], out: &mut VectorPath) {
for node in nodes {
match node {
SvgNode::Path(p) => {
for cmd in &p.path.cmds {
let t = &p.transform;
out.cmds.push(match *cmd {
PathCmd::MoveTo(x, y) => {
let (x, y) = t.apply(x, y);
PathCmd::MoveTo(x, y)
}
PathCmd::LineTo(x, y) => {
let (x, y) = t.apply(x, y);
PathCmd::LineTo(x, y)
}
PathCmd::BezierTo(x1, y1, x2, y2, x, y) => {
let (x1, y1) = t.apply(x1, y1);
let (x2, y2) = t.apply(x2, y2);
let (x, y) = t.apply(x, y);
PathCmd::BezierTo(x1, y1, x2, y2, x, y)
}
PathCmd::Close => PathCmd::Close,
PathCmd::Winding(w) => PathCmd::Winding(w),
});
}
}
SvgNode::Group(g) => collect_paths(&g.children, out),
_ => {}
}
}
}
/// Parse + tessellate one symbol at its final screen size, centered on the
/// origin so vertices double as screen-px offsets from the anchor point.
fn build_icon_mesh(svg: &str) -> Option<IconMesh> {
build_icon_mesh_sized(svg, ICON_SIZE_PX)
}
fn build_icon_mesh_sized_offset(
svg: &str,
size_px: f32,
dx: f32,
dy: f32,
) -> Option<IconMesh> {
let mut mesh = build_icon_mesh_sized(svg, size_px)?;
for vertex in &mut mesh.verts {
vertex.x += dx;
vertex.y += dy;
}
Some(mesh)
}
fn build_icon_mesh_sized(svg: &str, size_px: f32) -> Option<IconMesh> {
let doc = parse_svg(svg);
let (width, height) = doc.logical_size();
if width <= 0.0 || height <= 0.0 {
return None;
}
let mut path = VectorPath::new();
collect_paths(&doc.root, &mut path);
if path.cmds.is_empty() {
return None;
}
let scale = size_px / width.max(height);
let (center_x, center_y) = (width * 0.5, height * 0.5);
for cmd in &mut path.cmds {
match cmd {
PathCmd::MoveTo(x, y) | PathCmd::LineTo(x, y) => {
*x = transform_coord(*x, center_x, scale);
*y = transform_coord(*y, center_y, scale);
}
PathCmd::BezierTo(x1, y1, x2, y2, x, y) => {
*x1 = transform_coord(*x1, center_x, scale);
*y1 = transform_coord(*y1, center_y, scale);
*x2 = transform_coord(*x2, center_x, scale);
*y2 = transform_coord(*y2, center_y, scale);
*x = transform_coord(*x, center_x, scale);
*y = transform_coord(*y, center_y, scale);
}
PathCmd::Close | PathCmd::Winding(_) => {}
}
}
let mut tess = Tessellator::default();
let mut verts = Vec::new();
let mut indices = Vec::new();
tess.flatten(&path, 0.05);
tess.fill(1.0, LineJoin::Miter, 4.0, false, &mut verts, &mut indices);
if verts.is_empty() || indices.is_empty() {
return None;
}
Some(IconMesh { verts, indices })
}
/// Circle mesh for canopy/dot symbols, centered on the origin like the SVG
/// icons (vertices double as screen-px offsets from the anchor).
fn build_disc_mesh(radius: f32) -> Option<IconMesh> {
const K: f32 = 0.552_284_75;
let r = radius;
let k = r * K;
let mut path = VectorPath::new();
path.cmds.push(PathCmd::MoveTo(r, 0.0));
path.cmds.push(PathCmd::BezierTo(r, k, k, r, 0.0, r));
path.cmds.push(PathCmd::BezierTo(-k, r, -r, k, -r, 0.0));
path.cmds.push(PathCmd::BezierTo(-r, -k, -k, -r, 0.0, -r));
path.cmds.push(PathCmd::BezierTo(k, -r, r, -k, r, 0.0));
path.cmds.push(PathCmd::Close);
let mut tess = Tessellator::default();
let mut verts = Vec::new();
let mut indices = Vec::new();
tess.flatten(&path, 0.05);
tess.fill(1.0, LineJoin::Miter, 4.0, false, &mut verts, &mut indices);
if verts.is_empty() || indices.is_empty() {
return None;
}
Some(IconMesh { verts, indices })
}
/// Micro-POI symbols sourced from the all-tag detail archive (not present in
/// shortbread pois): trees, benches, bins, recycling, playgrounds, artwork.
pub fn micro_icon_for_tags(tags: &HashMap<String, String>) -> Option<(&'static str, u8)> {
if tags.get("natural").map(|v| v.as_str()) == Some("tree") {
return Some(("tree", LABEL_CLASS_TREE));
}
if let Some(amenity) = tags.get("amenity") {
return match amenity.as_str() {
"bench" => Some(("bench", LABEL_CLASS_MUTED)),
"waste_basket" | "waste_disposal" => Some(("waste_basket", LABEL_CLASS_MUTED)),
"recycling" => Some(("recycling", LABEL_CLASS_MUTED)),
"bicycle_parking" => Some(("bicycle", LABEL_CLASS_TRANSPORT)),
"parking" => Some(("parking", LABEL_CLASS_TRANSPORT)),
"parking_entrance" => Some(("parking", LABEL_CLASS_TRANSPORT)),
"charging_station" => Some(("charger", LABEL_CLASS_TRANSPORT)),
_ => None,
};
}
if tags.get("highway").map(|v| v.as_str()) == Some("traffic_signals") {
return Some(("traffic_signals", LABEL_CLASS_MUTED));
}
// Offices (TomTom etc.) only exist in the detail archive; carto shows
// them as a small dot + name from street-level zoom.
if tags.contains_key("office") && tags.contains_key("name") {
return Some(("dot", LABEL_CLASS_MUTED));
}
if let Some(leisure) = tags.get("leisure") {
return match leisure.as_str() {
"playground" => Some(("playground", LABEL_CLASS_GREEN)),
"picnic_table" => Some(("bench", LABEL_CLASS_GREEN)),
_ => None,
};
}
if let Some(tourism) = tags.get("tourism") {
return match tourism.as_str() {
"artwork" => Some(("statue", LABEL_CLASS_CULTURE)),
"information" => Some(("information", LABEL_CLASS_CULTURE)),
_ => None,
};
}
// Building/station entrances (door icon, high zoom only — the caller
// gates the zoom).
if tags.get("railway").map(|v| v.as_str()) == Some("subway_entrance") {
return Some(("entrance", LABEL_CLASS_TRANSPORT));
}
if let Some(entrance) = tags.get("entrance") {
return match entrance.as_str() {
"no" => None,
_ => Some(("entrance", LABEL_CLASS_MUTED)),
};
}
if let Some(historic) = tags.get("historic") {
return match historic.as_str() {
"memorial" | "monument" | "statue" => Some(("statue", LABEL_CLASS_CULTURE)),
_ => None,
};
}
None
}
/// Map shortbread poi attributes to a symbol + label color class.
pub fn icon_for_tags(tags: &HashMap<String, String>) -> Option<(&'static str, u8)> {
match tags.get("layer").map(|v| v.as_str()) {
Some("micro_pois") => return micro_icon_for_tags(tags),
// Geodata overlays (layers.md). Charger pins color by BRAND —
// red is exclusively Tesla Superchargers; other brands split
// fast DC amber / street AC blue (kW still shows in the bubble).
Some("chargers") => {
let kw = tags
.get("max_kw")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0);
let is_tesla = tags
.get("operator")
.or_else(|| tags.get("brand"))
.is_some_and(|value| value.to_lowercase().contains("tesla"));
let class = if is_tesla {
LABEL_CLASS_HEALTH
} else if kw >= 50.0 {
LABEL_CLASS_AMENITY
} else {
LABEL_CLASS_TRANSPORT
};
return Some(("charger", class));
}
Some("stops") => return Some(("dot", LABEL_CLASS_TRANSPORT)),
_ => {}
}
if let Some(shop) = tags.get("shop") {
let name = match shop.as_str() {
"supermarket" => "supermarket",
"bakery" => "bakery",
"butcher" => "butcher",
"clothes" | "fashion" | "shoes" => "clothes",
"hairdresser" => "hairdresser",
"coffee" => "coffee",
"alcohol" | "wine" => "alcohol",
"beauty" | "cosmetics" => "beauty",
"bicycle" => "bicycle",
"florist" => "florist",
"gift" => "gift",
"greengrocer" => "greengrocer",
"laundry" | "dry_cleaning" => "laundry",
_ => "convenience",
};
return Some((name, LABEL_CLASS_SHOP));
}
if let Some(amenity) = tags.get("amenity") {
return match amenity.as_str() {
"restaurant" | "food_court" => Some(("restaurant", LABEL_CLASS_AMENITY)),
"cafe" => Some(("cafe", LABEL_CLASS_AMENITY)),
"fast_food" => Some(("fast_food", LABEL_CLASS_AMENITY)),
"bar" => Some(("bar", LABEL_CLASS_AMENITY)),
"pub" | "biergarten" => Some(("pub", LABEL_CLASS_AMENITY)),
"ice_cream" => Some(("ice_cream", LABEL_CLASS_AMENITY)),
"nightclub" => Some(("nightclub", LABEL_CLASS_AMENITY)),
"pharmacy" => Some(("pharmacy", LABEL_CLASS_DEFAULT)),
"bank" => Some(("bank", LABEL_CLASS_DEFAULT)),
"atm" => Some(("atm", LABEL_CLASS_DEFAULT)),
"parking" => Some(("parking", LABEL_CLASS_TRANSPORT)),
"charging_station" => Some(("charger", LABEL_CLASS_TRANSPORT)),
"place_of_worship" => Some(("place_of_worship", LABEL_CLASS_CULTURE)),
"cinema" => Some(("cinema", LABEL_CLASS_CULTURE)),
"theatre" => Some(("theatre", LABEL_CLASS_CULTURE)),
"library" => Some(("library", LABEL_CLASS_CULTURE)),
_ => None,
};
}
if let Some(tourism) = tags.get("tourism") {
return match tourism.as_str() {
"hotel" | "guest_house" | "hostel" => Some(("hotel", LABEL_CLASS_CULTURE)),
"museum" | "gallery" => Some(("museum", LABEL_CLASS_CULTURE)),
"information" => Some(("information", LABEL_CLASS_CULTURE)),
_ => Some(("dot", LABEL_CLASS_CULTURE)),
};
}
if tags.contains_key("leisure") {
return Some(("dot", LABEL_CLASS_GREEN));
}
// Named POI with no matched symbol: carto's small colored dot, so the
// place still shows up (and its label gets the class color).
if tags.contains_key("name")
&& (tags.contains_key("amenity") || tags.contains_key("office") || tags.contains_key("craft"))
{
return Some(("dot", LABEL_CLASS_DEFAULT));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_icon_size_px() {
assert_eq!(ICON_SIZE_PX, 14.0);
}
#[test]
fn test_icon_min_zoom() {
assert_eq!(ICON_MIN_ZOOM, 17);
}
#[test]
fn test_label_class_constants() {
assert_eq!(LABEL_CLASS_DEFAULT, 0);
assert_eq!(LABEL_CLASS_AMENITY, 1);
assert_eq!(LABEL_CLASS_SHOP, 2);
assert_eq!(LABEL_CLASS_TRANSPORT, 3);
assert_eq!(LABEL_CLASS_CULTURE, 4);
assert_eq!(LABEL_CLASS_GREEN, 5);
assert_eq!(LABEL_CLASS_MUTED, 6);
assert_eq!(LABEL_CLASS_TREE, 7);
assert_eq!(LABEL_CLASS_HEALTH, 8);
}
#[test]
fn test_icons_singleton() {
let icons1 = icons();
let icons2 = icons();
// Should return the same static reference
assert_eq!(icons1.len(), icons2.len());
assert!(!icons1.is_empty());
}
#[test]
fn test_icons_contains_common_icons() {
let icons = icons();
assert!(icons.contains_key("restaurant"));
assert!(icons.contains_key("cafe"));
assert!(icons.contains_key("hotel"));
assert!(icons.contains_key("bank"));
assert!(icons.contains_key("pharmacy"));
}
#[test]
fn test_icon_mesh_existing() {
let mesh = icon_mesh("restaurant");
assert!(mesh.is_some());
let mesh = mesh.unwrap();
assert!(!mesh.verts.is_empty());
assert!(!mesh.indices.is_empty());
}
#[test]
fn test_icon_mesh_nonexistent() {
let mesh = icon_mesh("nonexistent_icon_12345");
assert!(mesh.is_none());
}
#[test]
fn test_icon_mesh_all_common_icons() {
let common_icons = vec![
"alcohol", "atm", "bakery", "bank", "bar", "beauty", "bicycle",
"butcher", "cafe", "cinema", "clothes", "coffee", "convenience",
"fast_food", "florist", "gift", "greengrocer", "hairdresser",
"hotel", "ice_cream", "laundry", "library", "museum", "nightclub",
"pharmacy", "place_of_worship", "pub", "restaurant", "supermarket",
"theatre", "bench", "waste_basket", "recycling", "playground",
"statue", "entrance", "information", "traffic_signals",
"charger_pin", "parking", "charger"
];
for icon_name in common_icons {
let mesh = icon_mesh(icon_name);
assert!(mesh.is_some(), "Icon '{}' should exist", icon_name);
let mesh = mesh.unwrap();
assert!(!mesh.verts.is_empty(), "Icon '{}' should have vertices", icon_name);
assert!(!mesh.indices.is_empty(), "Icon '{}' should have indices", icon_name);
}
}
#[test]
fn test_micro_icon_for_tags_bench() {
let mut tags = HashMap::new();
tags.insert("amenity".to_string(), "bench".to_string());
let result = micro_icon_for_tags(&tags);
assert!(result.is_some());
let (icon_name, label_class) = result.unwrap();
assert_eq!(icon_name, "bench");
assert_eq!(label_class, LABEL_CLASS_MUTED);
}
#[test]
fn test_micro_icon_for_tags_waste_basket() {
let mut tags = HashMap::new();
tags.insert("amenity".to_string(), "waste_basket".to_string());
let result = micro_icon_for_tags(&tags);
assert!(result.is_some());
let (icon_name, _) = result.unwrap();
assert_eq!(icon_name, "waste_basket");
}
#[test]
fn test_micro_icon_for_tags_tree() {
let mut tags = HashMap::new();
tags.insert("natural".to_string(), "tree".to_string());
let result = micro_icon_for_tags(&tags);
assert!(result.is_some());
let (icon_name, label_class) = result.unwrap();
assert_eq!(icon_name, "tree");
assert_eq!(label_class, LABEL_CLASS_TREE);
}
#[test]
fn test_micro_icon_for_tags_no_match() {
let mut tags = HashMap::new();
tags.insert("highway".to_string(), "residential".to_string());
let result = micro_icon_for_tags(&tags);
assert!(result.is_none());
}
#[test]
fn test_micro_icon_for_tags_empty_tags() {
let tags = HashMap::new();
let result = micro_icon_for_tags(&tags);
assert!(result.is_none());
}
#[test]
fn test_icon_for_tags_restaurant() {
let mut tags = HashMap::new();
tags.insert("amenity".to_string(), "restaurant".to_string());
let result = icon_for_tags(&tags);
assert!(result.is_some());
let (icon_name, label_class) = result.unwrap();
assert_eq!(icon_name, "restaurant");
assert_eq!(label_class, LABEL_CLASS_AMENITY);
}
#[test]
fn test_icon_for_tags_cafe() {
let mut tags = HashMap::new();
tags.insert("amenity".to_string(), "cafe".to_string());
let result = icon_for_tags(&tags);
assert!(result.is_some());
let (icon_name, _) = result.unwrap();
assert_eq!(icon_name, "cafe");
}
#[test]
fn test_icon_for_tags_hotel() {
let mut tags = HashMap::new();
tags.insert("tourism".to_string(), "hotel".to_string());
let result = icon_for_tags(&tags);
assert!(result.is_some());
let (icon_name, label_class) = result.unwrap();
assert_eq!(icon_name, "hotel");
assert_eq!(label_class, LABEL_CLASS_AMENITY);
}
#[test]
fn test_icon_for_tags_bank() {
let mut tags = HashMap::new();
tags.insert("amenity".to_string(), "bank".to_string());
let result = icon_for_tags(&tags);
assert!(result.is_some());
let (icon_name, label_class) = result.unwrap();
assert_eq!(icon_name, "bank");
assert_eq!(label_class, LABEL_CLASS_SHOP);
}
#[test]
fn test_icon_for_tags_pharmacy() {
let mut tags = HashMap::new();
tags.insert("amenity".to_string(), "pharmacy".to_string());
let result = icon_for_tags(&tags);
assert!(result.is_some());
let (icon_name, label_class) = result.unwrap();
assert_eq!(icon_name, "pharmacy");
assert_eq!(label_class, LABEL_CLASS_HEALTH);
}
#[test]
fn test_icon_for_tags_supermarket() {
let mut tags = HashMap::new();
tags.insert("shop".to_string(), "supermarket".to_string());
let result = icon_for_tags(&tags);
assert!(result.is_some());
let (icon_name, label_class) = result.unwrap();
assert_eq!(icon_name, "supermarket");
assert_eq!(label_class, LABEL_CLASS_SHOP);
}
#[test]
fn test_icon_for_tags_museum() {
let mut tags = HashMap::new();
tags.insert("tourism".to_string(), "museum".to_string());
let result = icon_for_tags(&tags);
assert!(result.is_some());
let (icon_name, label_class) = result.unwrap();
assert_eq!(icon_name, "museum");
assert_eq!(label_class, LABEL_CLASS_CULTURE);
}
#[test]
fn test_icon_for_tags_park() {
let mut tags = HashMap::new();
tags.insert("leisure".to_string(), "park".to_string());
let result = icon_for_tags(&tags);
assert!(result.is_some());
let (_, label_class) = result.unwrap();
assert_eq!(label_class, LABEL_CLASS_GREEN);
}
#[test]
fn test_icon_for_tags_no_match() {
let mut tags = HashMap::new();
tags.insert("highway".to_string(), "residential".to_string());
let result = icon_for_tags(&tags);
assert!(result.is_none());
}
#[test]
fn test_icon_for_tags_empty_tags() {
let tags = HashMap::new();
let result = icon_for_tags(&tags);
assert!(result.is_none());
}
#[test]
fn test_icon_for_tags_priority() {
// If multiple tags match, should return the first match
let mut tags = HashMap::new();
tags.insert("amenity".to_string(), "restaurant".to_string());
tags.insert("tourism".to_string(), "hotel".to_string());
let result = icon_for_tags(&tags);
assert!(result.is_some());
// The function should return one of them (implementation-dependent)
}
#[test]
fn test_transform_coord() {
let result = transform_coord(10.0, 5.0, 2.0);
assert!((result - 10.0).abs() < 0.001); // (10 - 5) * 2 = 10
}
#[test]
fn test_transform_coord_zero() {
let result = transform_coord(0.0, 0.0, 1.0);
assert!((result - 0.0).abs() < 0.001);
}
#[test]
fn test_transform_coord_negative() {
let result = transform_coord(-5.0, 0.0, 1.0);
assert!((result - (-5.0)).abs() < 0.001);
}
#[test]
fn test_build_icon_mesh_valid_svg() {
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14"><path d="M7 0L14 14H0z"/></svg>"#;
let mesh = build_icon_mesh(svg);
assert!(mesh.is_some());
let mesh = mesh.unwrap();
assert!(!mesh.verts.is_empty());
assert!(!mesh.indices.is_empty());
}
#[test]
fn test_build_icon_mesh_invalid_svg() {
let svg = "not valid svg";
let mesh = build_icon_mesh(svg);
// Should return None for invalid SVG
assert!(mesh.is_none());
}
#[test]
fn test_build_disc_mesh() {
let mesh = build_disc_mesh(5.0);
assert!(mesh.is_some());
let mesh = mesh.unwrap();
assert!(!mesh.verts.is_empty());
assert!(!mesh.indices.is_empty());
}
#[test]
fn test_build_disc_mesh_zero_radius() {
let mesh = build_disc_mesh(0.0);
// Should still create a mesh, even if degenerate
assert!(mesh.is_some());
}
#[test]
fn test_icon_mesh_consistency() {
// Getting the same icon twice should return the same mesh
let mesh1 = icon_mesh("restaurant");
let mesh2 = icon_mesh("restaurant");
assert!(mesh1.is_some());
assert!(mesh2.is_some());
let mesh1 = mesh1.unwrap();
let mesh2 = mesh2.unwrap();
assert_eq!(mesh1.verts.len(), mesh2.verts.len());
assert_eq!(mesh1.indices.len(), mesh2.indices.len());
}
#[test]
fn test_micro_icon_for_tags_playground() {
let mut tags = HashMap::new();
tags.insert("leisure".to_string(), "playground".to_string());
let result = micro_icon_for_tags(&tags);
assert!(result.is_some());
let (icon_name, _) = result.unwrap();
assert_eq!(icon_name, "playground");
}
#[test]
fn test_icon_for_tags_charger() {
let mut tags = HashMap::new();
tags.insert("amenity".to_string(), "charging_station".to_string());
let result = icon_for_tags(&tags);
assert!(result.is_some());
let (icon_name, _) = result.unwrap();
assert_eq!(icon_name, "charger");
}
}