1mod wire;
8
9use num_rational::BigRational;
10use num_traits::{Signed, Zero};
11
12use crate::{CoverageSource, CoverageSpecification, Error, KineticEdgeKey, Result};
13
14pub use wire::{GeometryBoundCoverageArtifact, GeometryBoundCoverageDecodeLimits};
15
16#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct PlanarPoint {
19 x: f64,
20 y: f64,
21}
22
23impl PlanarPoint {
24 pub fn new(x: f64, y: f64) -> Result<Self> {
26 if !x.is_finite() || !y.is_finite() {
27 return Err(geometry_error("planar coordinates must be finite"));
28 }
29 Ok(Self {
30 x: canonical_zero(x),
31 y: canonical_zero(y),
32 })
33 }
34
35 pub fn x(self) -> f64 {
37 self.x
38 }
39
40 pub fn y(self) -> f64 {
42 self.y
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48#[non_exhaustive]
49pub struct CoverageGeometryLimits {
50 pub max_states: usize,
52 pub max_vertices: usize,
54 pub max_coordinates: usize,
56 pub max_pairs_per_state: u64,
58}
59
60impl Default for CoverageGeometryLimits {
61 fn default() -> Self {
62 Self {
63 max_states: 4_096,
64 max_vertices: 1_000_000,
65 max_coordinates: 10_000_000,
66 max_pairs_per_state: 100_000_000,
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq)]
73pub struct CoverageGeometry {
74 coordinates: Vec<Vec<PlanarPoint>>,
75}
76
77impl CoverageGeometry {
78 pub fn new(
82 specification: &CoverageSpecification,
83 coordinates: Vec<Vec<PlanarPoint>>,
84 limits: CoverageGeometryLimits,
85 ) -> Result<Self> {
86 let geometry = Self { coordinates };
87 geometry.verify(specification, limits)?;
88 Ok(geometry)
89 }
90
91 pub fn coordinates(&self) -> &[Vec<PlanarPoint>] {
93 &self.coordinates
94 }
95
96 pub fn verify(
98 &self,
99 specification: &CoverageSpecification,
100 limits: CoverageGeometryLimits,
101 ) -> Result<()> {
102 validate_geometry_scope(specification, &self.coordinates, limits)?;
103 for (state, coordinates) in specification.states().iter().zip(&self.coordinates) {
104 validate_state_geometry(specification, state.possible_edges(), coordinates, limits)?;
105 }
106 Ok(())
107 }
108}
109
110fn validate_geometry_scope(
111 specification: &CoverageSpecification,
112 coordinates: &[Vec<PlanarPoint>],
113 limits: CoverageGeometryLimits,
114) -> Result<()> {
115 if !matches!(specification.source(), CoverageSource::Finite) {
116 return Err(geometry_error(
117 "geometry binding accepts finite coverage states only",
118 ));
119 }
120 if specification.states().is_empty()
121 || specification.states().len() > limits.max_states
122 || coordinates.len() != specification.states().len()
123 || specification.vertex_count() > limits.max_vertices
124 {
125 return Err(geometry_error(
126 "geometry state or vertex count is empty, mismatched, or over its limit",
127 ));
128 }
129 let total = coordinates.iter().try_fold(0usize, |total, state| {
130 total
131 .checked_add(state.len())
132 .ok_or_else(|| geometry_error("geometry coordinate count overflows"))
133 })?;
134 if total > limits.max_coordinates {
135 return Err(geometry_error(
136 "geometry coordinates exceed their total limit",
137 ));
138 }
139 Ok(())
140}
141
142fn validate_state_geometry(
143 specification: &CoverageSpecification,
144 declared_edges: &[KineticEdgeKey],
145 coordinates: &[PlanarPoint],
146 limits: CoverageGeometryLimits,
147) -> Result<()> {
148 if coordinates.len() != specification.vertex_count() {
149 return Err(geometry_error(
150 "geometry state does not have one point per vertex",
151 ));
152 }
153 let exact = coordinates
154 .iter()
155 .copied()
156 .map(ExactPoint::from)
157 .collect::<Vec<_>>();
158 let polygon = specification
159 .fence()
160 .vertices()
161 .iter()
162 .map(|&vertex| exact[vertex].clone())
163 .collect::<Vec<_>>();
164 validate_polygon(&polygon)?;
165 if exact.iter().any(|point| !point_in_polygon(point, &polygon)) {
166 return Err(geometry_error(
167 "geometry places a sensor outside the fence polygon",
168 ));
169 }
170 let expected = euclidean_edges(
171 &exact,
172 specification.model().broadcast_radius(),
173 limits.max_pairs_per_state,
174 )?;
175 if expected != declared_edges {
176 return Err(geometry_error(
177 "coverage state differs from its complete Euclidean radius graph",
178 ));
179 }
180 Ok(())
181}
182
183#[derive(Clone, PartialEq, Eq)]
184struct ExactPoint {
185 x: BigRational,
186 y: BigRational,
187}
188
189impl From<PlanarPoint> for ExactPoint {
190 fn from(point: PlanarPoint) -> Self {
191 Self {
192 x: rational(point.x),
193 y: rational(point.y),
194 }
195 }
196}
197
198fn validate_polygon(polygon: &[ExactPoint]) -> Result<()> {
199 if polygon.len() < 3 || has_repeated_point(polygon) || signed_double_area(polygon).is_zero() {
200 return Err(geometry_error(
201 "fence coordinates do not form a nondegenerate polygon",
202 ));
203 }
204 for left in 0..polygon.len() {
205 for right in left + 1..polygon.len() {
206 if !segments_are_adjacent(left, right, polygon.len())
207 && segments_intersect(
208 &polygon[left],
209 &polygon[(left + 1) % polygon.len()],
210 &polygon[right],
211 &polygon[(right + 1) % polygon.len()],
212 )
213 {
214 return Err(geometry_error("fence polygon intersects itself"));
215 }
216 }
217 }
218 Ok(())
219}
220
221fn has_repeated_point(points: &[ExactPoint]) -> bool {
222 points
223 .iter()
224 .enumerate()
225 .any(|(index, point)| points[..index].contains(point))
226}
227
228fn signed_double_area(polygon: &[ExactPoint]) -> BigRational {
229 let mut area = BigRational::from_integer(0.into());
230 for index in 0..polygon.len() {
231 let next = (index + 1) % polygon.len();
232 area += &polygon[index].x * &polygon[next].y - &polygon[next].x * &polygon[index].y;
233 }
234 area
235}
236
237fn segments_are_adjacent(left: usize, right: usize, count: usize) -> bool {
238 left == right || (left + 1) % count == right || (right + 1) % count == left
239}
240
241fn segments_intersect(a: &ExactPoint, b: &ExactPoint, c: &ExactPoint, d: &ExactPoint) -> bool {
242 let abc = orientation(a, b, c);
243 let abd = orientation(a, b, d);
244 let cda = orientation(c, d, a);
245 let cdb = orientation(c, d, b);
246 opposite_sign(&abc, &abd) && opposite_sign(&cda, &cdb)
247 || abc.is_zero() && on_segment(c, a, b)
248 || abd.is_zero() && on_segment(d, a, b)
249 || cda.is_zero() && on_segment(a, c, d)
250 || cdb.is_zero() && on_segment(b, c, d)
251}
252
253fn orientation(a: &ExactPoint, b: &ExactPoint, c: &ExactPoint) -> BigRational {
254 (&b.x - &a.x) * (&c.y - &a.y) - (&b.y - &a.y) * (&c.x - &a.x)
255}
256
257fn opposite_sign(left: &BigRational, right: &BigRational) -> bool {
258 (left.is_negative() && right.is_positive()) || (left.is_positive() && right.is_negative())
259}
260
261fn on_segment(point: &ExactPoint, start: &ExactPoint, end: &ExactPoint) -> bool {
262 orientation(start, end, point).is_zero()
263 && between(&point.x, &start.x, &end.x)
264 && between(&point.y, &start.y, &end.y)
265}
266
267fn between(value: &BigRational, left: &BigRational, right: &BigRational) -> bool {
268 value >= left.min(right) && value <= left.max(right)
269}
270
271fn point_in_polygon(point: &ExactPoint, polygon: &[ExactPoint]) -> bool {
272 if polygon
273 .iter()
274 .enumerate()
275 .any(|(index, start)| on_segment(point, start, &polygon[(index + 1) % polygon.len()]))
276 {
277 return true;
278 }
279 let mut inside = false;
280 for index in 0..polygon.len() {
281 let start = &polygon[index];
282 let end = &polygon[(index + 1) % polygon.len()];
283 if (start.y > point.y) != (end.y > point.y) {
284 let crossing =
285 &start.x + (&point.y - &start.y) * (&end.x - &start.x) / (&end.y - &start.y);
286 if crossing > point.x {
287 inside = !inside;
288 }
289 }
290 }
291 inside
292}
293
294fn euclidean_edges(
295 points: &[ExactPoint],
296 broadcast_radius: f64,
297 maximum_pairs: u64,
298) -> Result<Vec<KineticEdgeKey>> {
299 let count = u64::try_from(points.len())
300 .map_err(|_| geometry_error("geometry vertex count does not fit u64"))?;
301 let pairs = count.saturating_mul(count.saturating_sub(1)) / 2;
302 if pairs > maximum_pairs {
303 return Err(geometry_error(
304 "geometry vertex pairs exceed their state limit",
305 ));
306 }
307 let radius = rational(broadcast_radius);
308 let radius_squared = &radius * &radius;
309 let mut edges = Vec::new();
310 for u in 0..points.len() {
311 for v in u + 1..points.len() {
312 if squared_distance(&points[u], &points[v]) <= radius_squared {
313 edges.push(KineticEdgeKey::new(u, v));
314 }
315 }
316 }
317 Ok(edges)
318}
319
320fn squared_distance(left: &ExactPoint, right: &ExactPoint) -> BigRational {
321 let dx = &left.x - &right.x;
322 let dy = &left.y - &right.y;
323 &dx * &dx + &dy * &dy
324}
325
326fn rational(value: f64) -> BigRational {
327 BigRational::from_float(value).expect("validated finite f64 has an exact rational form")
328}
329
330fn canonical_zero(value: f64) -> f64 {
331 if value == 0.0 { 0.0 } else { value }
332}
333
334fn geometry_error(message: impl Into<String>) -> Error {
335 Error::InvalidInput(format!("coverage geometry: {}", message.into()))
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use crate::{
342 CoverageFence, CoverageLimits, CoverageState, PlanarCoverageModel, SparseDistanceMatrix,
343 };
344
345 fn square_points() -> Vec<PlanarPoint> {
346 [(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0), (1.0, 1.0)]
347 .into_iter()
348 .map(|(x, y)| PlanarPoint::new(x, y).unwrap())
349 .collect()
350 }
351
352 fn specification(points: &[PlanarPoint]) -> CoverageSpecification {
353 let exact = points
354 .iter()
355 .copied()
356 .map(ExactPoint::from)
357 .collect::<Vec<_>>();
358 let edges = euclidean_edges(&exact, 2.0, 100).unwrap();
359 let triplets = edges
360 .iter()
361 .map(|edge| (edge.u, edge.v, 1.0))
362 .collect::<Vec<_>>();
363 let graph = SparseDistanceMatrix::from_triplets(points.len(), &triplets).unwrap();
364 CoverageSpecification::new(
365 points.len(),
366 PlanarCoverageModel::new(2.0, 2.0).unwrap(),
367 2,
368 CoverageFence::new(vec![0, 1, 2, 3]).unwrap(),
369 Vec::new(),
370 0,
371 vec![CoverageState::new(0, 0, &graph, (0..4).collect(), 2.0).unwrap()],
372 CoverageLimits::default(),
373 )
374 .unwrap()
375 }
376
377 #[test]
378 fn exact_square_geometry_binds_the_radius_graph() {
379 let points = square_points();
380 let specification = specification(&points);
381 CoverageGeometry::new(
382 &specification,
383 vec![points],
384 CoverageGeometryLimits::default(),
385 )
386 .unwrap();
387 }
388
389 #[test]
390 fn geometry_rejects_an_outside_sensor_and_a_crossed_fence() {
391 let points = square_points();
392 let specification = specification(&points);
393 let mut outside = points.clone();
394 outside[4] = PlanarPoint::new(3.0, 1.0).unwrap();
395 assert!(
396 CoverageGeometry::new(
397 &specification,
398 vec![outside],
399 CoverageGeometryLimits::default(),
400 )
401 .is_err()
402 );
403 let crossed = vec![points[0], points[2], points[1], points[3], points[4]];
404 assert!(
405 CoverageGeometry::new(
406 &specification,
407 vec![crossed],
408 CoverageGeometryLimits::default(),
409 )
410 .is_err()
411 );
412 }
413}