diff --git a/nusamai-citygml/Cargo.toml b/nusamai-citygml/Cargo.toml index ea9009661..b3739fbe6 100644 --- a/nusamai-citygml/Cargo.toml +++ b/nusamai-citygml/Cargo.toml @@ -8,8 +8,8 @@ default = ["serde"] serde = ["dep:serde", "serde_json", "nusamai-geometry/serde"] [dependencies] -ahash = "0.8.7" -chrono = { version = "0.4.31", features = ["serde"], default-features = false } +ahash = "0.8.8" +chrono = { version = "0.4.34", features = ["serde"], default-features = false } indexmap = { version = "2.1", features = ["serde"] } log = "0.4.20" macros = { path = "./macros" } diff --git a/nusamai-citygml/src/values.rs b/nusamai-citygml/src/values.rs index 050b565e5..ee76a66a0 100644 --- a/nusamai-citygml/src/values.rs +++ b/nusamai-citygml/src/values.rs @@ -15,6 +15,7 @@ pub type MeasureOrNullList = String; // TODO? pub type BuildingLODType = String; // TODO? pub type DoubleList = String; // TODO? pub type LODType = u64; // TODO? +pub type Double01 = f64; // TODO? impl CityGmlElement for String { #[inline] @@ -367,6 +368,121 @@ impl CityGmlAttribute for LocalId { } } +#[derive(Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)] +pub struct Color { + pub r: f64, + pub g: f64, + pub b: f64, +} + +impl Color { + pub fn new(r: f64, g: f64, b: f64) -> Self { + Self { r, g, b } + } +} + +impl std::hash::Hash for Color { + fn hash(&self, state: &mut H) { + self.r.to_bits().hash(state); + self.g.to_bits().hash(state); + self.b.to_bits().hash(state); + } +} + +impl CityGmlElement for Color { + fn parse(&mut self, st: &mut SubTreeReader) -> Result<(), ParseError> { + let text = st.parse_text()?; + let r: Result, _> = text + .split_ascii_whitespace() + .map(|s| s.parse::()) + .collect(); + match r { + Ok(v) if v.len() == 3 => { + (self.r, self.g, self.b) = (v[0], v[1], v[2]); + } + _ => { + return Err(ParseError::InvalidValue(format!( + "Failed to parse color value: {}", + text + ))) + } + } + Ok(()) + } + + fn into_object(self) -> Option { + Some(Value::Array(vec![ + Value::Double(self.r), + Value::Double(self.g), + Value::Double(self.b), + ])) + } + + fn collect_schema(_schema: &mut schema::Schema) -> schema::Attribute { + schema::Attribute { + type_ref: schema::TypeRef::Double, + min_occurs: 3, + max_occurs: Some(3), + } + } +} + +#[derive(Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)] +pub struct ColorPlusOpacity { + pub r: Double01, + pub g: Double01, + pub b: Double01, + pub a: Double01, +} + +impl ColorPlusOpacity { + pub fn new(r: f64, g: f64, b: f64, a: f64) -> Self { + Self { r, g, b, a } + } +} + +impl CityGmlElement for ColorPlusOpacity { + fn parse(&mut self, st: &mut SubTreeReader) -> Result<(), ParseError> { + let text = st.parse_text()?; + let r: Result, _> = text + .split_ascii_whitespace() + .map(|s| s.parse::()) + .collect(); + match r { + Ok(v) if v.len() == 3 => { + (self.r, self.g, self.b, self.a) = (v[0], v[1], v[2], 1.0); + } + Ok(v) if v.len() == 4 => { + (self.r, self.g, self.b, self.a) = (v[0], v[1], v[2], v[3]); + } + _ => { + return Err(ParseError::InvalidValue(format!( + "Failed to parse color value: {}", + text + ))) + } + } + Ok(()) + } + + fn into_object(self) -> Option { + Some(Value::Array(vec![ + Value::Double(self.r), + Value::Double(self.g), + Value::Double(self.b), + Value::Double(self.a), + ])) + } + + fn collect_schema(_schema: &mut schema::Schema) -> schema::Attribute { + schema::Attribute { + type_ref: schema::TypeRef::Double, + min_occurs: 4, + max_occurs: Some(4), + } + } +} + impl CityGmlElement for Option { #[inline] fn parse(&mut self, st: &mut SubTreeReader) -> Result<(), ParseError> { diff --git a/nusamai-czml/Cargo.toml b/nusamai-czml/Cargo.toml index 21816101f..5e3713137 100644 --- a/nusamai-czml/Cargo.toml +++ b/nusamai-czml/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -chrono = { version = "0.4.33", features = ["serde"] } +chrono = { version = "0.4.34", features = ["serde"] } serde = { version = "1.0.195", features = ["derive"] } serde_json = { version = "1.0.111", features = ["float_roundtrip"] } nusamai-geometry = { path = "../nusamai-geometry" } diff --git a/nusamai-geometry/Cargo.toml b/nusamai-geometry/Cargo.toml index 6e939507e..bfda593e7 100644 --- a/nusamai-geometry/Cargo.toml +++ b/nusamai-geometry/Cargo.toml @@ -4,14 +4,14 @@ version = "0.1.0" edition = "2021" [dependencies] -num-traits = "0.2.17" +num-traits = "0.2.18" serde = { version = "1.0.193", features = ["derive"], optional = true } [dev-dependencies] byteorder = "1.5.0" geo-types = "0.7.11" geojson = "0.24.1" -indexmap = "2.1.0" +indexmap = "2.2.3" quick-xml = "0.31.0" thiserror = "1.0.50" earcut-rs = { git = "https://github.com/MIERUNE/earcut-rs.git" } diff --git a/nusamai-gltf/Cargo.toml b/nusamai-gltf/Cargo.toml index a042189f5..a6b371010 100644 --- a/nusamai-gltf/Cargo.toml +++ b/nusamai-gltf/Cargo.toml @@ -12,6 +12,6 @@ nusamai-geometry = { path = "../nusamai-geometry" } quick-xml = "0.31.0" thiserror = "1.0.50" earcut-rs = { git = "https://github.com/MIERUNE/earcut-rs.git" } -indexmap = "2.1.0" +indexmap = "2.2.3" byteorder = "1.5.0" serde_json = "1.0.113" diff --git a/nusamai-gpkg/Cargo.toml b/nusamai-gpkg/Cargo.toml index 4fdc358c7..e679905b7 100644 --- a/nusamai-gpkg/Cargo.toml +++ b/nusamai-gpkg/Cargo.toml @@ -11,4 +11,4 @@ url = "2.5.0" indexmap = "2.2.2" [dev-dependencies] -tokio = { version = "1.35.1", features = ["full"] } +tokio = { version = "1.36", features = ["full"] } diff --git a/nusamai-mvt/Cargo.toml b/nusamai-mvt/Cargo.toml index a38a344f2..cad50aef8 100644 --- a/nusamai-mvt/Cargo.toml +++ b/nusamai-mvt/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -ahash = "0.8.7" -indexmap = "2.1.0" +ahash = "0.8.8" +indexmap = "2.2.3" prost = "0.12.3" [build-dependencies] diff --git a/nusamai-plateau/Cargo.toml b/nusamai-plateau/Cargo.toml index 6cb2f69c4..9f04acdf0 100644 --- a/nusamai-plateau/Cargo.toml +++ b/nusamai-plateau/Cargo.toml @@ -11,11 +11,12 @@ quick-xml = "0.31.0" serde = { version = "1.0.193", features = ["derive"], optional = true } nusamai-citygml = { path = "../nusamai-citygml", features = ["serde"]} nusamai-geometry = { path = "../nusamai-geometry" } -chrono = { version = "0.4.31", features = ["serde"], default-features = false } +chrono = { version = "0.4.34", features = ["serde"], default-features = false } url = "2.5.0" -stretto = "0.8.2" +stretto = { git = "https://github.com/ciscorn/stretto.git" , branch = "update-wg" } hashbrown = "0.14.3" indexmap = "2.2.2" +log = "0.4.20" [dev-dependencies] zstd = { version = "0.13.0", features = ["zdict_builder"] } diff --git a/nusamai-plateau/src/appearance.rs b/nusamai-plateau/src/appearance.rs index 833d06a8a..dbad910db 100644 --- a/nusamai-plateau/src/appearance.rs +++ b/nusamai-plateau/src/appearance.rs @@ -2,20 +2,76 @@ use crate::models::appearance::{self, ParameterizedTexture, SurfaceDataProperty, X3DMaterial}; use hashbrown::HashMap; -use nusamai_citygml::{appearance::TextureAssociation, LocalId, SurfaceSpan}; +use nusamai_citygml::{appearance::TextureAssociation, Color, LocalId, SurfaceSpan, URI}; use nusamai_geometry::LineString2; +use std::hash::{Hash, Hasher}; +#[derive(Debug, Default, serde::Serialize, serde::Deserialize)] +pub struct Theme { + ring_id_to_texture: HashMap)>, + surface_id_to_material: HashMap, +} + +/// Material (CityGML's X3DMaterial) +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct Material { + pub diffuse_color: Color, + pub specular_color: Color, + pub ambient_intensity: f64, + // TOOD: other parameters +} + +impl From for Material { + fn from(src: X3DMaterial) -> Self { + Self { + diffuse_color: src.diffuse_color.unwrap_or(Color::new(0.8, 0.8, 0.8)), + specular_color: src.specular_color.unwrap_or(Color::new(1., 1., 1.)), + ambient_intensity: src.ambient_intensity.unwrap_or(0.2), + } + } +} + +impl Default for Material { + fn default() -> Self { + Self { + diffuse_color: Color::new(0.8, 0.8, 0.8), + specular_color: Color::new(1., 1., 1.), + ambient_intensity: 0.2, + } + } +} + +impl Hash for Material { + fn hash(&self, state: &mut H) { + self.diffuse_color.hash(state); + self.specular_color.hash(state); + self.ambient_intensity.to_bits().hash(state); + } +} #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] pub struct AppearanceStore { - textures: Vec, - materials: Vec, + textures: Vec, + materials: Vec, themes: HashMap, } -#[derive(Debug, Default, serde::Serialize, serde::Deserialize)] -pub struct Theme { - ring_id_to_texture: HashMap)>, - surface_id_to_material: HashMap, +/// Texture (CityGML's ParameterizedTexture) +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct Texture { + pub image_uri: String, + // TOOD: other parameters +} + +impl From for Texture { + fn from(src: ParameterizedTexture) -> Self { + let url = src.image_uri.unwrap_or_else(|| { + log::warn!("image_uri is not set"); + URI::new("url_not_found.jpg") + }); + Self { + image_uri: url.value().to_string(), + } + } } impl AppearanceStore { @@ -37,14 +93,14 @@ impl AppearanceStore { } } } - self.textures.push(texture); + self.textures.push(texture.into()); } SurfaceDataProperty::X3DMaterial(mut material) => { let mat_idx = self.materials.len() as u32; for target in material.target.drain(..) { theme.surface_id_to_material.insert(target, mat_idx); } - self.materials.push(material); + self.materials.push(material.into()); } _ => unimplemented!(), } @@ -125,17 +181,21 @@ mod tests { #[test] fn merge_appearance() { - use crate::models::appearance::{ParameterizedTexture, X3DMaterial}; - let mut app_local = AppearanceStore::default(); let mut app_global = AppearanceStore::default(); { - app_local.textures.push(ParameterizedTexture::default()); - app_local.textures.push(ParameterizedTexture::default()); - app_local.textures.push(ParameterizedTexture::default()); - app_local.materials.push(X3DMaterial::default()); - app_local.materials.push(X3DMaterial::default()); + app_local.textures.push(Texture { + image_uri: "local1.jpg".to_string(), + }); + app_local.textures.push(Texture { + image_uri: "local2.jpg".to_string(), + }); + app_local.textures.push(Texture { + image_uri: "local3.jpg".to_string(), + }); + app_local.materials.push(Material::default()); + app_local.materials.push(Material::default()); let theme = app_local.themes.entry("default".to_string()).or_default(); theme .ring_id_to_texture @@ -152,12 +212,18 @@ mod tests { } { - app_global.textures.push(ParameterizedTexture::default()); - app_global.textures.push(ParameterizedTexture::default()); - app_global.textures.push(ParameterizedTexture::default()); - app_global.materials.push(X3DMaterial::default()); - app_global.materials.push(X3DMaterial::default()); - app_global.materials.push(X3DMaterial::default()); + app_global.textures.push(Texture { + image_uri: "global1.jpg".to_string(), + }); + app_global.textures.push(Texture { + image_uri: "global2.jpg".to_string(), + }); + app_global.textures.push(Texture { + image_uri: "global3.jpg".to_string(), + }); + app_global.materials.push(Material::default()); + app_global.materials.push(Material::default()); + app_global.materials.push(Material::default()); let theme = app_global.themes.entry("default".to_string()).or_default(); theme .ring_id_to_texture diff --git a/nusamai-plateau/src/models/appearance.rs b/nusamai-plateau/src/models/appearance.rs index 577266829..4dd5e53d5 100644 --- a/nusamai-plateau/src/models/appearance.rs +++ b/nusamai-plateau/src/models/appearance.rs @@ -1,111 +1,13 @@ -use std::io::BufRead; - use nusamai_citygml::appearance::TextureAssociation; use nusamai_citygml::{ - citygml_feature, citygml_property, schema, CityGmlElement, Code, LocalId, ParseError, Point, - SubTreeReader, Value, URI, + citygml_feature, citygml_property, CityGmlElement, Code, Color, ColorPlusOpacity, Double01, + LocalId, Point, URI, }; -type Double01 = f64; // TODO? type TextureType = String; // TODO? type WrapMode = String; // TODO? type TransformationMatrix2x2 = String; // FIXME -#[derive(Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)] -pub struct Color { - pub r: f64, - pub g: f64, - pub b: f64, -} - -impl CityGmlElement for Color { - fn parse(&mut self, st: &mut SubTreeReader) -> Result<(), ParseError> { - let text = st.parse_text()?; - let r: Result, _> = text - .split_ascii_whitespace() - .map(|s| s.parse::()) - .collect(); - match r { - Ok(v) if v.len() == 3 => { - (self.r, self.g, self.b) = (v[0], v[1], v[2]); - } - _ => { - return Err(ParseError::InvalidValue(format!( - "Failed to parse color value: {}", - text - ))) - } - } - Ok(()) - } - - fn into_object(self) -> Option { - Some(Value::Array(vec![ - Value::Double(self.r), - Value::Double(self.g), - Value::Double(self.b), - ])) - } - - fn collect_schema(_schema: &mut schema::Schema) -> schema::Attribute { - schema::Attribute { - type_ref: schema::TypeRef::Double, - min_occurs: 3, - max_occurs: Some(3), - } - } -} - -#[derive(Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)] -pub struct ColorPlusOpacity { - pub r: Double01, - pub g: Double01, - pub b: Double01, - pub a: Double01, -} - -impl CityGmlElement for ColorPlusOpacity { - fn parse(&mut self, st: &mut SubTreeReader) -> Result<(), ParseError> { - let text = st.parse_text()?; - let r: Result, _> = text - .split_ascii_whitespace() - .map(|s| s.parse::()) - .collect(); - match r { - Ok(v) if v.len() == 3 => { - (self.r, self.g, self.b, self.a) = (v[0], v[1], v[2], 1.0); - } - Ok(v) if v.len() == 4 => { - (self.r, self.g, self.b, self.a) = (v[0], v[1], v[2], v[3]); - } - _ => { - return Err(ParseError::InvalidValue(format!( - "Failed to parse color value: {}", - text - ))) - } - } - Ok(()) - } - - fn into_object(self) -> Option { - Some(Value::Array(vec![ - Value::Double(self.r), - Value::Double(self.g), - Value::Double(self.b), - Value::Double(self.a), - ])) - } - - fn collect_schema(_schema: &mut schema::Schema) -> schema::Attribute { - schema::Attribute { - type_ref: schema::TypeRef::Double, - min_occurs: 4, - max_occurs: Some(4), - } - } -} - #[citygml_property(name = "_:_AppearanceProperty")] pub enum AppearanceProperty { #[citygml(path = b"app:Appearance")] diff --git a/nusamai/Cargo.toml b/nusamai/Cargo.toml index 13df6d2bb..1dba116f9 100644 --- a/nusamai/Cargo.toml +++ b/nusamai/Cargo.toml @@ -4,7 +4,7 @@ version.workspace = true edition = "2021" [dependencies] -indexmap = { version = "2.1.0", features = ["serde"] } +indexmap = { version = "2.2.3", features = ["serde"] } rayon = "1.8.0" serde = { version = "1.0.193", features = ["derive"] } nusamai-plateau = { path = "../nusamai-plateau" } @@ -26,7 +26,7 @@ geojson = "0.24.1" serde_json = { version = "1.0.108", features = ["indexmap"] } url = "2.5.0" nusamai-gpkg = { path = "../nusamai-gpkg" } -tokio = { version = "1.35.1", features = ["full"] } +tokio = { version = "1.36", features = ["full"] } byteorder = "1.5.0" hashbrown = { version = "0.14.3", features = ["serde"] } ext-sort = { version = "0.1.4", features = ["memory-limit"] } @@ -37,7 +37,7 @@ pretty_env_logger = "0.5.0" itertools = "0.12.0" prost = "0.12.3" bytesize = "1.3.0" -ahash = "0.8.7" +ahash = "0.8.8" nusamai-shapefile = { path = "../nusamai-shapefile" } shapefile = "0.5.0" earcut-rs = { git = "https://github.com/MIERUNE/earcut-rs.git" } @@ -48,6 +48,6 @@ nusamai-kml = { path = "../nusamai-kml" } [dev-dependencies] rand = "0.8.5" -tokio = { version = "1.35.1", features = ["full"] } +tokio = { version = "1.36", features = ["full"] } nusamai-geometry = { path = "../nusamai-geometry" } byteorder = "1.5.0" diff --git a/nusamai/src/sink/cesiumtiles/mod.rs b/nusamai/src/sink/cesiumtiles/mod.rs index ee88633f4..a603d7f2d 100644 --- a/nusamai/src/sink/cesiumtiles/mod.rs +++ b/nusamai/src/sink/cesiumtiles/mod.rs @@ -90,10 +90,7 @@ struct SlicedFeature<'a> { impl DataSink for CesiumTilesSink { fn make_transform_requirements(&self) -> transformer::Requirements { - use transformer::RequirementItem; - transformer::Requirements { - mergedown: RequirementItem::Recommended(transformer::Mergedown::Full), ..Default::default() } } diff --git a/nusamai/src/sink/czml/mod.rs b/nusamai/src/sink/czml/mod.rs index 4b5f1d9f9..f3d95169d 100644 --- a/nusamai/src/sink/czml/mod.rs +++ b/nusamai/src/sink/czml/mod.rs @@ -60,10 +60,7 @@ pub struct CzmlSink { impl DataSink for CzmlSink { fn make_transform_requirements(&self) -> transformer::Requirements { - use transformer::RequirementItem; - transformer::Requirements { - mergedown: RequirementItem::Required(transformer::Mergedown::Geometry), ..Default::default() } } diff --git a/nusamai/src/sink/geojson/mod.rs b/nusamai/src/sink/geojson/mod.rs index 328ae48bd..1a44cd542 100644 --- a/nusamai/src/sink/geojson/mod.rs +++ b/nusamai/src/sink/geojson/mod.rs @@ -59,10 +59,7 @@ pub struct GeoJsonSink { impl DataSink for GeoJsonSink { fn make_transform_requirements(&self) -> transformer::Requirements { - use transformer::RequirementItem; - transformer::Requirements { - mergedown: RequirementItem::Required(transformer::Mergedown::Geometry), ..Default::default() } } diff --git a/nusamai/src/sink/geojson_transform_exp/mod.rs b/nusamai/src/sink/geojson_transform_exp/mod.rs index 332a088c5..8f2222956 100644 --- a/nusamai/src/sink/geojson_transform_exp/mod.rs +++ b/nusamai/src/sink/geojson_transform_exp/mod.rs @@ -60,8 +60,6 @@ pub struct GeoJsonTfExpSink { impl DataSink for GeoJsonTfExpSink { fn make_transform_requirements(&self) -> transformer::Requirements { - // use transformer::RequirementItem; - transformer::Requirements { ..Default::default() } diff --git a/nusamai/src/sink/gltf_poc/mod.rs b/nusamai/src/sink/gltf_poc/mod.rs index 40bae05c9..eb755cf37 100644 --- a/nusamai/src/sink/gltf_poc/mod.rs +++ b/nusamai/src/sink/gltf_poc/mod.rs @@ -2,7 +2,7 @@ use std::fs::File; use std::io::{BufWriter, Write}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use ahash::RandomState; use byteorder::{ByteOrder, LittleEndian}; @@ -79,10 +79,7 @@ pub struct GltfPocSink { impl DataSink for GltfPocSink { fn make_transform_requirements(&self) -> transformer::Requirements { - use transformer::RequirementItem; - transformer::Requirements { - mergedown: RequirementItem::Required(transformer::Mergedown::Geometry), ..Default::default() } } @@ -516,7 +513,7 @@ fn write_gltf( } // FIXME: This is the code to verify the operation with Cesium -fn write_3dtiles(bounding_volume: [f64; 6], output_path: &PathBuf) { +fn write_3dtiles(bounding_volume: [f64; 6], output_path: &Path) { // write 3DTiles let tileset_path = output_path.with_file_name("tileset.json"); let content_uri = output_path @@ -545,7 +542,7 @@ fn write_3dtiles(bounding_volume: [f64; 6], output_path: &PathBuf) { ..Default::default() }; - let mut tileset_file = File::create(&tileset_path).unwrap(); + let mut tileset_file = File::create(tileset_path).unwrap(); let tileset_writer = BufWriter::with_capacity(1024 * 1024, &mut tileset_file); serde_json::to_writer_pretty(tileset_writer, &tileset).unwrap(); } diff --git a/nusamai/src/sink/gpkg/mod.rs b/nusamai/src/sink/gpkg/mod.rs index c001e4382..d9cf69ac3 100644 --- a/nusamai/src/sink/gpkg/mod.rs +++ b/nusamai/src/sink/gpkg/mod.rs @@ -191,8 +191,6 @@ impl GpkgSink { impl DataSink for GpkgSink { fn make_transform_requirements(&self) -> transformer::Requirements { - // use transformer::RequirementItem; - transformer::Requirements { ..Default::default() } diff --git a/nusamai/src/sink/kml/mod.rs b/nusamai/src/sink/kml/mod.rs index 6344dd81d..b47e2b690 100644 --- a/nusamai/src/sink/kml/mod.rs +++ b/nusamai/src/sink/kml/mod.rs @@ -60,10 +60,7 @@ pub struct KmlSink { impl DataSink for KmlSink { fn make_transform_requirements(&self) -> transformer::Requirements { - use transformer::RequirementItem; - transformer::Requirements { - mergedown: RequirementItem::Required(transformer::Mergedown::Geometry), ..Default::default() } } diff --git a/nusamai/src/sink/mvt/mod.rs b/nusamai/src/sink/mvt/mod.rs index aaff79779..4db05b786 100644 --- a/nusamai/src/sink/mvt/mod.rs +++ b/nusamai/src/sink/mvt/mod.rs @@ -86,10 +86,8 @@ struct SlicedFeature<'a> { impl DataSink for MVTSink { fn make_transform_requirements(&self) -> transformer::Requirements { - use transformer::RequirementItem; - transformer::Requirements { - mergedown: RequirementItem::Recommended(transformer::Mergedown::Full), + key_value: transformer::KeyValueSpec::DotNotation, ..Default::default() } } diff --git a/nusamai/src/sink/noop/mod.rs b/nusamai/src/sink/noop/mod.rs index 79288364f..637943e5e 100644 --- a/nusamai/src/sink/noop/mod.rs +++ b/nusamai/src/sink/noop/mod.rs @@ -65,8 +65,6 @@ pub struct NoopSink { impl DataSink for NoopSink { fn make_transform_requirements(&self) -> transformer::Requirements { - // use transformer::RequirementItem; - transformer::Requirements { ..Default::default() } diff --git a/nusamai/src/sink/ply/mod.rs b/nusamai/src/sink/ply/mod.rs index f49107fde..c71f8473a 100644 --- a/nusamai/src/sink/ply/mod.rs +++ b/nusamai/src/sink/ply/mod.rs @@ -72,10 +72,7 @@ pub struct StanfordPlySink { impl DataSink for StanfordPlySink { fn make_transform_requirements(&self) -> transformer::Requirements { - use transformer::RequirementItem; - transformer::Requirements { - mergedown: RequirementItem::Required(transformer::Mergedown::Geometry), ..Default::default() } } diff --git a/nusamai/src/sink/serde/mod.rs b/nusamai/src/sink/serde/mod.rs index bd6f9b3eb..de18ae64f 100644 --- a/nusamai/src/sink/serde/mod.rs +++ b/nusamai/src/sink/serde/mod.rs @@ -59,8 +59,6 @@ pub struct SerdeSink { impl DataSink for SerdeSink { fn make_transform_requirements(&self) -> transformer::Requirements { - // use transformer::RequirementItem; - transformer::Requirements { ..Default::default() } diff --git a/nusamai/src/sink/shapefile/mod.rs b/nusamai/src/sink/shapefile/mod.rs index bc6a4a86e..bb26bff8d 100644 --- a/nusamai/src/sink/shapefile/mod.rs +++ b/nusamai/src/sink/shapefile/mod.rs @@ -56,10 +56,7 @@ pub struct ShapefileSink { impl DataSink for ShapefileSink { fn make_transform_requirements(&self) -> transformer::Requirements { - use transformer::RequirementItem; - transformer::Requirements { - mergedown: RequirementItem::Required(transformer::Mergedown::Geometry), ..Default::default() } } diff --git a/nusamai/src/transformer/builder.rs b/nusamai/src/transformer/builder.rs index eb203f613..4b868e445 100644 --- a/nusamai/src/transformer/builder.rs +++ b/nusamai/src/transformer/builder.rs @@ -6,80 +6,68 @@ use nusamai_projection::vshift::Jgd2011ToWgs84; use super::{transform::*, Transform}; use crate::transformer; -pub enum RequirementItem { - /// Required value (user must not override) - Required(T), - /// Recommended value (recommended value but user may override) - Recommended(T), - /// Default value - Default(T), -} - -impl RequirementItem { - pub fn into_value(self) -> T { - match self { - Self::Required(v) => v, - Self::Recommended(v) => v, - Self::Default(v) => v, - } - } -} - pub struct Requirements { /// Whether to shorten field names to 10 characters or less for Shapefiles. - pub shorten_names_for_shapefile: RequirementItem, - pub mergedown: RequirementItem, - pub feature_flattening: RequirementItem, + pub shorten_names_for_shapefile: bool, + pub tree_flattening: TreeFlatteningSpec, + pub mergedown: MergedownSpec, + pub key_value: KeyValueSpec, } impl Default for Requirements { fn default() -> Self { Self { - shorten_names_for_shapefile: RequirementItem::Required(false), - mergedown: RequirementItem::Required(Mergedown::Geometry), - feature_flattening: RequirementItem::Recommended( - FeatureFlattening::AllExceptThematicSurfaces, - ), + shorten_names_for_shapefile: false, + tree_flattening: TreeFlatteningSpec::None, + mergedown: MergedownSpec::RemoveDescendantFeatures, + key_value: KeyValueSpec::Jsonify, } } } pub struct Request { pub shorten_names_for_shapefile: bool, - pub mergedown: Mergedown, - pub feat_flattening: FeatureFlattening, + pub tree_flattening: TreeFlatteningSpec, + pub mergedown: MergedownSpec, + pub key_value: KeyValueSpec, } impl From for Request { fn from(req: Requirements) -> Self { Self { - shorten_names_for_shapefile: req.shorten_names_for_shapefile.into_value(), - mergedown: req.mergedown.into_value(), - feat_flattening: req.feature_flattening.into_value(), + shorten_names_for_shapefile: req.shorten_names_for_shapefile, + tree_flattening: req.tree_flattening, + mergedown: req.mergedown, + key_value: req.key_value, } } } -#[derive(Default)] -pub enum FeatureFlattening { +pub enum TreeFlatteningSpec { /// No feature flattening None, /// Flatten all features except thematic surfaces - #[default] AllExceptThematicSurfaces, /// Flatten all features All, } -#[derive(Default)] -pub enum Mergedown { +pub enum MergedownSpec { /// No mergedown + NoMergedown, + /// merge the children's geometries into the root and retain the children features + RetainDescendantFeatures, + /// merge the children's geometries into the root and remove the children features + RemoveDescendantFeatures, +} + +/// Specifies how to transform nested objects and arrays +pub enum KeyValueSpec { None, - /// Merge all geometries into the root feature - #[default] - Geometry, - /// Merge all geometries and data attributes into the root feature - Full, + // JSONify nested objects and arrays + Jsonify, + // Flatten nested objects and arrays as dot-split keys (e.g. `buildingDisasterRiskAttribute.0.rankOrg`) + DotNotation, } pub trait TransformBuilder: Send + Sync { @@ -98,7 +86,7 @@ pub struct NusamaiTransformBuilder { impl TransformBuilder for NusamaiTransformBuilder { fn build(&self) -> Box { let mut transforms = SerialTransform::default(); - // TODO: build transformation based on config + // TODO: build transformation based on config file // Transform the coordinate system transforms.push(Box::new(ProjectionTransform::new(self.jgd2wgs.clone()))); @@ -113,24 +101,36 @@ impl TransformBuilder for NusamaiTransformBuilder { transforms.push(Box::::default()); - match self.request.feat_flattening { - FeatureFlattening::None => {} - FeatureFlattening::AllExceptThematicSurfaces => { - transforms.push(Box::new(FlattenFeatureTransform::new(false))); + match self.request.tree_flattening { + TreeFlatteningSpec::None => {} + TreeFlatteningSpec::AllExceptThematicSurfaces => { + transforms.push(Box::new(FlattenTreeTransform::new())); } - FeatureFlattening::All => { - transforms.push(Box::new(FlattenFeatureTransform::new(true))); + TreeFlatteningSpec::All => { + transforms.push(Box::new(FlattenTreeTransform::new())); } } match self.request.mergedown { - Mergedown::Geometry => { - transforms.push(Box::::default()); + MergedownSpec::NoMergedown => {} + MergedownSpec::RemoveDescendantFeatures => { + transforms.push(Box::new(GeometricMergedownTransform::new(false))); + } + MergedownSpec::RetainDescendantFeatures => { + transforms.push(Box::new(GeometricMergedownTransform::new(true))); + } + } + + match self.request.key_value { + KeyValueSpec::Jsonify => { + transforms.push(Box::::default()); + } + KeyValueSpec::DotNotation => { + transforms.push(Box::::default()); } - Mergedown::Full => { - transforms.push(Box::::default()); + KeyValueSpec::None => { + // No-op } - Mergedown::None => {} } Box::new(transforms) diff --git a/nusamai/src/transformer/transform/dots.rs b/nusamai/src/transformer/transform/dots.rs new file mode 100644 index 000000000..537a63cba --- /dev/null +++ b/nusamai/src/transformer/transform/dots.rs @@ -0,0 +1,71 @@ +use crate::transformer::Transform; + +use nusamai_citygml::object::{Map, Value}; +use nusamai_citygml::schema::{Schema, TypeDef}; +use nusamai_plateau::Entity; + +/// Collect all attributes in dot notation +#[derive(Default, Clone)] +pub struct DotNotationTransform { + path_buf: String, +} + +impl Transform for DotNotationTransform { + fn transform(&mut self, mut entity: Entity, out: &mut Vec) { + if let Value::Object(mut obj) = entity.root { + let mut new_attrs = Default::default(); + let path = &mut self.path_buf; + path.clear(); + + collect_all_attrs(&mut new_attrs, path, obj.attributes); + + obj.attributes = new_attrs; + entity.root = Value::Object(obj); + out.push(entity); + } + } + + fn transform_schema(&self, schema: &mut Schema) { + for ty in schema.types.values_mut() { + match ty { + TypeDef::Data(data) => data.additional_attributes = true, + TypeDef::Feature(feat) => feat.additional_attributes = true, + TypeDef::Property(_) => continue, + }; + } + } +} + +fn collect_all_attrs(new_attrs: &mut Map, path: &mut String, attributes: Map) { + for (key, value) in attributes { + let path_len = path.len(); + path.push_str(&key); + match value { + Value::Object(obj) => { + path.push('.'); + collect_all_attrs(new_attrs, path, obj.attributes); + } + Value::Array(arr) => { + path.push('.'); + for (i, value) in arr.into_iter().enumerate() { + let len = path.len(); + path.push_str(&format!("{i}")); + match value { + Value::Object(obj) => { + path.push('.'); + collect_all_attrs(new_attrs, path, obj.attributes); + } + _ => { + new_attrs.insert(path.clone(), value); + } + } + path.truncate(len); + } + } + _ => { + new_attrs.insert(path.to_string(), value); + } + } + path.truncate(path_len); + } +} diff --git a/nusamai/src/transformer/transform/flatten.rs b/nusamai/src/transformer/transform/flatten.rs index 86132c432..8fb2a50f6 100644 --- a/nusamai/src/transformer/transform/flatten.rs +++ b/nusamai/src/transformer/transform/flatten.rs @@ -8,14 +8,34 @@ use nusamai_citygml::GeometryStore; use nusamai_plateau::appearance::AppearanceStore; use nusamai_plateau::Entity; -pub struct FlattenFeatureTransform { +#[derive(Default)] +pub struct FlattenTreeTransform { split_thematic_surfaces: bool, + split_data_stereotype: bool, + split_object_stereotype: bool, } -impl Transform for FlattenFeatureTransform { +impl FlattenTreeTransform { + pub fn with_split_thematic_surfaces(mut self, split: bool) -> Self { + self.split_thematic_surfaces = split; + self + } + + pub fn with_split_data_stereotype(mut self, split: bool) -> Self { + self.split_data_stereotype = split; + self + } + + pub fn with_split_object_stereotype(mut self, split: bool) -> Self { + self.split_object_stereotype = split; + self + } +} + +impl Transform for FlattenTreeTransform { fn transform(&mut self, entity: Entity, out: &mut Vec) { let geom_store = entity.geometry_store; - let appearance_store = &entity.appearance_store; + let appearance_store = entity.appearance_store; self.flatten_feature(entity.root, &geom_store, &appearance_store, out, &None); } @@ -48,11 +68,9 @@ struct Parent { typename: String, } -impl FlattenFeatureTransform { - pub fn new(split_thematic_surfaces: bool) -> Self { - Self { - split_thematic_surfaces, - } +impl FlattenTreeTransform { + pub fn new() -> Self { + Default::default() } fn flatten_feature( @@ -105,7 +123,7 @@ impl FlattenFeatureTransform { Some(Value::Object(obj)) } Value::Array(mut arr) => { - let mut new_arr = Vec::new(); + let mut new_arr = Vec::with_capacity(arr.len()); for value in arr.drain(..) { if let Some(v) = self.flatten_feature(value, geom_store, appearance_store, out, parent) diff --git a/nusamai/src/transformer/transform/geommerge.rs b/nusamai/src/transformer/transform/geommerge.rs new file mode 100644 index 000000000..f7f6b5958 --- /dev/null +++ b/nusamai/src/transformer/transform/geommerge.rs @@ -0,0 +1,66 @@ +use crate::transformer::Transform; + +use hashbrown::HashSet; +use nusamai_citygml::object::{Object, ObjectStereotype, Value}; +use nusamai_citygml::schema::Schema; +use nusamai_citygml::GeometryRef; +use nusamai_plateau::Entity; + +#[derive(Default, Clone)] +pub struct GeometricMergedownTransform { + geoms_buf: HashSet, + /// If false, all descendant features will be removed after merging geometries. + retain_descendant_features: bool, +} + +impl GeometricMergedownTransform { + pub fn new(retain_descendant_features: bool) -> Self { + Self { + retain_descendant_features, + ..Default::default() + } + } +} + +impl Transform for GeometricMergedownTransform { + fn transform(&mut self, mut entity: Entity, out: &mut Vec) { + if let Value::Object(obj) = &mut entity.root { + self.collect_all_geoms(obj); + if let ObjectStereotype::Feature { geometries, .. } = &mut obj.stereotype { + *geometries = self.geoms_buf.drain().collect(); + } + out.push(entity); + } + } + + fn transform_schema(&self, _schema: &mut Schema) { + // do nothing + } +} + +impl GeometricMergedownTransform { + fn collect_all_geoms(&mut self, obj: &mut Object) -> bool { + let mut is_feature = false; + if let ObjectStereotype::Feature { geometries, .. } = &mut obj.stereotype { + is_feature = true; + self.geoms_buf.extend(geometries.drain(..)); + } + + obj.attributes.retain(|_key, value| match value { + Value::Object(obj) => self.retain_descendant_features || !self.collect_all_geoms(obj), + Value::Array(arr) => { + arr.retain_mut(|value| { + if let Value::Object(obj) = value { + self.retain_descendant_features || !self.collect_all_geoms(obj) + } else { + true + } + }); + !arr.is_empty() + } + _ => true, + }); + + is_feature + } +} diff --git a/nusamai/src/transformer/transform/jsonify.rs b/nusamai/src/transformer/transform/jsonify.rs new file mode 100644 index 000000000..05de01d7b --- /dev/null +++ b/nusamai/src/transformer/transform/jsonify.rs @@ -0,0 +1,43 @@ +use crate::transformer::Transform; + +use nusamai_citygml::object::Value; +use nusamai_citygml::schema::{DataTypeDef, FeatureTypeDef, Schema, TypeDef, TypeRef}; +use nusamai_plateau::Entity; + +/// Jsonify all objects and arrays in the entity. +#[derive(Default, Clone)] +pub struct JsonifyTransform {} + +impl Transform for JsonifyTransform { + fn transform(&mut self, mut entity: Entity, out: &mut Vec) { + if let Value::Object(obj) = &mut entity.root { + for value in obj.attributes.values_mut() { + match value { + Value::Object(_) | Value::Array(_) => { + *value = Value::String(value.to_attribute_json().to_string()) + } + _ => {} + } + } + out.push(entity) + } + } + + fn transform_schema(&self, schema: &mut Schema) { + for ty in schema.types.values_mut() { + match ty { + TypeDef::Feature(FeatureTypeDef { attributes, .. }) + | TypeDef::Data(DataTypeDef { attributes, .. }) => { + for attr in attributes.values_mut() { + if let TypeRef::Named(_) = attr.type_ref { + attr.type_ref = TypeRef::JsonString; + } else if attr.max_occurs != Some(1) { + attr.type_ref = TypeRef::JsonString; + } + } + } + TypeDef::Property(_) => {} + } + } + } +} diff --git a/nusamai/src/transformer/transform/lods.rs b/nusamai/src/transformer/transform/lods.rs index 1505705e8..ff1ee2227 100644 --- a/nusamai/src/transformer/transform/lods.rs +++ b/nusamai/src/transformer/transform/lods.rs @@ -9,6 +9,7 @@ use nusamai_plateau::Entity; #[derive(Default, Clone)] pub struct FilterLodTransform {} +/// Transform to filter and split the LODs impl Transform for FilterLodTransform { fn transform(&mut self, mut entity: Entity, out: &mut Vec) { let lodmask = find_lods(&entity.root); diff --git a/nusamai/src/transformer/transform/merge.rs b/nusamai/src/transformer/transform/merge.rs deleted file mode 100644 index ef0cd3cc1..000000000 --- a/nusamai/src/transformer/transform/merge.rs +++ /dev/null @@ -1,163 +0,0 @@ -use crate::transformer::Transform; - -use hashbrown::HashSet; -use nusamai_citygml::object::{Map, Object, ObjectStereotype, Value}; -use nusamai_citygml::schema::{Schema, TypeDef}; -use nusamai_citygml::GeometryRef; -use nusamai_plateau::Entity; - -/// Collect all attributes and geometries from the descendants and merge them into the root object. -#[derive(Default, Clone)] -pub struct FullMergedownTransform { - geoms_buf: HashSet, - path_buf: String, -} - -impl Transform for FullMergedownTransform { - fn transform(&mut self, mut entity: Entity, out: &mut Vec) { - if let Value::Object(obj) = entity.root { - let mut new_attrs = Default::default(); - let new_geoms = &mut self.geoms_buf; - let path = &mut self.path_buf; - path.clear(); - - collect_all_attrs_and_geoms(&mut new_attrs, new_geoms, path, obj.attributes, false); - - if let ObjectStereotype::Feature { id, geometries } = obj.stereotype { - new_geoms.extend(geometries); - entity.root = Value::Object(Object { - typename: obj.typename, - attributes: new_attrs, - stereotype: nusamai_citygml::object::ObjectStereotype::Feature { - id, - geometries: new_geoms.drain().collect(), - }, - }); - out.push(entity); - } - } - } - - fn transform_schema(&self, schema: &mut Schema) { - for ty in schema.types.values_mut() { - match ty { - TypeDef::Data(data) => data.additional_attributes = true, - TypeDef::Feature(feat) => feat.additional_attributes = true, - TypeDef::Property(_) => continue, - }; - } - } -} - -fn collect_all_attrs_and_geoms( - new_attrs: &mut Map, - new_geoms: &mut HashSet, - path: &mut String, - attributes: Map, - in_child_feature: bool, -) { - for (key, value) in attributes { - let path_len = path.len(); - path.push_str(&key); - path.push('.'); - match value { - Value::Object(obj) => { - let in_child_feature = - if let ObjectStereotype::Feature { geometries, .. } = obj.stereotype { - new_geoms.extend(geometries); - true - } else { - in_child_feature - }; - collect_all_attrs_and_geoms( - new_attrs, - new_geoms, - path, - obj.attributes, - in_child_feature, - ); - } - Value::Array(arr) => { - for (i, value) in arr.into_iter().enumerate() { - let len = path.len(); - path.push_str(&format!("{i}")); - path.push('.'); - match value { - Value::Object(obj) => { - let in_child_feature = if let ObjectStereotype::Feature { - geometries, - .. - } = obj.stereotype - { - new_geoms.extend(geometries); - true - } else { - in_child_feature - }; - collect_all_attrs_and_geoms( - new_attrs, - new_geoms, - path, - obj.attributes, - in_child_feature, - ); - } - _ => { - new_attrs.insert(path.clone(), value); - } - } - path.truncate(len); - } - } - _ => { - if !in_child_feature { - new_attrs.insert(path[..path.len() - 1].to_string(), value); - } - } - } - path.truncate(path_len); - } -} - -#[derive(Default, Clone)] -pub struct GeometricMergedownTransform { - geoms_buf: HashSet, -} - -impl Transform for GeometricMergedownTransform { - fn transform(&mut self, mut entity: Entity, out: &mut Vec) { - if let Value::Object(obj) = &mut entity.root { - let new_geoms = &mut self.geoms_buf; - collect_all_geoms(new_geoms, obj); - if let ObjectStereotype::Feature { geometries, .. } = &mut obj.stereotype { - *geometries = new_geoms.drain().collect(); - } - out.push(entity); - } - } - - fn transform_schema(&self, _schema: &mut Schema) { - // do nothing - } -} - -fn collect_all_geoms(new_geoms: &mut HashSet, obj: &mut Object) { - if let ObjectStereotype::Feature { geometries, .. } = &mut obj.stereotype { - new_geoms.extend(geometries.drain(..)); - } - for value in obj.attributes.values_mut() { - match value { - Value::Object(obj) => { - collect_all_geoms(new_geoms, obj); - } - Value::Array(arr) => { - for value in arr { - if let Value::Object(obj) = value { - collect_all_geoms(new_geoms, obj); - } - } - } - _ => {} - } - } -} diff --git a/nusamai/src/transformer/transform/mod.rs b/nusamai/src/transformer/transform/mod.rs index 33120a939..4bf2bb230 100644 --- a/nusamai/src/transformer/transform/mod.rs +++ b/nusamai/src/transformer/transform/mod.rs @@ -1,13 +1,17 @@ mod attrname; +mod dots; mod flatten; +mod geommerge; +mod jsonify; mod lods; -mod merge; mod projection; pub use attrname::*; +pub use dots::*; pub use flatten::*; +pub use geommerge::*; +pub use jsonify::*; pub use lods::*; -pub use merge::*; pub use projection::*; use super::Transform;