skyway 0.7.1

A command-line OpenStreetMap file converter
Documentation
use json::stringify;
use lexical;

use std::{
    fmt::Write,
    fs::File,
    io::stdout,
    path::PathBuf,
    sync::mpsc::{Receiver, channel},
};

use crate::{
    SkywayError,
    chunks::{Chunk, ElementChunk, OrderedChunkIterator},
    elements::{Element, ElementType, Metadata, SimpleElementType},
};

use super::Writer;

// wrapper struct that implements std::fmt::Write for any type
// that implements std::io::Write
struct ToFmtWrite<T>(pub T);

impl<T> Write for ToFmtWrite<T>
where
    T: std::io::Write,
{
    fn write_str(&mut self, s: &str) -> std::fmt::Result {
        self.0.write_all(s.as_bytes()).map_err(|_| std::fmt::Error)
    }
}

fn create_header(metadata: Metadata, overpass: bool) -> String {
    let mut header = String::from("{");

    // TODO: add skyway details to this?
    if let Some(g) = metadata.generator {
        header.push_str("\"generator\":");
        header.push_str(&stringify(g));
        header.push(',');
    }

    if overpass {
        let mut item_appended = false;
        header.push_str("\"osm3s\":{");

        if let Some(t) = metadata.timestamp {
            header.push_str("\"timestamp_osm_base\":");
            header.push_str(&stringify(t));
            item_appended = true;
        }

        if let Some(c) = metadata.copyright {
            if item_appended {
                header.push(',')
            };
            header.push_str("\"copyright\":");
            header.push_str(&stringify(c));
        }

        // numeric version value
        header.push_str("},\"version\":0.6,\"elements\":[");
    } else {
        if let Some(c) = metadata.copyright {
            header.push_str("\"copyright\":");
            header.push_str(&stringify(c));
            header.push(',');
        }

        // TODO: attribution as well as copyright?

        if let Some(l) = metadata.license {
            header.push_str("\"license\":");
            header.push_str(&stringify(l));
            header.push(',');
        }

        // string version value
        header.push_str("\"version\":\"0.6\",\"elements\":[");
    }
    header
}

fn append_serialized_element(base: &mut String, element: Element) {
    // start this node
    base.push_str("{\"type\":");

    // take care of type-specific attributes
    // (dict order doesn't matter in JSON)
    match element.element_type {
        ElementType::Node { lat, lon } => {
            base.push_str("\"node\"");

            base.push_str(",\"lat\":");
            base.push_str(&lexical::to_string(lat));

            base.push_str(",\"lon\":");
            base.push_str(&lexical::to_string(lon));
        }
        ElementType::Way { nodes } => {
            // finish "type": "way", then start nodes dict
            base.push_str("\"way\",\"nodes\":[");

            // fill in nodes dict
            let mut first_node_appended = false;
            for n in nodes {
                if first_node_appended {
                    base.push(',');
                }
                first_node_appended = true;
                base.push_str(&lexical::to_string(n));
            }
            // close nodes dict
            base.push(']');
        }
        ElementType::Relation { members } => {
            // finish "type": "relation", then start members list
            base.push_str("\"relation\",\"members\":[");

            // fill in members list
            let mut first_node_appended = false;
            for m in members {
                if first_node_appended {
                    base.push(',');
                }
                first_node_appended = true;

                base.push('{');

                base.push_str("\"ref\":");
                base.push_str(&lexical::to_string(m.id));

                match m.t {
                    Some(SimpleElementType::Node) => base.push_str(",\"type\":\"node\""),
                    Some(SimpleElementType::Way) => base.push_str(",\"type\":\"way\""),
                    Some(SimpleElementType::Relation) => base.push_str(",\"type\":\"relation\""),
                    None => (),
                }

                base.push_str(",\"role\":");
                match m.role {
                    Some(r) => base.push_str(&stringify(r)),
                    None => base.push_str("\"\""),
                }

                base.push('}');
            }
            base.push(']');
        }
    }

    base.push_str(",\"id\":");
    base.push_str(&lexical::to_string(element.id));

    if let Some(c) = element.changeset {
        base.push_str(",\"changeset\":");
        base.push_str(&lexical::to_string(c));
    }

    if let Some(t) = element.timestamp {
        base.push_str(",\"timestamp\":");
        base.push_str(&stringify(t));
    }

    if let Some(u) = element.uid {
        base.push_str(",\"uid\":");
        base.push_str(&lexical::to_string(u));
    }

    if let Some(u) = element.user {
        base.push_str(",\"user\":");
        base.push_str(&stringify(u));
    }

    // add visible field only if it is false
    if element.visible == Some(false) {
        base.push_str(",\"visible\":false");
    }

    // append this element's tags to base
    if !element.tags.is_empty() {
        base.push_str(",\"tags\":{");
        let mut first_tag_appended = false;
        for (k, v) in element.tags {
            if first_tag_appended {
                base.push(',');
            }
            first_tag_appended = true;
            base.push_str(&stringify(k));
            base.push(':');
            base.push_str(&stringify(v));
        }
        base.push('}');
    }

    // finish element
    base.push('}');
}

