Skip to main content

hello_map/
hello_map.rs

1//! A map in your terminal in about 50 lines: OpenFreeMap vector tiles drawn by a few style
2//! layers, a GeoJSON pin that lights up under the pointer, and MapLibre-style pan and zoom
3//! (drag, scroll, double-click, arrows, +/-). Click the pin or a place name. `q` quits.
4//!
5//!     cargo run --release --example hello_map
6
7use std::time::{Duration, Instant};
8
9use crossterm::event::{self, Event, KeyCode};
10use mapterm::{Camera, Controller, Map, MapSource, Source, StyleLayer, events::{Events, MapEvent}};
11use serde_json::json;
12
13#[tokio::main]
14async fn main() -> anyhow::Result<()> {
15    let mut map = Map::new();
16    map.add_source("omt", MapSource::vector(Source::open(mapterm::source::OPENFREEMAP).await?))?;
17    map.add_source("pin", MapSource::geojson(&json!({"type": "Feature", "id": 1, "properties": {"name": "Washington Monument"},
18        "geometry": {"type": "Point", "coordinates": [-77.0353, 38.8895]}}))?)?;
19    map.add_layer(StyleLayer::background("bg").color("#101418"))?;
20    map.add_layer(StyleLayer::fill("park", "omt").source_layer("park").color("#1a3220"))?;
21    map.add_layer(StyleLayer::fill("water", "omt").source_layer("water").color("#16304a"))?;
22    map.add_layer(StyleLayer::line("roads", "omt").source_layer("transportation").color(json!(
23        ["match", ["get", "class"], "motorway", "#eb6e3c", "trunk", "#e1a046", "primary", "#d2c36e", "#5a5a5a"])))?;
24    map.add_layer(StyleLayer::symbol("places", "omt").source_layer("place").text(json!(["get", "name"]))
25        .sort_key(json!(["get", "rank"])).color("#e1e1e1"))?;
26    map.add_layer(StyleLayer::symbol("pin", "pin").glyph("◆").text("{name}")
27        .color(json!(["case", ["boolean", ["feature-state", "hover"], false], "yellow", "cyan"])))?;
28    let mut events = Events::new(&["pin", "places"]).hover_state("hover");
29
30    let mut ctl = Controller::new(Camera::new(-77.0353, 38.8895, 13.0), 0.0, 17.0);
31    let mut status = String::new();
32    let mut term = ratatui::init();
33    crossterm::execute!(std::io::stdout(), event::EnableMouseCapture)?;
34    loop {
35        ctl.tick(Instant::now());
36        let area = term.get_frame().area();
37        ctl.set_area(area);
38        let _ = map.load_tiles(&ctl.cam, area).await; // a real app loads in the background
39        term.draw(|f| {
40            f.render_widget(map.view(&ctl.cam), area);
41            f.render_widget(ratatui::text::Line::from(status.as_str()), area);
42        })?;
43        events.pointer(&mut map, &ctl.cam, area, ctl.pointer()); // hover: the pin turns yellow
44        if let Some(click) = ctl.take_click() {
45            for e in events.click(&map, &ctl.cam, area, &click) {
46                if let MapEvent::Click { layer, features, .. } = e {
47                    status = format!("{layer}: {}", features[0].title());
48                }
49            }
50        }
51        if event::poll(Duration::from_millis(if ctl.animating() { 16 } else { 250 }))? {
52            match event::read()? {
53                Event::Key(k) if k.code == KeyCode::Char('q') => break,
54                Event::Key(k) => _ = ctl.key(k),
55                Event::Mouse(m) => _ = ctl.mouse(m),
56                _ => {}
57            }
58        }
59    }
60    crossterm::execute!(std::io::stdout(), event::DisableMouseCapture)?;
61    ratatui::restore();
62    Ok(())
63}