rustial-engine 0.0.1

Framework-agnostic 2.5D map engine for rustial
Documentation
// ---------------------------------------------------------------------------
//! # Shape generators -- create geographic geometries from parameters
//!
//! Generate common geometric shapes as [`GeoCoord`] rings suitable for
//! use with the tessellator, vector layers, or direct rendering:
//!
//! - [`circle`] -- regular polygon approximating a circle
//! - [`arc`] -- partial circle (sector boundary)
//! - [`ellipse`] -- elliptical ring
//! - [`rectangle`] -- axis-aligned geographic rectangle
//! - [`regular_polygon`] -- equilateral N-gon
//! - [`sector`] -- pie-slice shape (arc closed to centre)
//! - [`line_along`] -- sample points along a great-circle path
//!
//! All generators produce `Vec<GeoCoord>` suitable for constructing
//! [`Polygon`](crate::geometry::Polygon) exteriors or
//! [`LineString`](crate::geometry::LineString) coordinates.
//!
//! Distance calculations use the geodesic destination formula on the
//! WGS-84 sphere (radius 6,378,137 m).
// ---------------------------------------------------------------------------

use rustial_math::GeoCoord;

/// Mean radius of the Earth in meters (WGS-84 semi-major axis).
const EARTH_RADIUS: f64 = 6_378_137.0;

/// Compute a destination point given a start, bearing (degrees), and
/// distance (meters) on the WGS-84 sphere.
fn destination(from: &GeoCoord, bearing_deg: f64, distance_m: f64) -> GeoCoord {
    let lat1 = from.lat.to_radians();
    let lon1 = from.lon.to_radians();
    let brng = bearing_deg.to_radians();
    let d = distance_m / EARTH_RADIUS;

    let lat2 = (lat1.sin() * d.cos() + lat1.cos() * d.sin() * brng.cos()).asin();
    let lon2 = lon1 + (brng.sin() * d.sin() * lat1.cos()).atan2(d.cos() - lat1.sin() * lat2.sin());

    GeoCoord::from_lat_lon(lat2.to_degrees(), lon2.to_degrees())
}

// ---------------------------------------------------------------------------
// Circle
// ---------------------------------------------------------------------------

/// Generate a circle as a closed polygon ring.
///
/// # Arguments
///
/// - `center` -- centre of the circle.
/// - `radius_m` -- radius in meters.
/// - `segments` -- number of line segments (more = smoother).
///   Clamped to a minimum of 4.
///
/// The returned ring is closed (first == last) and counter-clockwise.
pub fn circle(center: &GeoCoord, radius_m: f64, segments: usize) -> Vec<GeoCoord> {
    let n = segments.max(4);
    let step = 360.0 / n as f64;
    let mut ring = Vec::with_capacity(n + 1);
    for i in 0..n {
        ring.push(destination(center, i as f64 * step, radius_m));
    }
    ring.push(ring[0]); // close
    ring
}

// ---------------------------------------------------------------------------
// Arc
// ---------------------------------------------------------------------------

/// Generate an arc (partial circle) as a polyline.
///
/// # Arguments
///
/// - `center` -- centre point.
/// - `radius_m` -- radius in meters.
/// - `start_bearing` -- start angle in degrees (0 = north, 90 = east).
/// - `end_bearing` -- end angle in degrees (swept clockwise from start).
/// - `segments` -- number of segments in the arc (clamped to min 2).
///
/// Returns a list of points along the arc from `start_bearing` to
/// `end_bearing`.  Does **not** close the ring.
pub fn arc(
    center: &GeoCoord,
    radius_m: f64,
    start_bearing: f64,
    end_bearing: f64,
    segments: usize,
) -> Vec<GeoCoord> {
    let n = segments.max(2);
    let mut sweep = end_bearing - start_bearing;
    if sweep <= 0.0 {
        sweep += 360.0;
    }
    let step = sweep / n as f64;
    let mut points = Vec::with_capacity(n + 1);
    for i in 0..=n {
        let bearing = start_bearing + i as f64 * step;
        points.push(destination(center, bearing, radius_m));
    }
    points
}

// ---------------------------------------------------------------------------
// Sector (pie slice)
// ---------------------------------------------------------------------------

