Skip to main content

ras_commander_fast_map/
boundary_contour.rs

1//! Deterministic zero-depth contours. No claim of native vector parity.
2//! Terrain sampling/surface reconstruction and AOI halos belong to the caller.
3use crate::MapError;
4use clipper2_rust::{ClipType, Clipper64, FillRule, Path64, Paths64, Point64, PolyTree64};
5use serde::Serialize;
6
7pub const METHOD_ID: &str = "deterministic-zero-depth-contours/1";
8pub const PRECISION: f64 = 1000.;
9pub type Result<T> = std::result::Result<T, MapError>;
10/// Planned admission only; not a hard process RSS limit. Includes clipping overhead
11/// estimated as 256 bytes per vertex, 128 per ring and a 64 KiB fixed allowance.
12#[derive(Clone, Copy)]
13pub struct Limits {
14    pub max_working_bytes: usize,
15}
16impl Limits {
17    fn admit(self, vertices: usize, paths: usize) -> Result<()> {
18        let estimate = vertices
19            .checked_mul(256)
20            .and_then(|n| paths.checked_mul(128).and_then(|m| n.checked_add(m)))
21            .and_then(|n| n.checked_add(65536))
22            .ok_or_else(|| {
23                MapError::Budget("boundary contour planned memory exceeds budget".into())
24            })?;
25        if estimate > self.max_working_bytes {
26            Err(MapError::Budget(
27                "boundary contour planned memory exceeds budget".into(),
28            ))
29        } else {
30            Ok(())
31        }
32    }
33}
34#[derive(Clone, Copy)]
35pub struct Grid {
36    pub cols: usize,
37    pub rows: usize,
38    pub transform: [f64; 6],
39}
40impl Grid {
41    fn validate(self, n: usize) -> Result<()> {
42        let t = self.transform;
43        if self.cols < 2
44            || self.rows < 2
45            || self.cols.checked_mul(self.rows) != Some(n)
46            || t.iter().any(|v| !v.is_finite())
47            || t[1] <= 0.
48            || t[5] >= 0.
49            || t[2] != 0.
50            || t[4] != 0.
51        {
52            return Err(MapError::Invalid("invalid contour sample grid".into()));
53        }
54        Ok(())
55    }
56    fn point(self, r: usize, c: usize) -> [f64; 2] {
57        [
58            self.transform[0] + (c as f64 + 0.5) * self.transform[1],
59            self.transform[3] + (r as f64 + 0.5) * self.transform[5],
60        ]
61    }
62    fn center(self, r: usize, c: usize) -> [f64; 2] {
63        [
64            self.transform[0] + (c + 1) as f64 * self.transform[1],
65            self.transform[3] + (r + 1) as f64 * self.transform[5],
66        ]
67    }
68}
69#[derive(Clone, Copy)]
70struct Sample {
71    xy: [f64; 2],
72    z: f32,
73}
74#[derive(Clone, Debug)]
75pub struct Unit {
76    pub sequence: u64,
77    paths: Paths64,
78}
79impl Unit {
80    pub fn vertex_count(&self) -> usize {
81        self.paths
82            .iter()
83            .fold(0usize, |n, p| n.saturating_add(p.len()))
84    }
85    pub fn ring_count(&self) -> usize {
86        self.paths.len()
87    }
88}
89struct Fragments {
90    paths: Paths64,
91    vertices: usize,
92    limits: Limits,
93}
94impl Fragments {
95    fn new(limits: Limits) -> Self {
96        Self {
97            paths: vec![],
98            vertices: 0,
99            limits,
100        }
101    }
102    fn push(&mut self, mut p: Path64) -> Result<()> {
103        if p.len() < 3 {
104            return Ok(());
105        }
106        // Raw fragments denote wet regions irrespective of stored TIN winding.
107        // Holes arise from union, not from triangle winding.
108        let area = area2_points(&p)?;
109        if area == 0 {
110            return Ok(());
111        }
112        if area < 0 {
113            p.reverse();
114        }
115        let n = self.vertices.checked_add(p.len()).ok_or_else(|| {
116            MapError::Budget("boundary contour planned memory exceeds budget".into())
117        })?;
118        self.limits.admit(n, self.paths.len() + 1)?;
119        self.vertices = n;
120        self.paths.push(p);
121        Ok(())
122    }
123    fn complete(self, sequence: u64) -> Result<Unit> {
124        let paths = union(&self.paths, self.limits)?;
125        Ok(Unit { sequence, paths })
126    }
127}
128fn quantize(p: [f64; 2]) -> Result<Point64> {
129    let q = [
130        (p[0] * PRECISION).round_ties_even(),
131        (p[1] * PRECISION).round_ties_even(),
132    ];
133    if q.iter()
134        .any(|v| !v.is_finite() || v.abs() >= ((1_u64 << 61) as f64))
135    {
136        return Err(MapError::Invalid(
137            "contour coordinate outside integer clipping range".into(),
138        ));
139    }
140    Ok(Point64::new(q[0] as i64, q[1] as i64))
141}
142fn check_field(values: &[Option<f32>]) -> Result<()> {
143    if values.iter().any(|v| v.is_some_and(|v| !v.is_finite())) {
144        Err(MapError::Invalid("nonfinite contour sample".into()))
145    } else {
146        Ok(())
147    }
148}
149fn edge(a: Sample, b: Sample, out: &mut Vec<[f64; 2]>) -> Result<()> {
150    let inside = |z: f32| z >= 0.;
151    if inside(a.z) {
152        out.push(a.xy)
153    }
154    if (a.z >= 0.) != (b.z >= 0.) {
155        let span = b.z - a.z;
156        if !span.is_finite() {
157            return Err(MapError::Invalid("zero-contour depth span overflow".into()));
158        }
159        let t = f64::from((0f32 - a.z) / span);
160        if !t.is_finite() {
161            return Err(MapError::Invalid(
162                "nonfinite zero-contour interpolation".into(),
163            ));
164        }
165        out.push([
166            a.xy[0] + (b.xy[0] - a.xy[0]) * t,
167            a.xy[1] + (b.xy[1] - a.xy[1]) * t,
168        ]);
169    }
170    Ok(())
171}
172fn triangle(v: [Sample; 3], out: &mut Fragments) -> Result<()> {
173    let mut coords = Vec::with_capacity(4);
174    for i in 0..3 {
175        edge(v[i], v[(i + 1) % 3], &mut coords)?
176    }
177    let mut p = coords
178        .into_iter()
179        .map(quantize)
180        .collect::<Result<Path64>>()?;
181    p.dedup();
182    if p.len() > 1 && p.first() == p.last() {
183        p.pop();
184    }
185    out.push(p)
186}
187fn block(v: [Option<Sample>; 4], xy: [f64; 2], out: &mut Fragments) -> Result<()> {
188    let count = v.iter().flatten().count();
189    if count < 3 {
190        return Ok(());
191    }
192    let sum = v.iter().flatten().fold(0f32, |a, b| a + b.z);
193    let z = if count == 4 {
194        (f64::from(sum) * 0.25) as f32
195    } else {
196        sum / 3f32
197    };
198    if !z.is_finite() {
199        return Err(MapError::Invalid("contour center depth overflow".into()));
200    }
201    let mid = Sample { xy, z };
202    for i in 0..4 {
203        let j = (i + 1) % 4;
204        if let (Some(a), Some(b)) = (v[i], v[j]) {
205            triangle([b, a, mid], out)?
206        }
207    }
208    Ok(())
209}
210fn sample(xy: [f64; 2], z: Option<f32>) -> Option<Sample> {
211    z.map(|z| Sample { xy, z })
212}
213/// One haloed native sample tile. Input depths must retain valid negative samples.
214/// Fully wet blocks are coalesced along rows to bound clipping work.
215/// Missing samples are `None`; a valid negative value, including -9999, remains numeric.
216pub fn grid(sequence: u64, grid: Grid, depth: &[Option<f32>], limits: Limits) -> Result<Unit> {
217    grid.validate(depth.len())?;
218    quantize(grid.point(0, 0))?;
219    quantize(grid.point(grid.rows - 1, grid.cols - 1))?;
220    limits.admit(0, 0)?;
221    check_field(depth)?;
222    let mut fragments = Fragments::new(limits);
223    for r in 0..grid.rows - 1 {
224        let mut c = 0;
225        while c < grid.cols - 1 {
226            let wet = |c: usize| {
227                let i = r * grid.cols + c;
228                [i, i + 1, i + grid.cols + 1, i + grid.cols]
229                    .iter()
230                    .all(|&i| depth[i].is_some_and(|v| v >= 0.))
231            };
232            if wet(c) {
233                let start = c;
234                while c + 1 < grid.cols - 1 && wet(c + 1) {
235                    c += 1
236                }
237                let mut p = [
238                    grid.point(r, start),
239                    grid.point(r + 1, start),
240                    grid.point(r + 1, c + 1),
241                    grid.point(r, c + 1),
242                ]
243                .into_iter()
244                .map(quantize)
245                .collect::<Result<Path64>>()?;
246                p.dedup();
247                fragments.push(p)?;
248                c += 1;
249                continue;
250            }
251            let i = r * grid.cols + c;
252            block(
253                [
254                    sample(grid.point(r, c), depth[i]),
255                    sample(grid.point(r, c + 1), depth[i + 1]),
256                    sample(grid.point(r + 1, c + 1), depth[i + grid.cols + 1]),
257                    sample(grid.point(r + 1, c), depth[i + grid.cols]),
258                ],
259                grid.center(r, c),
260                &mut fragments,
261            )?;
262            c += 1;
263        }
264    }
265    fragments.complete(sequence)
266}
267/// Terrain stitch triangles, using original point XY and reconstructed signed depth.
268/// Missing samples are explicit; no numeric nodata sentinel is used for depths.
269pub fn tin(
270    sequence: u64,
271    xy: &[[f64; 2]],
272    depth: &[Option<f32>],
273    triangles: &[[u32; 3]],
274    limits: Limits,
275) -> Result<Unit> {
276    limits.admit(0, 0)?;
277    if xy.len() != depth.len() {
278        return Err(MapError::Invalid(
279            "stitch contour field length mismatch".into(),
280        ));
281    }
282    check_field(depth)?;
283    for &point in xy {
284        quantize(point)?;
285    }
286    let mut out = Fragments::new(limits);
287    for t in triangles {
288        let mut v = [None; 3];
289        for i in 0..3 {
290            let k = t[i] as usize;
291            if k >= xy.len() {
292                return Err(MapError::Invalid(
293                    "stitch contour point out of range".into(),
294                ));
295            }
296            v[i] = sample(xy[k], depth[k]);
297        }
298        if let [Some(a), Some(b), Some(c)] = v {
299            triangle([a, b, c], &mut out)?
300        }
301    }
302    out.complete(sequence)
303}
304
305fn count_vertices(paths: &Paths64) -> Result<usize> {
306    paths.iter().try_fold(0usize, |n, p| {
307        n.checked_add(p.len())
308            .ok_or_else(|| MapError::Budget("contour vertex count overflow".into()))
309    })
310}
311
312fn union(paths: &Paths64, limits: Limits) -> Result<Paths64> {
313    limits.admit(count_vertices(paths)?, paths.len())?;
314    if paths.is_empty() {
315        return Ok(vec![]);
316    }
317    let mut c = Clipper64::new();
318    c.set_preserve_collinear(false);
319    c.add_subject(paths);
320    let mut out = vec![];
321    if !c.execute(ClipType::Union, FillRule::NonZero, &mut out, None) {
322        return Err(MapError::Invalid("contour union failed".into()));
323    }
324    limits.admit(count_vertices(&out)?, out.len())?;
325    Ok(out)
326}
327fn area2_points(p: &Path64) -> Result<i128> {
328    let a = p[0];
329    let mut sum = 0i128;
330    for (p, q) in p.iter().zip(p.iter().cycle().skip(1)).take(p.len()) {
331        let (px, py) = (p.x as i128 - a.x as i128, p.y as i128 - a.y as i128);
332        let (qx, qy) = (q.x as i128 - a.x as i128, q.y as i128 - a.y as i128);
333        let cross = px
334            .checked_mul(qy)
335            .and_then(|a| qx.checked_mul(py).and_then(|b| a.checked_sub(b)))
336            .ok_or_else(|| MapError::Invalid("contour area overflow".into()))?;
337        sum = sum
338            .checked_add(cross)
339            .ok_or_else(|| MapError::Invalid("contour area overflow".into()))?;
340    }
341    Ok(sum)
342}
343
344fn area2(p: &[[i64; 2]]) -> Result<i128> {
345    let a = p[0];
346    let mut sum = 0i128;
347    for (p, q) in p.iter().zip(p.iter().cycle().skip(1)).take(p.len()) {
348        let (px, py) = (p[0] as i128 - a[0] as i128, p[1] as i128 - a[1] as i128);
349        let (qx, qy) = (q[0] as i128 - a[0] as i128, q[1] as i128 - a[1] as i128);
350        sum = sum
351            .checked_add(
352                px.checked_mul(qy)
353                    .and_then(|a| qx.checked_mul(py).and_then(|b| a.checked_sub(b)))
354                    .ok_or_else(|| MapError::Invalid("contour area overflow".into()))?,
355            )
356            .ok_or_else(|| MapError::Invalid("contour area overflow".into()))?;
357    }
358    Ok(sum)
359}
360/// Booth minimal rotation; stable in linear time even with duplicate vertices.
361fn rotate_min(p: &mut [[i64; 2]]) {
362    let n = p.len();
363    if n < 2 {
364        return;
365    }
366    let (mut i, mut j, mut k) = (0, 1, 0);
367    while i < n && j < n && k < n {
368        let a = p[(i + k) % n];
369        let b = p[(j + k) % n];
370        if a == b {
371            k += 1;
372            continue;
373        }
374        if a > b {
375            i += k + 1;
376            if i == j {
377                i += 1
378            }
379        } else {
380            j += k + 1;
381            if i == j {
382                j += 1
383            }
384        }
385        k = 0;
386    }
387    p.rotate_left(i.min(j) % n);
388}
389fn canonical(path: &Path64, ccw: bool) -> Result<Vec<[i64; 2]>> {
390    let mut p: Vec<_> = path.iter().map(|p| [p.x, p.y]).collect();
391    p.dedup();
392    if p.len() > 1 && p.first() == p.last() {
393        p.pop();
394    }
395    if p.len() < 3 {
396        return Err(MapError::Invalid("degenerate contour ring".into()));
397    }
398    let area = area2(&p)?;
399    if area == 0 {
400        return Err(MapError::Invalid("zero area contour ring".into()));
401    }
402    if (area > 0) != ccw {
403        p.reverse()
404    }
405    rotate_min(&mut p);
406    Ok(p)
407}
408pub type MultiPolygon = Vec<Vec<Vec<[f64; 2]>>>;
409#[derive(Serialize, Debug, PartialEq)]
410pub struct Output {
411    pub method_id: &'static str,
412    pub coordinate_precision: f64,
413    pub coordinates: MultiPolygon,
414}
415fn aoi_paths(aoi: &MultiPolygon, limits: Limits) -> Result<Paths64> {
416    let vertices = aoi.iter().flatten().try_fold(0usize, |n, r| {
417        n.checked_add(r.len())
418            .ok_or_else(|| MapError::Budget("AOI contour vertex count overflow".into()))
419    })?;
420    let rings = aoi.iter().try_fold(0usize, |n, p| {
421        n.checked_add(p.len())
422            .ok_or_else(|| MapError::Budget("AOI contour ring count overflow".into()))
423    })?;
424    limits.admit(vertices, rings)?;
425    let mut paths = vec![];
426    for polygon in aoi {
427        if polygon.is_empty() {
428            return Err(MapError::Invalid("empty AOI polygon".into()));
429        }
430        for (i, ring) in polygon.iter().enumerate() {
431            let path = ring
432                .iter()
433                .copied()
434                .map(quantize)
435                .collect::<Result<Path64>>()?;
436            let p = canonical(&path, i == 0)?;
437            paths.push(p.into_iter().map(|p| Point64::new(p[0], p[1])).collect());
438        }
439    }
440    Ok(paths)
441}
442/// Deterministic input sequence, all-subject NonZero union, then vector AOI clip.
443/// AOI does not change sampling, connectivity, tile traversal or zero-crossings.
444pub fn finish(units: &[Unit], aoi: Option<&MultiPolygon>, limits: Limits) -> Result<Output> {
445    limits.admit(0, units.len())?;
446    let mut order: Vec<_> = units.iter().collect();
447    order.sort_by_key(|u| u.sequence);
448    if order.windows(2).any(|w| w[0].sequence == w[1].sequence) {
449        return Err(MapError::Invalid("duplicate contour unit sequence".into()));
450    }
451    let vertices = order.iter().try_fold(0usize, |n, u| {
452        n.checked_add(u.vertex_count()).ok_or_else(|| {
453            MapError::Budget("boundary contour planned memory exceeds budget".into())
454        })
455    })?;
456    let count = order.iter().try_fold(0usize, |n, u| {
457        n.checked_add(u.paths.len())
458            .ok_or_else(|| MapError::Budget("contour ring count overflow".into()))
459    })?;
460    limits.admit(vertices, count)?;
461    let paths = order
462        .into_iter()
463        .flat_map(|u| u.paths.iter().cloned())
464        .collect();
465    let paths = union(&paths, limits)?;
466    if paths.is_empty() {
467        if let Some(aoi) = aoi {
468            aoi_paths(aoi, limits)?;
469        }
470        return Ok(Output {
471            method_id: METHOD_ID,
472            coordinate_precision: 1. / PRECISION,
473            coordinates: vec![],
474        });
475    }
476    let mut c = Clipper64::new();
477    c.set_preserve_collinear(false);
478    c.add_subject(&paths);
479    let operation = if let Some(aoi) = aoi {
480        let clips = aoi_paths(aoi, limits)?;
481        let clipped_vertices = vertices
482            .checked_add(count_vertices(&clips)?)
483            .ok_or_else(|| MapError::Budget("AOI contour vertex count overflow".into()))?;
484        let clipped_count = count
485            .checked_add(clips.len())
486            .ok_or_else(|| MapError::Budget("AOI contour ring count overflow".into()))?;
487        limits.admit(clipped_vertices, clipped_count)?;
488        c.add_clip(&clips);
489        ClipType::Intersection
490    } else {
491        ClipType::Union
492    };
493    let mut tree = PolyTree64::new();
494    let mut open = vec![];
495    if !c.execute_tree(operation, FillRule::NonZero, &mut tree, &mut open) {
496        return Err(MapError::Invalid("contour topology assembly failed".into()));
497    }
498    let output_vertices = tree.nodes.iter().try_fold(0usize, |n, p| {
499        n.checked_add(p.polygon().len())
500            .ok_or_else(|| MapError::Budget("contour output vertex count overflow".into()))
501    })?;
502    limits.admit(output_vertices, tree.nodes.len())?;
503    let mut polygons = vec![];
504    for i in 1..tree.nodes.len() {
505        if tree.is_hole(i) {
506            continue;
507        }
508        let node = &tree.nodes[i];
509        let exterior = canonical(node.polygon(), true)?;
510        let mut holes = node
511            .children()
512            .iter()
513            .map(|&j| canonical(tree.nodes[j].polygon(), false))
514            .collect::<Result<Vec<_>>>()?;
515        holes.sort();
516        let mut polygon = vec![exterior];
517        polygon.extend(holes);
518        polygons.push(polygon);
519    }
520    polygons.sort();
521    let coordinates = polygons
522        .into_iter()
523        .map(|p| {
524            p.into_iter()
525                .map(|ring| {
526                    let mut r: Vec<_> = ring
527                        .into_iter()
528                        .map(|p| {
529                            [
530                                p[0] as f64 * (1. / PRECISION),
531                                p[1] as f64 * (1. / PRECISION),
532                            ]
533                        })
534                        .collect();
535                    r.push(r[0]);
536                    r
537                })
538                .collect()
539        })
540        .collect();
541    Ok(Output {
542        method_id: METHOD_ID,
543        coordinate_precision: 1. / PRECISION,
544        coordinates,
545    })
546}