pbfhogg 0.5.0

Fast OpenStreetMap PBF reader and writer for Rust. Read, write, and merge .osm.pbf files with pipelined parallel decoding.
Documentation
use crate::Way;
use crate::coord_fmt::format_coord;

pub(super) const AREA_KEYS: &[&str] = &[
    "building", "landuse", "natural", "leisure", "amenity", "boundary", "waterway",
];

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum WayGeom {
    Written,
    Invalid,
}

fn push_position(buf: &mut String, scratch: &mut String, lon: f64, lat: f64) {
    buf.push('[');
    format_coord(scratch, lon);
    buf.push_str(scratch);
    buf.push(',');
    format_coord(scratch, lat);
    buf.push_str(scratch);
    buf.push(']');
}

pub(super) fn write_point(buf: &mut String, scratch: &mut String, lon: f64, lat: f64) {
    buf.clear();
    buf.push_str("{\"type\":\"Point\",\"coordinates\":");
    push_position(buf, scratch, lon, lat);
    buf.push('}');
}

pub(super) fn collect_coords(way: &Way<'_>, coords: &mut Vec<(f64, f64)>) {
    coords.clear();
    coords.extend(
        way.node_locations()
            .map(|location| (location.lon(), location.lat())),
    );
}

fn signed_area(coords: &[(f64, f64)], closed: bool) -> f64 {
    let mut twice_area = coords
        .windows(2)
        .map(|pair| pair[0].0 * pair[1].1 - pair[1].0 * pair[0].1)
        .sum::<f64>();
    if !closed {
        let first = coords[0];
        let last = coords[coords.len() - 1];
        twice_area += last.0 * first.1 - first.0 * last.1;
    }
    twice_area / 2.0
}

fn has_three_distinct_positions(coords: &[(f64, f64)]) -> bool {
    let Some(&first) = coords.first() else {
        return false;
    };
    let Some(second) = coords.iter().copied().find(|coord| *coord != first) else {
        return false;
    };
    coords
        .iter()
        .any(|coord| *coord != first && *coord != second)
}

fn push_positions<'a, I>(
    buf: &mut String,
    scratch: &mut String,
    positions: I,
    wrote_position: &mut bool,
) where
    I: Iterator<Item = &'a (f64, f64)>,
{
    for &(lon, lat) in positions {
        if *wrote_position {
            buf.push(',');
        }
        push_position(buf, scratch, lon, lat);
        *wrote_position = true;
    }
}

pub(super) fn write_way_geometry(
    buf: &mut String,
    scratch: &mut String,
    coords: &[(f64, f64)],
    is_area: bool,
) -> WayGeom {
    if !is_area && coords.len() < 2 {
        return WayGeom::Invalid;
    }

    let closed = coords.first() == coords.last();
    if is_area {
        let output_len = coords.len() + usize::from(!closed);
        if output_len < 4 || !has_three_distinct_positions(coords) {
            return WayGeom::Invalid;
        }
    }
    let reverse = is_area && signed_area(coords, closed) < 0.0;

    buf.clear();
    if is_area {
        buf.push_str("{\"type\":\"Polygon\",\"coordinates\":[[");
    } else {
        buf.push_str("{\"type\":\"LineString\",\"coordinates\":[");
    }
    let mut wrote_position = false;
    if reverse && !closed {
        push_positions(buf, scratch, coords[..1].iter(), &mut wrote_position);
        push_positions(buf, scratch, coords[1..].iter().rev(), &mut wrote_position);
    } else if reverse {
        push_positions(buf, scratch, coords.iter().rev(), &mut wrote_position);
    } else {
        push_positions(buf, scratch, coords.iter(), &mut wrote_position);
    }
    if is_area && !closed {
        buf.push(',');
        let &(lon, lat) = &coords[0];
        push_position(buf, scratch, lon, lat);
    }
    if is_area {
        buf.push_str("]]}");
    } else {
        buf.push_str("]}");
    }
    WayGeom::Written
}

fn is_area_tags<'a, T>(tags: T) -> bool
where
    T: Iterator<Item = (&'a str, &'a str)>,
{
    let mut area_yes = false;
    let mut area_no = false;
    let mut area_key = false;
    for (key, value) in tags {
        if key == "area" {
            area_yes |= value == "yes";
            area_no |= value == "no";
        }
        area_key |= AREA_KEYS.contains(&key);
    }
    !area_no && (area_yes || area_key)
}

#[cfg(test)]
fn is_area_parts<'a, T>(refs: &[i64], tags: T) -> bool
where
    T: Iterator<Item = (&'a str, &'a str)>,
{
    refs.len() >= 4 && refs.first() == refs.last() && is_area_tags(tags)
}

pub(super) fn is_area_way(way: &Way<'_>) -> bool {
    let mut refs = way.refs();
    let Some(first) = refs.next() else {
        return false;
    };
    let mut count = 1_usize;
    let mut last = first;
    for reference in refs {
        count += 1;
        last = reference;
    }
    if count < 4 || first != last {
        return false;
    }
    is_area_tags(way.tags())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn point_and_line_format() {
        let mut buf = String::new();
        let mut scratch = String::new();
        write_point(&mut buf, &mut scratch, 12.5, 55.123_456_7);
        assert_eq!(
            buf,
            "{\"type\":\"Point\",\"coordinates\":[12.5,55.1234567]}"
        );
        assert_eq!(
            write_way_geometry(&mut buf, &mut scratch, &[(1.0, 2.0), (3.0, 4.0)], false),
            WayGeom::Written
        );
        assert_eq!(
            buf,
            "{\"type\":\"LineString\",\"coordinates\":[[1,2],[3,4]]}"
        );
    }

    #[test]
    fn polygon_closes_and_reorients_clockwise_ring() {
        let mut buf = String::new();
        let mut scratch = String::new();
        assert_eq!(
            write_way_geometry(
                &mut buf,
                &mut scratch,
                &[(0.0, 0.0), (0.0, 1.0), (1.0, 0.0)],
                true
            ),
            WayGeom::Written
        );
        assert_eq!(
            buf,
            "{\"type\":\"Polygon\",\"coordinates\":[[[0,0],[1,0],[0,1],[0,0]]]}"
        );
        assert_eq!(
            write_way_geometry(&mut buf, &mut scratch, &[(0.0, 0.0)], false),
            WayGeom::Invalid
        );
        assert_eq!(
            write_way_geometry(&mut buf, &mut scratch, &[(0.0, 0.0)], true),
            WayGeom::Invalid
        );
        assert_eq!(
            write_way_geometry(
                &mut buf,
                &mut scratch,
                &[(0.0, 0.0), (0.0, 0.0), (1.0, 1.0)],
                true
            ),
            WayGeom::Invalid
        );
    }

    #[test]
    fn area_heuristic_truth_table() {
        let closed = [1, 2, 3, 1];
        assert!(is_area_parts(&closed, [("building", "yes")].into_iter()));
        assert!(!is_area_parts(
            &closed,
            [("building", "yes"), ("area", "no")].into_iter()
        ));
        assert!(is_area_parts(&closed, [("area", "yes")].into_iter()));
        assert!(!is_area_parts(
            &closed,
            [("highway", "service")].into_iter()
        ));
        assert!(!is_area_parts(
            &[1, 2, 3],
            [("building", "yes")].into_iter()
        ));
    }
}