fn serialize_chunk(chunk: ElementChunk) -> Chunk<String> {
    let mut output = String::new();
    let mut first_element_appended = false;
    for element in chunk.content {
        if first_element_appended {
            output.push(',');
        }
        first_element_appended = true;
        append_serialized_element(&mut output, element);
    }
    Chunk {
        index: chunk.index,
        content: output,
    }
}

fn write_output(
    metadata_receiver: Receiver<Metadata>,
    data_receiver: Receiver<Chunk<String>>,
    dest: impl std::io::Write,
    overpass: bool,
) {
    let metadata = metadata_receiver.into_iter().next();
    let mut writer = ToFmtWrite(dest);
    let header = create_header(metadata.unwrap(), overpass); // TODO: better error message if this unexpectedly panics
    writer
        .write_str(&header)
        .expect("Couldn't write opening metadata to output.");

    let ordered_chunks = OrderedChunkIterator::new(data_receiver.into_iter());

    let mut first_chunk_written = false;
    for chunk_content in ordered_chunks {
        if first_chunk_written {
            writer
                .write_str(",")
                .expect("Failed to write comma between chunks");
        }
        first_chunk_written = true;
        writer
            .write_str(&chunk_content)
            .expect("Failed to write chunk");
    }

    writer
        .write_str("]}")
        .expect("Couldn't write final closing curly brace to output.");
}

pub struct JsonWriter {
    pub overpass: bool,
}

impl JsonWriter {
    pub fn new(overpass: bool) -> Self {
        JsonWriter { overpass }
    }
}

impl Writer for JsonWriter {
    fn write(
        &self,
        element_receiver: Receiver<ElementChunk>,
        metadata_receiver: Receiver<Metadata>,
        dest: Option<PathBuf>,
    ) -> Result<(), SkywayError> {
        let (sender, receiver) = channel();
        let overpass = self.overpass.clone();
        let write_thread = std::thread::spawn({
            move || {
                match dest {
                    None => write_output(metadata_receiver, receiver, stdout(), overpass),
                    Some(a) => match File::create(PathBuf::from(a)) {
                        Ok(b) => write_output(metadata_receiver, receiver, b, overpass),
                        Err(e) => {
                            panic!("Unable to open output file: {e:?}");
                        }
                    },
                };
            }
        });

        for chunk in element_receiver {
            sender
                .send(serialize_chunk(chunk))
                .expect("Failed to send serialized chunk.")
        }

        drop(sender);

        write_thread.join().map_err(|e| {
            SkywayError::UnexpectedError(format!("Could not join writer thread: {:?}", e))
        })?;

        Ok(())
    }
}