Skip to main content

rustial_engine/
shapes.rs

1// ---------------------------------------------------------------------------
2//! # Shape generators -- create geographic geometries from parameters
3//!
4//! Generate common geometric shapes as [`GeoCoord`] rings suitable for
5//! use with the tessellator, vector layers, or direct rendering:
6//!
7//! - [`circle`] -- regular polygon approximating a circle
8//! - [`arc`] -- partial circle (sector boundary)
9//! - [`ellipse`] -- elliptical ring
10//! - [`rectangle`] -- axis-aligned geographic rectangle
11//! - [`regular_polygon`] -- equilateral N-gon
12//! - [`sector`] -- pie-slice shape (arc closed to centre)
13//! - [`line_along`] -- sample points along a great-circle path
14//!
15//! All generators produce `Vec<GeoCoord>` suitable for constructing
16//! [`Polygon`](crate::geometry::Polygon) exteriors or
17//! [`LineString`](crate::geometry::LineString) coordinates.
18//!
19//! Distance calculations use the geodesic destination formula on the
20//! WGS-84 sphere (radius 6,378,137 m).
21// ---------------------------------------------------------------------------
22
23use rustial_math::GeoCoord;
24
25/// Mean radius of the Earth in meters (WGS-84 semi-major axis).
26const EARTH_RADIUS: f64 = 6_378_137.0;
27
28/// Compute a destination point given a start, bearing (degrees), and
29/// distance (meters) on the WGS-84 sphere.
30fn destination(from: &GeoCoord, bearing_deg: f64, distance_m: f64) -> GeoCoord {
31    let lat1 = from.lat.to_radians();
32    let lon1 = from.lon.to_radians();
33    let brng = bearing_deg.to_radians();
34    let d = distance_m / EARTH_RADIUS;
35
36    let lat2 = (lat1.sin() * d.cos() + lat1.cos() * d.sin() * brng.cos()).asin();
37    let lon2 = lon1 + (brng.sin() * d.sin() * lat1.cos()).atan2(d.cos() - lat1.sin() * lat2.sin());
38
39    GeoCoord::from_lat_lon(lat2.to_degrees(), lon2.to_degrees())
40}
41
42// ---------------------------------------------------------------------------
43// Circle
44// ---------------------------------------------------------------------------
45
46/// Generate a circle as a closed polygon ring.
47///
48/// # Arguments
49///
50/// - `center` -- centre of the circle.
51/// - `radius_m` -- radius in meters.
52/// - `segments` -- number of line segments (more = smoother).
53///   Clamped to a minimum of 4.
54///
55/// The returned ring is closed (first == last) and counter-clockwise.
56pub fn circle(center: &GeoCoord, radius_m: f64, segments: usize) -> Vec<GeoCoord> {
57    let n = segments.max(4);
58    let step = 360.0 / n as f64;
59    let mut ring = Vec::with_capacity(n + 1);
60    for i in 0..n {
61        ring.push(destination(center, i as f64 * step, radius_m));
62    }
63    ring.push(ring[0]); // close
64    ring
65}
66
67// ---------------------------------------------------------------------------
68// Arc
69// ---------------------------------------------------------------------------
70
71/// Generate an arc (partial circle) as a polyline.
72///
73/// # Arguments
74///
75/// - `center` -- centre point.
76/// - `radius_m` -- radius in meters.
77/// - `start_bearing` -- start angle in degrees (0 = north, 90 = east).
78/// - `end_bearing` -- end angle in degrees (swept clockwise from start).
79/// - `segments` -- number of segments in the arc (clamped to min 2).
80///
81/// Returns a list of points along the arc from `start_bearing` to
82/// `end_bearing`.  Does **not** close the ring.
83pub fn arc(
84    center: &GeoCoord,
85    radius_m: f64,
86    start_bearing: f64,
87    end_bearing: f64,
88    segments: usize,
89) -> Vec<GeoCoord> {
90    let n = segments.max(2);
91    let mut sweep = end_bearing - start_bearing;
92    if sweep <= 0.0 {
93        sweep += 360.0;
94    }
95    let step = sweep / n as f64;
96    let mut points = Vec::with_capacity(n + 1);
97    for i in 0..=n {
98        let bearing = start_bearing + i as f64 * step;
99        points.push(destination(center, bearing, radius_m));
100    }
101    points
102}
103
104// ---------------------------------------------------------------------------
105// Sector (pie slice)
106// ---------------------------------------------------------------------------
107
108/// Generate a sector (pie slice) as a closed polygon ring.
109///
110/// The sector starts at the centre, sweeps from `start_bearing` to
111/// `end_bearing` along the arc, and closes back to the centre.
112pub fn sector(
113    center: &GeoCoord,
114    radius_m: f64,
115    start_bearing: f64,
116    end_bearing: f64,
117    segments: usize,
118) -> Vec<GeoCoord> {
119    let arc_points = arc(center, radius_m, start_bearing, end_bearing, segments);
120    let mut ring = Vec::with_capacity(arc_points.len() + 2);
121    ring.push(*center);
122    ring.extend(arc_points);
123    ring.push(*center); // close
124    ring
125}
126
127// ---------------------------------------------------------------------------
128// Ellipse
129// ---------------------------------------------------------------------------
130
131/// Generate an ellipse as a closed polygon ring.
132///
133/// # Arguments
134///
135/// - `center` -- centre of the ellipse.
136/// - `semi_major_m` -- semi-major axis in meters (along `rotation`).
137/// - `semi_minor_m` -- semi-minor axis in meters (perpendicular).
138/// - `rotation_deg` -- rotation of the semi-major axis in degrees
139///   clockwise from north.
140/// - `segments` -- number of segments (clamped to min 4).
141pub fn ellipse(
142    center: &GeoCoord,
143    semi_major_m: f64,
144    semi_minor_m: f64,
145    rotation_deg: f64,
146    segments: usize,
147) -> Vec<GeoCoord> {
148    let n = segments.max(4);
149    let step = std::f64::consts::TAU / n as f64;
150    let rot = rotation_deg.to_radians();
151    let mut ring = Vec::with_capacity(n + 1);
152    for i in 0..n {
153        let angle = i as f64 * step;
154        // Parametric ellipse in local frame.
155        let lx = semi_major_m * angle.cos();
156        let ly = semi_minor_m * angle.sin();
157        // Rotate into geographic bearing.
158        let bearing = (lx * rot.sin() + ly * rot.cos()).atan2(lx * rot.cos() - ly * rot.sin());
159        let dist = (lx * lx + ly * ly).sqrt();
160        ring.push(destination(center, bearing.to_degrees(), dist));
161    }
162    ring.push(ring[0]); // close
163    ring
164}
165
166// ---------------------------------------------------------------------------
167// Rectangle
168// ---------------------------------------------------------------------------
169
170/// Generate an axis-aligned geographic rectangle as a closed polygon ring.
171///
172/// # Arguments
173///
174/// - `sw` -- southwest corner (min lat, min lon).
175/// - `ne` -- northeast corner (max lat, max lon).
176///
177/// Returns a CCW ring: SW → SE → NE → NW → SW.
178pub fn rectangle(sw: &GeoCoord, ne: &GeoCoord) -> Vec<GeoCoord> {
179    vec![
180        GeoCoord::from_lat_lon(sw.lat, sw.lon),
181        GeoCoord::from_lat_lon(sw.lat, ne.lon),
182        GeoCoord::from_lat_lon(ne.lat, ne.lon),
183        GeoCoord::from_lat_lon(ne.lat, sw.lon),
184        GeoCoord::from_lat_lon(sw.lat, sw.lon), // close
185    ]
186}
187
188// ---------------------------------------------------------------------------
189// Regular polygon
190// ---------------------------------------------------------------------------
191
192/// Generate a regular N-gon as a closed polygon ring.
193///
194/// # Arguments
195///
196/// - `center` -- centre of the polygon.
197/// - `radius_m` -- circumscribed circle radius in meters.
198/// - `sides` -- number of sides (clamped to min 3).
199/// - `rotation_deg` -- rotation of the first vertex in degrees
200///   clockwise from north.
201pub fn regular_polygon(
202    center: &GeoCoord,
203    radius_m: f64,
204    sides: usize,
205    rotation_deg: f64,
206) -> Vec<GeoCoord> {
207    let n = sides.max(3);
208    let step = 360.0 / n as f64;
209    let mut ring = Vec::with_capacity(n + 1);
210    for i in 0..n {
211        let bearing = rotation_deg + i as f64 * step;
212        ring.push(destination(center, bearing, radius_m));
213    }
214    ring.push(ring[0]); // close
215    ring
216}
217
218// ---------------------------------------------------------------------------
219// Line along (great-circle sampling)
220// ---------------------------------------------------------------------------
221
222/// Sample `count` evenly-spaced points along the great-circle path
223/// between `from` and `to`.
224///
225/// Returns `count` points including both endpoints.  If `count < 2`,
226/// returns just `[from, to]`.
227pub fn line_along(from: &GeoCoord, to: &GeoCoord, count: usize) -> Vec<GeoCoord> {
228    let n = count.max(2);
229    let mut points = Vec::with_capacity(n);
230    for i in 0..n {
231        let fraction = i as f64 / (n - 1) as f64;
232        points.push(crate::geometry_ops::interpolate_great_circle(
233            from, to, fraction,
234        ));
235    }
236    points
237}
238
239// ---------------------------------------------------------------------------
240// Tests
241// ---------------------------------------------------------------------------
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn circle_has_correct_vertex_count() {
249        let center = GeoCoord::from_lat_lon(0.0, 0.0);
250        let ring = circle(&center, 1000.0, 64);
251        // 64 segments + 1 closing vertex.
252        assert_eq!(ring.len(), 65);
253        // First and last should match.
254        assert!((ring[0].lat - ring[64].lat).abs() < 1e-10);
255        assert!((ring[0].lon - ring[64].lon).abs() < 1e-10);
256    }
257
258    #[test]
259    fn circle_radius_is_approximately_correct() {
260        let center = GeoCoord::from_lat_lon(0.0, 0.0);
261        let radius = 10_000.0; // 10 km
262        let ring = circle(&center, radius, 64);
263        // Check that each vertex is approximately `radius` meters from centre.
264        for v in &ring[..64] {
265            let d = crate::geometry_ops::haversine(&center, v);
266            assert!((d - radius).abs() < 1.0, "expected ~{radius}m, got {d}m");
267        }
268    }
269
270    #[test]
271    fn arc_spans_correct_angles() {
272        let center = GeoCoord::from_lat_lon(0.0, 0.0);
273        let points = arc(&center, 1000.0, 0.0, 90.0, 4);
274        // 4 segments + 1 = 5 points.
275        assert_eq!(points.len(), 5);
276        // First point should be roughly north.
277        assert!(points[0].lat > center.lat);
278        // Last point should be roughly east.
279        assert!(points[4].lon > center.lon);
280    }
281
282    #[test]
283    fn rectangle_has_five_vertices() {
284        let sw = GeoCoord::from_lat_lon(0.0, 0.0);
285        let ne = GeoCoord::from_lat_lon(1.0, 1.0);
286        let ring = rectangle(&sw, &ne);
287        assert_eq!(ring.len(), 5);
288        assert_eq!(ring[0].lat, ring[4].lat);
289        assert_eq!(ring[0].lon, ring[4].lon);
290    }
291
292    #[test]
293    fn regular_polygon_triangle() {
294        let center = GeoCoord::from_lat_lon(0.0, 0.0);
295        let ring = regular_polygon(&center, 1000.0, 3, 0.0);
296        assert_eq!(ring.len(), 4); // 3 + closing
297    }
298
299    #[test]
300    fn sector_includes_center() {
301        let center = GeoCoord::from_lat_lon(10.0, 20.0);
302        let ring = sector(&center, 5000.0, 0.0, 90.0, 8);
303        // First vertex should be the center.
304        assert_eq!(ring[0].lat, center.lat);
305        assert_eq!(ring[0].lon, center.lon);
306        // Last vertex should also be the center (closing).
307        let last = ring.last().unwrap();
308        assert_eq!(last.lat, center.lat);
309        assert_eq!(last.lon, center.lon);
310    }
311
312    #[test]
313    fn ellipse_has_correct_vertex_count() {
314        let center = GeoCoord::from_lat_lon(0.0, 0.0);
315        let ring = ellipse(&center, 2000.0, 1000.0, 45.0, 32);
316        assert_eq!(ring.len(), 33); // 32 + closing
317    }
318
319    #[test]
320    fn line_along_endpoints() {
321        let a = GeoCoord::from_lat_lon(0.0, 0.0);
322        let b = GeoCoord::from_lat_lon(10.0, 0.0);
323        let pts = line_along(&a, &b, 5);
324        assert_eq!(pts.len(), 5);
325        assert!((pts[0].lat - a.lat).abs() < 1e-10);
326        assert!((pts[4].lat - b.lat).abs() < 0.01);
327    }
328
329    #[test]
330    fn circle_minimum_segments() {
331        let center = GeoCoord::from_lat_lon(0.0, 0.0);
332        let ring = circle(&center, 1000.0, 1); // requested 1, clamped to 4
333        assert_eq!(ring.len(), 5); // 4 + closing
334    }
335}