Skip to main content

oxigdal_core/buffer/
mask.rs

1//! Nodata and validity bitmask for raster regions.
2//!
3//! # Bit-packing convention
4//!
5//! Pixels are stored in row-major order: pixel `(x, y)` maps to bit index
6//! `i = y * width + x`.  Each bit index maps to word `data[i / 64]`, bit
7//! position `i % 64` (LSB = bit 0 = leftmost pixel in each group of 64).
8//!
9//! **Masking semantics**: a `1` bit means the pixel is **invalid / nodata**;
10//! a `0` bit means the pixel is **valid**.  This convention matches GDAL's
11//! nodata mask band where non-zero → valid data is the *inverse*, but
12//! follows the more common raster tradition where "masked out" = 1.
13//!
14//! # Tail-word invariant
15//!
16//! When `width * height` is not a multiple of 64 the high bits of the last
17//! word are always kept **zero**.  Every mutating method that could set high
18//! bits calls `Mask::clear_tail_bits` before returning.  This invariant
19//! makes `count_set`, `all_unset`, and `any_set` trivially correct without
20//! any per-call tail masking.
21
22#[cfg(not(feature = "std"))]
23#[allow(unused_imports)]
24use crate::compat::*;
25use crate::error::{OxiGdalError, Result};
26
27/// A 2-D boolean bitmask for a raster region.
28///
29/// `Mask` uses bit-packed `u64` words (64 pixels per word) for memory
30/// efficiency.  See the [module documentation][self] for the storage and
31/// semantic conventions.
32///
33/// # Examples
34///
35/// ```
36/// use oxigdal_core::buffer::Mask;
37///
38/// let mut mask = Mask::new(10, 10);         // all valid (0)
39/// mask.set(3, 3, true);                      // mark (3,3) as nodata
40/// assert!(mask.get(3, 3));
41/// assert_eq!(mask.count_set(), 1);
42/// ```
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Mask {
45    /// Width of the raster region in pixels.
46    width: usize,
47    /// Height of the raster region in pixels.
48    height: usize,
49    /// Bit-packed words.  Bit `i % 64` of `data[i / 64]` holds pixel
50    /// `(i % width, i / width)` where `i = y * width + x`.
51    data: Vec<u64>,
52}
53
54// ─── Internal helpers ──────────────────────────────────────────────────────────
55
56impl Mask {
57    /// Returns the number of `u64` words needed for `pixel_count` pixels.
58    #[inline]
59    fn words_for(pixel_count: usize) -> usize {
60        pixel_count.saturating_add(63) / 64
61    }
62
63    /// Bit-index → (word index, bit position within word).
64    #[inline]
65    fn coords(i: usize) -> (usize, u32) {
66        (i / 64, (i % 64) as u32)
67    }
68
69    /// Clears the unused high bits of the last word so that the tail-word
70    /// invariant holds.  This is a no-op when `width * height` is an exact
71    /// multiple of 64.
72    fn clear_tail_bits(&mut self) {
73        let pixel_count = self.width * self.height;
74        let tail = pixel_count % 64;
75        if tail != 0
76            && let Some(last) = self.data.last_mut()
77        {
78            // Keep only the lower `tail` bits.
79            *last &= (1u64 << tail).wrapping_sub(1);
80        }
81    }
82
83    /// Validates that `other` has the same dimensions as `self`.
84    #[inline]
85    fn check_dims(&self, other: &Mask) -> Result<()> {
86        if self.width != other.width || self.height != other.height {
87            Err(OxiGdalError::InvalidParameter {
88                parameter: "other",
89                message: format!(
90                    "Mask dimension mismatch: self={}×{}, other={}×{}",
91                    self.width, self.height, other.width, other.height
92                ),
93            })
94        } else {
95            Ok(())
96        }
97    }
98}
99
100// ─── Constructors ─────────────────────────────────────────────────────────────
101
102impl Mask {
103    /// Creates a new mask with all bits **unset** (all pixels valid).
104    ///
105    /// A mask with `width == 0` or `height == 0` is valid and has no pixels.
106    #[must_use]
107    pub fn new(width: usize, height: usize) -> Self {
108        let words = Self::words_for(width * height);
109        Self {
110            width,
111            height,
112            data: vec![0u64; words],
113        }
114    }
115
116    /// Creates a new mask with all bits **set** (all pixels invalid/nodata).
117    ///
118    /// The tail-word invariant is enforced — high bits of the last word are
119    /// always zero even after the fill.
120    #[must_use]
121    pub fn new_filled(width: usize, height: usize) -> Self {
122        let mut mask = Self {
123            width,
124            height,
125            data: vec![!0u64; Self::words_for(width * height)],
126        };
127        mask.clear_tail_bits();
128        mask
129    }
130
131    /// Constructs a mask from a `bool` slice.
132    ///
133    /// The slice is interpreted in row-major order: `values[y * width + x]`
134    /// is the mask value for pixel `(x, y)`.  `true` → bit set (nodata).
135    ///
136    /// # Errors
137    ///
138    /// Returns [`OxiGdalError::InvalidParameter`] if `values.len() !=
139    /// width * height`.
140    pub fn from_slice(width: usize, height: usize, values: &[bool]) -> Result<Self> {
141        let pixel_count = width * height;
142        if values.len() != pixel_count {
143            return Err(OxiGdalError::InvalidParameter {
144                parameter: "values",
145                message: format!(
146                    "Slice length {} does not match {}×{} = {} pixels",
147                    values.len(),
148                    width,
149                    height,
150                    pixel_count
151                ),
152            });
153        }
154        let mut mask = Self::new(width, height);
155        for (i, &v) in values.iter().enumerate() {
156            if v {
157                let (word, bit) = Self::coords(i);
158                mask.data[word] |= 1u64 << bit;
159            }
160        }
161        // No tail clearing needed: we only ever set individual bits; the
162        // constructor already initialised unused bits to 0.
163        Ok(mask)
164    }
165}
166
167// ─── Getters / basic accessors ────────────────────────────────────────────────
168
169impl Mask {
170    /// Width of the raster region in pixels.
171    #[must_use]
172    #[inline]
173    pub const fn width(&self) -> usize {
174        self.width
175    }
176
177    /// Height of the raster region in pixels.
178    #[must_use]
179    #[inline]
180    pub const fn height(&self) -> usize {
181        self.height
182    }
183
184    /// Total number of pixels (`width * height`).
185    #[must_use]
186    #[inline]
187    pub fn pixel_count(&self) -> usize {
188        self.width * self.height
189    }
190
191    /// Returns the mask bit for pixel `(x, y)`.
192    ///
193    /// `true` means the pixel is **invalid / nodata**.
194    ///
195    /// # Panics
196    ///
197    /// Panics in debug builds if `x >= width` or `y >= height`.  In release
198    /// builds the access is clamped silently.
199    #[must_use]
200    pub fn get(&self, x: usize, y: usize) -> bool {
201        debug_assert!(
202            x < self.width,
203            "x={} out of bounds (width={})",
204            x,
205            self.width
206        );
207        debug_assert!(
208            y < self.height,
209            "y={} out of bounds (height={})",
210            y,
211            self.height
212        );
213        let i = y * self.width + x;
214        let (word, bit) = Self::coords(i);
215        if word < self.data.len() {
216            (self.data[word] >> bit) & 1 == 1
217        } else {
218            false
219        }
220    }
221
222    /// Sets the mask bit for pixel `(x, y)`.
223    ///
224    /// `value = true` marks the pixel as **invalid / nodata**.
225    ///
226    /// # Panics
227    ///
228    /// Panics in debug builds if `x >= width` or `y >= height`.
229    pub fn set(&mut self, x: usize, y: usize, value: bool) {
230        debug_assert!(
231            x < self.width,
232            "x={} out of bounds (width={})",
233            x,
234            self.width
235        );
236        debug_assert!(
237            y < self.height,
238            "y={} out of bounds (height={})",
239            y,
240            self.height
241        );
242        let i = y * self.width + x;
243        let (word, bit) = Self::coords(i);
244        if word < self.data.len() {
245            if value {
246                self.data[word] |= 1u64 << bit;
247            } else {
248                self.data[word] &= !(1u64 << bit);
249            }
250        }
251    }
252}
253
254// ─── Fill operations ──────────────────────────────────────────────────────────
255
256impl Mask {
257    /// Sets all bits to `value`.
258    ///
259    /// The tail-word invariant is maintained.
260    pub fn fill(&mut self, value: bool) {
261        let fill_word = if value { !0u64 } else { 0u64 };
262        for w in &mut self.data {
263            *w = fill_word;
264        }
265        if value {
266            self.clear_tail_bits();
267        }
268    }
269
270    /// Sets all bits in the rectangle `[x, x+w) × [y, y+h)` to `value`.
271    ///
272    /// Coordinates that fall outside the mask bounds are silently clamped.
273    /// The tail-word invariant is maintained.
274    pub fn fill_rect(&mut self, x: usize, y: usize, w: usize, h: usize, value: bool) {
275        // Clamp rectangle to mask bounds.
276        let x_end = (x + w).min(self.width);
277        let y_end = (y + h).min(self.height);
278        for row in y..y_end {
279            for col in x..x_end {
280                self.set(col, row, value);
281            }
282        }
283        // `set` never touches tail bits beyond pixel bounds, so no extra
284        // clear_tail_bits call is required here.
285    }
286}
287
288// ─── Bitwise operations ───────────────────────────────────────────────────────
289
290impl Mask {
291    /// Applies bitwise AND with `other` in place.
292    ///
293    /// # Errors
294    ///
295    /// Returns [`OxiGdalError::InvalidParameter`] if dimensions differ.
296    pub fn and_assign(&mut self, other: &Mask) -> Result<()> {
297        self.check_dims(other)?;
298        for (a, b) in self.data.iter_mut().zip(other.data.iter()) {
299            *a &= b;
300        }
301        Ok(())
302    }
303
304    /// Applies bitwise OR with `other` in place.
305    ///
306    /// # Errors
307    ///
308    /// Returns [`OxiGdalError::InvalidParameter`] if dimensions differ.
309    pub fn or_assign(&mut self, other: &Mask) -> Result<()> {
310        self.check_dims(other)?;
311        for (a, b) in self.data.iter_mut().zip(other.data.iter()) {
312            *a |= b;
313        }
314        // OR can set tail bits only if `other` has tail bits set, but by our
315        // invariant `other`'s tail bits are 0, so no extra clear needed.
316        Ok(())
317    }
318
319    /// Applies bitwise NOT in place.
320    ///
321    /// The tail-word invariant is re-established after inversion.
322    pub fn not_in_place(&mut self) {
323        for w in &mut self.data {
324            *w = !*w;
325        }
326        self.clear_tail_bits();
327    }
328
329    /// Applies bitwise XOR with `other` in place.
330    ///
331    /// # Errors
332    ///
333    /// Returns [`OxiGdalError::InvalidParameter`] if dimensions differ.
334    pub fn xor_assign(&mut self, other: &Mask) -> Result<()> {
335        self.check_dims(other)?;
336        for (a, b) in self.data.iter_mut().zip(other.data.iter()) {
337            *a ^= b;
338        }
339        // Same argument as or_assign — tail stays zero.
340        Ok(())
341    }
342
343    /// Returns a new mask that is the bitwise AND of `self` and `other`.
344    ///
345    /// # Errors
346    ///
347    /// Returns [`OxiGdalError::InvalidParameter`] if dimensions differ.
348    pub fn and(&self, other: &Mask) -> Result<Mask> {
349        self.check_dims(other)?;
350        let data: Vec<u64> = self
351            .data
352            .iter()
353            .zip(other.data.iter())
354            .map(|(a, b)| a & b)
355            .collect();
356        Ok(Mask {
357            width: self.width,
358            height: self.height,
359            data,
360        })
361    }
362
363    /// Returns a new mask that is the bitwise OR of `self` and `other`.
364    ///
365    /// # Errors
366    ///
367    /// Returns [`OxiGdalError::InvalidParameter`] if dimensions differ.
368    pub fn or(&self, other: &Mask) -> Result<Mask> {
369        self.check_dims(other)?;
370        let data: Vec<u64> = self
371            .data
372            .iter()
373            .zip(other.data.iter())
374            .map(|(a, b)| a | b)
375            .collect();
376        Ok(Mask {
377            width: self.width,
378            height: self.height,
379            data,
380        })
381    }
382
383    /// Returns a new mask that is the bitwise NOT of `self`.
384    ///
385    /// The tail-word invariant is preserved.
386    #[must_use]
387    pub fn not(&self) -> Mask {
388        let mut result = Mask {
389            width: self.width,
390            height: self.height,
391            data: self.data.iter().map(|w| !w).collect(),
392        };
393        result.clear_tail_bits();
394        result
395    }
396}
397
398// ─── Statistics ───────────────────────────────────────────────────────────────
399
400impl Mask {
401    /// Number of bits that are set (pixels marked invalid/nodata).
402    #[must_use]
403    pub fn count_set(&self) -> usize {
404        self.data.iter().map(|w| w.count_ones() as usize).sum()
405    }
406
407    /// Number of bits that are unset (pixels that are valid).
408    #[must_use]
409    pub fn count_unset(&self) -> usize {
410        self.pixel_count() - self.count_set()
411    }
412
413    /// Returns `true` if **all** pixels are masked (all bits set).
414    #[must_use]
415    pub fn all_set(&self) -> bool {
416        self.count_set() == self.pixel_count()
417    }
418
419    /// Returns `true` if **no** pixels are masked (all bits unset).
420    #[must_use]
421    pub fn all_unset(&self) -> bool {
422        self.data.iter().all(|&w| w == 0)
423    }
424
425    /// Returns `true` if at least one pixel is masked.
426    #[must_use]
427    pub fn any_set(&self) -> bool {
428        self.data.iter().any(|&w| w != 0)
429    }
430}
431
432// ─── Iterator over set positions ─────────────────────────────────────────────
433
434impl Mask {
435    /// Returns an iterator over `(x, y)` coordinates of all **set** bits
436    /// (masked/nodata pixels).
437    ///
438    /// Iterates in row-major order.
439    pub fn set_positions(&self) -> impl Iterator<Item = (usize, usize)> + '_ {
440        SetPositions {
441            mask: self,
442            word_idx: 0,
443            current_word: self.data.first().copied().unwrap_or(0),
444            pixel_base: 0,
445        }
446    }
447}
448
449/// Iterator over `(x, y)` positions of set bits.
450struct SetPositions<'a> {
451    mask: &'a Mask,
452    /// Index of the word currently being scanned.
453    word_idx: usize,
454    /// Copy of the current word with already-visited bits cleared.
455    current_word: u64,
456    /// Bit index of word `word_idx` bit 0.
457    pixel_base: usize,
458}
459
460impl<'a> Iterator for SetPositions<'a> {
461    type Item = (usize, usize);
462
463    fn next(&mut self) -> Option<Self::Item> {
464        loop {
465            if self.current_word != 0 {
466                // Find the lowest set bit.
467                let bit = self.current_word.trailing_zeros() as usize;
468                // Clear that bit.
469                self.current_word &= self.current_word - 1;
470                let i = self.pixel_base + bit;
471                if i < self.mask.pixel_count() {
472                    let x = i % self.mask.width;
473                    let y = i / self.mask.width;
474                    return Some((x, y));
475                }
476                // Bit was in the tail padding — skip.
477                continue;
478            }
479            // Advance to the next word.
480            self.word_idx += 1;
481            if self.word_idx >= self.mask.data.len() {
482                return None;
483            }
484            self.pixel_base = self.word_idx * 64;
485            self.current_word = self.mask.data[self.word_idx];
486        }
487    }
488}
489
490// ─── Nodata-based constructors ────────────────────────────────────────────────
491
492impl Mask {
493    /// Builds a nodata mask from an `f32` slice.
494    ///
495    /// A pixel is marked **invalid** if its value is NaN **or** exactly equals
496    /// `nodata` (bitwise-exact comparison after handling NaN).
497    ///
498    /// # Errors
499    ///
500    /// Returns [`OxiGdalError::InvalidParameter`] if `data.len() != width *
501    /// height`.
502    pub fn from_nodata_f32(data: &[f32], width: usize, height: usize, nodata: f32) -> Result<Self> {
503        let pixel_count = width * height;
504        if data.len() != pixel_count {
505            return Err(OxiGdalError::InvalidParameter {
506                parameter: "data",
507                message: format!(
508                    "Slice length {} ≠ {}×{} = {} pixels",
509                    data.len(),
510                    width,
511                    height,
512                    pixel_count
513                ),
514            });
515        }
516        let nodata_is_nan = nodata.is_nan();
517        let mut mask = Self::new(width, height);
518        for (i, &v) in data.iter().enumerate() {
519            let is_nodata = if nodata_is_nan {
520                v.is_nan()
521            } else {
522                v == nodata
523            };
524            if is_nodata {
525                let (word, bit) = Self::coords(i);
526                mask.data[word] |= 1u64 << bit;
527            }
528        }
529        Ok(mask)
530    }
531
532    /// Builds a nodata mask from an `f64` slice.
533    ///
534    /// A pixel is marked **invalid** if its value is NaN **or** exactly equals
535    /// `nodata`.
536    ///
537    /// # Errors
538    ///
539    /// Returns [`OxiGdalError::InvalidParameter`] if `data.len() != width *
540    /// height`.
541    pub fn from_nodata_f64(data: &[f64], width: usize, height: usize, nodata: f64) -> Result<Self> {
542        let pixel_count = width * height;
543        if data.len() != pixel_count {
544            return Err(OxiGdalError::InvalidParameter {
545                parameter: "data",
546                message: format!(
547                    "Slice length {} ≠ {}×{} = {} pixels",
548                    data.len(),
549                    width,
550                    height,
551                    pixel_count
552                ),
553            });
554        }
555        let nodata_is_nan = nodata.is_nan();
556        let mut mask = Self::new(width, height);
557        for (i, &v) in data.iter().enumerate() {
558            let is_nodata = if nodata_is_nan {
559                v.is_nan()
560            } else {
561                v == nodata
562            };
563            if is_nodata {
564                let (word, bit) = Self::coords(i);
565                mask.data[word] |= 1u64 << bit;
566            }
567        }
568        Ok(mask)
569    }
570
571    /// Builds a nodata mask from an `i32` slice.
572    ///
573    /// A pixel is marked **invalid** if its value exactly equals `nodata`.
574    ///
575    /// # Errors
576    ///
577    /// Returns [`OxiGdalError::InvalidParameter`] if `data.len() != width *
578    /// height`.
579    pub fn from_nodata_i32(data: &[i32], width: usize, height: usize, nodata: i32) -> Result<Self> {
580        let pixel_count = width * height;
581        if data.len() != pixel_count {
582            return Err(OxiGdalError::InvalidParameter {
583                parameter: "data",
584                message: format!(
585                    "Slice length {} ≠ {}×{} = {} pixels",
586                    data.len(),
587                    width,
588                    height,
589                    pixel_count
590                ),
591            });
592        }
593        let mut mask = Self::new(width, height);
594        for (i, &v) in data.iter().enumerate() {
595            if v == nodata {
596                let (word, bit) = Self::coords(i);
597                mask.data[word] |= 1u64 << bit;
598            }
599        }
600        Ok(mask)
601    }
602
603    /// Builds a nodata mask from a `u8` slice.
604    ///
605    /// A pixel is marked **invalid** if its value exactly equals `nodata`.
606    ///
607    /// # Errors
608    ///
609    /// Returns [`OxiGdalError::InvalidParameter`] if `data.len() != width *
610    /// height`.
611    pub fn from_nodata_u8(data: &[u8], width: usize, height: usize, nodata: u8) -> Result<Self> {
612        let pixel_count = width * height;
613        if data.len() != pixel_count {
614            return Err(OxiGdalError::InvalidParameter {
615                parameter: "data",
616                message: format!(
617                    "Slice length {} ≠ {}×{} = {} pixels",
618                    data.len(),
619                    width,
620                    height,
621                    pixel_count
622                ),
623            });
624        }
625        let mut mask = Self::new(width, height);
626        for (i, &v) in data.iter().enumerate() {
627            if v == nodata {
628                let (word, bit) = Self::coords(i);
629                mask.data[word] |= 1u64 << bit;
630            }
631        }
632        Ok(mask)
633    }
634}
635
636// ─── Interop ──────────────────────────────────────────────────────────────────
637
638impl Mask {
639    /// Converts the mask to a `Vec<bool>` in row-major order.
640    ///
641    /// The returned vector has length `pixel_count()`.  `true` at index
642    /// `y * width + x` means pixel `(x, y)` is **invalid / nodata**.
643    #[must_use]
644    pub fn to_bool_vec(&self) -> Vec<bool> {
645        let n = self.pixel_count();
646        let mut out = Vec::with_capacity(n);
647        for i in 0..n {
648            let (word, bit) = Self::coords(i);
649            out.push((self.data[word] >> bit) & 1 == 1);
650        }
651        out
652    }
653}
654
655// ─── Apply-to-data methods ────────────────────────────────────────────────────
656
657impl Mask {
658    /// Replaces every masked pixel in `data` with `nodata_value`.
659    ///
660    /// Operates on an `f32` slice of the same shape as the mask.
661    /// Pixels where the mask bit is **set** (invalid) are written with
662    /// `nodata_value`.
663    pub fn apply_to_f32(&self, data: &mut [f32], nodata_value: f32) {
664        let n = self.pixel_count().min(data.len());
665        for (i, elem) in data.iter_mut().enumerate().take(n) {
666            let (word, bit) = Self::coords(i);
667            if word < self.data.len() && (self.data[word] >> bit) & 1 == 1 {
668                *elem = nodata_value;
669            }
670        }
671    }
672
673    /// Replaces every masked pixel in `data` with `nodata_value`.
674    ///
675    /// Operates on an `f64` slice of the same shape as the mask.
676    pub fn apply_to_f64(&self, data: &mut [f64], nodata_value: f64) {
677        let n = self.pixel_count().min(data.len());
678        for (i, elem) in data.iter_mut().enumerate().take(n) {
679            let (word, bit) = Self::coords(i);
680            if word < self.data.len() && (self.data[word] >> bit) & 1 == 1 {
681                *elem = nodata_value;
682            }
683        }
684    }
685
686    /// Replaces every masked pixel in `data` with `nodata_value`.
687    ///
688    /// Operates on a `u8` slice of the same shape as the mask.
689    pub fn apply_to_u8(&self, data: &mut [u8], nodata_value: u8) {
690        let n = self.pixel_count().min(data.len());
691        for (i, elem) in data.iter_mut().enumerate().take(n) {
692            let (word, bit) = Self::coords(i);
693            if word < self.data.len() && (self.data[word] >> bit) & 1 == 1 {
694                *elem = nodata_value;
695            }
696        }
697    }
698
699    /// Replaces every masked pixel in `data` with `nodata_value`.
700    ///
701    /// Operates on an `i32` slice of the same shape as the mask.
702    pub fn apply_to_i32(&self, data: &mut [i32], nodata_value: i32) {
703        let n = self.pixel_count().min(data.len());
704        for (i, elem) in data.iter_mut().enumerate().take(n) {
705            let (word, bit) = Self::coords(i);
706            if word < self.data.len() && (self.data[word] >> bit) & 1 == 1 {
707                *elem = nodata_value;
708            }
709        }
710    }
711}
712
713// ─── Tests ────────────────────────────────────────────────────────────────────
714
715#[cfg(test)]
716mod tests {
717    #![allow(clippy::expect_used)]
718
719    use super::*;
720
721    // ── Basic construction ─────────────────────────────────────────────────
722
723    #[test]
724    fn test_mask_basic_set_get() {
725        let mut mask = Mask::new(8, 8);
726        // All zero initially.
727        for y in 0..8 {
728            for x in 0..8 {
729                assert!(!mask.get(x, y), "({x},{y}) should be unset");
730            }
731        }
732        mask.set(0, 0, true);
733        mask.set(7, 7, true);
734        mask.set(3, 5, true);
735        assert!(mask.get(0, 0));
736        assert!(mask.get(7, 7));
737        assert!(mask.get(3, 5));
738        assert!(!mask.get(1, 1));
739        assert_eq!(mask.count_set(), 3);
740    }
741
742    #[test]
743    fn test_mask_fill_true() {
744        let mut mask = Mask::new(10, 10);
745        mask.fill(true);
746        assert_eq!(mask.count_set(), 100);
747        assert_eq!(mask.count_unset(), 0);
748    }
749
750    #[test]
751    fn test_mask_fill_false() {
752        let mut mask = Mask::new_filled(5, 5);
753        mask.fill(false);
754        assert_eq!(mask.count_set(), 0);
755        assert!(mask.all_unset());
756    }
757
758    #[test]
759    fn test_mask_fill_rect() {
760        let mut mask = Mask::new(10, 10);
761        // Fill the 3×3 block starting at (2,2).
762        mask.fill_rect(2, 2, 3, 3, true);
763        assert_eq!(mask.count_set(), 9);
764        // Verify interior.
765        for dy in 0..3 {
766            for dx in 0..3 {
767                assert!(mask.get(2 + dx, 2 + dy));
768            }
769        }
770        // Verify boundary untouched.
771        assert!(!mask.get(1, 2));
772        assert!(!mask.get(5, 5));
773    }
774
775    // ── Bitwise operations ─────────────────────────────────────────────────
776
777    #[test]
778    fn test_mask_and_or_not() {
779        let mut a = Mask::new(4, 4); // all 0
780        a.set(0, 0, true);
781        a.set(1, 1, true);
782
783        let mut b = Mask::new(4, 4); // all 0
784        b.set(1, 1, true);
785        b.set(2, 2, true);
786
787        // AND: only (1,1) common.
788        let and = a.and(&b).expect("and should succeed");
789        assert_eq!(and.count_set(), 1);
790        assert!(and.get(1, 1));
791
792        // OR: (0,0),(1,1),(2,2) = 3 pixels.
793        let or = a.or(&b).expect("or should succeed");
794        assert_eq!(or.count_set(), 3);
795
796        // NOT of a: 16 - 2 = 14 set.
797        let not_a = a.not();
798        assert_eq!(not_a.count_set(), 14);
799        assert!(!not_a.get(0, 0));
800        assert!(!not_a.get(1, 1));
801        assert!(not_a.get(0, 1));
802    }
803
804    #[test]
805    fn test_mask_dimension_mismatch_err() {
806        let mut a = Mask::new(4, 4);
807        let b = Mask::new(4, 5);
808        assert!(a.and_assign(&b).is_err());
809        assert!(a.or_assign(&b).is_err());
810        assert!(a.xor_assign(&b).is_err());
811        assert!(a.and(&b).is_err());
812        assert!(a.or(&b).is_err());
813    }
814
815    // ── from_slice roundtrip ───────────────────────────────────────────────
816
817    #[test]
818    fn test_mask_from_slice_roundtrip() {
819        let values: Vec<bool> = (0..20usize).map(|i| i % 3 == 0).collect();
820        let mask = Mask::from_slice(4, 5, &values).expect("from_slice");
821        let back = mask.to_bool_vec();
822        assert_eq!(back, values);
823    }
824
825    #[test]
826    fn test_mask_from_slice_length_err() {
827        // Wrong length should error.
828        assert!(Mask::from_slice(4, 4, &[true; 10]).is_err());
829    }
830
831    // ── Nodata constructors ────────────────────────────────────────────────
832
833    #[test]
834    fn test_mask_from_nodata_f32() {
835        let nodata = -9999.0f32;
836        let data = vec![1.0f32, nodata, f32::NAN, 3.0, nodata];
837        let mask = Mask::from_nodata_f32(&data, 5, 1, nodata).expect("from_nodata_f32");
838        // index 1 and 4 = exact nodata, index 2 = NaN (only for NaN nodata)
839        // nodata is -9999 (not NaN), so NaN at index 2 is NOT marked.
840        assert!(!mask.get(0, 0));
841        assert!(mask.get(1, 0));
842        assert!(!mask.get(2, 0)); // NaN but nodata != NaN → not masked
843        assert!(!mask.get(3, 0));
844        assert!(mask.get(4, 0));
845        assert_eq!(mask.count_set(), 2);
846    }
847
848    #[test]
849    fn test_mask_from_nodata_f32_nan_nodata() {
850        // When nodata itself is NaN, NaN pixels are masked; non-NaN are not.
851        let data = vec![1.0f32, f32::NAN, 2.0, f32::NAN];
852        let mask = Mask::from_nodata_f32(&data, 4, 1, f32::NAN).expect("from_nodata_f32 NaN");
853        assert!(!mask.get(0, 0));
854        assert!(mask.get(1, 0));
855        assert!(!mask.get(2, 0));
856        assert!(mask.get(3, 0));
857    }
858
859    #[test]
860    fn test_mask_from_nodata_u8_zero() {
861        let data: Vec<u8> = vec![0, 1, 0, 5, 0];
862        let mask = Mask::from_nodata_u8(&data, 5, 1, 0).expect("from_nodata_u8");
863        assert!(mask.get(0, 0));
864        assert!(!mask.get(1, 0));
865        assert!(mask.get(2, 0));
866        assert!(!mask.get(3, 0));
867        assert!(mask.get(4, 0));
868        assert_eq!(mask.count_set(), 3);
869    }
870
871    // ── apply_to_* ─────────────────────────────────────────────────────────
872
873    #[test]
874    fn test_mask_apply_to_f32() {
875        let mut mask = Mask::new(4, 1);
876        mask.set(1, 0, true);
877        mask.set(3, 0, true);
878        let mut data = vec![1.0f32, 2.0, 3.0, 4.0];
879        mask.apply_to_f32(&mut data, -9999.0);
880        assert!((data[0] - 1.0).abs() < 1e-7);
881        assert!((data[1] - (-9999.0)).abs() < 1e-1);
882        assert!((data[2] - 3.0).abs() < 1e-7);
883        assert!((data[3] - (-9999.0)).abs() < 1e-1);
884    }
885
886    #[test]
887    fn test_mask_apply_to_u8() {
888        let mut mask = Mask::new(3, 1);
889        mask.set(0, 0, true);
890        mask.set(2, 0, true);
891        let mut data: Vec<u8> = vec![10, 20, 30];
892        mask.apply_to_u8(&mut data, 0);
893        assert_eq!(data, vec![0, 20, 0]);
894    }
895
896    // ── count_set / popcount ────────────────────────────────────────────────
897
898    #[test]
899    fn test_mask_count_set_checkerboard() {
900        // 8×8 checkerboard: even bit indices set.
901        let values: Vec<bool> = (0..64usize).map(|i| i % 2 == 0).collect();
902        let mask = Mask::from_slice(8, 8, &values).expect("from_slice");
903        assert_eq!(mask.count_set(), 32);
904        assert_eq!(mask.count_unset(), 32);
905    }
906
907    // ── set_positions iterator ─────────────────────────────────────────────
908
909    #[test]
910    fn test_mask_set_positions_iterator() {
911        let mut mask = Mask::new(5, 5);
912        let positions = [(1usize, 0usize), (4, 2), (0, 4)];
913        for &(x, y) in &positions {
914            mask.set(x, y, true);
915        }
916        let mut collected: Vec<(usize, usize)> = mask.set_positions().collect();
917        collected.sort_unstable();
918        let mut expected = positions.to_vec();
919        expected.sort_unstable();
920        assert_eq!(collected, expected);
921    }
922
923    // ── all_set / all_unset edge cases ─────────────────────────────────────
924
925    #[test]
926    fn test_mask_all_set_all_unset() {
927        let empty = Mask::new(0, 0);
928        // Zero-pixel mask: both are vacuously true.
929        assert!(empty.all_set());
930        assert!(empty.all_unset());
931
932        let zeros = Mask::new(10, 10);
933        assert!(zeros.all_unset());
934        assert!(!zeros.all_set());
935
936        let ones = Mask::new_filled(10, 10);
937        assert!(ones.all_set());
938        assert!(!ones.all_unset());
939    }
940
941    // ── Word-boundary correctness ──────────────────────────────────────────
942
943    #[test]
944    fn test_mask_large_width_crosses_word_boundary() {
945        // width=65 means pixel (64, 0) is in word 1, bit 0.
946        let mut mask = Mask::new(65, 1);
947        mask.set(63, 0, true); // last bit of word 0
948        mask.set(64, 0, true); // first bit of word 1
949        assert!(mask.get(63, 0));
950        assert!(mask.get(64, 0));
951        assert!(!mask.get(0, 0));
952        assert_eq!(mask.count_set(), 2);
953    }
954
955    // ── Single-pixel edge case ─────────────────────────────────────────────
956
957    #[test]
958    fn test_mask_single_pixel() {
959        let mut mask = Mask::new(1, 1);
960        assert!(!mask.get(0, 0));
961        assert!(mask.all_unset());
962        mask.set(0, 0, true);
963        assert!(mask.get(0, 0));
964        assert!(mask.all_set());
965        assert_eq!(mask.count_set(), 1);
966        mask.not_in_place();
967        assert!(!mask.get(0, 0));
968        assert!(mask.all_unset());
969    }
970
971    // ── XOR ────────────────────────────────────────────────────────────────
972
973    #[test]
974    fn test_mask_xor_self_is_zero() {
975        let mut mask = Mask::new(6, 6);
976        mask.fill_rect(0, 0, 3, 3, true);
977        let clone = mask.clone();
978        mask.xor_assign(&clone).expect("xor_assign");
979        assert!(mask.all_unset());
980    }
981
982    // ── new_filled tail-word invariant ─────────────────────────────────────
983
984    #[test]
985    fn test_mask_new_filled_all_set() {
986        // Non-multiple-of-64 dimensions.
987        let mask = Mask::new_filled(9, 7); // 63 pixels
988        assert_eq!(mask.pixel_count(), 63);
989        assert_eq!(mask.count_set(), 63);
990        assert!(mask.all_set());
991    }
992
993    #[test]
994    fn test_mask_not_in_place_tail_invariant() {
995        let mut mask = Mask::new(9, 7); // 63 pixels
996        mask.not_in_place(); // should give all-set
997        assert_eq!(mask.count_set(), 63);
998        mask.not_in_place(); // back to all-unset
999        assert_eq!(mask.count_set(), 0);
1000    }
1001
1002    // ── or_assign idempotent ───────────────────────────────────────────────
1003
1004    #[test]
1005    fn test_mask_or_idempotent() {
1006        let mut a = Mask::new(4, 4);
1007        a.fill_rect(0, 0, 2, 2, true);
1008        let b = a.clone();
1009        a.or_assign(&b).expect("or_assign");
1010        assert_eq!(a.count_set(), 4);
1011    }
1012
1013    // ── to_bool_vec ─────────────────────────────────────────────────────────
1014
1015    #[test]
1016    fn test_mask_to_bool_vec_roundtrip() {
1017        let mask = Mask::new_filled(5, 3);
1018        let bv = mask.to_bool_vec();
1019        assert_eq!(bv.len(), 15);
1020        assert!(bv.iter().all(|&b| b));
1021    }
1022}