use std::collections::BTreeMap;
use serde::{Deserialize, Deserializer, Serialize};
pub const OVERVIEWS_KEY: &str = "geo:overviews";
pub const COGP_KEY: &str = "cogp";
pub const SPEC_VERSION: &str = "0.2.0";
pub const WEBMERC_CIRCUMFERENCE_M: f64 = 40_075_016.69;
pub const GSD_TILE_BASE: f64 = 1024.0;
pub const METERS_PER_DEGREE: f64 = 111_320.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Crs {
Epsg4326,
Epsg3857,
}
impl Crs {
#[inline]
pub fn meters_to_units(self, meters: f64) -> f64 {
match self {
Crs::Epsg3857 => meters,
Crs::Epsg4326 => meters / METERS_PER_DEGREE,
}
}
}
pub fn gsd_with_base(z: u8, base: f64) -> f64 {
WEBMERC_CIRCUMFERENCE_M / base / 2f64.powi(z as i32)
}
pub fn gsd(z: u8) -> f64 {
gsd_with_base(z, GSD_TILE_BASE)
}
pub fn zoom_for_gsd(target_gsd: f64) -> f64 {
(WEBMERC_CIRCUMFERENCE_M / GSD_TILE_BASE / target_gsd).log2()
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Level {
pub row_group_end: i64,
pub gsd: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub zoom: Option<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Mode {
Duplicating,
Partitioning,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MemoryProfile {
#[default]
Auto,
Speed,
Bounded,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Generalization {
pub engine: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gsd_base: Option<f64>,
pub levels: Vec<GeneralizationLevel>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cascade: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub collapse: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub representation: Option<Vec<RepresentationBandProvenance>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ranking: Option<RankingProvenance>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub density_drop: Option<DensityProvenance>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub clustering: Option<ClusteringProvenance>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub coalescing: Option<CoalescingProvenance>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub renamed_columns: Option<BTreeMap<String, String>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CoalescingProvenance {
pub enabled: bool,
pub snap_tolerance_gsd_factor: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub junction_angle: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_level_rows: Option<u64>,
pub coalesced_count_column: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClusteringProvenance {
pub enabled: bool,
pub point_count_column: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub accumulated: Vec<AccumulatedColumn>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AccumulatedColumn {
pub column: String,
pub op: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DensityProvenance {
pub drop_rate: f64,
pub gamma: f64,
pub supercell_gsd_factor: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RankingProvenance {
pub mode: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub column: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_ranks"
)]
pub ranks: Option<BTreeMap<String, f64>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unknown_rank: Option<f64>,
}
fn deserialize_ranks<'de, D>(deserializer: D) -> Result<Option<BTreeMap<String, f64>>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum RanksShape {
Map(BTreeMap<String, f64>),
LegacyPairs(Vec<(String, f64)>),
}
Ok(
Option::<RanksShape>::deserialize(deserializer)?.map(|s| match s {
RanksShape::Map(m) => m,
RanksShape::LegacyPairs(pairs) => pairs.into_iter().collect(),
}),
)
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RepresentationBandProvenance {
pub zooms: [u8; 2],
pub repr: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GeneralizationLevel {
pub simplify_tolerance_m: f64,
pub thinning_factor: f64,
pub visibility_gate_m: f64,
pub geometry_types: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OverviewsMeta {
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<Mode>,
#[serde(default)]
pub canonical_level: Option<i64>,
pub levels: Vec<Level>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub generalization: Option<Generalization>,
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum OverviewValidationError {
#[error("levels must be non-empty")]
EmptyLevels,
#[error("version {0:?} is not valid semver MAJOR.MINOR.PATCH")]
InvalidVersion(String),
#[error("levels[{index}].row_group_end = {value} out of range [0, {num_row_groups})")]
RowGroupOutOfRange {
index: usize,
value: i64,
num_row_groups: i64,
},
#[error(
"levels[{index}].row_group_end = {value} is not strictly greater than previous {previous}"
)]
RowGroupNotIncreasing {
index: usize,
value: i64,
previous: i64,
},
#[error("final row_group_end = {final_value} must equal num_row_groups - 1 = {expected}")]
RowGroupFinalMismatch {
final_value: i64,
expected: i64,
},
#[error("levels[{index}].gsd = {value} must be > 0")]
GsdNotPositive {
index: usize,
value: f64,
},
#[error("levels[{index}].gsd = {value} is not strictly less than previous {previous}")]
GsdNotDecreasing {
index: usize,
value: f64,
previous: f64,
},
#[error("zoom must be present on all levels or none; levels[{index}] disagrees")]
ZoomPartial {
index: usize,
},
#[error("levels[{index}].zoom = {value} is not strictly greater than previous {previous}")]
ZoomNotIncreasing {
index: usize,
value: u8,
previous: u8,
},
#[error("duplicating mode requires canonical_level = {expected} (L-1), got {actual:?}")]
CanonicalLevelMismatch {
expected: i64,
actual: Option<i64>,
},
#[error("partitioning mode requires canonical_level = null, got {actual}")]
CanonicalLevelNotNull {
actual: i64,
},
}
impl OverviewsMeta {
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
pub fn from_json(s: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(s)
}
pub fn to_cogp_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(&CogpMeta {
version: self.version.clone(),
levels: self
.levels
.iter()
.map(|l| CogpLevel {
row_group_end: l.row_group_end,
gsd: l.gsd,
})
.collect(),
})
}
pub fn validate(&self, num_row_groups: i64) -> Result<(), OverviewValidationError> {
if !is_semver(&self.version) {
return Err(OverviewValidationError::InvalidVersion(
self.version.clone(),
));
}
if self.levels.is_empty() {
return Err(OverviewValidationError::EmptyLevels);
}
let mut prev_end: Option<i64> = None;
for (i, level) in self.levels.iter().enumerate() {
if level.row_group_end < 0 || level.row_group_end >= num_row_groups {
return Err(OverviewValidationError::RowGroupOutOfRange {
index: i,
value: level.row_group_end,
num_row_groups,
});
}
if let Some(prev) = prev_end {
if level.row_group_end <= prev {
return Err(OverviewValidationError::RowGroupNotIncreasing {
index: i,
value: level.row_group_end,
previous: prev,
});
}
}
prev_end = Some(level.row_group_end);
}
let final_end = self.levels.last().unwrap().row_group_end;
if final_end != num_row_groups - 1 {
return Err(OverviewValidationError::RowGroupFinalMismatch {
final_value: final_end,
expected: num_row_groups - 1,
});
}
let mut prev_gsd: Option<f64> = None;
for (i, level) in self.levels.iter().enumerate() {
if !matches!(
level.gsd.partial_cmp(&0.0),
Some(std::cmp::Ordering::Greater)
) {
return Err(OverviewValidationError::GsdNotPositive {
index: i,
value: level.gsd,
});
}
if let Some(prev) = prev_gsd {
if level.gsd >= prev {
return Err(OverviewValidationError::GsdNotDecreasing {
index: i,
value: level.gsd,
previous: prev,
});
}
}
prev_gsd = Some(level.gsd);
}
let any_zoom = self.levels.iter().any(|l| l.zoom.is_some());
if any_zoom {
let mut prev_zoom: Option<u8> = None;
for (i, level) in self.levels.iter().enumerate() {
let Some(z) = level.zoom else {
return Err(OverviewValidationError::ZoomPartial { index: i });
};
if let Some(prev) = prev_zoom {
if z <= prev {
return Err(OverviewValidationError::ZoomNotIncreasing {
index: i,
value: z,
previous: prev,
});
}
}
prev_zoom = Some(z);
}
}
match self.mode {
Some(Mode::Duplicating) => {
let expected = self.levels.len() as i64 - 1;
if self.canonical_level != Some(expected) {
return Err(OverviewValidationError::CanonicalLevelMismatch {
expected,
actual: self.canonical_level,
});
}
}
Some(Mode::Partitioning) | None => {
if let Some(actual) = self.canonical_level {
return Err(OverviewValidationError::CanonicalLevelNotNull { actual });
}
}
}
Ok(())
}
}
#[derive(Serialize)]
struct CogpMeta {
version: String,
levels: Vec<CogpLevel>,
}
#[derive(Serialize)]
struct CogpLevel {
row_group_end: i64,
gsd: f64,
}
fn is_semver(v: &str) -> bool {
let parts: Vec<&str> = v.split('.').collect();
parts.len() == 3
&& parts
.iter()
.all(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()))
}
#[cfg(test)]
mod tests {
use super::*;
fn duplicating_example() -> OverviewsMeta {
OverviewsMeta {
version: "0.1.0".to_string(),
mode: Some(Mode::Duplicating),
canonical_level: Some(2),
levels: vec![
Level {
row_group_end: 1,
gsd: 9783.94,
zoom: Some(2),
},
Level {
row_group_end: 5,
gsd: 2445.98,
zoom: Some(4),
},
Level {
row_group_end: 14,
gsd: 611.50,
zoom: Some(6),
},
],
generalization: None,
}
}
fn partitioning_example() -> OverviewsMeta {
OverviewsMeta {
version: "0.1.0".to_string(),
mode: Some(Mode::Partitioning),
canonical_level: None,
levels: vec![
Level {
row_group_end: 0,
gsd: 1000.0,
zoom: Some(6),
},
Level {
row_group_end: 3,
gsd: 500.0,
zoom: Some(7),
},
Level {
row_group_end: 12,
gsd: 100.0,
zoom: Some(9),
},
],
generalization: None,
}
}
#[test]
fn gsd_reference_values() {
assert!((gsd(2) - 9783.94).abs() < 0.01);
assert!((gsd(4) - 2445.98).abs() < 0.01);
assert!((gsd(6) - 611.50).abs() < 0.01);
assert!((gsd(9) - 76.44).abs() < 0.01);
}
#[test]
fn gsd_with_base_default_matches_const_gsd() {
for z in 0u8..=16 {
assert_eq!(gsd_with_base(z, GSD_TILE_BASE), gsd(z), "z={z}");
}
}
#[test]
fn gsd_with_base_scales_inversely_with_base() {
for z in 0u8..=12 {
let d = gsd(z);
assert!((gsd_with_base(z, GSD_TILE_BASE * 2.0) - d / 2.0).abs() < 1e-9);
assert!((gsd_with_base(z, GSD_TILE_BASE / 2.0) - d * 2.0).abs() < 1e-9);
}
}
#[test]
fn zoom_for_gsd_inverts_gsd() {
for z in 0u8..=16 {
let back = zoom_for_gsd(gsd(z));
assert!((back - z as f64).abs() < 1e-9, "z={z} back={back}");
}
}
#[test]
fn roundtrip_duplicating() {
let meta = duplicating_example();
let json = meta.to_json().unwrap();
let parsed = OverviewsMeta::from_json(&json).unwrap();
assert_eq!(meta, parsed);
}
#[test]
fn roundtrip_partitioning_null_canonical() {
let meta = partitioning_example();
let json = meta.to_json().unwrap();
assert!(
json.contains("\"canonical_level\":null"),
"expected explicit null canonical_level, got {json}"
);
let parsed = OverviewsMeta::from_json(&json).unwrap();
assert_eq!(meta, parsed);
}
#[test]
fn parse_spec_example_field_names() {
let src = r#"{
"version": "0.1.0",
"mode": "duplicating",
"canonical_level": 2,
"levels": [
{ "row_group_end": 1, "gsd": 9783.94, "zoom": 2 },
{ "row_group_end": 5, "gsd": 2445.98, "zoom": 4 },
{ "row_group_end": 14, "gsd": 611.50, "zoom": 6 }
]
}"#;
let meta = OverviewsMeta::from_json(src).unwrap();
assert_eq!(meta.version, "0.1.0");
assert_eq!(meta.mode, Some(Mode::Duplicating));
assert_eq!(meta.canonical_level, Some(2));
assert_eq!(meta.levels.len(), 3);
assert_eq!(meta.levels[0].row_group_end, 1);
assert_eq!(meta.levels[2].zoom, Some(6));
meta.validate(15).unwrap();
}
#[test]
fn parse_partitioning_example_absent_and_null_canonical_equivalent() {
let with_null = r#"{"version":"0.1.0","mode":"partitioning","canonical_level":null,
"levels":[{"row_group_end":0,"gsd":1000},{"row_group_end":3,"gsd":500}]}"#;
let absent = r#"{"version":"0.1.0","mode":"partitioning",
"levels":[{"row_group_end":0,"gsd":1000},{"row_group_end":3,"gsd":500}]}"#;
let a = OverviewsMeta::from_json(with_null).unwrap();
let b = OverviewsMeta::from_json(absent).unwrap();
assert_eq!(a.canonical_level, None);
assert_eq!(b.canonical_level, None);
assert_eq!(a, b);
}
#[test]
fn cogp_subset_serializer() {
let meta = partitioning_example();
let json = meta.to_cogp_json().unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(v["version"], "0.1.0");
assert!(v.get("mode").is_none());
assert!(v.get("canonical_level").is_none());
assert_eq!(v["levels"][0]["row_group_end"], 0);
assert_eq!(v["levels"][0]["gsd"], 1000.0);
assert!(v["levels"][0].get("zoom").is_none());
}
#[test]
fn validate_accepts_valid_duplicating() {
duplicating_example().validate(15).unwrap();
}
#[test]
fn validate_rejects_empty_levels() {
let meta = OverviewsMeta {
version: "0.1.0".to_string(),
mode: Some(Mode::Duplicating),
canonical_level: Some(0),
levels: vec![],
generalization: None,
};
assert_eq!(
meta.validate(0).unwrap_err(),
OverviewValidationError::EmptyLevels
);
}
#[test]
fn validate_rejects_bad_version() {
let mut meta = duplicating_example();
meta.version = "1.0".to_string();
assert!(matches!(
meta.validate(15).unwrap_err(),
OverviewValidationError::InvalidVersion(_)
));
}
#[test]
fn validate_rejects_row_group_out_of_range() {
let meta = duplicating_example();
assert!(matches!(
meta.validate(10).unwrap_err(),
OverviewValidationError::RowGroupOutOfRange { .. }
));
}
#[test]
fn validate_rejects_non_increasing_row_group_end() {
let mut meta = duplicating_example();
meta.levels[1].row_group_end = 1; assert!(matches!(
meta.validate(15).unwrap_err(),
OverviewValidationError::RowGroupNotIncreasing { .. }
));
}
#[test]
fn validate_rejects_final_row_group_mismatch() {
let meta = duplicating_example(); assert!(matches!(
meta.validate(20).unwrap_err(),
OverviewValidationError::RowGroupFinalMismatch {
final_value: 14,
expected: 19
}
));
}
#[test]
fn validate_rejects_non_positive_gsd() {
let mut meta = duplicating_example();
meta.levels[2].gsd = 0.0;
assert!(matches!(
meta.validate(15).unwrap_err(),
OverviewValidationError::GsdNotPositive { .. }
));
}
#[test]
fn validate_rejects_non_decreasing_gsd() {
let mut meta = duplicating_example();
meta.levels[1].gsd = 9783.94; assert!(matches!(
meta.validate(15).unwrap_err(),
OverviewValidationError::GsdNotDecreasing { .. }
));
}
#[test]
fn validate_rejects_partial_zoom() {
let mut meta = duplicating_example();
meta.levels[1].zoom = None;
assert!(matches!(
meta.validate(15).unwrap_err(),
OverviewValidationError::ZoomPartial { index: 1 }
));
}
#[test]
fn validate_rejects_non_increasing_zoom() {
let mut meta = duplicating_example();
meta.levels[1].zoom = Some(2); assert!(matches!(
meta.validate(15).unwrap_err(),
OverviewValidationError::ZoomNotIncreasing { .. }
));
}
#[test]
fn validate_rejects_duplicating_wrong_canonical() {
let mut meta = duplicating_example();
meta.canonical_level = Some(1); assert!(matches!(
meta.validate(15).unwrap_err(),
OverviewValidationError::CanonicalLevelMismatch { expected: 2, .. }
));
}
#[test]
fn validate_rejects_partitioning_non_null_canonical() {
let mut meta = partitioning_example();
meta.canonical_level = Some(2);
assert!(matches!(
meta.validate(13).unwrap_err(),
OverviewValidationError::CanonicalLevelNotNull { actual: 2 }
));
}
#[test]
fn validate_absent_mode_requires_null_canonical() {
let mut meta = partitioning_example();
meta.mode = None;
meta.canonical_level = None;
meta.validate(13).unwrap();
meta.canonical_level = Some(0);
assert!(matches!(
meta.validate(13).unwrap_err(),
OverviewValidationError::CanonicalLevelNotNull { .. }
));
}
#[test]
fn renamed_columns_provenance_roundtrips_and_is_omitted_when_absent() {
let mut meta = duplicating_example();
meta.generalization = Some(Generalization {
engine: "tylertoo test".to_string(),
gsd_base: None,
cascade: None,
collapse: None,
representation: None,
levels: vec![],
ranking: None,
density_drop: None,
clustering: None,
coalescing: None,
renamed_columns: None,
});
let json = meta.to_json().unwrap();
assert!(
!json.contains("renamed_columns"),
"absent renames must not appear in the footer, got {json}"
);
let g = meta.generalization.as_mut().unwrap();
g.renamed_columns = Some(BTreeMap::from([
("level_".to_string(), "level".to_string()),
("point_count_".to_string(), "point_count".to_string()),
]));
let json = meta.to_json().unwrap();
assert!(
json.contains(r#""renamed_columns":{"level_":"level","point_count_":"point_count"}"#),
"renamed_columns must serialize as an object map, got {json}"
);
let parsed = OverviewsMeta::from_json(&json).unwrap();
assert_eq!(meta, parsed);
let r = parsed.generalization.unwrap().renamed_columns.unwrap();
assert_eq!(r.get("level_").map(String::as_str), Some("level"));
}
#[test]
fn ranking_provenance_roundtrip_class_ranking() {
let mut meta = duplicating_example();
meta.generalization = Some(Generalization {
engine: "tylertoo test".to_string(),
gsd_base: None,
cascade: None,
collapse: None,
representation: None,
levels: vec![],
ranking: Some(RankingProvenance {
mode: "auto-overture-roads".to_string(),
column: Some("road_class".to_string()),
ranks: Some(BTreeMap::from([
("motorway".to_string(), 18.0),
("service".to_string(), 11.0),
])),
unknown_rank: Some(0.0),
}),
density_drop: None,
clustering: None,
coalescing: None,
renamed_columns: None,
});
let json = meta.to_json().unwrap();
assert!(
json.contains(r#""ranks":{"motorway":18.0,"service":11.0}"#),
"ranks must serialize as an object map, got {json}"
);
let parsed = OverviewsMeta::from_json(&json).unwrap();
assert_eq!(meta, parsed);
let r = parsed.generalization.unwrap().ranking.unwrap();
assert_eq!(r.mode, "auto-overture-roads");
assert_eq!(r.column.as_deref(), Some("road_class"));
assert_eq!(r.unknown_rank, Some(0.0));
assert_eq!(r.ranks.unwrap().len(), 2);
}
#[test]
fn ranking_ranks_legacy_array_of_pairs_still_reads() {
let src = r#"{
"version": "0.1.0", "mode": "duplicating", "canonical_level": 0,
"levels": [ { "row_group_end": 0, "gsd": 611.50, "zoom": 6 } ],
"generalization": {
"engine": "tylertoo 0.5.0",
"levels": [],
"ranking": {
"mode": "class-ranking",
"column": "road_class",
"ranks": [["motorway", 5.0], ["primary", 4.0]],
"unknown_rank": 0.0
}
}
}"#;
let meta = OverviewsMeta::from_json(src).unwrap();
let r = meta.generalization.clone().unwrap().ranking.unwrap();
let ranks = r.ranks.unwrap();
assert_eq!(ranks.get("motorway"), Some(&5.0));
assert_eq!(ranks.get("primary"), Some(&4.0));
let json = meta.to_json().unwrap();
assert!(
json.contains(r#""ranks":{"motorway":5.0,"primary":4.0}"#),
"re-emit must use the map shape, got {json}"
);
}
#[test]
fn density_provenance_roundtrip_and_absent_tolerated() {
let mut meta = duplicating_example();
meta.generalization = Some(Generalization {
engine: "tylertoo test".to_string(),
gsd_base: None,
cascade: None,
collapse: None,
representation: None,
levels: vec![],
ranking: None,
density_drop: Some(DensityProvenance {
drop_rate: 1.8,
gamma: 1.5,
supercell_gsd_factor: 128.0,
}),
clustering: None,
coalescing: None,
renamed_columns: None,
});
let json = meta.to_json().unwrap();
let parsed = OverviewsMeta::from_json(&json).unwrap();
assert_eq!(meta, parsed);
let d = parsed.generalization.unwrap().density_drop.unwrap();
assert_eq!(d.drop_rate, 1.8);
assert_eq!(d.gamma, 1.5);
assert_eq!(d.supercell_gsd_factor, 128.0);
let src = r#"{
"version": "0.1.0", "mode": "duplicating", "canonical_level": 0,
"levels": [ { "row_group_end": 0, "gsd": 611.50, "zoom": 6 } ],
"generalization": {
"engine": "tylertoo 0.1.0",
"levels": []
}
}"#;
let m = OverviewsMeta::from_json(src).unwrap();
assert!(m.generalization.unwrap().density_drop.is_none());
}
#[test]
fn coalescing_provenance_roundtrip_and_absent_tolerated() {
let mut meta = duplicating_example();
meta.generalization = Some(Generalization {
engine: "tylertoo test".to_string(),
gsd_base: None,
cascade: None,
collapse: None,
representation: None,
levels: vec![],
ranking: None,
density_drop: None,
clustering: None,
coalescing: Some(CoalescingProvenance {
enabled: true,
snap_tolerance_gsd_factor: 1.0,
junction_angle: Some(0.0),
max_level_rows: Some(2_000_000),
coalesced_count_column: "coalesced_count".to_string(),
}),
renamed_columns: None,
});
let json = meta.to_json().unwrap();
for key in [
r#""enabled":true"#,
r#""snap_tolerance_gsd_factor":1.0"#,
r#""junction_angle":0.0"#,
r#""max_level_rows":2000000"#,
r#""coalesced_count_column":"coalesced_count""#,
] {
assert!(json.contains(key), "missing {key} in {json}");
}
let parsed = OverviewsMeta::from_json(&json).unwrap();
assert_eq!(meta, parsed);
let c = parsed.generalization.unwrap().coalescing.unwrap();
assert!(c.enabled);
assert_eq!(c.snap_tolerance_gsd_factor, 1.0);
assert_eq!(c.junction_angle, Some(0.0));
assert_eq!(c.max_level_rows, Some(2_000_000));
assert_eq!(c.coalesced_count_column, "coalesced_count");
let src = r#"{
"version": "0.1.0", "mode": "duplicating", "canonical_level": 0,
"levels": [ { "row_group_end": 0, "gsd": 611.50, "zoom": 6 } ],
"generalization": { "engine": "tylertoo 0.1.0", "levels": [] }
}"#;
let m = OverviewsMeta::from_json(src).unwrap();
assert!(m.generalization.unwrap().coalescing.is_none());
}
#[test]
fn coalescing_provenance_older_file_missing_new_members_reads_as_unknown() {
let src = r#"{
"version": "0.1.0", "mode": "duplicating", "canonical_level": 0,
"levels": [ { "row_group_end": 0, "gsd": 611.50, "zoom": 6 } ],
"generalization": {
"engine": "tylertoo 0.5.0", "levels": [],
"coalescing": {
"enabled": true,
"snap_tolerance_gsd_factor": 1.0,
"coalesced_count_column": "coalesced_count"
}
}
}"#;
let m = OverviewsMeta::from_json(src).unwrap();
let c = m.generalization.unwrap().coalescing.unwrap();
assert!(c.enabled);
assert_eq!(c.junction_angle, None, "absent means unknown, not 0");
assert_eq!(
c.max_level_rows, None,
"absent means unknown, not a default"
);
}
#[test]
fn clustering_provenance_roundtrip_and_absent_tolerated() {
let mut meta = duplicating_example();
meta.generalization = Some(Generalization {
engine: "tylertoo test".to_string(),
gsd_base: None,
cascade: None,
collapse: None,
representation: None,
levels: vec![],
ranking: None,
density_drop: None,
clustering: Some(ClusteringProvenance {
enabled: true,
point_count_column: "point_count".to_string(),
accumulated: vec![AccumulatedColumn {
column: "confidence".to_string(),
op: "mean".to_string(),
}],
}),
coalescing: None,
renamed_columns: None,
});
let json = meta.to_json().unwrap();
let parsed = OverviewsMeta::from_json(&json).unwrap();
assert_eq!(meta, parsed);
let c = parsed.generalization.unwrap().clustering.unwrap();
assert!(c.enabled);
assert_eq!(c.point_count_column, "point_count");
assert_eq!(c.accumulated.len(), 1);
assert_eq!(c.accumulated[0].column, "confidence");
assert_eq!(c.accumulated[0].op, "mean");
let src = r#"{
"version": "0.1.0", "mode": "duplicating", "canonical_level": 0,
"levels": [ { "row_group_end": 0, "gsd": 611.50, "zoom": 6 } ],
"generalization": { "engine": "tylertoo 0.1.0", "levels": [] }
}"#;
let m = OverviewsMeta::from_json(src).unwrap();
assert!(m.generalization.unwrap().clustering.is_none());
}
#[test]
fn ranking_provenance_absent_field_tolerated() {
let src = r#"{
"version": "0.1.0",
"mode": "duplicating",
"canonical_level": 0,
"levels": [ { "row_group_end": 0, "gsd": 611.50, "zoom": 6 } ],
"generalization": {
"engine": "tylertoo 0.1.0",
"levels": [
{ "simplify_tolerance_m": 0, "thinning_factor": 1.0,
"visibility_gate_m": 0, "geometry_types": [] }
]
}
}"#;
let meta = OverviewsMeta::from_json(src).unwrap();
assert!(meta.generalization.unwrap().ranking.is_none());
}
#[test]
fn validate_single_level_degenerate() {
let meta = OverviewsMeta {
version: "0.1.0".to_string(),
mode: Some(Mode::Duplicating),
canonical_level: Some(0),
levels: vec![Level {
row_group_end: 0,
gsd: 611.5,
zoom: Some(6),
}],
generalization: None,
};
meta.validate(1).unwrap();
}
}