Your first map
A complete app: it builds a basemap from a few style layers over OpenFreeMap tiles, adds a GeoJSON pin that turns yellow under the pointer, and handles MapLibre-style pan and zoom. Clicking the pin or a place name shows what it is.
Make a project with mapterm and the crates the example uses, all from crates.io:
cargo new hello-map && cd hello-mapcargo add mapterm ratatui crossterm serde_json anyhowcargo add tokio -F rt-multi-thread,macrosPut the code below in src/main.rs, then run it with cargo run --release.
It’s also examples/hello_map.rs in the mapterm
repository.
use std::time::{Duration, Instant};
use crossterm::event::{self, Event, KeyCode};use mapterm::{Camera, Controller, Map, MapSource, Source, StyleLayer, events::{Events, MapEvent}};use serde_json::json;
#[tokio::main]async fn main() -> anyhow::Result<()> { let mut map = Map::new(); map.add_source("omt", MapSource::vector(Source::open(mapterm::source::OPENFREEMAP).await?))?; map.add_source("pin", MapSource::geojson(&json!({"type": "Feature", "id": 1, "properties": {"name": "Washington Monument"}, "geometry": {"type": "Point", "coordinates": [-77.0353, 38.8895]}}))?)?; map.add_layer(StyleLayer::background("bg").color("#101418"))?; map.add_layer(StyleLayer::fill("park", "omt").source_layer("park").color("#1a3220"))?; map.add_layer(StyleLayer::fill("water", "omt").source_layer("water").color("#16304a"))?; map.add_layer(StyleLayer::line("roads", "omt").source_layer("transportation").color(json!( ["match", ["get", "class"], "motorway", "#eb6e3c", "trunk", "#e1a046", "primary", "#d2c36e", "#5a5a5a"])))?; map.add_layer(StyleLayer::symbol("places", "omt").source_layer("place").text(json!(["get", "name"])) .sort_key(json!(["get", "rank"])).color("#e1e1e1"))?; map.add_layer(StyleLayer::symbol("pin", "pin").glyph("◆").text("{name}") .color(json!(["case", ["boolean", ["feature-state", "hover"], false], "yellow", "cyan"])))?; let mut events = Events::new(&["pin", "places"]).hover_state("hover");
let mut ctl = Controller::new(Camera::new(-77.0353, 38.8895, 13.0), 0.0, 17.0); let mut status = String::new(); let mut term = ratatui::init(); crossterm::execute!(std::io::stdout(), event::EnableMouseCapture)?; loop { ctl.tick(Instant::now()); let area = term.get_frame().area(); ctl.set_area(area); let _ = map.load_tiles(&ctl.cam, area).await; // a real app loads in the background term.draw(|f| { f.render_widget(map.view(&ctl.cam), area); f.render_widget(ratatui::text::Line::from(status.as_str()), area); })?; events.pointer(&mut map, &ctl.cam, area, ctl.pointer()); // hover: the pin turns yellow if let Some(click) = ctl.take_click() { for e in events.click(&map, &ctl.cam, area, &click) { if let MapEvent::Click { layer, features, .. } = e { status = format!("{layer}: {}", features[0].title()); } } } if event::poll(Duration::from_millis(if ctl.animating() { 16 } else { 250 }))? { match event::read()? { Event::Key(k) if k.code == KeyCode::Char('q') => break, Event::Key(k) => _ = ctl.key(k), Event::Mouse(m) => _ = ctl.mouse(m), _ => {} } } } crossterm::execute!(std::io::stdout(), event::DisableMouseCapture)?; ratatui::restore(); Ok(())}What’s going on
Section titled “What’s going on”- Sources.
omtis the OpenFreeMap planet (OpenMapTiles schema), andpinis an inline GeoJSON feature with anid, so it can have feature state. - Layers, bottom to top: a background, park and water fills, roads colored
by a
matchexpression on theirclass, place names sorted byrank, and the pin, drawn as◆with its name, yellow when itshoverfeature state is set and cyan otherwise. - Events.
Events::new(&["pin", "places"]).hover_state("hover")watches those two layers and setshoveron the feature under the pointer. - The loop.
Controller::tickadvances easing and inertia;load_tilesfetches what the view needs;map.view(&ctl.cam)is the widget.events.pointerupdates the hover state fromctl.pointer(). - Clicks.
take_clickreturns a single click (not a drag or double-click);events.clickturns it into aMapEvent::Clickper layer, with the features under it, top first. - Input. Keys and mouse events go straight to the controller, which knows the MapLibre gestures.
Shorter still: a ready-made style
Section titled “Shorter still: a ready-made style”Replace the hand-built layers with any MapLibre style:
let mut map = Map::from_style("liberty").await?;examples/style.rs in the repository does exactly that,
taking the style name as an argument.
Making it production-ready
Section titled “Making it production-ready”load_tiles fetches one tile at a time, and the loop waits for it. A real app
loads tiles in the background with tile_requests and insert_loaded; see
Tiles, images and output.