/// Generate a sector (pie slice) as a closed polygon ring.
///
/// The sector starts at the centre, sweeps from `start_bearing` to
/// `end_bearing` along the arc, and closes back to the centre.
pub fn sector(
    center: &GeoCoord,
    radius_m: f64,
    start_bearing: f64,
    end_bearing: f64,
    segments: usize,
) -> Vec<GeoCoord> {
    let arc_points = arc(center, radius_m, start_bearing, end_bearing, segments);
    let mut ring = Vec::with_capacity(arc_points.len() + 2);
    ring.push(*center);
    ring.extend(arc_points);
    ring.push(*center); // close
    ring
}

// ---------------------------------------------------------------------------
// Ellipse
// ---------------------------------------------------------------------------

/// Generate an ellipse as a closed polygon ring.
///
/// # Arguments
///
/// - `center` -- centre of the ellipse.
/// - `semi_major_m` -- semi-major axis in meters (along `rotation`).
/// - `semi_minor_m` -- semi-minor axis in meters (perpendicular).
/// - `rotation_deg` -- rotation of the semi-major axis in degrees
///   clockwise from north.
/// - `segments` -- number of segments (clamped to min 4).
pub fn ellipse(
    center: &GeoCoord,
    semi_major_m: f64,
    semi_minor_m: f64,
    rotation_deg: f64,
    segments: usize,
) -> Vec<GeoCoord> {
    let n = segments.max(4);
    let step = std::f64::consts::TAU / n as f64;
    let rot = rotation_deg.to_radians();
    let mut ring = Vec::with_capacity(n + 1);
    for i in 0..n {
        let angle = i as f64 * step;
        // Parametric ellipse in local frame.
        let lx = semi_major_m * angle.cos();
        let ly = semi_minor_m * angle.sin();
        // Rotate into geographic bearing.
        let bearing = (lx * rot.sin() + ly * rot.cos()).atan2(lx * rot.cos() - ly * rot.sin());
        let dist = (lx * lx + ly * ly).sqrt();
        ring.push(destination(center, bearing.to_degrees(), dist));
    }
    ring.push(ring[0]); // close
    ring
}

// ---------------------------------------------------------------------------
// Rectangle
// ---------------------------------------------------------------------------

/// Generate an axis-aligned geographic rectangle as a closed polygon ring.
///
/// # Arguments
///
/// - `sw` -- southwest corner (min lat, min lon).
/// - `ne` -- northeast corner (max lat, max lon).
///
/// Returns a CCW ring: SW → SE → NE → NW → SW.
pub fn rectangle(sw: &GeoCoord, ne: &GeoCoord) -> Vec<GeoCoord> {
    vec![
        GeoCoord::from_lat_lon(sw.lat, sw.lon),
        GeoCoord::from_lat_lon(sw.lat, ne.lon),
        GeoCoord::from_lat_lon(ne.lat, ne.lon),
        GeoCoord::from_lat_lon(ne.lat, sw.lon),
        GeoCoord::from_lat_lon(sw.lat, sw.lon), // close
    ]
}

// ---------------------------------------------------------------------------
// Regular polygon
// ---------------------------------------------------------------------------

/// Generate a regular N-gon as a closed polygon ring.
///
/// # Arguments
///
/// - `center` -- centre of the polygon.
/// - `radius_m` -- circumscribed circle radius in meters.
/// - `sides` -- number of sides (clamped to min 3).
/// - `rotation_deg` -- rotation of the first vertex in degrees
///   clockwise from north.
pub fn regular_polygon(
    center: &GeoCoord,
    radius_m: f64,
    sides: usize,
    rotation_deg: f64,
) -> Vec<GeoCoord> {
    let n = sides.max(3);
    let step = 360.0 / n as f64;
    let mut ring = Vec::with_capacity(n + 1);
    for i in 0..n {
        let bearing = rotation_deg + i as f64 * step;
        ring.push(destination(center, bearing, radius_m));
    }
    ring.push(ring[0]); // close
    ring
}

// ---------------------------------------------------------------------------
// Line along (great-circle sampling)
// ---------------------------------------------------------------------------

