diff --git a/Cargo.toml b/Cargo.toml index 267b7aa0..956f5343 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,6 @@ authors = ["MIERUNE Inc. "] inherits = "release" lto = "fat" panic = "abort" + +[profile.dev.package.zune-image] +opt-level = 3 diff --git a/nusamai-citygml/Cargo.toml b/nusamai-citygml/Cargo.toml index bc93471c..b184fa93 100644 --- a/nusamai-citygml/Cargo.toml +++ b/nusamai-citygml/Cargo.toml @@ -19,4 +19,4 @@ quick-xml = "0.31" serde = { version = "1.0", features = ["derive"], optional = true } serde_json = { version = "1.0.113", features = ["indexmap"], optional = true } thiserror = "1.0" -url = "2.5.0" +url = { version = "2.5.0", features = ["serde"] } diff --git a/nusamai-citygml/src/object.rs b/nusamai-citygml/src/object.rs index dbf6d3d9..144d9143 100644 --- a/nusamai-citygml/src/object.rs +++ b/nusamai-citygml/src/object.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use crate::geometry::GeometryRefs; -use crate::values::{Code, Date, Point, URI}; +use crate::values::{Code, Date, Point, Uri}; use crate::Measure; use serde::{Deserialize, Serialize}; @@ -49,7 +49,7 @@ pub enum Value { Double(f64), Measure(Measure), Boolean(bool), - URI(URI), + Uri(Uri), Date(Date), Point(Point), Array(Vec), @@ -92,7 +92,7 @@ impl Value { serde_json::Value::Number(serde_json::Number::from_f64(m.value()).unwrap()) } Boolean(b) => serde_json::Value::Bool(*b), - URI(u) => serde_json::Value::String(u.value().clone()), + Uri(u) => serde_json::Value::String(u.value().to_string()), Date(d) => serde_json::Value::String(d.to_string()), // ISO 8601 Point(_) => { // TODO: Handle Point @@ -149,8 +149,8 @@ mod tests { let obj = Value::Boolean(true); assert_eq!(obj.to_attribute_json(), json!(true)); - let obj = Value::URI(URI::new("http://example.com")); - assert_eq!(obj.to_attribute_json(), json!("http://example.com")); + let obj = Value::Uri(Uri::new(url::Url::parse("http://example.com").unwrap())); + assert_eq!(obj.to_attribute_json(), json!("http://example.com/")); let obj = Value::Date(Date::from_ymd_opt(2020, 1, 1).unwrap()); assert_eq!(obj.to_attribute_json(), json!("2020-01-01")); diff --git a/nusamai-citygml/src/parser.rs b/nusamai-citygml/src/parser.rs index 3ead5909..6ef48ba5 100644 --- a/nusamai-citygml/src/parser.rs +++ b/nusamai-citygml/src/parser.rs @@ -71,7 +71,7 @@ impl<'a> InternalState<'a> { } pub struct ParseContext<'a> { - source_uri: Option, + source_uri: Url, code_resolver: &'a dyn CodeResolver, // Mapping a string gml:id to an integer ID, unique in a single document id_map: indexmap::IndexSet, @@ -80,14 +80,14 @@ pub struct ParseContext<'a> { impl<'a> ParseContext<'a> { pub fn new(source_uri: Url, code_resolver: &'a dyn CodeResolver) -> Self { Self { - source_uri: Some(source_uri), + source_uri, code_resolver, ..Default::default() } } - pub fn source_url(&self) -> Option<&Url> { - self.source_uri.as_ref() + pub fn source_url(&self) -> &Url { + &self.source_uri } pub fn code_resolver(&self) -> &dyn CodeResolver { @@ -103,7 +103,7 @@ impl<'a> ParseContext<'a> { impl<'a> Default for ParseContext<'a> { fn default() -> Self { Self { - source_uri: None, + source_uri: Url::parse("file:///").unwrap(), code_resolver: &codelist::NoopResolver {}, id_map: indexmap::IndexSet::default(), } diff --git a/nusamai-citygml/src/values.rs b/nusamai-citygml/src/values.rs index 7ddcc772..f13e2ca0 100644 --- a/nusamai-citygml/src/values.rs +++ b/nusamai-citygml/src/values.rs @@ -5,6 +5,7 @@ use crate::{CityGmlElement, ParseContext}; pub use chrono::NaiveDate; use serde::{Deserialize, Serialize}; use std::io::BufRead; +use url::Url; // type aliases pub type Date = chrono::NaiveDate; @@ -33,27 +34,46 @@ impl CityGmlElement for String { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq, Eq)] -pub struct URI(String); +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] +pub struct Uri(url::Url); -impl URI { - pub fn new(s: &str) -> Self { - Self(s.into()) +impl Uri { + pub fn new(s: url::Url) -> Self { + Self(s) } - pub fn value(&self) -> &String { + pub fn value(&self) -> &Url { &self.0 } + pub fn into_inner(self) -> Url { + self.0 + } +} + +impl From for Uri { + fn from(url: url::Url) -> Self { + Self(url) + } } -impl CityGmlElement for URI { +impl Default for Uri { + fn default() -> Self { + Self(Url::parse("file:///default").unwrap()) + } +} + +impl CityGmlElement for Uri { #[inline] fn parse(&mut self, st: &mut SubTreeReader) -> Result<(), ParseError> { - self.0.push_str(st.parse_text()?); + let text = st.parse_text()?.to_string(); + let base_url = st.context().source_url(); + self.0 = base_url + .join(&text) + .map_err(|_| ParseError::InvalidValue("Invalid URI: {text}".to_string()))?; Ok(()) } fn into_object(self) -> Option { - Some(Value::String(self.0)) + Some(Value::String(self.0.to_string())) } fn collect_schema(_schema: &mut schema::Schema) -> schema::Attribute { @@ -88,23 +108,22 @@ impl CityGmlElement for Code { self.code = code.clone(); if let Some(code_space) = code_space { - if let Some(base_url) = st.context().source_url() { - match st - .context() - .code_resolver() - .resolve(base_url, &code_space, &code) - { - Ok(Some(v)) => { - self.value = v; - return Ok(()); - } - Ok(None) => {} - Err(_) => { - // FIXME - log::warn!("Failed to lookup code {} form {}", code, code_space); - self.value = code; - return Ok(()); - } + let base_url = st.context().source_url(); + match st + .context() + .code_resolver() + .resolve(base_url, &code_space, &code) + { + Ok(Some(v)) => { + self.value = v; + return Ok(()); + } + Ok(None) => {} + Err(_) => { + // FIXME + log::warn!("Failed to lookup code {} form {}", code, code_space); + self.value = code; + return Ok(()); } } } @@ -569,7 +588,7 @@ pub struct GenericAttribute { pub measure_attrs: Vec<(String, Measure)>, pub code_attrs: Vec<(String, Code)>, pub date_attrs: Vec<(String, Date)>, - pub uri_attrs: Vec<(String, URI)>, + pub uri_attrs: Vec<(String, Uri)>, pub generic_attr_set: Vec<(String, GenericAttribute)>, } @@ -634,7 +653,7 @@ impl CityGmlElement for GenericAttribute { .into_iter() .map(|(k, v)| (k, Value::Date(v))), ); - map.extend(self.uri_attrs.into_iter().map(|(k, v)| (k, Value::URI(v)))); + map.extend(self.uri_attrs.into_iter().map(|(k, v)| (k, Value::Uri(v)))); map.extend( self.generic_attr_set .into_iter() diff --git a/nusamai-citygml/tests/values.rs b/nusamai-citygml/tests/values.rs index 84176ef3..4e37fdcd 100644 --- a/nusamai-citygml/tests/values.rs +++ b/nusamai-citygml/tests/values.rs @@ -1,7 +1,8 @@ use nusamai_citygml::{ citygml_feature, values, CityGmlElement, CityGmlReader, Date, Measure, ParseContext, - ParseError, Value, URI, + ParseError, Uri, Value, }; +use url::Url; #[test] fn parse_date() { @@ -79,7 +80,7 @@ fn parse_basic_types() { #[citygml(path = b"measure")] measure: Option, #[citygml(path = b"uri")] - uri: Option, + uri: Option, #[citygml(path = b"date")] date: Option, } @@ -109,7 +110,7 @@ fn parse_basic_types() { assert!((root.measure.as_ref().unwrap().value() - 3.4).abs() < 1e-15); assert_eq!(root.string.as_ref().unwrap(), "hello"); assert_eq!( - root.uri.as_ref().unwrap().value(), + root.uri.as_ref().unwrap().value().to_string(), "https://example.com/foo?bar=2000" ); assert!(root.bool.unwrap()); @@ -244,7 +245,9 @@ fn generics() { ); assert_eq!( data.attributes["u1"], - Value::URI(values::URI::new("https://foo.com/hoge")) + Value::Uri(values::Uri::new( + Url::parse("https://foo.com/hoge").unwrap() + )) ); let Value::Object(set1) = &data.attributes["set1"] else { diff --git a/nusamai-plateau/src/appearance.rs b/nusamai-plateau/src/appearance.rs index a99ae8f9..a5de9b94 100644 --- a/nusamai-plateau/src/appearance.rs +++ b/nusamai-plateau/src/appearance.rs @@ -2,9 +2,10 @@ use crate::models::appearance::{self, ParameterizedTexture, SurfaceDataProperty, X3DMaterial}; use hashbrown::HashMap; -use nusamai_citygml::{appearance::TextureAssociation, Color, LocalId, SurfaceSpan, URI}; +use nusamai_citygml::{appearance::TextureAssociation, Color, LocalId, SurfaceSpan}; use nusamai_geometry::LineString2; use std::hash::{Hash, Hasher}; +use url::Url; #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] pub struct Theme { @@ -59,19 +60,20 @@ pub struct AppearanceStore { /// Texture (CityGML's ParameterizedTexture) #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Texture { - pub image_uri: String, - // TOOD: other parameters + pub image_url: Url, + // TODO: 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(), - } + let image_url = src + .image_uri + .map(|uri| uri.into_inner()) + .unwrap_or_else(|| { + log::warn!("image_uri is not set"); + url::Url::parse("url_not_found.jpg").unwrap() + }); + Self { image_url } } } @@ -187,13 +189,13 @@ mod tests { { app_local.textures.push(Texture { - image_uri: "local1.jpg".to_string(), + image_url: Url::parse("file:///local1.jpg").unwrap(), }); app_local.textures.push(Texture { - image_uri: "local2.jpg".to_string(), + image_url: Url::parse("file:///local2.jpg").unwrap(), }); app_local.textures.push(Texture { - image_uri: "local3.jpg".to_string(), + image_url: Url::parse("file:///local3.jpg").unwrap(), }); app_local.materials.push(Material::default()); app_local.materials.push(Material::default()); @@ -214,13 +216,13 @@ mod tests { { app_global.textures.push(Texture { - image_uri: "global1.jpg".to_string(), + image_url: Url::parse("file:///global1.jpg").unwrap(), }); app_global.textures.push(Texture { - image_uri: "global2.jpg".to_string(), + image_url: Url::parse("file:///global2.jpg").unwrap(), }); app_global.textures.push(Texture { - image_uri: "global3.jpg".to_string(), + image_url: Url::parse("file:///global3.jpg").unwrap(), }); app_global.materials.push(Material::default()); app_global.materials.push(Material::default()); diff --git a/nusamai-plateau/src/entity.rs b/nusamai-plateau/src/entity.rs index a938b8d9..6d90a67c 100644 --- a/nusamai-plateau/src/entity.rs +++ b/nusamai-plateau/src/entity.rs @@ -10,6 +10,8 @@ use crate::appearance::AppearanceStore; pub struct Entity { /// Attribute tree pub root: Value, + /// Base url of the entity + pub base_url: url::Url, /// All geometries referenced by the attribute tree pub geometry_store: Arc>, /// All appearances used in this city object diff --git a/nusamai-plateau/src/models/appearance.rs b/nusamai-plateau/src/models/appearance.rs index 4dd5e53d..b90a8650 100644 --- a/nusamai-plateau/src/models/appearance.rs +++ b/nusamai-plateau/src/models/appearance.rs @@ -1,7 +1,7 @@ use nusamai_citygml::appearance::TextureAssociation; use nusamai_citygml::{ citygml_feature, citygml_property, CityGmlElement, Code, Color, ColorPlusOpacity, Double01, - LocalId, Point, URI, + LocalId, Point, Uri, }; type TextureType = String; // TODO? @@ -72,7 +72,7 @@ pub struct ParameterizedTexture { pub is_front: Option, #[citygml(path = b"app:imageURI", required)] - pub image_uri: Option, + pub image_uri: Option, #[citygml(path = b"app:mimeType")] pub mime_type: Option, @@ -96,7 +96,7 @@ pub struct GeoreferencedTexture { pub is_front: Option, #[citygml(path = b"app:imageURI", required)] - pub image_uri: Option, + pub image_uri: Option, #[citygml(path = b"app:mimeType")] pub mime_type: Option, @@ -120,5 +120,5 @@ pub struct GeoreferencedTexture { pub orientation: Option, #[citygml(path = b"app:target")] - pub target: Vec, + pub target: Vec, } diff --git a/nusamai-plateau/src/models/iur/urf/zone.rs b/nusamai-plateau/src/models/iur/urf/zone.rs index d78f538c..6104933c 100644 --- a/nusamai-plateau/src/models/iur/urf/zone.rs +++ b/nusamai-plateau/src/models/iur/urf/zone.rs @@ -1,6 +1,6 @@ use nusamai_citygml::{ citygml_data, citygml_feature, citygml_property, CityGmlElement, Code, Date, GYear, Length, - Measure, URI, + Measure, Uri, }; #[citygml_property(name = "urf:DistrictFacilityProperty")] @@ -95,7 +95,7 @@ pub struct Zone { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -173,7 +173,7 @@ pub struct Agreement { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -257,7 +257,7 @@ pub struct AircraftNoiseControlZone { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -338,7 +338,7 @@ pub struct AreaClassification { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -419,7 +419,7 @@ pub struct CollectiveFacilitiesForReconstruction { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -530,7 +530,7 @@ pub struct CollectiveFacilitiesForReconstructionAndRevitalization { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -641,7 +641,7 @@ pub struct CollectiveFacilitiesForTsunamiDisasterPrevention { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -752,7 +752,7 @@ pub struct CollectiveGovernmentAndPublicOfficeFacilities { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -848,7 +848,7 @@ pub struct CollectiveHousingFacilities { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -956,7 +956,7 @@ pub struct CollectiveUrbanDisasterPreventionFacilities { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -1058,7 +1058,7 @@ pub struct ConservationZoneForClustersOfTraditionalStructures { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -1139,7 +1139,7 @@ pub struct DisasterPreventionBlockImprovementProject { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -1229,7 +1229,7 @@ pub struct DisasterPreventionBlockImprovementZonePlan { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -1328,7 +1328,7 @@ pub struct DistributionBusinessPark { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -1436,7 +1436,7 @@ pub struct DistributionBusinessZone { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -1520,7 +1520,7 @@ pub struct District { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -1658,7 +1658,7 @@ pub struct DistrictDevelopmentPlan { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -1757,7 +1757,7 @@ pub struct DistrictFacility { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -1837,7 +1837,7 @@ pub struct DistrictImprovementPlanForDisasterPreventionBlockImprovementZonePlan pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -1938,7 +1938,7 @@ pub struct DistrictImprovementPlanForHistoricSceneryMaintenanceAndImprovementDis pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -2037,7 +2037,7 @@ pub struct DistrictPlan { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -2133,7 +2133,7 @@ pub struct DistrictsAndZones { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -2214,7 +2214,7 @@ pub struct EducationalAndCulturalFacility { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -2298,7 +2298,7 @@ pub struct ExceptionalFloorAreaRateDistrict { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -2382,7 +2382,7 @@ pub struct FirePreventionDistrict { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -2463,7 +2463,7 @@ pub struct FireProtectionFacility { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -2553,7 +2553,7 @@ pub struct FloodPreventionFacility { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -2643,7 +2643,7 @@ pub struct GlobalHubCityDevelopmentProject { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -2730,7 +2730,7 @@ pub struct GreenSpaceConservationDistrict { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -2811,7 +2811,7 @@ pub struct HeightControlDistrict { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -2898,7 +2898,7 @@ pub struct HighLevelUseDistrict { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -2994,7 +2994,7 @@ pub struct HighRiseResidentialAttractionDistrict { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -3084,7 +3084,7 @@ pub struct HistoricSceneryMaintenanceAndImprovementDistrictPlan { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -3177,7 +3177,7 @@ pub struct HousingControlArea { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -3258,7 +3258,7 @@ pub struct IndustrialParkDevelopmentProject { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -3345,7 +3345,7 @@ pub struct LandReadjustmentProject { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -3432,7 +3432,7 @@ pub struct LandReadjustmentPromotionArea { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -3516,7 +3516,7 @@ pub struct LandReadjustmentPromotionAreasForCoreBusinessUrbanDevelopment { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -3600,7 +3600,7 @@ pub struct LandscapeZone { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -3696,7 +3696,7 @@ pub struct MarketsSlaughterhousesCrematoria { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -3780,7 +3780,7 @@ pub struct MedicalFacility { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -3864,7 +3864,7 @@ pub struct NewHousingAndUrbanDevelopmentProject { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -3954,7 +3954,7 @@ pub struct NewUrbanInfrastructureProject { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -4044,7 +4044,7 @@ pub struct OpenSpaceForPublicUse { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -4131,7 +4131,7 @@ pub struct ParkingPlaceDevelopmentZone { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -4212,7 +4212,7 @@ pub struct PortZone { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -4296,7 +4296,7 @@ pub struct PrivateUrbanRenewalProjectPlan { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -4380,7 +4380,7 @@ pub struct ProductiveGreenZone { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -4467,7 +4467,7 @@ pub struct ProjectPromotionArea { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -4551,7 +4551,7 @@ pub struct PromotionDistrict { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -4629,7 +4629,7 @@ pub struct QuasiUrbanPlanningArea { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -4716,7 +4716,7 @@ pub struct Regulation { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -4794,7 +4794,7 @@ pub struct ResidenceAttractionArea { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -4872,7 +4872,7 @@ pub struct ResidentialBlockConstructionProject { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -4965,7 +4965,7 @@ pub struct ResidentialBlockConstructionPromotionArea { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -5049,7 +5049,7 @@ pub struct ResidentialEnvironmentImprovementDistrict { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -5148,7 +5148,7 @@ pub struct RoadsideDistrictFacility { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -5226,7 +5226,7 @@ pub struct RoadsideDistrictImprovementPlan { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -5328,7 +5328,7 @@ pub struct RoadsideDistrictPlan { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -5424,7 +5424,7 @@ pub struct RuralDistrictFacility { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -5502,7 +5502,7 @@ pub struct RuralDistrictImprovementPlan { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -5604,7 +5604,7 @@ pub struct RuralDistrictPlan { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -5694,7 +5694,7 @@ pub struct SandControlFacility { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -5784,7 +5784,7 @@ pub struct ScenicDistrict { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -5877,7 +5877,7 @@ pub struct ScheduledAreaForCollectiveGovernmentAndPublicOfficeFacilities { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -5958,7 +5958,7 @@ pub struct ScheduledAreaForCollectiveHousingFacilities { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -6039,7 +6039,7 @@ pub struct ScheduledAreaForDistributionBusinessPark { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -6120,7 +6120,7 @@ pub struct ScheduledAreaForIndustrialParkDevelopmentProjects { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -6201,7 +6201,7 @@ pub struct ScheduledAreaForNewHousingAndUrbanDevelopmentProjects { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -6282,7 +6282,7 @@ pub struct ScheduledAreaForNewUrbanInfrastructureProjects { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -6363,7 +6363,7 @@ pub struct ScheduledAreaForUrbanDevelopmentProject { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -6444,7 +6444,7 @@ pub struct SedimentDisasterProneArea { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -6537,7 +6537,7 @@ pub struct SnowProtectionFacility { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -6627,7 +6627,7 @@ pub struct SocialWelfareFacility { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -6711,7 +6711,7 @@ pub struct SpecialGreenSpaceConservationDistrict { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -6795,7 +6795,7 @@ pub struct SpecialUrbanRenaissanceDistrict { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -6900,7 +6900,7 @@ pub struct SpecialUseAttractionDistrict { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -6999,7 +6999,7 @@ pub struct SpecialUseDistrict { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -7086,7 +7086,7 @@ pub struct SpecialUseRestrictionDistrict { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -7173,7 +7173,7 @@ pub struct SpecialZoneForPreservationOfHistoricalLandscape { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -7254,7 +7254,7 @@ pub struct SpecifiedBlock { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -7344,7 +7344,7 @@ pub struct SpecifiedBuildingZoneImprovementPlan { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -7443,7 +7443,7 @@ pub struct SpecifiedDisasterPreventionBlockImprovementZone { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -7536,7 +7536,7 @@ pub struct SpecifiedUrgentUrbanRenewalArea { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -7629,7 +7629,7 @@ pub struct SupplyFacility { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -7716,7 +7716,7 @@ pub struct TelecommunicationFacility { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -7806,7 +7806,7 @@ pub struct TideFacility { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -7896,7 +7896,7 @@ pub struct TrafficFacility { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -8009,7 +8009,7 @@ pub struct TreatmentFacility { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -8096,7 +8096,7 @@ pub struct TreePlantingDistrict { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -8180,7 +8180,7 @@ pub struct UnclassifiedBlankArea { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -8258,7 +8258,7 @@ pub struct UnclassifiedUseDistrict { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -8336,7 +8336,7 @@ pub struct UnusedLandUsePromotionArea { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -8414,7 +8414,7 @@ pub struct UrbanDevelopmentProject { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -8495,7 +8495,7 @@ pub struct UrbanDisasterRecoveryPromotionArea { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -8582,7 +8582,7 @@ pub struct UrbanFacility { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -8666,7 +8666,7 @@ pub struct UrbanFacilityStipulatedByCabinetOrder { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -8756,7 +8756,7 @@ pub struct UrbanFunctionAttractionArea { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -8834,7 +8834,7 @@ pub struct UrbanPlanningArea { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -8936,7 +8936,7 @@ pub struct UrbanRedevelopmentProject { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -9035,7 +9035,7 @@ pub struct UrbanRedevelopmentPromotionArea { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -9125,7 +9125,7 @@ pub struct UrbanRenewalProject { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -9224,7 +9224,7 @@ pub struct UrgentUrbanRenewalArea { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -9314,7 +9314,7 @@ pub struct UseDistrict { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -9431,7 +9431,7 @@ pub struct Waterway { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -9530,7 +9530,7 @@ pub struct WindProtectionFacility { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -9620,7 +9620,7 @@ pub struct ZonalDisasterPreventionFacility { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -9701,7 +9701,7 @@ pub struct ZoneForPreservationOfHistoricalLandscape { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, @@ -9782,7 +9782,7 @@ pub struct ThreeDimensionalExtent { pub city: Option, #[citygml(path = b"urf:reference")] - pub reference: Option, + pub reference: Option, #[citygml(path = b"urf:reason")] pub reason: Option, diff --git a/nusamai-plateau/src/models/iur/uro/common.rs b/nusamai-plateau/src/models/iur/uro/common.rs index d92fa695..41839361 100644 --- a/nusamai-plateau/src/models/iur/uro/common.rs +++ b/nusamai-plateau/src/models/iur/uro/common.rs @@ -1,4 +1,4 @@ -use nusamai_citygml::{citygml_data, CityGmlElement, Code, Date, Length, Measure, Point, URI}; +use nusamai_citygml::{citygml_data, CityGmlElement, Code, Date, Length, Measure, Point, Uri}; #[citygml_data(name = "uro:UserDefinedValue")] pub struct UserDefinedValue { @@ -18,7 +18,7 @@ pub struct UserDefinedValue { pub date_value: Option, #[citygml(path = b"uro:uriValue")] - pub uri_value: Option, + pub uri_value: Option, #[citygml(path = b"uro:measuredValue")] pub measured_value: Option, diff --git a/nusamai-plateau/src/models/iur/uro/ifc.rs b/nusamai-plateau/src/models/iur/uro/ifc.rs index 72376aca..b269d304 100644 --- a/nusamai-plateau/src/models/iur/uro/ifc.rs +++ b/nusamai-plateau/src/models/iur/uro/ifc.rs @@ -1,6 +1,6 @@ use crate::models::core::Address; use nusamai_citygml::{ - citygml_data, citygml_property, CityGmlElement, Code, Date, DoubleList, Measure, Point, URI, + citygml_data, citygml_property, CityGmlElement, Code, Date, DoubleList, Measure, Point, Uri, }; // TODO? @@ -248,7 +248,7 @@ pub struct IfcBuildingStorey { #[citygml_data(name = "uro:IfcClassificationReference")] pub struct IfcClassificationReference { #[citygml(path = b"uro:location")] - pub location: Option, + pub location: Option, #[citygml(path = b"uro:itemReference")] pub item_reference: Option, diff --git a/nusamai-plateau/src/models/transportation.rs b/nusamai-plateau/src/models/transportation.rs index 20ae8ea6..c0631bbc 100644 --- a/nusamai-plateau/src/models/transportation.rs +++ b/nusamai-plateau/src/models/transportation.rs @@ -38,7 +38,7 @@ pub struct Road { pub road_status: Vec, #[citygml(path = b"uro:roadStructureAttribute/uro:RoadStructureAttribute")] - pub road_structure_attribute: Option, + pub road_structure_attribute: Vec, #[citygml(path = b"uro:trafficVolumeAttribute/uro:TrafficVolumeAttribute")] pub traffic_volume_attribute: Option, diff --git a/nusamai-plateau/tests/data/yokosuka-shi/udx/bldg/52397519_bldg_6697_appearance/14201-bldg-110968-1.jpg b/nusamai-plateau/tests/data/yokosuka-shi/udx/bldg/52397519_bldg_6697_appearance/14201-bldg-110968-1.jpg new file mode 100755 index 00000000..41ab91e1 Binary files /dev/null and b/nusamai-plateau/tests/data/yokosuka-shi/udx/bldg/52397519_bldg_6697_appearance/14201-bldg-110968-1.jpg differ diff --git a/nusamai-plateau/tests/data/yokosuka-shi/udx/bldg/52397519_bldg_6697_appearance/14201-bldg-156842-1.jpg b/nusamai-plateau/tests/data/yokosuka-shi/udx/bldg/52397519_bldg_6697_appearance/14201-bldg-156842-1.jpg new file mode 100755 index 00000000..bd478741 Binary files /dev/null and b/nusamai-plateau/tests/data/yokosuka-shi/udx/bldg/52397519_bldg_6697_appearance/14201-bldg-156842-1.jpg differ diff --git a/nusamai-plateau/tests/data/yokosuka-shi/udx/bldg/52397519_bldg_6697_appearance/14201-bldg-158971-1.jpg b/nusamai-plateau/tests/data/yokosuka-shi/udx/bldg/52397519_bldg_6697_appearance/14201-bldg-158971-1.jpg new file mode 100755 index 00000000..c1dcb793 Binary files /dev/null and b/nusamai-plateau/tests/data/yokosuka-shi/udx/bldg/52397519_bldg_6697_appearance/14201-bldg-158971-1.jpg differ diff --git a/nusamai-plateau/tests/load_examples.rs b/nusamai-plateau/tests/load_examples.rs index f1b69c26..b0fdac40 100644 --- a/nusamai-plateau/tests/load_examples.rs +++ b/nusamai-plateau/tests/load_examples.rs @@ -329,7 +329,7 @@ fn load_road_example() { vec![Code::new("歩道部の段差".into(), "2000".into())] ); assert_eq!( - road.road_structure_attribute.as_ref().unwrap().width, + road.road_structure_attribute[0].width, Some(Measure::new(22.0)), ); assert_eq!( diff --git a/nusamai/Cargo.toml b/nusamai/Cargo.toml index 9bac7931..51633c4e 100644 --- a/nusamai/Cargo.toml +++ b/nusamai/Cargo.toml @@ -46,6 +46,8 @@ glob = "0.3.1" shellexpand = "3.1.0" kml = "0.8.5" nusamai-kml = { path = "../nusamai-kml" } +zune-image = { version = "0.4.15", default-features = false, features = ["simd", "threads", "jpeg", "png", "metadata"] } +image = { version = "0.24.8", default-features = false, features = ["tiff", "jpeg", "jpeg_rayon"] } [dev-dependencies] rand = "0.8.5" diff --git a/nusamai/src/pipeline/mod.rs b/nusamai/src/pipeline/mod.rs index bd4f43ed..dd791b88 100644 --- a/nusamai/src/pipeline/mod.rs +++ b/nusamai/src/pipeline/mod.rs @@ -16,9 +16,10 @@ pub use runner::*; pub type Sender = mpsc::SyncSender; pub type Receiver = mpsc::Receiver; -/// Message passing through pipeline stages +/// Message passing through the main processing pipeline #[derive(Debug)] pub struct Parcel { + // Entity (Feature, Data, etc.) pub entity: Entity, } diff --git a/nusamai/src/sink/cesiumtiles/gltf.rs b/nusamai/src/sink/cesiumtiles/gltf.rs index d869ce20..2bfdeedf 100644 --- a/nusamai/src/sink/cesiumtiles/gltf.rs +++ b/nusamai/src/sink/cesiumtiles/gltf.rs @@ -1,38 +1,156 @@ use std::io::Write; +use super::material; +use ahash::HashMap; use byteorder::{ByteOrder, LittleEndian}; +use indexmap::IndexSet; + +pub type Primitives = HashMap>; /// とりいそぎの実装 pub fn write_gltf_glb( writer: W, - min: [f64; 3], - max: [f64; 3], translation: [f64; 3], - vertices: impl IntoIterator, - indices: impl IntoIterator, + vertices: impl IntoIterator, + primitives: Primitives, ) -> std::io::Result<()> { use nusamai_gltf_json::*; + // The buffer for the BIN part let mut bin_content: Vec = Vec::new(); + let mut gltf_buffer_views = vec![]; + let mut gltf_accessors = vec![]; + + // vertices + { + let mut vertices_count = 0; + let mut position_max = [f64::MIN; 3]; + let mut position_min = [f64::MAX; 3]; + + let buffer_offset = bin_content.len(); + let mut buf = [0; 4 * 5]; + for v in vertices { + let [x, y, z, u, v] = v; + position_min = [ + f64::min(position_min[0], f32::from_bits(x) as f64), + f64::min(position_min[1], f32::from_bits(y) as f64), + f64::min(position_min[2], f32::from_bits(z) as f64), + ]; + position_max = [ + f64::max(position_max[0], f32::from_bits(x) as f64), + f64::max(position_max[1], f32::from_bits(y) as f64), + f64::max(position_max[2], f32::from_bits(z) as f64), + ]; + + LittleEndian::write_u32_into(&[x, y, z, u, v], &mut buf); + bin_content.write_all(&buf)?; + vertices_count += 1; + } + + gltf_buffer_views.push(BufferView { + byte_offset: buffer_offset as u32, + byte_length: (bin_content.len() - buffer_offset) as u32, + byte_stride: Some(4 * 5), + target: Some(BufferViewTarget::ArrayBuffer), + ..Default::default() + }); - let vertices_offset = bin_content.len(); - let mut buf = [0; 12]; - let mut vertices_count = 0; - for v in vertices { - LittleEndian::write_u32_into(&v, &mut buf); - bin_content.write_all(&buf)?; - vertices_count += 1; + // accessor (positions) + gltf_accessors.push(Accessor { + buffer_view: Some(gltf_buffer_views.len() as u32 - 1), + component_type: ComponentType::Float, + count: vertices_count, + min: Some(position_min.to_vec()), + max: Some(position_max.to_vec()), + type_: AccessorType::Vec3, + ..Default::default() + }); + + // accessor (texcoords) + gltf_accessors.push(Accessor { + buffer_view: Some(gltf_buffer_views.len() as u32 - 1), + byte_offset: 4 * 3, + component_type: ComponentType::Float, + count: vertices_count, + type_: AccessorType::Vec2, + ..Default::default() + }); } - let vertices_len = bin_content.len() - vertices_offset; - let indices_offset = bin_content.len(); - let mut indices_count = 0; - for idx in indices { - bin_content.write_all(&idx.to_le_bytes())?; - indices_count += 1; + let mut gltf_primitives = vec![]; + + // indices + { + let indices_offset = bin_content.len(); + + let mut byte_offset = 0; + for (mat_i, (mat, primitive)) in primitives.iter().enumerate() { + let mut indices_count = 0; + for idx in primitive { + bin_content.write_all(&idx.to_le_bytes())?; + indices_count += 1; + } + + gltf_accessors.push(Accessor { + buffer_view: Some(gltf_buffer_views.len() as u32), + byte_offset, + component_type: ComponentType::UnsignedInt, + count: indices_count, + type_: AccessorType::Scalar, + ..Default::default() + }); + + let mut attributes = vec![("POSITION".to_string(), 0)]; + if mat.base_texture.is_some() { + attributes.push(("TEXCOORD_0".to_string(), 1)); + } + + gltf_primitives.push(MeshPrimitive { + attributes: attributes.into_iter().collect(), + indices: Some(gltf_accessors.len() as u32 - 1), + material: Some(mat_i as u32), // TODO + mode: PrimitiveMode::Triangles, + ..Default::default() + }); + + byte_offset += indices_count * 4; + } + + let indices_len = bin_content.len() - indices_offset; + + gltf_buffer_views.push(BufferView { + byte_offset: indices_offset as u32, + byte_length: indices_len as u32, + target: Some(BufferViewTarget::ElementArrayBuffer), + ..Default::default() + }) } - let indices_len = bin_content.len() - indices_offset; + let mut image_set: IndexSet = Default::default(); + let mut texture_set: IndexSet = Default::default(); + + // materials + let gltf_materials = primitives + .keys() + .map(|material| material.to_gltf(&mut texture_set)) + .collect(); + + let gltf_textures: Vec<_> = texture_set + .into_iter() + .map(|t| t.to_gltf(&mut image_set)) + .collect(); + + let gltf_images = image_set + .into_iter() + .map(|img| img.to_gltf(&mut gltf_buffer_views, &mut bin_content)) + .collect::>>()?; + + let gltf_buffers = vec![Buffer { + byte_length: bin_content.len() as u32, + ..Default::default() + }]; + + // Build the JSON part of glTF let gltf = Gltf { scenes: vec![Scene { nodes: Some(vec![0]), @@ -43,61 +161,16 @@ pub fn write_gltf_glb( translation, ..Default::default() }], - materials: vec![Material { - pbr_metallic_roughness: Some(MaterialPbrMetallicRoughness { - base_color_factor: [0.5, 0.7, 0.7, 1.0], - metallic_factor: 0.5, - roughness_factor: 0.5, - ..Default::default() - }), - ..Default::default() - }], meshes: vec![Mesh { - primitives: vec![MeshPrimitive { - attributes: vec![("POSITION".to_string(), 0)].into_iter().collect(), - indices: Some(1), - material: Some(0), - mode: PrimitiveMode::Triangles, - ..Default::default() - }], - ..Default::default() - }], - accessors: vec![ - Accessor { - buffer_view: Some(0), - component_type: ComponentType::Float, - count: vertices_count, - min: Some(min.to_vec()), - max: Some(max.to_vec()), - type_: AccessorType::Vec3, - ..Default::default() - }, - Accessor { - buffer_view: Some(1), - component_type: ComponentType::UnsignedInt, - count: indices_count, - type_: AccessorType::Scalar, - ..Default::default() - }, - ], - buffer_views: vec![ - BufferView { - byte_offset: vertices_offset as u32, - byte_length: vertices_len as u32, - target: Some(BufferViewTarget::ArrayBuffer), - ..Default::default() - }, - BufferView { - byte_offset: indices_offset as u32, - byte_length: indices_len as u32, - target: Some(BufferViewTarget::ElementArrayBuffer), - ..Default::default() - }, - ], - buffers: vec![Buffer { - byte_length: bin_content.len() as u32, + primitives: gltf_primitives, ..Default::default() }], + materials: gltf_materials, + textures: gltf_textures, + images: gltf_images, + accessors: gltf_accessors, + buffer_views: gltf_buffer_views, + buffers: gltf_buffers, ..Default::default() }; diff --git a/nusamai/src/sink/cesiumtiles/material.rs b/nusamai/src/sink/cesiumtiles/material.rs index 0e2566b2..0be9b0c5 100644 --- a/nusamai/src/sink/cesiumtiles/material.rs +++ b/nusamai/src/sink/cesiumtiles/material.rs @@ -1,12 +1,16 @@ //! Material mangement -use std::hash::Hash; +use std::{hash::Hash, path::Path, time::Instant}; +use indexmap::IndexSet; +use nusamai_gltf_json::BufferView; use serde::{Deserialize, Serialize}; +use url::Url; -#[derive(Serialize, Clone, PartialEq, Deserialize)] +#[derive(Debug, Serialize, Clone, PartialEq, Deserialize)] pub struct Material { pub base_color: [f32; 4], + pub base_texture: Option, // NOTE: Adjust the hash implementation if you add more fields } @@ -15,5 +19,146 @@ impl Eq for Material {} impl Hash for Material { fn hash(&self, state: &mut H) { self.base_color.iter().for_each(|c| c.to_bits().hash(state)); + self.base_texture.hash(state); } } + +impl Material { + pub fn to_gltf( + &self, + texture_set: &mut IndexSet, + ) -> nusamai_gltf_json::Material { + let tex = if let Some(texture) = &self.base_texture { + let (tex_idx, _) = texture_set.insert_full(texture.clone()); + Some(nusamai_gltf_json::TextureInfo { + index: tex_idx as u32, + tex_coord: 0, + ..Default::default() + }) + } else { + None + }; + nusamai_gltf_json::Material { + pbr_metallic_roughness: Some(nusamai_gltf_json::MaterialPbrMetallicRoughness { + base_color_factor: to_f64x4(self.base_color), + metallic_factor: 0.2, + roughness_factor: 0.5, + base_color_texture: tex, + ..Default::default() + }), + ..Default::default() + } + } +} + +#[derive(Debug, Serialize, Clone, Hash, PartialEq, Eq, Deserialize)] +pub struct Texture { + pub uri: Url, +} + +impl Texture { + pub fn to_gltf( + &self, + images: &mut IndexSet, + ) -> nusamai_gltf_json::Texture { + let (image_index, _) = images.insert_full(Image { + uri: self.uri.clone(), + }); + nusamai_gltf_json::Texture { + source: Some(image_index as u32), + ..Default::default() + } + } +} + +#[derive(Debug, Serialize, Clone, Hash, PartialEq, Eq, Deserialize)] +pub struct Image { + pub uri: Url, +} + +impl Image { + pub fn to_gltf( + &self, + buffer_view: &mut Vec, + bin_content: &mut Vec, + ) -> std::io::Result { + if let Ok(path) = self.uri.to_file_path() { + // NOTE: temporary implementation + let content = load_image(&path)?; + + buffer_view.push(BufferView { + byte_offset: bin_content.len() as u32, + byte_length: content.len() as u32, + ..Default::default() + }); + + bin_content.extend(content); + + Ok(nusamai_gltf_json::Image { + mime_type: Some(nusamai_gltf_json::MimeType::ImageJpeg), + buffer_view: Some(buffer_view.len() as u32 - 1), + ..Default::default() + }) + } else { + Ok(nusamai_gltf_json::Image { + uri: Some(self.uri.to_string()), + ..Default::default() + }) + } + } +} + +// NOTE: temporary implementation +fn load_image(path: &Path) -> std::io::Result> { + log::info!("Decoding image: {:?}", path); + + if let Some(ext) = path.extension() { + match ext.to_str().unwrap() { + // use `image crate` for TIFF + "tif" | "tiff" => { + let t = Instant::now(); + let image = image::open(path) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?; + log::debug!("Image decoding took {:?}", t.elapsed()); + + let content: Vec = Vec::new(); + let mut writer = std::io::Cursor::new(content); + image + .write_to(&mut writer, image::ImageOutputFormat::Jpeg(100)) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?; + Ok(writer.into_inner()) + } + // use `zune-image` crate for JPEG and PNG + "png" | "jpg" | "jpeg" => { + let t = Instant::now(); + let image = zune_image::image::Image::open(path) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?; + log::debug!("Image decoding took {:?}", t.elapsed()); + + let content = image + .write_to_vec(zune_image::codecs::ImageFormat::JPEG) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?; + log::debug!("Image encoding took {:?}", t.elapsed()); + Ok(content) + } + _ => { + let err = format!("Unsupported image format: {:?}", path); + log::error!("{}", err); + Err(std::io::Error::new(std::io::ErrorKind::InvalidData, err)) + } + } + } else { + let err = format!("Unsupported image format: {:?}", path); + log::error!("{}", err); + Err(std::io::Error::new(std::io::ErrorKind::InvalidData, err)) + } +} + +fn to_f64x4(c: [f32; 4]) -> [f64; 4] { + [ + f64::from(c[0]), + f64::from(c[1]), + f64::from(c[2]), + f64::from(c[3]), + ] +} diff --git a/nusamai/src/sink/cesiumtiles/mod.rs b/nusamai/src/sink/cesiumtiles/mod.rs index 52defb97..5fabaeb9 100644 --- a/nusamai/src/sink/cesiumtiles/mod.rs +++ b/nusamai/src/sink/cesiumtiles/mod.rs @@ -17,7 +17,6 @@ use ahash::RandomState; use earcut_rs::utils3d::project3d_to_2d; use earcut_rs::Earcut; use ext_sort::{buffer::mem::MemoryLimitedBufferBuilder, ExternalSorter, ExternalSorterBuilder}; -use nusamai_mvt::TileZXY; use nusamai_projection::cartesian::geographic_to_geocentric; use rayon::prelude::*; use serde::{Deserialize, Serialize}; @@ -114,7 +113,11 @@ impl DataSink for CesiumTilesSink { // Sort features by tile_id (using external sorter) { s.spawn(move || { - feature_sorting_stage(receiver_sliced, sender_sorted); + if let Err(error) = + feature_sorting_stage(feedback, receiver_sliced, sender_sorted) + { + feedback.report_fatal_error(error); + } }); } @@ -154,6 +157,8 @@ fn geometry_slicing_stage( // TODO: zoom level from parameters slice_to_tiles(&parcel.entity, 7, 17, |(z, x, y), feature| { + feedback.ensure_not_canceled()?; + let bytes = bincode::serialize(&feature).unwrap(); let serialized_feature = SerializedSlicedFeature { tile_id: tile_id_conv.zxy_to_id(z, x, y), @@ -170,9 +175,10 @@ fn geometry_slicing_stage( } fn feature_sorting_stage( + feedback: &Feedback, receiver_sliced: mpsc::Receiver, sender_sorted: mpsc::SyncSender<(u64, Vec)>, -) { +) -> Result<()> { let sorter: ExternalSorter< SerializedSlicedFeature, std::io::Error, @@ -195,11 +201,15 @@ fn feature_sorting_stage( .map(std::result::Result::unwrap) .group_by(|ser_feat| ser_feat.tile_id) { + feedback.ensure_not_canceled()?; + let ser_feats: Vec<_> = ser_feats.collect(); if sender_sorted.send((tile_id, ser_feats)).is_err() { - return; + return Err(PipelineError::Canceled); }; } + + Ok(()) } fn tile_writing_stage( @@ -211,6 +221,7 @@ fn tile_writing_stage( let ellipsoid = nusamai_projection::ellipsoid::wgs84(); let tiles: Arc>> = Default::default(); + // Make a glTF (.glb) file for each tile receiver_sorted .into_iter() .par_bridge() @@ -218,7 +229,7 @@ fn tile_writing_stage( feedback.ensure_not_canceled()?; let mut tilespec = TileSpec { - zxy: (0, 0, 0), + zxy: tile_id_conv.id_to_zxy(tile_id), min_lng: f64::MAX, max_lng: f64::MIN, min_lat: f64::MAX, @@ -227,13 +238,32 @@ fn tile_writing_stage( max_height: f64::MIN, }; + // Tile information + let (tile_zoom, tile_x, tile_y) = tilespec.zxy; + let (min_lat, max_lat) = tiling::y_slice_range(tile_zoom, tile_y); + let (min_lng, max_lng) = tiling::x_slice_range(tile_zoom, tile_x as i32, tiling::x_step(tile_zoom, tile_y)); + log::info!( + "tile: z={tile_zoom}, x={tile_x}, y={tile_y} (lng: [{min_lng} => {max_lng}], lat: [{min_lat} => {max_lat})" + ); + + // Use the tile center as the translation of glTF + let translation = { + let (tx, ty, tz) = geographic_to_geocentric(&ellipsoid, (min_lng + max_lng) / 2.0, (min_lat + max_lat) / 2.0, 0.); + [tx, tz, -ty] + }; + + // Triangulation let mut earcutter = Earcut::new(); - let mut buf5d: Vec = Vec::new(); // [x, y, z, u, v] let mut buf2d: Vec = Vec::new(); // 2d-projected [x, y] - let mut triangles_buf: Vec = Vec::new(); - let mut triangles = Vec::new(); + let mut index_buf: Vec = Vec::new(); + let mut vertices: IndexSet<[u32; 5], RandomState> = IndexSet::default(); + let mut primitives: gltf::Primitives = Default::default(); + + // make vertices and indices for serialized_feat in serialized_feats { + feedback.ensure_not_canceled()?; + let mut feature: SlicedFeature = bincode::deserialize(&serialized_feat.body) .map_err(|err| { PipelineError::Other(format!( @@ -253,117 +283,61 @@ fn tile_writing_stage( tilespec.max_height = tilespec.max_height.max(height); let (x, y, z) = geographic_to_geocentric(&ellipsoid, lng, lat, height); - [x, z, -y, u, v] + [x - translation[0], z - translation[1], -y - translation[2], u, 1.0 - v] }); - for poly in &feature.polygons { + for (poly, orig_mat_id) in feature.polygons.iter().zip_eq(feature.polygon_material_ids.iter()) { let num_outer = match poly.hole_indices().first() { Some(&v) => v as usize, None => poly.coords().len() / 5, }; - buf5d.clear(); - buf5d.extend(poly.coords()); + let mat = feature.materials[*orig_mat_id as usize].clone(); + let indices = primitives.entry(mat).or_default(); - if project3d_to_2d(&buf5d, num_outer, 5, &mut buf2d) { + if project3d_to_2d(poly.coords(), num_outer, 5, &mut buf2d) { // earcut - earcutter.earcut(&buf2d, poly.hole_indices(), 2, &mut triangles_buf); - triangles.extend(triangles_buf.iter().map(|idx| { - [ - buf5d[*idx as usize * 5], - buf5d[*idx as usize * 5 + 1], - buf5d[*idx as usize * 5 + 2], - ] + earcutter.earcut(&buf2d, poly.hole_indices(), 2, &mut index_buf); + + // collect triangles + indices.extend(index_buf.iter().map(|idx| { + let pos = *idx as usize * 5; + let [x, y, z, u, v] = poly.coords()[pos..pos + 5].try_into().unwrap(); + let vbits = [ + (x as f32).to_bits(), + (y as f32).to_bits(), + (z as f32).to_bits(), + (u as f32).to_bits(), + (v as f32).to_bits(), + ]; + let (index, _) = vertices.insert_full(vbits); + index as u32 })); } } } - // calculate the centroid and min/max - let mut pos_max = [f64::MIN; 3]; - let mut pos_min = [f64::MAX; 3]; - let mut translation = [0.; 3]; - - for &[x, y, z] in &triangles { - pos_min = [ - f64::min(pos_min[0], x), - f64::min(pos_min[1], y), - f64::min(pos_min[2], z), - ]; - pos_max = [ - f64::max(pos_max[0], x), - f64::max(pos_max[1], y), - f64::max(pos_max[2], z), - ]; - } - // TODO: Use a library for 3d linalg - translation[0] = (pos_max[0] + pos_min[0]) / 2.; - translation[1] = (pos_max[1] + pos_min[1]) / 2.; - translation[2] = (pos_max[2] + pos_min[2]) / 2.; - pos_min[0] -= translation[0]; - pos_max[0] -= translation[0]; - pos_min[1] -= translation[1]; - pos_max[1] -= translation[1]; - pos_min[2] -= translation[2]; - pos_max[2] -= translation[2]; - - // make vertices and indices - let mut vertices: IndexSet<[u32; 3], RandomState> = IndexSet::default(); - let indices: Vec<_> = triangles - .iter() - .map(|&[x, y, z]| { - let (x, y, z) = (x - translation[0], y - translation[1], z - translation[2]); - let vbits = [ - (x as f32).to_bits(), - (y as f32).to_bits(), - (z as f32).to_bits(), - ]; - let (index, _) = vertices.insert_full(vbits); - index as u32 - }) - .collect(); - - // Remove degenerate triangles - let indices: Vec = indices - .chunks_exact(3) - .filter(|idx| (idx[0] != idx[1] && idx[1] != idx[2] && idx[2] != idx[0])) - .flatten() - .copied() - .collect(); - - let zxy: TileZXY = tile_id_conv.id_to_zxy(tile_id); - let (zoom, x, y) = zxy; - tilespec.zxy = zxy; tiles.lock().unwrap().push(tilespec); - // print tile information - let (min_y, max_y) = tiling::y_slice_range(zoom, y); - let xs = tiling::x_step(zoom, y); - let (min_x, max_x) = tiling::x_slice_range(zoom, x as i32, xs); - log::info!( - "tile: z={zoom}, x={x}, y={y} (lng: [{min_x} => {max_x}], lat: [{min_y} => {max_y})" - ); - // write to file - let path_glb = output_path.join(Path::new(&format!("{zoom}/{x}/{y}.glb"))); + let path_glb = + output_path.join(Path::new(&format!("{tile_zoom}/{tile_x}/{tile_y}.glb"))); if let Some(dir) = path_glb.parent() { fs::create_dir_all(dir)?; } let mut file = std::fs::File::create(path_glb)?; - let mut writer = BufWriter::new(&mut file); write_gltf_glb( - &mut writer, - pos_min, - pos_max, + &mut BufWriter::new(&mut file), translation, vertices, - indices, + primitives, )?; Ok::<(), PipelineError>(()) })?; + // Generate tileset.json let mut tree = TileTree::default(); for tilespec in tiles.lock().unwrap().drain(..) { tree.add_node(tilespec); diff --git a/nusamai/src/sink/cesiumtiles/slice.rs b/nusamai/src/sink/cesiumtiles/slice.rs index 0742cadd..beb6cb43 100644 --- a/nusamai/src/sink/cesiumtiles/slice.rs +++ b/nusamai/src/sink/cesiumtiles/slice.rs @@ -5,8 +5,6 @@ use indexmap::IndexSet; use itertools::Itertools; use serde::{Deserialize, Serialize}; -use super::material::Material; -use super::tiling; use nusamai_citygml::{ geometry::GeometryType, object::{ObjectStereotype, Value}, @@ -15,6 +13,10 @@ use nusamai_geometry::{MultiPolygon, Polygon, Polygon2, Polygon3}; use nusamai_mvt::TileZXY; use nusamai_plateau::{appearance, Entity}; +use super::material::Material; +use super::tiling; +use crate::sink::cesiumtiles::material::Texture; + #[derive(Serialize, Deserialize)] pub struct SlicedFeature { // polygons [x, y, z, u, v] @@ -52,65 +54,78 @@ pub fn slice_to_tiles( } let appearance_store = entity.appearance_store.read().unwrap(); - let mut materials: IndexSet = IndexSet::new(); let mut sliced_tiles: HashMap<(u8, u32, u32), SlicedFeature> = HashMap::new(); + let mut materials: IndexSet = IndexSet::new(); let default_mat = appearance::Material::default(); - geometries.iter().for_each(|entry| match entry.ty { - GeometryType::Solid | GeometryType::Surface | GeometryType::Triangle => { - // for each polygon - for ((idx_poly, poly_uv), poly_mat) in geom_store - .multipolygon - .iter_range(entry.pos as usize..(entry.pos + entry.len) as usize) - .zip_eq( - geom_store - .polygon_uvs - .iter_range(entry.pos as usize..(entry.pos + entry.len) as usize), - ) - .zip_eq( - geom_store.polygon_materials - [entry.pos as usize..(entry.pos + entry.len) as usize] - .iter(), - ) - { - let poly = idx_poly.transform(|c| geom_store.vertices[c[0] as usize]); - let orig_mat = poly_mat - .and_then(|idx| appearance_store.materials.get(idx as usize)) - .unwrap_or(&default_mat) - .clone(); - - let mat = Material { - base_color: orig_mat.diffuse_color.into(), - }; - let (mat_idx, _) = materials.insert_full(mat); - - // Slice polygon for each zoom level - for zoom in min_z..=max_z { - slice_polygon(zoom, &poly, &poly_uv, |(z, x, y), poly| { - let sliced_feature = - sliced_tiles - .entry((z, x, y)) - .or_insert_with(|| SlicedFeature { - polygons: MultiPolygon::new(), - attributes: entity.root.clone(), - polygon_material_ids: Default::default(), - materials: Default::default(), - }); - - sliced_feature.polygons.push(poly); - sliced_feature.polygon_material_ids.push(mat_idx as u32); - }); + geometries.iter().for_each(|entry| { + match entry.ty { + GeometryType::Solid | GeometryType::Surface | GeometryType::Triangle => { + // for each polygon + for (((idx_poly, poly_uv), poly_mat), poly_tex) in geom_store + .multipolygon + .iter_range(entry.pos as usize..(entry.pos + entry.len) as usize) + .zip_eq( + geom_store + .polygon_uvs + .iter_range(entry.pos as usize..(entry.pos + entry.len) as usize), + ) + .zip_eq( + geom_store.polygon_materials + [entry.pos as usize..(entry.pos + entry.len) as usize] + .iter(), + ) + .zip_eq( + geom_store.polygon_textures + [entry.pos as usize..(entry.pos + entry.len) as usize] + .iter(), + ) + { + let poly = idx_poly.transform(|c| geom_store.vertices[c[0] as usize]); + let orig_mat = poly_mat + .and_then(|idx| appearance_store.materials.get(idx as usize)) + .unwrap_or(&default_mat) + .clone(); + let orig_tex = + poly_tex.and_then(|idx| appearance_store.textures.get(idx as usize)); + + let mat = Material { + base_color: orig_mat.diffuse_color.into(), + base_texture: orig_tex.map(|tex| Texture { + uri: tex.image_url.clone(), + }), + }; + let (mat_idx, _) = materials.insert_full(mat); + + // Slice polygon for each zoom level + for zoom in min_z..=max_z { + slice_polygon(zoom, &poly, &poly_uv, |(z, x, y), poly| { + let sliced_feature = + sliced_tiles + .entry((z, x, y)) + .or_insert_with(|| SlicedFeature { + polygons: MultiPolygon::new(), + attributes: entity.root.clone(), + polygon_material_ids: Default::default(), + materials: Default::default(), + }); + + sliced_feature.polygons.push(poly); + sliced_feature.polygon_material_ids.push(mat_idx as u32); + }); + } } } - } - GeometryType::Curve => { - // TODO: implement - } - GeometryType::Point => { - // TODO: implement + GeometryType::Curve => { + // TODO: implement + } + GeometryType::Point => { + // TODO: implement + } } }); + // Send tiled features for ((z, x, y), mut sliced_feature) in sliced_tiles { sliced_feature.materials = materials.clone(); send_feature((z, x, y), sliced_feature)?; diff --git a/nusamai/src/sink/czml/mod.rs b/nusamai/src/sink/czml/mod.rs index c88a34e6..56bbd0d1 100644 --- a/nusamai/src/sink/czml/mod.rs +++ b/nusamai/src/sink/czml/mod.rs @@ -314,6 +314,7 @@ mod tests { ], }, }), + base_url: url::Url::parse("file:///dummy").unwrap(), geometry_store: RwLock::new(geometries).into(), appearance_store: Default::default(), }; diff --git a/nusamai/src/sink/geojson/mod.rs b/nusamai/src/sink/geojson/mod.rs index 4cb2e83c..30e3fb85 100644 --- a/nusamai/src/sink/geojson/mod.rs +++ b/nusamai/src/sink/geojson/mod.rs @@ -270,6 +270,7 @@ mod tests { }], }, }), + base_url: url::Url::parse("file:///dummy").unwrap(), geometry_store: RwLock::new(geometries).into(), appearance_store: Default::default(), }; diff --git a/nusamai/src/sink/gltf_poc/mod.rs b/nusamai/src/sink/gltf_poc/mod.rs new file mode 100644 index 00000000..0efb11f1 --- /dev/null +++ b/nusamai/src/sink/gltf_poc/mod.rs @@ -0,0 +1,650 @@ +//! gltf sink poc + +use std::fs::File; +use std::io::{BufWriter, Write}; +use std::path::{Path, PathBuf}; + +use ahash::RandomState; +use byteorder::{ByteOrder, LittleEndian}; +use earcut_rs::utils3d::project3d_to_2d; +use earcut_rs::Earcut; +use indexmap::IndexSet; +use nusamai_gltf_json::extensions::mesh::ext_mesh_features::FeatureId; +use nusamai_projection::cartesian::geographic_to_geocentric; +use rayon::prelude::*; + +use nusamai_citygml::object::ObjectStereotype; +use nusamai_citygml::schema::Schema; +use nusamai_citygml::{GeometryType, Value}; +use nusamai_gltf_json::*; + +use crate::parameters::*; +use crate::pipeline::{Feedback, PipelineError, Receiver}; +use crate::sink::{DataSink, DataSinkProvider, SinkInfo}; +use crate::{get_parameter_value, transformer}; + +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Hash)] +pub struct Vertex { + pub position: [u32; 3], // f32.to_bits() + pub tex_coord: [u32; 2], // f32.to_bits() + pub feature_id: u32, +} + +pub struct BoundingVolume { + pub min_lng: f64, + pub max_lng: f64, + pub min_lat: f64, + pub max_lat: f64, + pub min_height: f64, + pub max_height: f64, +} + +pub struct GltfPocSinkProvider {} + +impl DataSinkProvider for GltfPocSinkProvider { + fn info(&self) -> SinkInfo { + SinkInfo { + id_name: "gltf-poc".to_string(), + name: "glTF".to_string(), + } + } + + fn parameters(&self) -> Parameters { + let mut params = Parameters::new(); + params.define( + "@output".into(), + ParameterEntry { + description: "Output file path".into(), + required: true, + parameter: ParameterType::FileSystemPath(FileSystemPathParameter { + value: None, + must_exist: false, + }), + }, + ); + params + } + + fn create(&self, params: &Parameters) -> Box { + let output_path = get_parameter_value!(params, "@output", FileSystemPath); + + Box::::new(GltfPocSink { + output_path: output_path.as_ref().unwrap().into(), + }) + } +} + +pub struct GltfPocSink { + output_path: PathBuf, +} + +impl DataSink for GltfPocSink { + fn make_transform_requirements(&self) -> transformer::Requirements { + transformer::Requirements { + resolve_appearance: true, + ..Default::default() + } + } + + fn run( + &mut self, + upstream: Receiver, + feedback: &Feedback, + _schema: &Schema, + ) -> Result<(), PipelineError> { + let (sender, receiver) = std::sync::mpsc::sync_channel(1000); + + rayon::join( + || { + let ellipsoid = nusamai_projection::ellipsoid::wgs84(); + + // Convert Entity to CzmlPolygon objects + let _ = upstream.into_iter().par_bridge().try_for_each_with( + sender, + |sender, parcel| { + if feedback.is_canceled() { + return Err(PipelineError::Canceled); + } + + let mut bounding_volume = BoundingVolume { + min_lng: f64::MAX, + max_lng: f64::MIN, + min_lat: f64::MAX, + max_lat: f64::MIN, + min_height: f64::MAX, + max_height: f64::MIN, + }; + + // todo: transformerからschemaを受け取る必要がある + + let entity = parcel.entity; + let geom_store = entity.geometry_store.read().unwrap(); + + let Value::Object(obj) = &entity.root else { + unimplemented!() + }; + let ObjectStereotype::Feature { id: _, geometries } = &obj.stereotype + else { + unimplemented!() + }; + + // Divide polygons into triangles + let mut earcutter = Earcut::new(); + let mut buf3d: Vec = Vec::new(); + let mut buf2d: Vec = Vec::new(); + let mut triangles_buf: Vec = Vec::new(); + let mut triangles = Vec::new(); + + // extract triangles from entity + geometries.iter().for_each(|entry| match entry.ty { + GeometryType::Solid + | GeometryType::Surface + | GeometryType::Triangle => { + for idx_poly in geom_store.multipolygon.iter_range( + entry.pos as usize..(entry.pos + entry.len) as usize, + ) { + let mut poly = idx_poly.transform(|idx| { + let [lng, lat, height] = + geom_store.vertices[idx[0] as usize]; + [lng, lat, height] + }); + + poly.transform_inplace(|[lng, lat, height]| { + bounding_volume.min_lng = bounding_volume.min_lng.min(*lng); + bounding_volume.max_lng = bounding_volume.max_lng.max(*lng); + bounding_volume.min_lat = bounding_volume.min_lat.min(*lat); + bounding_volume.max_lat = bounding_volume.max_lat.max(*lat); + bounding_volume.min_height = + bounding_volume.min_height.min(*height); + bounding_volume.max_height = + bounding_volume.max_height.max(*height); + + // Convert to geocentric (x, y, z) coordinate. + // (Earcut do not work in geographic space) + + // WGS84 to Geocentric + let (x, y, z) = geographic_to_geocentric( + &ellipsoid, *lng, *lat, *height, + ); + // OpenGL is a right-handed y-up + [x, z, -y] + }); + + let num_outer = match poly.hole_indices().first() { + Some(&v) => v as usize, + None => poly.coords().len() / 3, + }; + + buf3d.clear(); + buf3d.extend(poly.coords()); + + if project3d_to_2d(&buf3d, num_outer, 3, &mut buf2d) { + // earcut + earcutter.earcut( + &buf2d, + poly.hole_indices(), + 2, + &mut triangles_buf, + ); + triangles.extend(triangles_buf.iter().map(|idx| { + [ + buf3d[*idx as usize * 3], + buf3d[*idx as usize * 3 + 1], + buf3d[*idx as usize * 3 + 2], + ] + })); + } + } + } + GeometryType::Curve => unimplemented!(), + GeometryType::Point => unimplemented!(), + }); + + // extract attributes from entity + let Value::Object(obj) = &entity.root else { + unimplemented!() + }; + let attributes = obj.attributes.clone(); + + // send triangles and attributes to sender + if sender + .send((triangles, attributes, bounding_volume)) + .is_err() + { + return Err(PipelineError::Canceled); + } + + Ok(()) + }, + ); + }, + || { + // Write glTF to a file + // todo: schemaから属性定義を行う必要がある + + let mut buffers: Vec<[f64; 4]> = Vec::new(); + + let mut all_max: [f64; 3] = [f64::MIN; 3]; + let mut all_min: [f64; 3] = [f64::MAX; 3]; + + let mut bounding_volume = BoundingVolume { + min_lng: f64::MAX, + max_lng: f64::MIN, + min_lat: f64::MAX, + max_lat: f64::MIN, + min_height: f64::MAX, + max_height: f64::MIN, + }; + + for (feature_id, (triangles, _attributes, _bounding_volume)) in + receiver.into_iter().enumerate() + { + let mut pos_max = [f64::MIN; 3]; + let mut pos_min = [f64::MAX; 3]; + + // calculate the centroid and min/max + for &[x, y, z] in &triangles { + pos_min = [ + f64::min(pos_min[0], x), + f64::min(pos_min[1], y), + f64::min(pos_min[2], z), + ]; + pos_max = [ + f64::max(pos_max[0], x), + f64::max(pos_max[1], y), + f64::max(pos_max[2], z), + ]; + buffers.push([x, y, z, feature_id as f64]); + } + + all_min = [ + f64::min(all_min[0], pos_min[0]), + f64::min(all_min[1], pos_min[1]), + f64::min(all_min[2], pos_min[2]), + ]; + all_max = [ + f64::max(all_max[0], pos_max[0]), + f64::max(all_max[1], pos_max[1]), + f64::max(all_max[2], pos_max[2]), + ]; + + bounding_volume.min_lng = + f64::min(bounding_volume.min_lng, _bounding_volume.min_lng); + bounding_volume.max_lng = + f64::max(bounding_volume.max_lng, _bounding_volume.max_lng); + bounding_volume.min_lat = + f64::min(bounding_volume.min_lat, _bounding_volume.min_lat); + bounding_volume.max_lat = + f64::max(bounding_volume.max_lat, _bounding_volume.max_lat); + bounding_volume.min_height = + f64::min(bounding_volume.min_height, _bounding_volume.min_height); + bounding_volume.max_height = + f64::max(bounding_volume.max_height, _bounding_volume.max_height); + } + + // calculate the centroid + let mut all_translation = [0.; 3]; + all_translation[0] = (all_max[0] + all_min[0]) / 2.; + all_translation[1] = (all_max[1] + all_min[1]) / 2.; + all_translation[2] = (all_max[2] + all_min[2]) / 2.; + + all_max[0] -= all_translation[0]; + all_min[0] -= all_translation[0]; + all_max[1] -= all_translation[1]; + all_min[1] -= all_translation[1]; + all_max[2] -= all_translation[2]; + all_min[2] -= all_translation[2]; + + // make vertices and indices + let mut vertices: IndexSet = IndexSet::default(); + + let indices: Vec = buffers + .iter() + .map(|&[x, y, z, feature_id]| { + let (x, y, z) = ( + x - all_translation[0], + y - all_translation[1], + z - all_translation[2], + ); + let vbits = [ + (x as f32).to_bits(), + (y as f32).to_bits(), + (z as f32).to_bits(), + ]; + + let vertex = Vertex { + position: vbits, + feature_id: feature_id as u32, + ..Default::default() + }; + + let (index, _) = vertices.insert_full(vertex); + + index as u32 + }) + .collect(); + + let indices: Vec = indices + .chunks_exact(3) + .filter(|idx| (idx[0] != idx[1] && idx[1] != idx[2] && idx[2] != idx[0])) + .flatten() + .copied() + .collect(); + + let mut file = File::create(&self.output_path).unwrap(); + let writer = BufWriter::with_capacity(1024 * 1024, &mut file); + + write_gltf(writer, all_min, all_max, all_translation, vertices, indices); + + let region: [f64; 6] = [ + bounding_volume.min_lng.to_radians(), + bounding_volume.min_lat.to_radians(), + bounding_volume.max_lng.to_radians(), + bounding_volume.max_lat.to_radians(), + bounding_volume.min_height, + bounding_volume.max_height, + ]; + + write_3dtiles(region, &self.output_path); + + // todo: 属性部分をbufferにするコードを書く + }, + ); + Ok(()) + } +} + +fn write_gltf( + mut writer: W, + min: [f64; 3], + max: [f64; 3], + translation: [f64; 3], + vertices: IndexSet, + indices: impl IntoIterator, +) { + let mut bin_content: Vec = Vec::new(); + + // write vertices + let vertices_offset = bin_content.len(); + let mut buf = [0; 12]; + let mut vertices_count = 0; + for vertex in vertices.clone() { + LittleEndian::write_u32_into(&vertex.position, &mut buf); + bin_content.write_all(&buf).unwrap(); + vertices_count += 1; + } + let vertices_len: usize = bin_content.len() - vertices_offset; + + // write indices + let indices_offset = bin_content.len(); + let mut indices_count = 0; + for idx in indices { + bin_content.write_all(&idx.to_le_bytes()).unwrap(); + indices_count += 1; + } + let indices_len = bin_content.len() - indices_offset; + + // write feature_ids + let feature_ids_offset = bin_content.len(); + let mut feature_ids_count = 0; + for vertex in vertices.clone() { + bin_content + .write_all(&vertex.feature_id.to_le_bytes()) + .unwrap(); + feature_ids_count += 1; + } + let feature_ids_len = bin_content.len() - feature_ids_offset; + + let gltf = Gltf { + extensions_used: vec!["EXT_mesh_features".to_string()], + scenes: vec![Scene { + nodes: Some(vec![0]), + ..Default::default() + }], + nodes: vec![Node { + mesh: Some(0), + translation, + ..Default::default() + }], + meshes: vec![Mesh { + primitives: vec![MeshPrimitive { + attributes: vec![ + ("POSITION".to_string(), 0), + ("_FEATURE_ID_0".to_string(), 2), + ] + .into_iter() + .collect(), + indices: Some(1), + mode: PrimitiveMode::Triangles, + extensions: Some(extensions::mesh::MeshPrimitive { + ext_mesh_features: Some(extensions::mesh::ext_mesh_features::ExtMeshFeatures { + feature_ids: vec![FeatureId { + attribute: Some(0), + feature_count: feature_ids_count, + ..Default::default() + }], + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }], + ..Default::default() + }], + accessors: vec![ + Accessor { + buffer_view: Some(0), + component_type: ComponentType::Float, + count: vertices_count, + min: Some(min.to_vec()), + max: Some(max.to_vec()), + type_: AccessorType::Vec3, + ..Default::default() + }, + Accessor { + buffer_view: Some(1), + component_type: ComponentType::UnsignedInt, + count: indices_count, + type_: AccessorType::Scalar, + ..Default::default() + }, + Accessor { + buffer_view: Some(2), + component_type: ComponentType::UnsignedInt, + count: feature_ids_count, + type_: AccessorType::Scalar, + ..Default::default() + }, + ], + buffer_views: vec![ + BufferView { + byte_offset: vertices_offset as u32, + byte_length: vertices_len as u32, + target: Some(BufferViewTarget::ArrayBuffer), + ..Default::default() + }, + BufferView { + byte_offset: indices_offset as u32, + byte_length: indices_len as u32, + target: Some(BufferViewTarget::ElementArrayBuffer), + ..Default::default() + }, + BufferView { + byte_offset: feature_ids_offset as u32, + byte_length: feature_ids_len as u32, + target: Some(BufferViewTarget::ArrayBuffer), + ..Default::default() + }, + ], + buffers: vec![Buffer { + byte_length: bin_content.len() as u32, + ..Default::default() + }], + ..Default::default() + }; + + { + let mut json_content = serde_json::to_vec(&gltf).unwrap(); + + // append padding + json_content.extend(vec![0x20; (4 - (json_content.len() % 4)) % 4].iter()); + bin_content.extend(vec![0x0; (4 - (bin_content.len() % 4)) % 4].iter()); + + let total_size = 12 + 8 + json_content.len() + 8 + bin_content.len(); + + writer.write_all(b"glTF").unwrap(); // magic + writer.write_all(&2u32.to_le_bytes()).unwrap(); // version: 2 + writer + .write_all(&(total_size as u32).to_le_bytes()) + .unwrap(); // total size + + writer + .write_all(&(json_content.len() as u32).to_le_bytes()) + .unwrap(); // json content + writer.write_all(b"JSON").unwrap(); // chunk type + writer.write_all(&json_content).unwrap(); // json content + + writer + .write_all(&(bin_content.len() as u32).to_le_bytes()) + .unwrap(); // json content + writer.write_all(b"BIN\0").unwrap(); // chunk type + writer.write_all(&bin_content).unwrap(); // bin content + + writer.flush().unwrap(); + } +} + +// FIXME: This is the code to verify the operation with Cesium +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 + .file_name() + .unwrap() + .to_string_lossy() + .into_owned(); + + let tileset = nusamai_3dtiles_json::tileset::Tileset { + geometric_error: 1e+100, + asset: nusamai_3dtiles_json::tileset::Asset { + version: "1.1".to_string(), + ..Default::default() + }, + root: nusamai_3dtiles_json::tileset::Tile { + bounding_volume: nusamai_3dtiles_json::tileset::BoundingVolume { + region: Some(bounding_volume), + ..Default::default() + }, + content: Some(nusamai_3dtiles_json::tileset::Content { + uri: content_uri, + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }; + + 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(); +} + +#[cfg(test)] +mod tests { + use std::sync::RwLock; + + use super::*; + use nusamai_citygml::{object::Object, GeometryRef, Value}; + use nusamai_geometry::MultiPolygon; + use nusamai_plateau::Entity; + use nusamai_projection::crs::EPSG_JGD2011_GEOGRAPHIC_3D; + + #[test] + fn test_entity_multipolygon() { + let vertices: Vec<[f64; 3]> = vec![ + // 1st polygon, exterior (vertex 0~3) + [0., 0., 111.], + [5., 0., 111.], + [5., 5., 111.], + [0., 5., 111.], + // 1st polygon, interior 1 (vertex 4~7) + [1., 1., 111.], + [2., 1., 111.], + [2., 2., 111.], + [1., 2., 111.], + // 1st polygon, interior 2 (vertex 8~11) + [3., 3., 111.], + [4., 3., 111.], + [4., 4., 111.], + [3., 4., 111.], + // 2nd polygon, exterior (vertex 12~15) + [4., 0., 222.], + [7., 0., 222.], + [7., 3., 222.], + [4., 3., 222.], + // 2nd polygon, interior (vertex 16~19) + [5., 1., 222.], + [6., 1., 222.], + [6., 2., 222.], + [5., 2., 222.], + // 3rd polygon, exterior (vertex 20~23) + [4., 0., 333.], + [7., 0., 333.], + [7., 3., 333.], + [4., 3., 333.], + ]; + + let mut mpoly = MultiPolygon::<1, u32>::new(); + // 1st polygon + mpoly.add_exterior([[0], [1], [2], [3], [0]]); + mpoly.add_interior([[4], [5], [6], [7], [4]]); + mpoly.add_interior([[8], [9], [10], [11], [8]]); + // 2nd polygon + mpoly.add_exterior([[12], [13], [14], [15], [12]]); + mpoly.add_interior([[16], [17], [18], [19], [16]]); + // 3rd polygon + mpoly.add_exterior([[20], [21], [22], [23], [20]]); + + let geometries = nusamai_citygml::GeometryStore { + epsg: EPSG_JGD2011_GEOGRAPHIC_3D, + vertices, + multipolygon: mpoly, + multilinestring: Default::default(), + multipoint: Default::default(), + ..Default::default() + }; + + let _entity = Entity { + root: Value::Object(Object { + typename: "dummy".into(), + attributes: Default::default(), + stereotype: nusamai_citygml::object::ObjectStereotype::Feature { + id: "dummy".into(), + geometries: vec![ + GeometryRef { + ty: GeometryType::Solid, + pos: 0, + len: 1, + lod: 1, + }, + GeometryRef { + ty: GeometryType::Solid, + pos: 1, + len: 1, + lod: 1, + }, + GeometryRef { + ty: GeometryType::Solid, + pos: 2, + len: 1, + lod: 1, + }, + ], + }, + }), + base_url: url::Url::parse("file:///dummy").unwrap(), + geometry_store: RwLock::new(geometries).into(), + appearance_store: Default::default(), + }; + } +} diff --git a/nusamai/src/sink/gpkg/attributes.rs b/nusamai/src/sink/gpkg/attributes.rs index 5e0c407d..a22734cb 100644 --- a/nusamai/src/sink/gpkg/attributes.rs +++ b/nusamai/src/sink/gpkg/attributes.rs @@ -30,7 +30,7 @@ pub fn prepare_object_attributes(obj: &Object) -> IndexMap { // 0 for false and 1 for true in SQLite attributes.insert(attr_name.into(), if *b { "1".into() } else { "0".into() }); } - Value::URI(u) => { + Value::Uri(u) => { // value of the URI attributes.insert(attr_name.into(), u.value().to_string()); } diff --git a/nusamai/src/sink/mvt/tags.rs b/nusamai/src/sink/mvt/tags.rs index ab8e14b6..4bfd88bc 100644 --- a/nusamai/src/sink/mvt/tags.rs +++ b/nusamai/src/sink/mvt/tags.rs @@ -29,7 +29,7 @@ pub fn convert_properties( nusamai_citygml::Value::Boolean(v) => { tags.extend(tags_enc.add(name, (*v).into())); } - nusamai_citygml::Value::URI(v) => { + nusamai_citygml::Value::Uri(v) => { tags.extend(tags_enc.add(name, v.value().to_string().into())); } nusamai_citygml::Value::Date(v) => { diff --git a/nusamai/src/sink/shapefile/mod.rs b/nusamai/src/sink/shapefile/mod.rs index c096285c..04419e99 100644 --- a/nusamai/src/sink/shapefile/mod.rs +++ b/nusamai/src/sink/shapefile/mod.rs @@ -221,6 +221,7 @@ mod tests { }], }, }), + base_url: url::Url::parse("file:///dummy").unwrap(), geometry_store: RwLock::new(geometries).into(), appearance_store: Default::default(), }; diff --git a/nusamai/src/source/citygml.rs b/nusamai/src/source/citygml.rs index f7a02ab5..81f7058b 100644 --- a/nusamai/src/source/citygml.rs +++ b/nusamai/src/source/citygml.rs @@ -57,7 +57,7 @@ impl DataSource for CityGmlSource { let mut xml_reader = quick_xml::NsReader::from_reader(reader); let source_url = Url::from_file_path(fs::canonicalize(Path::new(filename))?).unwrap(); - let context = nusamai_citygml::ParseContext::new(source_url, &code_resolver); + let context = nusamai_citygml::ParseContext::new(source_url.clone(), &code_resolver); let mut citygml_reader = CityGmlReader::new(context); let mut st = citygml_reader.start_root(&mut xml_reader)?; @@ -100,6 +100,7 @@ fn toplevel_dispatcher( if let Some(root) = cityobj.into_object() { let entity = Entity { root, + base_url: url::Url::parse("file:///dummy").unwrap(), geometry_store: RwLock::new(geometry_store).into(), appearance_store: Default::default(), // TODO: from local appearances }; @@ -137,6 +138,10 @@ fn toplevel_dispatcher( } })?; + // for texture in &mut global_appearances.textures { + // texture.image_url = base_url.join(texture.image_url.as_str()).unwrap(); + // } + for entity in entities { if feedback.is_canceled() { break; diff --git a/nusamai/src/transformer/transform/flatten.rs b/nusamai/src/transformer/transform/flatten.rs index 52fc0fa8..6bd2284d 100644 --- a/nusamai/src/transformer/transform/flatten.rs +++ b/nusamai/src/transformer/transform/flatten.rs @@ -165,6 +165,7 @@ impl FlattenTreeTransform { } out.push(Entity { root: Value::Object(obj), + base_url: url::Url::parse("file:///dummy").unwrap(), geometry_store: geom_store.clone(), appearance_store: appearance_store.clone(), }); diff --git a/nusamai/src/transformer/transform/mod.rs b/nusamai/src/transformer/transform/mod.rs index 7243f3fe..b9c53500 100644 --- a/nusamai/src/transformer/transform/mod.rs +++ b/nusamai/src/transformer/transform/mod.rs @@ -97,6 +97,7 @@ mod tests { geometries: Default::default(), }, }), + base_url: url::Url::parse("file:///dummy").unwrap(), geometry_store: RwLock::new(GeometryStore::default()).into(), appearance_store: Default::default(), }, diff --git a/nusamai/tests/pipeline.rs b/nusamai/tests/pipeline.rs index c0fcd269..e0baa92f 100644 --- a/nusamai/tests/pipeline.rs +++ b/nusamai/tests/pipeline.rs @@ -9,6 +9,7 @@ use nusamai::transformer::{self, Transformer}; use nusamai_citygml::schema::Schema; use nusamai_plateau::Entity; use rand::prelude::*; +use url::Url; static INIT: Once = Once::new(); @@ -42,6 +43,7 @@ impl DataSource for DummySource { let obj = Parcel { entity: Entity { root: nusamai_citygml::Value::Double(0.), + base_url: Url::parse("file:///dummy").unwrap(), geometry_store: Default::default(), appearance_store: Default::default(), },