Skip to content

Custom layers

When a style layer can’t express it, write a CustomLayer. It draws through a DrawContext and can answer queries with pick. Its markers and labels are placed together with the map’s own, so they never overlap.

pub trait CustomLayer {
/// A short name, shown when inspecting its features.
fn name(&self) -> &str;
/// Add this layer's lines, fills, markers and labels for the current view.
fn draw(&self, ctx: &mut DrawContext);
/// Features of this layer under `ctx.target`, most specific first.
fn pick(&self, _ctx: &PickContext) -> Vec<Pick> { Vec::new() }
/// Lon/lat bounds (west, south, east, north), for fitting the view to the layer.
fn bounds(&self) -> Option<(f64, f64, f64, f64)> { None }
}
use mapterm::{CustomLayer, DrawContext};
use ratatui::style::{Color, Style};
struct Vehicles(Vec<(String, f64, f64)>); // (id, lon, lat)
impl CustomLayer for Vehicles {
fn name(&self) -> &str { "vehicles" }
fn draw(&self, ctx: &mut DrawContext) {
let style = Style::default().fg(Color::Rgb(0, 229, 255));
for (id, lon, lat) in &self.0 {
// glyph, style, priority, optional label
ctx.marker(*lon, *lat, "▲", style, 100, Some((id.clone(), style)));
}
}
}
map.add_custom_layer("vehicles", Vehicles(positions), None)?;
// Later, update it in place:
if let Some(v) = map.custom_layer_mut::<Vehicles>("vehicles") {
v.0 = new_positions;
}

Positions are in braille dots relative to the map area (2 per column, 4 per row). Helpers take lon/lat or world coordinates (Web Mercator, 0 to 1).

Method Draws
line_lonlat(&pts, color) A polyline through lon/lat points
line(&pts, color), line_dots(&pts, color) The same in world or dot coordinates
fill(&rings, color), fill_dots(&rings, color) Polygons (outer ring and holes) as cell backgrounds
cell_bg(col, row, color) One cell’s background
marker(lon, lat, glyph, style, priority, label) A glyph with an optional label to its right
marker_dot(...), label_dot(p, text, style, priority) Markers and labels at dot positions
label_dots(candidates, text, style, priority) A label at the first of several positions that fits
label_spaced(candidates, text, style, priority, repeat) Labels at many positions, skipping copies within repeat dots of one already placed (road names)
dot(x, y), inside(p), visible(bbox), world_view() Coordinate conversion and culling

Lines and fills draw immediately, in layer order. Markers and labels are collected and placed after every layer, highest priority first; a later one that would overlap an earlier one is dropped.

For simple overlays there’s Markers, a custom layer holding a list of Overlay::Line and Overlay::Marker values:

use mapterm::{Markers, Overlay};
let pins = Markers(vec![Overlay::Marker {
lon: -77.0353, lat: 38.8895, glyph: "◆".into(),
style: Style::default().fg(Color::Cyan),
label: Some(("Washington Monument".into(), Style::default())),
priority: 50,
}]);
map.add_custom_layer("pins", pins, None)?;

The viewer uses one for its highlights and crosshair.