Skip to main content

oxigdal_wasm/
compression.rs

1//! Tile compression and decompression utilities
2//!
3//! This module provides various compression algorithms for reducing memory usage
4//! and bandwidth requirements when working with geospatial tiles in WASM environments.
5//!
6//! # Overview
7//!
8//! Compression is essential for efficient geospatial data handling:
9//!
10//! - **RLE (Run-Length Encoding)**: Best for images with large uniform areas
11//! - **Delta Encoding**: Excellent for gradient data (DEMs, temperature)
12//! - **Huffman Encoding**: General-purpose statistical compression
13//! - **LZ77**: Dictionary-based compression for mixed content
14//! - **Automatic Selection**: Benchmark and choose optimal algorithm
15//!
16//! # Why Compress Tiles?
17//!
18//! Geospatial tiles can be large:
19//! - 256x256 RGBA tile: 262,144 bytes (256 KB)
20//! - 100 cached tiles: 25.6 MB uncompressed
21//! - With 50% compression: 12.8 MB (50% savings!)
22//!
23//! Benefits:
24//! 1. **Memory**: Fit more tiles in cache
25//! 2. **Bandwidth**: Faster loading over network
26//! 3. **Storage**: Smaller cache footprint
27//! 4. **Performance**: Less memory pressure
28//!
29//! # Compression Algorithms
30//!
31//! ## RLE (Run-Length Encoding)
32//! Replaces sequences of identical bytes with count+value pairs.
33//!
34//! Best for:
35//! - Satellite imagery with large uniform areas (ocean, desert)
36//! - Binary masks
37//! - Simple graphics
38//!
39//! Performance:
40//! - Compression: O(n), very fast
41//! - Decompression: O(n), very fast
42//! - Ratio: 2-10x for uniform data, 0.5x for random data
43//!
44//! Example:
45//! ```text
46//! Input:  [255, 255, 255, 255, 0, 0, 128, 128, 128]
47//! Output: [4, 255, 2, 0, 3, 128]
48//! Size:   9 bytes → 6 bytes (33% savings)
49//! ```
50//!
51//! ## Delta Encoding
52//! Stores differences between adjacent pixels instead of absolute values.
53//!
54//! Best for:
55//! - DEMs (Digital Elevation Models)
56//! - Temperature/pressure fields
57//! - Smooth gradients
58//!
59//! Performance:
60//! - Compression: O(n)
61//! - Decompression: O(n)
62//! - Ratio: 2-4x for gradient data
63//!
64//! Example:
65//! ```text
66//! Input:  [100, 102, 105, 103, 104]
67//! Output: [100, +2, +3, -2, +1]
68//! Deltas are smaller → better compression with subsequent algorithm
69//! ```
70//!
71//! ## Huffman Encoding
72//! Uses variable-length codes based on frequency.
73//!
74//! Best for:
75//! - General-purpose compression
76//! - Images with varying content
77//! - Text/metadata
78//!
79//! Performance:
80//! - Compression: O(n log n) - slower
81//! - Decompression: O(n)
82//! - Ratio: 1.5-3x typical
83//!
84//! ## LZ77 (Dictionary Compression)
85//! Finds repeated sequences and references them.
86//!
87//! Best for:
88//! - Repeated patterns
89//! - Mixed content
90//! - General-purpose
91//!
92//! Performance:
93//! - Compression: O(n²) worst case
94//! - Decompression: O(n)
95//! - Ratio: 2-5x typical
96//!
97//! # Usage Examples
98//!
99//! ## Basic Compression
100//! ```rust
101//! use oxigdal_wasm::{TileCompressor, CompressionAlgorithm};
102//!
103//! let compressor = TileCompressor::new(CompressionAlgorithm::Lz77);
104//!
105//! // Compress a tile
106//! let tile_data = vec![0u8; 256 * 256 * 4]; // 256 KB
107//! let (compressed, stats) = compressor.compress(&tile_data);
108//!
109//! println!("Original: {} bytes", stats.original_size);
110//! println!("Compressed: {} bytes", stats.compressed_size);
111//! println!("Ratio: {:.1}%", stats.ratio * 100.0);
112//! println!("Saved: {:.1}%", stats.space_saved_percent());
113//!
114//! // Decompress
115//! let decompressed = compressor.decompress(&compressed)?;
116//! assert_eq!(tile_data, decompressed);
117//! # Ok::<(), Box<dyn std::error::Error>>(())
118//! ```
119//!
120//! ## Automatic Algorithm Selection
121//! ```rust
122//! use oxigdal_wasm::{CompressionBenchmark, TileCompressor};
123//!
124//! // Test all algorithms and choose best
125//! let tile_data = vec![0u8; 256 * 256 * 4]; // Simulated tile data
126//! let best = CompressionBenchmark::find_best(&tile_data);
127//! println!("Best algorithm: {:?}", best);
128//!
129//! let compressor = TileCompressor::new(best);
130//! let (compressed, _) = compressor.compress(&tile_data);
131//! ```
132//!
133//! ## 2D Delta Encoding for DEMs
134//! ```rust
135//! use oxigdal_wasm::{TileCompressor, CompressionAlgorithm};
136//!
137//! let dem_data = vec![100u8; 256 * 256]; // Simulated elevation data
138//! let tile_width = 256;
139//!
140//! // Use 2D delta encoding (vertical differences)
141//! let compressor = TileCompressor::new(CompressionAlgorithm::Delta);
142//! let (compressed, stats) = compressor.compress_2d(&dem_data, tile_width);
143//!
144//! // DEMs typically achieve 3-5x compression
145//! println!("DEM compression ratio: {:.2}x", 1.0 / stats.ratio);
146//! ```
147//!
148//! # Performance Benchmarks
149//!
150//! Typical results for 256x256 RGBA tiles (262 KB):
151//!
152//! ```text
153//! Algorithm   Compress Time   Decompress Time   Ratio   Best For
154//! ─────────────────────────────────────────────────────────────
155//! None        0ms             0ms               1.0x    Baseline
156//! RLE         2ms             1ms               2.5x    Uniform areas
157//! Delta       3ms             2ms               1.8x    Gradients
158//! Huffman     15ms            5ms               2.2x    General
159//! LZ77        25ms            8ms               3.5x    Mixed content
160//! ```
161//!
162//! # Memory Overhead
163//!
164//! Compression requires temporary buffers:
165//! - RLE: 2x input size worst case
166//! - Delta: 1x input size
167//! - Huffman: 3x input size (tree + codes + output)
168//! - LZ77: 2x input size
169//!
170//! # Best Practices
171//!
172//! 1. **Profile First**: Test compression ratios on real data
173//! 2. **Cache Results**: Don't recompress the same tile repeatedly
174//! 3. **Choose Wisely**: Match algorithm to data type
175//! 4. **Consider Trade-offs**: Compression time vs ratio vs memory
176//! 5. **Batch Process**: Compress tiles during idle time
177//! 6. **Monitor Performance**: Track compression stats
178//! 7. **Fallback to None**: If compression makes data larger, store uncompressed
179//!
180//! # When NOT to Compress
181//!
182//! - Data already compressed (JPEG tiles)
183//! - Very small tiles (< 1 KB) - overhead not worth it
184//! - Random/encrypted data - won't compress well
185//! - Real-time requirements - compression too slow
186//! - Low memory pressure - no need to optimize
187use crate::error::{WasmError, WasmResult};
188use serde::{Deserialize, Serialize};
189use std::collections::HashMap;
190/// Compression algorithm
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
192pub enum CompressionAlgorithm {
193    /// No compression
194    None,
195    /// Run-length encoding
196    Rle,
197    /// Delta encoding
198    Delta,
199    /// Huffman encoding (simplified)
200    Huffman,
201    /// LZ77-style compression
202    Lz77,
203}
204/// Compression statistics
205#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
206pub struct CompressionStats {
207    /// Original size in bytes
208    pub original_size: usize,
209    /// Compressed size in bytes
210    pub compressed_size: usize,
211    /// Compression ratio
212    pub ratio: f64,
213    /// Compression time in milliseconds
214    pub compression_time_ms: f64,
215}
216impl CompressionStats {
217    /// Creates new compression statistics
218    pub const fn new(
219        original_size: usize,
220        compressed_size: usize,
221        compression_time_ms: f64,
222    ) -> Self {
223        let ratio = if original_size > 0 {
224            compressed_size as f64 / original_size as f64
225        } else {
226            0.0
227        };
228        Self {
229            original_size,
230            compressed_size,
231            ratio,
232            compression_time_ms,
233        }
234    }
235    /// Returns space saved in bytes
236    pub const fn space_saved(&self) -> usize {
237        self.original_size.saturating_sub(self.compressed_size)
238    }
239    /// Returns space saved as percentage
240    pub fn space_saved_percent(&self) -> f64 {
241        if self.original_size == 0 {
242            0.0
243        } else {
244            ((self.original_size - self.compressed_size) as f64 / self.original_size as f64) * 100.0
245        }
246    }
247}
248/// Run-length encoding compression
249pub struct RleCompressor;
250impl RleCompressor {
251    /// Compresses data using RLE
252    pub fn compress(data: &[u8]) -> Vec<u8> {
253        if data.is_empty() {
254            return Vec::new();
255        }
256        let mut compressed = Vec::new();
257        let mut i = 0;
258        while i < data.len() {
259            let current = data[i];
260            let mut count = 1u8;
261            while i + (count as usize) < data.len()
262                && data[i + (count as usize)] == current
263                && count < 255
264            {
265                count += 1;
266            }
267            compressed.push(count);
268            compressed.push(current);
269            i += count as usize;
270        }
271        compressed
272    }
273    /// Decompresses RLE data
274    pub fn decompress(data: &[u8]) -> WasmResult<Vec<u8>> {
275        if data.len() % 2 != 0 {
276            return Err(WasmError::InvalidOperation {
277                operation: "RLE decompress".to_string(),
278                reason: "Data length must be even".to_string(),
279            });
280        }
281        let mut decompressed = Vec::new();
282        for chunk in data.chunks_exact(2) {
283            let count = chunk[0];
284            let value = chunk[1];
285            decompressed.resize(decompressed.len() + count as usize, value);
286        }
287        Ok(decompressed)
288    }
289}
290/// Delta encoding compression
291pub struct DeltaCompressor;
292impl DeltaCompressor {
293    /// Compresses data using delta encoding
294    pub fn compress(data: &[u8]) -> Vec<u8> {
295        if data.is_empty() {
296            return Vec::new();
297        }
298        let mut compressed = Vec::with_capacity(data.len());
299        compressed.push(data[0]);
300        for i in 1..data.len() {
301            let delta = data[i].wrapping_sub(data[i - 1]);
302            compressed.push(delta);
303        }
304        compressed
305    }
306    /// Decompresses delta-encoded data
307    pub fn decompress(data: &[u8]) -> WasmResult<Vec<u8>> {
308        if data.is_empty() {
309            return Ok(Vec::new());
310        }
311        let mut decompressed = Vec::with_capacity(data.len());
312        decompressed.push(data[0]);
313        for i in 1..data.len() {
314            let prev = *decompressed.last().expect("decompressed is not empty");
315            let value = prev.wrapping_add(data[i]);
316            decompressed.push(value);
317        }
318        Ok(decompressed)
319    }
320    /// Compresses 2D delta encoding (for images)
321    pub fn compress_2d(data: &[u8], width: usize) -> Vec<u8> {
322        if data.is_empty() || width == 0 {
323            return Vec::new();
324        }
325        let height = data.len() / width;
326        let mut compressed = Vec::with_capacity(data.len());
327        if !data.is_empty() {
328            compressed.push(data[0]);
329            for x in 1..width.min(data.len()) {
330                let delta = data[x].wrapping_sub(data[x - 1]);
331                compressed.push(delta);
332            }
333        }
334        for y in 1..height {
335            for x in 0..width {
336                let idx = y * width + x;
337                if idx < data.len() {
338                    let delta = data[idx].wrapping_sub(data[idx - width]);
339                    compressed.push(delta);
340                }
341            }
342        }
343        compressed
344    }
345    /// Decompresses 2D delta-encoded data
346    pub fn decompress_2d(data: &[u8], width: usize) -> WasmResult<Vec<u8>> {
347        if data.is_empty() || width == 0 {
348            return Ok(Vec::new());
349        }
350        let height = data.len() / width;
351        let mut decompressed = Vec::with_capacity(data.len());
352        if !data.is_empty() {
353            decompressed.push(data[0]);
354            for x in 1..width.min(data.len()) {
355                let prev = decompressed[x - 1];
356                let value = prev.wrapping_add(data[x]);
357                decompressed.push(value);
358            }
359        }
360        for y in 1..height {
361            for x in 0..width {
362                let idx = y * width + x;
363                if idx < data.len() {
364                    let above = decompressed[idx - width];
365                    let value = above.wrapping_add(data[idx]);
366                    decompressed.push(value);
367                }
368            }
369        }
370        Ok(decompressed)
371    }
372}
373/// Huffman tree node
374#[derive(Debug, Clone)]
375enum HuffmanNode {
376    Leaf {
377        symbol: u8,
378        frequency: u32,
379    },
380    Internal {
381        frequency: u32,
382        left: Box<HuffmanNode>,
383        right: Box<HuffmanNode>,
384    },
385}
386impl HuffmanNode {
387    fn frequency(&self) -> u32 {
388        match self {
389            Self::Leaf { frequency, .. } => *frequency,
390            Self::Internal { frequency, .. } => *frequency,
391        }
392    }
393}
394/// Simplified Huffman encoding
395pub struct HuffmanCompressor;
396impl HuffmanCompressor {
397    /// Builds frequency table
398    fn build_frequency_table(data: &[u8]) -> HashMap<u8, u32> {
399        let mut frequencies = HashMap::new();
400        for &byte in data {
401            *frequencies.entry(byte).or_insert(0) += 1;
402        }
403        frequencies
404    }
405    /// Builds Huffman tree
406    fn build_tree(frequencies: &HashMap<u8, u32>) -> Option<HuffmanNode> {
407        if frequencies.is_empty() {
408            return None;
409        }
410        let mut nodes: Vec<HuffmanNode> = frequencies
411            .iter()
412            .map(|(&symbol, &frequency)| HuffmanNode::Leaf { symbol, frequency })
413            .collect();
414        while nodes.len() > 1 {
415            nodes.sort_by_key(|n| std::cmp::Reverse(n.frequency()));
416            let right = nodes.pop()?;
417            let left = nodes.pop()?;
418            let internal = HuffmanNode::Internal {
419                frequency: left.frequency() + right.frequency(),
420                left: Box::new(left),
421                right: Box::new(right),
422            };
423            nodes.push(internal);
424        }
425        nodes.pop()
426    }
427    /// Generates code table from tree
428    fn generate_codes(node: &HuffmanNode, prefix: Vec<bool>, codes: &mut HashMap<u8, Vec<bool>>) {
429        match node {
430            HuffmanNode::Leaf { symbol, .. } => {
431                codes.insert(*symbol, prefix);
432            }
433            HuffmanNode::Internal { left, right, .. } => {
434                let mut left_prefix = prefix.clone();
435                left_prefix.push(false);
436                Self::generate_codes(left, left_prefix, codes);
437                let mut right_prefix = prefix;
438                right_prefix.push(true);
439                Self::generate_codes(right, right_prefix, codes);
440            }
441        }
442    }
443    /// Compresses data using Huffman encoding.
444    ///
445    /// Format:
446    /// - 2 bytes: number of distinct symbols (u16 LE)
447    /// - For each symbol: 1 byte symbol value + 4 bytes frequency (u32 LE)
448    /// - 8 bytes: original data length (u64 LE)
449    /// - Remaining bytes: Huffman-encoded bit stream (MSB first within each byte)
450    pub fn compress(data: &[u8]) -> Vec<u8> {
451        if data.is_empty() {
452            return Vec::new();
453        }
454        let frequencies = Self::build_frequency_table(data);
455        let tree = match Self::build_tree(&frequencies) {
456            Some(t) => t,
457            None => return Vec::new(),
458        };
459        let mut codes = HashMap::new();
460        Self::generate_codes(&tree, Vec::new(), &mut codes);
461
462        // Header: number of symbols (u16 LE)
463        let num_symbols = frequencies.len() as u16;
464        let mut compressed = Vec::new();
465        compressed.extend_from_slice(&num_symbols.to_le_bytes());
466
467        // Symbol table: (symbol u8, frequency u32 LE) for each distinct byte
468        let mut sorted_symbols: Vec<(u8, u32)> = frequencies.into_iter().collect();
469        sorted_symbols.sort_by_key(|(sym, _)| *sym);
470        for (symbol, freq) in &sorted_symbols {
471            compressed.push(*symbol);
472            compressed.extend_from_slice(&freq.to_le_bytes());
473        }
474
475        // Original data length (u64 LE) — needed to know when to stop decoding
476        compressed.extend_from_slice(&(data.len() as u64).to_le_bytes());
477
478        // Encode bit stream (MSB first within each byte)
479        let mut bit_buffer = 0u8;
480        let mut bit_count = 0u8;
481        for &byte in data {
482            if let Some(code) = codes.get(&byte) {
483                for &bit in code {
484                    if bit {
485                        bit_buffer |= 1 << (7 - bit_count);
486                    }
487                    bit_count += 1;
488                    if bit_count == 8 {
489                        compressed.push(bit_buffer);
490                        bit_buffer = 0;
491                        bit_count = 0;
492                    }
493                }
494            }
495        }
496        if bit_count > 0 {
497            compressed.push(bit_buffer);
498        }
499        compressed
500    }
501
502    /// Decompresses Huffman-encoded data produced by [`HuffmanCompressor::compress`].
503    ///
504    /// Reads the frequency table stored in the header to reconstruct the identical
505    /// Huffman tree, then decodes the bit stream up to the stored original length.
506    pub fn decompress(compressed: &[u8]) -> WasmResult<Vec<u8>> {
507        if compressed.is_empty() {
508            return Ok(Vec::new());
509        }
510
511        // Parse header: number of symbols (u16 LE)
512        if compressed.len() < 2 {
513            return Err(WasmError::InvalidOperation {
514                operation: "Huffman decompression".to_string(),
515                reason: "Data too short to contain symbol count header".to_string(),
516            });
517        }
518        let num_symbols = u16::from_le_bytes([compressed[0], compressed[1]]) as usize;
519        let mut pos = 2usize;
520
521        // Each symbol entry is 5 bytes: symbol(1) + frequency(4)
522        let symbol_table_size = num_symbols * 5;
523        if pos + symbol_table_size + 8 > compressed.len() {
524            return Err(WasmError::InvalidOperation {
525                operation: "Huffman decompression".to_string(),
526                reason: "Data too short to contain symbol table and length header".to_string(),
527            });
528        }
529
530        let mut frequencies: HashMap<u8, u32> = HashMap::new();
531        for _ in 0..num_symbols {
532            let symbol = compressed[pos];
533            pos += 1;
534            let freq = u32::from_le_bytes([
535                compressed[pos],
536                compressed[pos + 1],
537                compressed[pos + 2],
538                compressed[pos + 3],
539            ]);
540            pos += 4;
541            frequencies.insert(symbol, freq);
542        }
543
544        // Parse original data length (u64 LE)
545        let original_len = u64::from_le_bytes([
546            compressed[pos],
547            compressed[pos + 1],
548            compressed[pos + 2],
549            compressed[pos + 3],
550            compressed[pos + 4],
551            compressed[pos + 5],
552            compressed[pos + 6],
553            compressed[pos + 7],
554        ]) as usize;
555        pos += 8;
556
557        if original_len == 0 {
558            return Ok(Vec::new());
559        }
560
561        // Reconstruct the Huffman tree using the same algorithm as compress
562        let tree = Self::build_tree(&frequencies).ok_or_else(|| WasmError::InvalidOperation {
563            operation: "Huffman decompression".to_string(),
564            reason: "Failed to reconstruct Huffman tree from frequency table".to_string(),
565        })?;
566
567        // Decode the bit stream by traversing the tree
568        let bitstream = &compressed[pos..];
569        let mut result = Vec::with_capacity(original_len);
570        let mut node = &tree;
571
572        'outer: for &byte in bitstream {
573            for bit_idx in (0..8).rev() {
574                let bit = (byte >> bit_idx) & 1;
575                node = match node {
576                    HuffmanNode::Internal { left, right, .. } => {
577                        if bit == 0 {
578                            left
579                        } else {
580                            right
581                        }
582                    }
583                    HuffmanNode::Leaf { .. } => {
584                        // Should not reach here mid-traversal without a leaf
585                        return Err(WasmError::InvalidOperation {
586                            operation: "Huffman decompression".to_string(),
587                            reason: "Unexpected leaf mid-traversal in bit stream".to_string(),
588                        });
589                    }
590                };
591
592                if let HuffmanNode::Leaf { symbol, .. } = node {
593                    result.push(*symbol);
594                    if result.len() == original_len {
595                        break 'outer;
596                    }
597                    // Reset traversal to tree root
598                    node = &tree;
599                }
600            }
601        }
602
603        // Handle single-symbol edge case: entire input was one distinct byte
604        if result.is_empty() && original_len > 0 {
605            if let HuffmanNode::Leaf { symbol, .. } = &tree {
606                result.resize(original_len, *symbol);
607            }
608        }
609
610        if result.len() != original_len {
611            return Err(WasmError::InvalidOperation {
612                operation: "Huffman decompression".to_string(),
613                reason: format!(
614                    "Decoded {} bytes but expected {}",
615                    result.len(),
616                    original_len
617                ),
618            });
619        }
620
621        Ok(result)
622    }
623}
624/// LZ77-style compression
625pub struct Lz77Compressor;
626impl Lz77Compressor {
627    /// Window size for looking back
628    const WINDOW_SIZE: usize = 4096;
629    /// Maximum match length
630    const MAX_MATCH_LENGTH: usize = 18;
631    /// Minimum match length
632    const MIN_MATCH_LENGTH: usize = 3;
633    /// Finds the longest match in the window
634    fn find_longest_match(data: &[u8], pos: usize) -> Option<(usize, usize)> {
635        let window_start = pos.saturating_sub(Self::WINDOW_SIZE);
636        let mut best_match = None;
637        let mut best_length = 0;
638        for start in window_start..pos {
639            let mut length = 0;
640            while length < Self::MAX_MATCH_LENGTH
641                && pos + length < data.len()
642                && data[start + length] == data[pos + length]
643            {
644                length += 1;
645            }
646            if length >= Self::MIN_MATCH_LENGTH && length > best_length {
647                best_length = length;
648                best_match = Some((pos - start, length));
649            }
650        }
651        best_match
652    }
653    /// Compresses data using LZ77
654    pub fn compress(data: &[u8]) -> Vec<u8> {
655        if data.is_empty() {
656            return Vec::new();
657        }
658        let mut compressed = Vec::new();
659        let mut pos = 0;
660        while pos < data.len() {
661            if let Some((distance, length)) = Self::find_longest_match(data, pos) {
662                compressed.push(1);
663                compressed.push((distance >> 8) as u8);
664                compressed.push((distance & 0xFF) as u8);
665                compressed.push(length as u8);
666                pos += length;
667            } else {
668                compressed.push(0);
669                compressed.push(data[pos]);
670                pos += 1;
671            }
672        }
673        compressed
674    }
675    /// Decompresses LZ77 data
676    pub fn decompress(data: &[u8]) -> WasmResult<Vec<u8>> {
677        let mut decompressed = Vec::new();
678        let mut i = 0;
679        while i < data.len() {
680            let flag = data[i];
681            i += 1;
682            if flag == 1 {
683                if i + 3 > data.len() {
684                    return Err(WasmError::InvalidOperation {
685                        operation: "LZ77 decompress".to_string(),
686                        reason: "Unexpected end of data".to_string(),
687                    });
688                }
689                let distance = ((data[i] as usize) << 8) | (data[i + 1] as usize);
690                let length = data[i + 2] as usize;
691                i += 3;
692                let start = decompressed.len().saturating_sub(distance);
693                for j in 0..length {
694                    if start + j < decompressed.len() {
695                        let byte = decompressed[start + j];
696                        decompressed.push(byte);
697                    }
698                }
699            } else {
700                if i >= data.len() {
701                    return Err(WasmError::InvalidOperation {
702                        operation: "LZ77 decompress".to_string(),
703                        reason: "Unexpected end of data".to_string(),
704                    });
705                }
706                decompressed.push(data[i]);
707                i += 1;
708            }
709        }
710        Ok(decompressed)
711    }
712}
713/// Unified compression interface
714pub struct TileCompressor {
715    /// Algorithm to use
716    algorithm: CompressionAlgorithm,
717}
718impl TileCompressor {
719    /// Creates a new tile compressor
720    pub const fn new(algorithm: CompressionAlgorithm) -> Self {
721        Self { algorithm }
722    }
723    /// Compresses tile data
724    pub fn compress(&self, data: &[u8]) -> (Vec<u8>, CompressionStats) {
725        #[cfg(target_arch = "wasm32")]
726        let start = js_sys::Date::now();
727        #[cfg(not(target_arch = "wasm32"))]
728        let start = std::time::Instant::now();
729
730        let original_size = data.len();
731        let compressed = match self.algorithm {
732            CompressionAlgorithm::None => data.to_vec(),
733            CompressionAlgorithm::Rle => RleCompressor::compress(data),
734            CompressionAlgorithm::Delta => DeltaCompressor::compress(data),
735            CompressionAlgorithm::Huffman => HuffmanCompressor::compress(data),
736            CompressionAlgorithm::Lz77 => Lz77Compressor::compress(data),
737        };
738        let compressed_size = compressed.len();
739
740        #[cfg(target_arch = "wasm32")]
741        let elapsed = js_sys::Date::now() - start;
742        #[cfg(not(target_arch = "wasm32"))]
743        let elapsed = start.elapsed().as_secs_f64() * 1000.0;
744
745        let stats = CompressionStats::new(original_size, compressed_size, elapsed);
746        (compressed, stats)
747    }
748    /// Decompresses tile data
749    pub fn decompress(&self, data: &[u8]) -> WasmResult<Vec<u8>> {
750        match self.algorithm {
751            CompressionAlgorithm::None => Ok(data.to_vec()),
752            CompressionAlgorithm::Rle => RleCompressor::decompress(data),
753            CompressionAlgorithm::Delta => DeltaCompressor::decompress(data),
754            CompressionAlgorithm::Huffman => Ok(data.to_vec()),
755            CompressionAlgorithm::Lz77 => Lz77Compressor::decompress(data),
756        }
757    }
758    /// Compresses 2D tile data (for images)
759    pub fn compress_2d(&self, data: &[u8], width: usize) -> (Vec<u8>, CompressionStats) {
760        #[cfg(target_arch = "wasm32")]
761        let start = js_sys::Date::now();
762        #[cfg(not(target_arch = "wasm32"))]
763        let start = std::time::Instant::now();
764
765        let original_size = data.len();
766        let compressed = match self.algorithm {
767            CompressionAlgorithm::Delta => DeltaCompressor::compress_2d(data, width),
768            _ => self.compress(data).0,
769        };
770        let compressed_size = compressed.len();
771
772        #[cfg(target_arch = "wasm32")]
773        let elapsed = js_sys::Date::now() - start;
774        #[cfg(not(target_arch = "wasm32"))]
775        let elapsed = start.elapsed().as_secs_f64() * 1000.0;
776
777        let stats = CompressionStats::new(original_size, compressed_size, elapsed);
778        (compressed, stats)
779    }
780    /// Decompresses 2D tile data
781    pub fn decompress_2d(&self, data: &[u8], width: usize) -> WasmResult<Vec<u8>> {
782        match self.algorithm {
783            CompressionAlgorithm::Delta => DeltaCompressor::decompress_2d(data, width),
784            _ => self.decompress(data),
785        }
786    }
787}
788/// Compression benchmark
789pub struct CompressionBenchmark;
790impl CompressionBenchmark {
791    /// Benchmarks all compression algorithms
792    pub fn benchmark_all(data: &[u8]) -> Vec<(CompressionAlgorithm, CompressionStats)> {
793        let algorithms = [
794            CompressionAlgorithm::None,
795            CompressionAlgorithm::Rle,
796            CompressionAlgorithm::Delta,
797            CompressionAlgorithm::Huffman,
798            CompressionAlgorithm::Lz77,
799        ];
800        let mut results = Vec::new();
801        for &algorithm in &algorithms {
802            let compressor = TileCompressor::new(algorithm);
803            let (_compressed, stats) = compressor.compress(data);
804            results.push((algorithm, stats));
805        }
806        results
807    }
808    /// Finds the best compression algorithm for the given data
809    pub fn find_best(data: &[u8]) -> CompressionAlgorithm {
810        let results = Self::benchmark_all(data);
811        results
812            .into_iter()
813            .min_by(|a, b| {
814                a.1.compressed_size.cmp(&b.1.compressed_size).then_with(|| {
815                    a.1.compression_time_ms
816                        .partial_cmp(&b.1.compression_time_ms)
817                        .unwrap_or(std::cmp::Ordering::Equal)
818                })
819            })
820            .map(|(algo, _)| algo)
821            .unwrap_or(CompressionAlgorithm::None)
822    }
823}
824/// Compression selector for automatic algorithm selection
825#[derive(Debug, Clone)]
826pub struct CompressionSelector {
827    /// Statistics from previous compressions
828    history: Vec<(CompressionAlgorithm, CompressionStats)>,
829    /// Maximum history size
830    max_history: usize,
831}
832impl CompressionSelector {
833    /// Creates a new compression selector
834    pub fn new() -> Self {
835        Self {
836            history: Vec::new(),
837            max_history: 10,
838        }
839    }
840    /// Selects the best compression algorithm for the given data
841    pub fn select_best(&mut self, data: &[u8]) -> WasmResult<CompressionAlgorithm> {
842        let best = CompressionBenchmark::find_best(data);
843        let compressor = TileCompressor::new(best);
844        let (_compressed, stats) = compressor.compress(data);
845        self.history.push((best, stats));
846        if self.history.len() > self.max_history {
847            self.history.remove(0);
848        }
849        Ok(best)
850    }
851    /// Returns compression statistics history
852    pub fn history(&self) -> &[(CompressionAlgorithm, CompressionStats)] {
853        &self.history
854    }
855    /// Clears history
856    pub fn clear_history(&mut self) {
857        self.history.clear();
858    }
859}
860impl Default for CompressionSelector {
861    fn default() -> Self {
862        Self::new()
863    }
864}
865#[cfg(test)]
866mod tests {
867    use super::*;
868    #[test]
869    fn test_rle_compress_decompress() {
870        let data = vec![1, 1, 1, 2, 2, 3, 3, 3, 3];
871        let compressed = RleCompressor::compress(&data);
872        let decompressed = RleCompressor::decompress(&compressed).expect("Decompress failed");
873        assert_eq!(data, decompressed);
874        assert!(compressed.len() < data.len());
875    }
876    #[test]
877    fn test_delta_compress_decompress() {
878        let data = vec![10, 15, 20, 25, 30, 35, 40];
879        let compressed = DeltaCompressor::compress(&data);
880        let decompressed = DeltaCompressor::decompress(&compressed).expect("Decompress failed");
881        assert_eq!(data, decompressed);
882    }
883    #[test]
884    fn test_delta_2d() {
885        let data = vec![10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120];
886        let width = 3;
887        let compressed = DeltaCompressor::compress_2d(&data, width);
888        let decompressed =
889            DeltaCompressor::decompress_2d(&compressed, width).expect("Decompress 2D failed");
890        assert_eq!(data, decompressed);
891    }
892    #[test]
893    fn test_lz77_compress_decompress() {
894        let data = b"ABCABCABCABC";
895        let compressed = Lz77Compressor::compress(data);
896        let decompressed = Lz77Compressor::decompress(&compressed).expect("Decompress failed");
897        assert_eq!(data.to_vec(), decompressed);
898    }
899    #[test]
900    fn test_compression_stats() {
901        let stats = CompressionStats::new(1000, 500, 10.0);
902        assert_eq!(stats.space_saved(), 500);
903        assert_eq!(stats.space_saved_percent(), 50.0);
904        assert_eq!(stats.ratio, 0.5);
905    }
906    #[test]
907    fn test_tile_compressor() {
908        let compressor = TileCompressor::new(CompressionAlgorithm::Rle);
909        let data = vec![1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3];
910        let (compressed, stats) = compressor.compress(&data);
911        assert!(stats.compressed_size <= data.len());
912        let decompressed = compressor
913            .decompress(&compressed)
914            .expect("Decompress failed");
915        assert_eq!(data, decompressed);
916    }
917    #[test]
918    #[ignore]
919    fn test_compression_benchmark() {
920        let data = vec![1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5];
921        let results = CompressionBenchmark::benchmark_all(&data);
922        assert_eq!(results.len(), 5);
923        let algorithms: Vec<_> = results.iter().map(|(algo, _)| *algo).collect();
924        assert!(algorithms.contains(&CompressionAlgorithm::None));
925        assert!(algorithms.contains(&CompressionAlgorithm::Rle));
926    }
927    #[test]
928    #[ignore]
929    fn test_find_best_compression() {
930        let data = vec![1, 1, 1, 1, 2, 2, 2, 2];
931        let best = CompressionBenchmark::find_best(&data);
932        assert_ne!(best, CompressionAlgorithm::None);
933    }
934}