Render OpenStreet map using Rust and Cairo

Render OpenStreet map using Rust and Cairo

used OSM Renderer as example: https://github.com/dfyz/osm-renderer

Project covers:

  • reading the OSM data using Serde and QuickXML;
  • filter data to extract highway Way nodes;
  • save filtered data as binary format on disk;
  • render the Way using nodes coordinates translated to Tiles coordinates using Cairo library.

Complete source code:

use std::{
    collections::{HashMap, HashSet},
    f64::consts::PI,
    fs::File,
    io::{BufReader, BufWriter},
    time::Instant,
};

use cairo::{Context, ImageSurface};
use ciborium::from_reader;
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
pub struct Node {
    #[serde(rename = "@id")]
    pub id: u64,
    #[serde(rename = "@lat")]
    pub lat: f64,
    #[serde(rename = "@lon")]
    pub lon: f64,
    pub tag: Option<Vec<Tag>>,
}

#[derive(Deserialize, Serialize)]
pub struct Nd {
    #[serde(rename = "@ref")]
    pub reference: u64,
}
#[derive(Deserialize, Serialize)]
pub struct Tag {
    #[serde(rename = "@k")]
    pub k: String,
    #[serde(rename = "@v")]
    pub v: String,
}
#[derive(Deserialize, Serialize)]
pub struct Way {
    #[serde(rename = "@id")]
    pub id: u64,
    pub nd: Vec<Nd>,
    pub tag: Option<Vec<Tag>>,
}

#[derive(Deserialize, Serialize)]
pub struct Osm {
    pub node: Vec<Node>,
    pub way: Vec<Way>,
}
fn build_index() {
    let start = Instant::now();
    let osm_path = "moldova-latest.osm";
    let buffer = BufReader::new(File::open(osm_path).unwrap());
    let osm: Osm = quick_xml::de::from_reader(buffer).unwrap();
    println!("nodes: {}", osm.node.len());
    println!("ways: {}", osm.way.len());
    println!("loading from osm: {:?}", start.elapsed());

    let writer = BufWriter::new(File::create("test.bin").unwrap());
    ciborium::ser::into_writer(&osm, writer).unwrap();

    let start = Instant::now();
    let mut osm: Osm = from_reader(BufReader::new(File::open("test.bin").unwrap())).unwrap();
    println!("nodes: {}", osm.node.len());
    println!("ways: {}", osm.way.len());

    let mut filters = HashMap::<String, HashSet<String>>::new();
    filters.insert(
        "highway".to_string(),
        HashSet::from_iter(
            vec![
                "primary",
                "secondary",
                "trunk",
                "motorway",
                "primary_link",
                "tertiary",
                "residential",
                "service",
                "unclassified",
            ]
            .iter()
            .map(|item| item.to_string())
            .collect::<Vec<String>>(),
        ),
    );

    osm.way = filter(osm.way, &filters);
    println!("ways: {}", osm.way.len());

    let items: HashSet<u64> = osm
        .way
        .iter()
        .flat_map(|item| item.nd.iter().map(|item| item.reference))
        .collect();

    osm.node = osm.node.into_iter().filter(|item| items.contains(&item.id)).collect();

    let writer = BufWriter::new(File::create("test-filter.bin").unwrap());
    ciborium::ser::into_writer(&osm, writer).unwrap();
    println!("loading from binary: {:?}", start.elapsed());

    let start = Instant::now();
    let osm: Osm = from_reader(BufReader::new(File::open("test-filter.bin").unwrap())).unwrap();
    println!("nodes: {}", osm.node.len());
    println!("ways: {}", osm.way.len());

    let mapped: HashMap<u64, (f64, f64)> = osm
        .node
        .iter()
        .fold(HashMap::<u64, (f64, f64)>::new(), |mut acc, item| {
            acc.insert(item.id, convert_to_tile(item, 12));
            acc
        });

    let x_s: Vec<f64> = mapped.iter().map(|a| a.1 .0).collect();
    let min_x = x_s.iter().max_by(|a, b| b.total_cmp(&a)).unwrap();
    let max_x = x_s.iter().max_by(|a, b| a.total_cmp(&b)).unwrap();

    let y_s: Vec<f64> = mapped.iter().map(|a| a.1 .1).collect();
    let min_y = y_s.iter().max_by(|a, b| b.total_cmp(&a)).unwrap();
    let max_y = y_s.iter().max_by(|a, b| a.total_cmp(&b)).unwrap();

    println!("coordinated: {:?} {:?} {:?} {:?}", min_x, min_y, max_x, max_y);
    println!("loading from binary: {:?}", start.elapsed());

    draw_to_image(
        &mapped,
        *min_x,
        *min_y,
        &osm.way,
        (max_x - min_x) as i32,
        (max_y - min_y) as i32,
    );
}

fn draw_to_image(
    mapped_nodes: &HashMap<u64, (f64, f64)>,
    min_x: f64,
    min_y: f64,
    way: &Vec<Way>,
    width: i32,
    height: i32,
) {
    println!("creating image width: {} height: {}", width, height);
    let surface = ImageSurface::create(cairo::Format::Rgb24, width, height).unwrap();
    let context = Context::new(&surface).unwrap();
    context.set_source_rgb(0.2, 0.2, 0.2);
    context.paint().unwrap();

    context.set_line_width(1f64);
    context.set_line_join(cairo::LineJoin::Round);
    context.set_source_rgb(0.5, 0.5, 0.5);

    way.iter().for_each(|way| {
        way.nd.iter().for_each(|node| {
            let point = mapped_nodes.get(&node.reference).unwrap();

            let x = point.0 - min_x;
            let y = point.1 - min_y;

            // println!("draw line {} {}", x, y);
            context.line_to(x, y);
        });
        // println!("done drawing line");
        context.stroke().unwrap();
    });

    let mut file = File::create("image.png").unwrap();
    surface.write_to_png(&mut file).unwrap();
}

fn filter(way: Vec<Way>, filter: &HashMap<String, HashSet<String>>) -> Vec<Way> {
    way.into_iter()
        .filter(|item| {
            if let Some(tag) = &item.tag {
                tag.iter()
                    .filter(|item| filter.get(&item.k).is_some() && filter.get(&item.k).unwrap().contains(&item.v))
                    .count()
                    > 0usize
            } else {
                false
            }
        })
        .collect()
}

const TILE_SIZE: u32 = 256;

fn convert_to_tile(node: &Node, zoom: u8) -> (f64, f64) {
    let (lat_rad, lon_rad) = (node.lat.to_radians(), node.lon.to_radians());
    let x = lon_rad + PI;
    let y = PI - ((PI / 4f64) + (lat_rad / 2f64)).tan().ln();

    let rescale = |x: f64| {
        let factor = x / (2f64 * PI);
        let dimension_in_pixels = f64::from(TILE_SIZE * (1 << zoom));
        factor * dimension_in_pixels
    };

    (rescale(x), rescale(y))
}

fn main() {
    build_index();
}

Related Posts