use std::collections::HashMap;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DeduplicationStats {
pub total_tiles: u64,
pub unique_tiles: u64,
pub duplicates_eliminated: u64,
pub bytes_saved: u64,
}
impl DeduplicationStats {
pub fn new() -> Self {
Self::default()
}
pub fn dedup_ratio(&self) -> f64 {
if self.total_tiles == 0 {
return 0.0;
}
self.duplicates_eliminated as f64 / self.total_tiles as f64
}
pub fn savings_percent(&self) -> f64 {
self.dedup_ratio() * 100.0
}
}
pub struct TileHasher;
impl TileHasher {
pub fn hash(data: &[u8]) -> u64 {
xxhash_rust::xxh3::xxh3_64(data)
}
}
#[derive(Debug, Default)]
pub struct DeduplicationCache {
seen: HashMap<u64, (u64, u32)>,
stats: DeduplicationStats,
}
impl DeduplicationCache {
pub fn new() -> Self {
Self::default()
}
pub fn check(&self, hash: u64) -> Option<(u64, u32)> {
self.seen.get(&hash).copied()
}
pub fn record_new(
&mut self,
hash: u64,
offset: u64,
compressed_len: u32,
uncompressed_len: u32,
) {
self.seen.insert(hash, (offset, compressed_len));
self.stats.total_tiles += 1;
self.stats.unique_tiles += 1;
let _ = uncompressed_len; }
pub fn record_duplicate(&mut self, uncompressed_len: u32) {
self.stats.total_tiles += 1;
self.stats.duplicates_eliminated += 1;
self.stats.bytes_saved += uncompressed_len as u64;
}
pub fn stats(&self) -> &DeduplicationStats {
&self.stats
}
pub fn into_stats(self) -> DeduplicationStats {
self.stats
}
pub fn unique_count(&self) -> usize {
self.seen.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dedup_stats_default() {
let stats = DeduplicationStats::new();
assert_eq!(stats.total_tiles, 0);
assert_eq!(stats.unique_tiles, 0);
assert_eq!(stats.duplicates_eliminated, 0);
assert_eq!(stats.bytes_saved, 0);
}
#[test]
fn test_dedup_stats_ratio_empty() {
let stats = DeduplicationStats::new();
assert_eq!(stats.dedup_ratio(), 0.0);
}
#[test]
fn test_dedup_stats_ratio_no_duplicates() {
let stats = DeduplicationStats {
total_tiles: 100,
unique_tiles: 100,
duplicates_eliminated: 0,
bytes_saved: 0,
};
assert_eq!(stats.dedup_ratio(), 0.0);
assert_eq!(stats.savings_percent(), 0.0);
}
#[test]
fn test_dedup_stats_ratio_half_duplicates() {
let stats = DeduplicationStats {
total_tiles: 100,
unique_tiles: 50,
duplicates_eliminated: 50,
bytes_saved: 50000,
};
assert_eq!(stats.dedup_ratio(), 0.5);
assert_eq!(stats.savings_percent(), 50.0);
}
#[test]
fn test_dedup_stats_ratio_mostly_duplicates() {
let stats = DeduplicationStats {
total_tiles: 1000,
unique_tiles: 10,
duplicates_eliminated: 990,
bytes_saved: 990000,
};
assert_eq!(stats.dedup_ratio(), 0.99);
assert_eq!(stats.savings_percent(), 99.0);
}
#[test]
fn test_hasher_empty_data() {
let hash = TileHasher::hash(&[]);
assert_ne!(hash, 0); }
#[test]
fn test_hasher_consistent() {
let data = b"Hello, PMTiles!";
let hash1 = TileHasher::hash(data);
let hash2 = TileHasher::hash(data);
assert_eq!(hash1, hash2, "Same input should produce same hash");
}
#[test]
fn test_hasher_different_data() {
let hash1 = TileHasher::hash(b"tile A content");
let hash2 = TileHasher::hash(b"tile B content");
assert_ne!(
hash1, hash2,
"Different input should produce different hash"
);
}
#[test]
fn test_hasher_mvt_like_data() {
let mvt1 = vec![0x1a, 0x10, 0x00, 0x01, 0x02, 0x03];
let mvt2 = vec![0x1a, 0x10, 0x00, 0x01, 0x02, 0x03];
let mvt3 = vec![0x1a, 0x10, 0x00, 0x01, 0x02, 0x04];
let hash1 = TileHasher::hash(&mvt1);
let hash2 = TileHasher::hash(&mvt2);
let hash3 = TileHasher::hash(&mvt3);
assert_eq!(hash1, hash2, "Identical MVT data should have same hash");
assert_ne!(
hash1, hash3,
"Different MVT data should have different hash"
);
}
#[test]
fn test_hasher_large_tile() {
let large_tile: Vec<u8> = (0..4096).map(|i| (i % 256) as u8).collect();
let hash = TileHasher::hash(&large_tile);
assert_ne!(hash, 0);
let hash2 = TileHasher::hash(&large_tile);
assert_eq!(hash, hash2);
}
#[test]
fn test_cache_new_is_empty() {
let cache = DeduplicationCache::new();
assert_eq!(cache.unique_count(), 0);
assert_eq!(cache.stats().total_tiles, 0);
}
#[test]
fn test_cache_check_unseen_returns_none() {
let cache = DeduplicationCache::new();
assert!(cache.check(12345).is_none());
}
#[test]
fn test_cache_record_new_tile() {
let mut cache = DeduplicationCache::new();
let hash = TileHasher::hash(b"tile content");
cache.record_new(hash, 0, 100, 200);
assert_eq!(cache.unique_count(), 1);
assert_eq!(cache.stats().total_tiles, 1);
assert_eq!(cache.stats().unique_tiles, 1);
assert_eq!(cache.stats().duplicates_eliminated, 0);
}
#[test]
fn test_cache_check_seen_returns_location() {
let mut cache = DeduplicationCache::new();
let hash = TileHasher::hash(b"tile content");
cache.record_new(hash, 1000, 150, 300);
let location = cache.check(hash);
assert_eq!(location, Some((1000, 150)));
}
#[test]
fn test_cache_record_duplicate() {
let mut cache = DeduplicationCache::new();
let hash = TileHasher::hash(b"tile content");
cache.record_new(hash, 0, 100, 200);
cache.record_duplicate(200);
assert_eq!(cache.unique_count(), 1); assert_eq!(cache.stats().total_tiles, 2);
assert_eq!(cache.stats().unique_tiles, 1);
assert_eq!(cache.stats().duplicates_eliminated, 1);
assert_eq!(cache.stats().bytes_saved, 200);
}
#[test]
fn test_cache_multiple_unique_tiles() {
let mut cache = DeduplicationCache::new();
for i in 0..5u32 {
let data = format!("unique tile {}", i);
let hash = TileHasher::hash(data.as_bytes());
cache.record_new(hash, i as u64 * 100, 50, 100);
}
assert_eq!(cache.unique_count(), 5);
assert_eq!(cache.stats().total_tiles, 5);
assert_eq!(cache.stats().unique_tiles, 5);
assert_eq!(cache.stats().duplicates_eliminated, 0);
}
#[test]
fn test_cache_mixed_unique_and_duplicate() {
let mut cache = DeduplicationCache::new();
let hash1 = TileHasher::hash(b"ocean tile");
cache.record_new(hash1, 0, 50, 100);
let hash2 = TileHasher::hash(b"land tile");
cache.record_new(hash2, 50, 200, 500);
cache.record_duplicate(100);
cache.record_duplicate(100);
cache.record_duplicate(500);
assert_eq!(cache.unique_count(), 2);
assert_eq!(cache.stats().total_tiles, 5);
assert_eq!(cache.stats().unique_tiles, 2);
assert_eq!(cache.stats().duplicates_eliminated, 3);
assert_eq!(cache.stats().bytes_saved, 700); }
#[test]
fn test_cache_into_stats() {
let mut cache = DeduplicationCache::new();
let hash = TileHasher::hash(b"test");
cache.record_new(hash, 0, 10, 20);
cache.record_duplicate(20);
let stats = cache.into_stats();
assert_eq!(stats.total_tiles, 2);
assert_eq!(stats.duplicates_eliminated, 1);
}
#[test]
fn test_dedup_ratio_realistic_scenario() {
let mut cache = DeduplicationCache::new();
let ocean_hash = TileHasher::hash(b"empty ocean tile");
cache.record_new(ocean_hash, 0, 50, 100);
for _ in 0..699 {
cache.record_duplicate(100);
}
for i in 0..300 {
let data = format!("land tile {}", i);
let hash = TileHasher::hash(data.as_bytes());
cache.record_new(hash, 50 + i * 200, 200, 500);
}
let stats = cache.stats();
assert_eq!(stats.total_tiles, 1000);
assert_eq!(stats.unique_tiles, 301);
assert_eq!(stats.duplicates_eliminated, 699);
assert!((stats.dedup_ratio() - 0.699).abs() < 0.001);
}
}