/// Sample `count` evenly-spaced points along the great-circle path
/// between `from` and `to`.
///
/// Returns `count` points including both endpoints.  If `count < 2`,
/// returns just `[from, to]`.
pub fn line_along(from: &GeoCoord, to: &GeoCoord, count: usize) -> Vec<GeoCoord> {
    let n = count.max(2);
    let mut points = Vec::with_capacity(n);
    for i in 0..n {
        let fraction = i as f64 / (n - 1) as f64;
        points.push(crate::geometry_ops::interpolate_great_circle(
            from, to, fraction,
        ));
    }
    points
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    #[test]
    fn circle_has_correct_vertex_count() {
        let center = GeoCoord::from_lat_lon(0.0, 0.0);
        let ring = circle(&center, 1000.0, 64);
        // 64 segments + 1 closing vertex.
        assert_eq!(ring.len(), 65);
        // First and last should match.
        assert!((ring[0].lat - ring[64].lat).abs() < 1e-10);
        assert!((ring[0].lon - ring[64].lon).abs() < 1e-10);
    }

    #[test]
    fn circle_radius_is_approximately_correct() {
        let center = GeoCoord::from_lat_lon(0.0, 0.0);
        let radius = 10_000.0; // 10 km
        let ring = circle(&center, radius, 64);
        // Check that each vertex is approximately `radius` meters from centre.
        for v in &ring[..64] {
            let d = crate::geometry_ops::haversine(&center, v);
            assert!((d - radius).abs() < 1.0, "expected ~{radius}m, got {d}m");
        }
    }

    #[test]
    fn arc_spans_correct_angles() {
        let center = GeoCoord::from_lat_lon(0.0, 0.0);
        let points = arc(&center, 1000.0, 0.0, 90.0, 4);
        // 4 segments + 1 = 5 points.
        assert_eq!(points.len(), 5);
        // First point should be roughly north.
        assert!(points[0].lat > center.lat);
        // Last point should be roughly east.
        assert!(points[4].lon > center.lon);
    }

    #[test]
    fn rectangle_has_five_vertices() {
        let sw = GeoCoord::from_lat_lon(0.0, 0.0);
        let ne = GeoCoord::from_lat_lon(1.0, 1.0);
        let ring = rectangle(&sw, &ne);
        assert_eq!(ring.len(), 5);
        assert_eq!(ring[0].lat, ring[4].lat);
        assert_eq!(ring[0].lon, ring[4].lon);
    }

    #[test]
    fn regular_polygon_triangle() {
        let center = GeoCoord::from_lat_lon(0.0, 0.0);
        let ring = regular_polygon(&center, 1000.0, 3, 0.0);
        assert_eq!(ring.len(), 4); // 3 + closing
    }

    #[test]
    fn sector_includes_center() {
        let center = GeoCoord::from_lat_lon(10.0, 20.0);
        let ring = sector(&center, 5000.0, 0.0, 90.0, 8);
        // First vertex should be the center.
        assert_eq!(ring[0].lat, center.lat);
        assert_eq!(ring[0].lon, center.lon);
        // Last vertex should also be the center (closing).
        let last = ring.last().unwrap();
        assert_eq!(last.lat, center.lat);
        assert_eq!(last.lon, center.lon);
    }

    #[test]
    fn ellipse_has_correct_vertex_count() {
        let center = GeoCoord::from_lat_lon(0.0, 0.0);
        let ring = ellipse(&center, 2000.0, 1000.0, 45.0, 32);
        assert_eq!(ring.len(), 33); // 32 + closing
    }

    #[test]
    fn line_along_endpoints() {
        let a = GeoCoord::from_lat_lon(0.0, 0.0);
        let b = GeoCoord::from_lat_lon(10.0, 0.0);
        let pts = line_along(&a, &b, 5);
        assert_eq!(pts.len(), 5);
        assert!((pts[0].lat - a.lat).abs() < 1e-10);
        assert!((pts[4].lat - b.lat).abs() < 0.01);
    }

    #[test]
    fn circle_minimum_segments() {
        let center = GeoCoord::from_lat_lon(0.0, 0.0);
        let ring = circle(&center, 1000.0, 1); // requested 1, clamped to 4
        assert_eq!(ring.len(), 5); // 4 + closing
    }
}