use std::fmt::Write as _;
use crate::inspect::model::InspectModel;
const MAX_LISTED_ATTRIBUTE_INDICES: usize = 10;
pub fn render(model: &InspectModel) -> String {
let mut out = String::new();
let _ = writeln!(out, "▶ File Details");
let _ = writeln!(out, " Source: {}", model.source);
let _ = writeln!(
out,
" Size: {}",
model
.size_bytes
.map(format_size)
.unwrap_or_else(|| "unknown (remote)".to_string())
);
let _ = writeln!(out, " Version: {}", model.version);
if let Some(title) = &model.title {
let _ = writeln!(out, " Title: {title}");
}
if let Some(identifier) = &model.identifier {
let _ = writeln!(out, " Identifier: {identifier}");
}
if let Some(date) = &model.reference_date {
let _ = writeln!(out, " Reference Date: {date}");
}
let _ = writeln!(out, "\n▶ Dataset");
let _ = writeln!(out, " Features: {}", model.features_count);
let _ = writeln!(out, " Columns: {}", model.columns.len());
match &model.extent {
Some(extent) => {
let _ = writeln!(out, " Geospatial Extent: Yes");
let _ = writeln!(
out,
" Min: [{:.2}, {:.2}, {:.2}]",
extent.min[0], extent.min[1], extent.min[2]
);
let _ = writeln!(
out,
" Max: [{:.2}, {:.2}, {:.2}]",
extent.max[0], extent.max[1], extent.max[2]
);
let [width, height, depth] = extent.dimensions();
let _ = writeln!(out, " Dimensions: {width:.2} × {height:.2} × {depth:.2}");
}
None => {
let _ = writeln!(out, " Geospatial Extent: Not set");
}
}
let _ = writeln!(out, "\n▶ Indices");
if model.has_spatial_index() {
let _ = writeln!(
out,
" Spatial R-tree: Yes (node size: {})",
model.index_node_size
);
} else {
let _ = writeln!(out, " Spatial R-tree: No");
}
if model.attribute_index_count == 0 {
let _ = writeln!(out, " Attribute Indices: None");
} else {
let _ = writeln!(
out,
" Attribute Indices: {} (B+Tree)",
model.attribute_index_count
);
for (i, name) in model
.attribute_index_names
.iter()
.enumerate()
.take(MAX_LISTED_ATTRIBUTE_INDICES)
{
let _ = writeln!(out, " {}. {name}", i + 1);
}
let hidden = model
.attribute_index_names
.len()
.saturating_sub(MAX_LISTED_ATTRIBUTE_INDICES);
if hidden > 0 {
let _ = writeln!(out, " ... {hidden} more attributes...");
}
}
let _ = writeln!(out, "\n▶ Coordinate Reference System");
match &model.crs {
Some(crs) => {
let _ = writeln!(out, " CRS: {}", crs.code_label());
if crs.version != 0 {
let _ = writeln!(out, " CRS Version: {}", crs.version);
}
if let Some(code_string) = &crs.code_string {
let _ = writeln!(out, " CRS Code String: {code_string}");
}
}
None => {
let _ = writeln!(out, " CRS: Not set");
}
}
let _ = writeln!(out, "\n▶ Coordinate Transform");
match &model.transform {
Some(transform) => {
let _ = writeln!(
out,
" Scale: [{:.6}, {:.6}, {:.6}]",
transform.scale[0], transform.scale[1], transform.scale[2]
);
let _ = writeln!(
out,
" Translate: [{:.6}, {:.6}, {:.6}]",
transform.translate[0], transform.translate[1], transform.translate[2]
);
}
None => {
let _ = writeln!(out, " Transform: Not set");
}
}
out
}
pub(crate) fn format_size(bytes: u64) -> String {
const KB: f64 = 1024.0;
const MB: f64 = KB * 1024.0;
const GB: f64 = MB * 1024.0;
let size = bytes as f64;
if size >= GB {
format!("{:.2} GB", size / GB)
} else if size >= MB {
format!("{:.2} MB", size / MB)
} else if size >= KB {
format!("{:.2} KB", size / KB)
} else {
format!("{bytes} bytes")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::inspect::model::{ColumnInfo, CrsInfo, ExtentInfo, InspectModel, TransformInfo};
fn bare_model() -> InspectModel {
InspectModel {
source: "city.fcb".into(),
size_bytes: Some(2048),
title: None,
identifier: None,
version: "2.0".into(),
features_count: 3,
reference_date: None,
index_node_size: 0,
attribute_index_count: 0,
attribute_index_names: Vec::new(),
columns: Vec::new(),
crs: None,
extent: None,
transform: None,
}
}
fn full_model() -> InspectModel {
InspectModel {
source: "/data/delft.fcb".into(),
size_bytes: Some(7_662_899),
title: Some("3DBAG".into()),
identifier: Some("delft-001".into()),
version: "2.0".into(),
features_count: 1115,
reference_date: Some("2024-01-31".into()),
index_node_size: 16,
attribute_index_count: 2,
attribute_index_names: vec!["b3_dak_type".into(), "identificatie".into()],
columns: vec![ColumnInfo {
name: "b3_dak_type".into(),
type_name: "String".into(),
description: None,
nullable: true,
primary_key: false,
unique: false,
}],
crs: Some(CrsInfo {
authority: Some("EPSG".into()),
code: 7415,
version: 2,
code_string: Some("EPSG:7415".into()),
}),
extent: Some(ExtentInfo {
min: [84501.55, 445805.03, -3.75],
max: [85675.23, 446983.47, 95.04],
}),
transform: Some(TransformInfo {
scale: [0.001, 0.001, 0.001],
translate: [85088.390625, 446394.25, 45.648003],
}),
}
}
#[test]
fn file_details_section_lists_source_size_and_version() {
let out = render(&full_model());
assert!(out.contains("▶ File Details"));
assert!(out.contains(" Source: /data/delft.fcb"));
assert!(out.contains(" Size: 7.31 MB"));
assert!(out.contains(" Version: 2.0"));
assert!(out.contains(" Title: 3DBAG"));
assert!(out.contains(" Identifier: delft-001"));
assert!(out.contains(" Reference Date: 2024-01-31"));
}
#[test]
fn optional_file_details_are_omitted_when_absent() {
let out = render(&bare_model());
assert!(!out.contains("Title:"));
assert!(!out.contains("Identifier:"));
assert!(!out.contains("Reference Date:"));
}
#[test]
fn unknown_size_is_reported_rather_than_guessed() {
let mut model = full_model();
model.size_bytes = None;
model.source = "https://example.com/city.fcb".into();
let out = render(&model);
assert!(out.contains(" Source: https://example.com/city.fcb"));
assert!(out.contains(" Size: unknown (remote)"));
}
#[test]
fn dataset_section_shows_features_columns_and_extent() {
let out = render(&full_model());
assert!(out.contains("▶ Dataset"));
assert!(out.contains(" Features: 1115"));
assert!(out.contains(" Columns: 1"));
assert!(out.contains(" Geospatial Extent: Yes"));
assert!(out.contains(" Min: [84501.55, 445805.03, -3.75]"));
assert!(out.contains(" Max: [85675.23, 446983.47, 95.04]"));
assert!(out.contains(" Dimensions: 1173.68 × 1178.44 × 98.79"));
}
#[test]
fn absent_extent_is_reported_as_not_set() {
let out = render(&bare_model());
assert!(out.contains(" Geospatial Extent: Not set"));
assert!(!out.contains("Dimensions:"));
}
#[test]
fn indices_section_names_indexed_attributes() {
let out = render(&full_model());
assert!(out.contains("▶ Indices"));
assert!(out.contains(" Spatial R-tree: Yes (node size: 16)"));
assert!(out.contains(" Attribute Indices: 2 (B+Tree)"));
assert!(out.contains(" 1. b3_dak_type"));
assert!(out.contains(" 2. identificatie"));
}
#[test]
fn absent_indices_are_reported_as_no_and_none() {
let out = render(&bare_model());
assert!(out.contains(" Spatial R-tree: No"));
assert!(!out.contains("node size"));
assert!(out.contains(" Attribute Indices: None"));
}
#[test]
fn long_attribute_index_lists_are_truncated_after_ten() {
let mut model = full_model();
model.attribute_index_names = (0..12).map(|i| format!("attr_{i:02}")).collect();
model.attribute_index_count = model.attribute_index_names.len();
let out = render(&model);
assert!(out.contains(" 10. attr_09"));
assert!(!out.contains("attr_10"));
assert!(out.contains(" ... 2 more attributes..."));
}
#[test]
fn crs_section_shows_code_version_and_code_string() {
let out = render(&full_model());
assert!(out.contains("▶ Coordinate Reference System"));
assert!(out.contains(" CRS: EPSG:7415"));
assert!(out.contains(" CRS Version: 2"));
assert!(out.contains(" CRS Code String: EPSG:7415"));
}
#[test]
fn absent_crs_is_reported_as_not_set() {
let out = render(&bare_model());
assert!(out.contains("▶ Coordinate Reference System"));
assert!(out.contains(" CRS: Not set"));
}
#[test]
fn crs_version_zero_is_omitted() {
let mut model = full_model();
if let Some(crs) = model.crs.as_mut() {
crs.version = 0;
}
let out = render(&model);
assert!(out.contains(" CRS: EPSG:7415"));
assert!(!out.contains(" CRS Version:"));
}
#[test]
fn transform_section_shows_scale_and_translate() {
let out = render(&full_model());
assert!(out.contains("▶ Coordinate Transform"));
assert!(out.contains(" Scale: [0.001000, 0.001000, 0.001000]"));
assert!(out.contains(" Translate: [85088.390625, 446394.250000, 45.648003]"));
}
#[test]
fn absent_transform_is_reported_as_not_set() {
let out = render(&bare_model());
assert!(out.contains("▶ Coordinate Transform"));
assert!(out.contains(" Transform: Not set"));
assert!(!out.contains("Scale:"));
}
#[test]
fn report_starts_with_a_section_and_ends_with_one_newline() {
let out = render(&bare_model());
assert!(out.starts_with("▶ File Details\n"));
assert!(out.ends_with('\n'));
assert!(!out.ends_with("\n\n"));
}
#[test]
fn format_size_scales_by_unit() {
assert_eq!(format_size(512), "512 bytes");
assert_eq!(format_size(2048), "2.00 KB");
assert_eq!(format_size(7_662_899), "7.31 MB");
assert_eq!(format_size(3 * 1024 * 1024 * 1024), "3.00 GB");
}
}