1use crate::tile::Feature;
7#[cfg(test)]
8use crate::types::GeometryType;
9
10#[derive(Debug, Clone, Copy)]
12pub enum ImportanceScorer {
13 GeometrySize,
15 PropertyCount,
17 FeatureId,
19 Random,
21 Combined,
23}
24
25impl ImportanceScorer {
26 pub fn score(&self, feature: &Feature) -> f64 {
28 match self {
29 Self::GeometrySize => feature.positions.len() as f64,
30 Self::PropertyCount => {
31 feature.properties.len() as f64
33 }
34 Self::FeatureId => {
35 1.0 / (feature.id as f64 + 1.0)
37 }
38 Self::Random => {
39 use std::collections::hash_map::RandomState;
41 use std::hash::{BuildHasher, Hash, Hasher};
42 let mut hasher = RandomState::new().build_hasher();
43 feature.id.hash(&mut hasher);
44 (hasher.finish() as f64) / (u64::MAX as f64)
45 }
46 Self::Combined => {
47 let geom_score = feature.positions.len() as f64;
48 let prop_score = feature.properties.len() as f64 * 10.0;
49 geom_score + prop_score
50 }
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct TileBudget {
58 pub max_uncompressed_size: usize,
60 pub max_compressed_size: usize,
62 pub max_feature_count: usize,
64 pub scorer: ImportanceScorer,
66}
67
68impl Default for TileBudget {
69 fn default() -> Self {
70 Self {
71 max_uncompressed_size: 500 * 1024, max_compressed_size: 128 * 1024, max_feature_count: 10_000,
74 scorer: ImportanceScorer::Combined,
75 }
76 }
77}
78
79impl TileBudget {
80 pub fn new(
82 max_uncompressed_size: usize,
83 max_compressed_size: usize,
84 max_feature_count: usize,
85 ) -> Self {
86 Self {
87 max_uncompressed_size,
88 max_compressed_size,
89 max_feature_count,
90 scorer: ImportanceScorer::Combined,
91 }
92 }
93
94 pub fn with_scorer(mut self, scorer: ImportanceScorer) -> Self {
96 self.scorer = scorer;
97 self
98 }
99
100 pub fn estimate_size(features: &[Feature]) -> usize {
102 let mut size = 0;
103
104 for feature in features {
105 size += feature.positions.len() * 16;
107
108 for (key, value) in &feature.properties {
110 size += key.len();
111 size += Self::estimate_value_size(value);
112 }
113
114 size += 32; }
117
118 size
119 }
120
121 fn estimate_value_size(value: &crate::tile::Value) -> usize {
123 match value {
124 crate::tile::Value::String(s) => s.len(),
125 crate::tile::Value::Double(_) => 8,
126 crate::tile::Value::Float(_) => 4,
127 crate::tile::Value::Int(_) => 8,
128 crate::tile::Value::UInt(_) => 8,
129 crate::tile::Value::Bool(_) => 1,
130 }
131 }
132
133 pub fn enforce_indexed<S, Z>(&self, count: usize, score: S, size: Z) -> Vec<usize>
157 where
158 S: Fn(usize) -> f64,
159 Z: Fn(usize) -> usize,
160 {
161 let total_size: usize = (0..count).map(&size).sum();
165 if count <= self.max_feature_count && total_size <= self.max_uncompressed_size {
166 return (0..count).collect();
167 }
168
169 let mut scored: Vec<(usize, f64, usize)> = (0..count)
171 .map(|i| (i, score(i), size(i)))
172 .collect();
173
174 if scored.len() > self.max_feature_count {
176 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
177 scored.truncate(self.max_feature_count);
178 }
179
180 let kept_size: usize = scored.iter().map(|(_, _, s)| *s).sum();
184 let mut keep: Vec<usize> = if kept_size > self.max_uncompressed_size {
185 let target = (self.max_uncompressed_size as f64 * 0.9) as usize;
186 scored.sort_by(|a, b| {
187 let ratio_a = a.1 / (a.2.max(1) as f64);
188 let ratio_b = b.1 / (b.2.max(1) as f64);
189 ratio_b
190 .partial_cmp(&ratio_a)
191 .unwrap_or(std::cmp::Ordering::Equal)
192 });
193 let mut kept = Vec::new();
194 let mut total = 0usize;
195 for (i, _, s) in &scored {
196 if total + s <= target {
197 total += s;
198 kept.push(*i);
199 } else if total < target * 95 / 100 && total + s <= target * 105 / 100 {
200 total += s;
202 kept.push(*i);
203 }
204 }
205 kept
206 } else {
207 scored.into_iter().map(|(i, _, _)| i).collect()
208 };
209
210 keep.sort_unstable();
212 keep
213 }
214
215 pub fn score_signals(&self, vertex_count: usize, property_count: usize) -> f64 {
222 match self.scorer {
223 ImportanceScorer::GeometrySize => vertex_count as f64,
224 ImportanceScorer::PropertyCount => property_count as f64,
225 ImportanceScorer::Combined
226 | ImportanceScorer::FeatureId
227 | ImportanceScorer::Random => {
228 vertex_count as f64 + property_count as f64 * 10.0
229 }
230 }
231 }
232
233 pub fn enforce(&self, mut features: Vec<Feature>) -> (Vec<Feature>, usize) {
236 let original_count = features.len();
237
238 if features.len() > self.max_feature_count {
240 features = self.drop_by_count(features, self.max_feature_count);
241 }
242
243 let size = Self::estimate_size(&features);
245 if size > self.max_uncompressed_size {
246 let target_size = (self.max_uncompressed_size as f64 * 0.9) as usize; features = self.drop_by_size(features, target_size);
248 }
249
250 let dropped_count = original_count - features.len();
251 (features, dropped_count)
252 }
253
254 fn drop_by_count(&self, features: Vec<Feature>, max_count: usize) -> Vec<Feature> {
256 if features.len() <= max_count {
257 return features;
258 }
259
260 let mut scored: Vec<(Feature, f64)> = features
262 .into_iter()
263 .map(|f| {
264 let score = self.scorer.score(&f);
265 (f, score)
266 })
267 .collect();
268
269 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
271
272 scored.truncate(max_count);
274 scored.into_iter().map(|(f, _)| f).collect()
275 }
276
277 fn drop_by_size(&self, features: Vec<Feature>, target_size: usize) -> Vec<Feature> {
279 let current_size = Self::estimate_size(&features);
280 if current_size <= target_size {
281 return features;
282 }
283
284 let mut scored: Vec<(Feature, f64, usize)> = features
286 .into_iter()
287 .map(|f| {
288 let score = self.scorer.score(&f);
289 let size = self.estimate_feature_size(&f);
290 (f, score, size)
291 })
292 .collect();
293
294 scored.sort_by(|a, b| {
296 let ratio_a = a.1 / (a.2 as f64);
297 let ratio_b = b.1 / (b.2 as f64);
298 ratio_b
299 .partial_cmp(&ratio_a)
300 .unwrap_or(std::cmp::Ordering::Equal)
301 });
302
303 let mut kept = Vec::new();
305 let mut total_size = 0;
306
307 for (feature, _, size) in scored {
308 if total_size + size <= target_size {
309 total_size += size;
310 kept.push(feature);
311 } else if total_size < target_size * 95 / 100 {
312 if total_size + size <= target_size * 105 / 100 {
315 total_size += size;
316 kept.push(feature);
317 }
318 }
319 }
320
321 kept
322 }
323
324 fn estimate_feature_size(&self, feature: &Feature) -> usize {
326 let mut size = feature.positions.len() * 16;
327
328 for (key, value) in &feature.properties {
329 size += key.len();
330 size += Self::estimate_value_size(value);
331 }
332
333 size + 32 }
335}
336
337#[derive(Debug, Clone, Default)]
339pub struct BudgetStats {
340 pub tiles_processed: usize,
341 pub tiles_reduced: usize,
342 pub features_dropped: usize,
343 pub original_size: usize,
344 pub reduced_size: usize,
345}
346
347impl BudgetStats {
348 pub fn add_tile(
350 &mut self,
351 original_count: usize,
352 final_count: usize,
353 original_size: usize,
354 final_size: usize,
355 ) {
356 self.tiles_processed += 1;
357 if final_count < original_count {
358 self.tiles_reduced += 1;
359 }
360 self.features_dropped += original_count - final_count;
361 self.original_size += original_size;
362 self.reduced_size += final_size;
363 }
364
365 pub fn reduction_ratio(&self) -> f64 {
367 if self.original_size == 0 {
368 return 1.0;
369 }
370 self.reduced_size as f64 / self.original_size as f64
371 }
372
373 pub fn features_dropped_pct(&self) -> f64 {
375 if self.tiles_processed == 0 {
376 return 0.0;
377 }
378 (self.features_dropped as f64 / self.tiles_processed as f64) * 100.0
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use std::collections::HashMap;
386
387 fn create_test_feature(id: u64, geometry_size: usize, property_count: usize) -> Feature {
388 let positions = vec![crate::tile::Position { lon: 0.0, lat: 0.0 }; geometry_size];
389 let mut properties = HashMap::new();
390
391 for i in 0..property_count {
392 properties.insert(format!("key_{}", i), crate::tile::Value::Int(i as i64));
393 }
394
395 Feature {
396 id,
397 geometry_type: GeometryType::Point,
398 positions,
399 properties,
400 time_range: None,
401 }
402 }
403
404 #[test]
405 fn test_importance_scoring() {
406 let scorer = ImportanceScorer::GeometrySize;
407
408 let feature1 = create_test_feature(1, 100, 0);
409 let feature2 = create_test_feature(2, 50, 0);
410
411 assert!(scorer.score(&feature1) > scorer.score(&feature2));
412 }
413
414 #[test]
415 fn test_budget_enforcement_by_count() {
416 let budget = TileBudget::default().with_scorer(ImportanceScorer::GeometrySize);
417
418 let features = vec![
419 create_test_feature(1, 100, 0),
420 create_test_feature(2, 50, 0),
421 create_test_feature(3, 75, 0),
422 create_test_feature(4, 200, 0), ];
424
425 let original_count = features.len();
426 let kept = budget.drop_by_count(features, 2);
427 let dropped = original_count - kept.len();
428
429 assert_eq!(kept.len(), 2);
430 assert_eq!(dropped, 2);
431
432 assert_eq!(kept[0].id, 4); assert_eq!(kept[1].id, 1); }
436
437 #[test]
438 fn test_size_estimation() {
439 let feature = create_test_feature(1, 100, 5);
440 let size = TileBudget::estimate_size(&[feature]);
441
442 assert!(size > 1000);
443 assert!(size < 3000);
444 }
445
446 #[test]
447 fn test_budget_enforcement_no_drop() {
448 let budget = TileBudget::default();
449
450 let features = vec![create_test_feature(1, 10, 1), create_test_feature(2, 10, 1)];
451
452 let original_count = features.len();
453 let (kept, dropped) = budget.enforce(features);
454
455 assert_eq!(kept.len(), original_count);
456 assert_eq!(dropped, 0);
457 }
458
459 #[test]
460 fn test_enforce_indexed_under_budget_is_noop() {
461 let budget = TileBudget::new(1_000_000, 256 * 1024, 1000);
463 let sizes = [100usize, 50, 200, 75];
464 let keep = budget.enforce_indexed(
465 sizes.len(),
466 |i| sizes[i] as f64,
467 |i| sizes[i],
468 );
469 assert_eq!(keep, vec![0, 1, 2, 3]);
470 }
471
472 #[test]
473 fn test_enforce_indexed_count_cap_keeps_highest_scored() {
474 let budget = TileBudget::new(1_000_000, 256 * 1024, 2);
478 let sizes = [10usize, 50, 200, 100];
479 let mut keep = budget.enforce_indexed(
480 sizes.len(),
481 |i| sizes[i] as f64,
482 |i| sizes[i],
483 );
484 keep.sort_unstable();
485 assert_eq!(keep, vec![2, 3]); }
487
488 #[test]
489 fn test_enforce_indexed_size_cap_drops_to_fit() {
490 let budget = TileBudget::new(150, 256 * 1024, 10_000);
492 let sizes = [100usize, 100, 100, 100];
493 let keep = budget.enforce_indexed(
494 sizes.len(),
495 |i| sizes[i] as f64,
496 |i| sizes[i],
497 );
498 let kept_bytes: usize = keep.iter().map(|&i| sizes[i]).sum();
499 assert!(keep.len() < sizes.len(), "expected some features dropped");
500 assert!(kept_bytes <= 150 * 105 / 100);
502 }
503
504 #[test]
505 fn test_score_signals_matches_scorer() {
506 let geo = TileBudget::default().with_scorer(ImportanceScorer::GeometrySize);
507 assert_eq!(geo.score_signals(10, 5), 10.0);
508 let combined = TileBudget::default().with_scorer(ImportanceScorer::Combined);
509 assert_eq!(combined.score_signals(10, 5), 10.0 + 50.0);
510 }
511
512 #[test]
513 fn test_budget_stats() {
514 let mut stats = BudgetStats::default();
515
516 stats.add_tile(1000, 800, 100_000, 80_000);
517 stats.add_tile(500, 500, 50_000, 50_000);
518
519 assert_eq!(stats.tiles_processed, 2);
520 assert_eq!(stats.tiles_reduced, 1);
521 assert_eq!(stats.features_dropped, 200);
522 assert!((stats.reduction_ratio() - 0.8666).abs() < 0.01);
523 }
524}