Skip to content

Commit

Permalink
XML読み込みの土台作り (#3)
Browse files Browse the repository at this point in the history
宣言的なスキーマ定義コードをもとに、Rust の proc_macro を使って、XMLツリーを探索するコードを自動生成します。

実現方法がある程度固まってきたので、このへんでいったんマージしたいです。

ジオメトリの扱いも一定程度実装してある。アピアランス周り(色、テクスチャ)は全く考慮していません。

現時点ではテストは未整備。

```
├── citygml -- CityGMLパーサの本体
│   ├── macros -- CityGMLパーサを利用したXMLの探索を自動導出 (derive) する手続きマクロ
│   │   └── src 
│   └── src
├── examples -- .gml ファイルをパースするサンプルコード
└── src
    └── models -- 上記のパーサの仕組みを利用した、code-first なスキーマ定義
```

実行例:

```
cargo run --example parse --release -- /path/to/13100_tokyo23-ku_2022_citygml_1_lod4-2_op/udx/bldg/53393680_bldg_6697_lod4.0_op.gml
```

(興味のある方向け)マクロの展開の様子は以下のようにすれば見れます:

```
cargo install cargo-expand
cargo expand models::building --no-default-feature
```
  • Loading branch information
ciscorn authored Dec 4, 2023
1 parent d1ec13f commit 57e86bb
Show file tree
Hide file tree
Showing 30 changed files with 2,241 additions and 1 deletion.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
members = [
"nusamai-geometry",
"nusamai-geojson",
"nusamai-plateau",
"app/src-tauri"
]
resolver = "2"
resolver = "2"
1 change: 1 addition & 0 deletions nusamai-plateau/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
20 changes: 20 additions & 0 deletions nusamai-plateau/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "nusamai-plateau"
version = "0.1.0"
edition = "2021"

[features]
default = ["serde"]

[dependencies]
quick-xml = "0.31"
serde = { version = "1.0.193", features = ["derive"], optional = true }
citygml = { path = "./citygml", features = ["serde"]}

[dev-dependencies]
zstd = { version = "0.13.0", features = ["zdict_builder"] }
bincode = { version = "2.0.0-rc", default-features = false, features = ["std", "serde"] }
clap = { version = "4.4", features = ["derive"] }
serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0.108"
lz4_flex = "0.11.1"
16 changes: 16 additions & 0 deletions nusamai-plateau/citygml/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "citygml"
version = "0.1.0"
edition = "2021"

[features]
default = ["serde"]
serde = ["dep:serde", "nusamai-geometry/serde"]

[dependencies]
indexmap = "2.1"
macros = { path = "./macros" }
nusamai-geometry = { path = "../../nusamai-geometry", features = ["serde"]}
quick-xml = "0.31"
serde = { version = "1.0", features = ["derive"], optional = true }
thiserror = "1.0"
12 changes: 12 additions & 0 deletions nusamai-plateau/citygml/macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "macros"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
189 changes: 189 additions & 0 deletions nusamai-plateau/citygml/macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
extern crate proc_macro;

use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, Data, DataEnum, DataStruct, DeriveInput, Error, LitByteStr};

const CITYGML_ATTR_IDENT: &str = "citygml";

fn generate_citygml_struct_model(
derive_input: &DeriveInput,
struct_data: &DataStruct,
) -> Result<TokenStream, Error> {
let mut attribute_arms = Vec::new();
let mut chlid_arms = Vec::new();

for field in &struct_data.fields {
let Some(field_ident) = &field.ident else {
continue;
};
let field_ty = &field.ty;
for attr in &field.attrs {
if !attr.path().is_ident(CITYGML_ATTR_IDENT) {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("path") {
let path: LitByteStr = meta.value()?.parse()?;

if path.value().starts_with(b"@") {
// XML attributes (e.g. @gml:id)
attribute_arms.push(quote! {
#path => {
self.id = <#field_ty as citygml::CityGMLAttribute>::parse_attr_value(
std::str::from_utf8(value).unwrap(),
)?;
Ok(())
}
});
} else {
// XML child elements (e.g. bldg:measuredHeight)
chlid_arms.push(quote! {
#path => <#field_ty as CityGMLElement>::parse(&mut self.#field_ident, st),
});
}
Ok(())
}
else if meta.path.is_ident("auto_geom") {
let prefix: LitByteStr = meta.value()?.parse()?;

let mut add_arm = |lod: u8, name: &[u8], geomtype: &str | {
let mut c = prefix.value().clone();
c.push(b':');
c.extend(name);
let pat = LitByteStr::new(c.as_ref(), prefix.span());
let geomtype = format_ident!("{}", geomtype);

chlid_arms.push(quote! {
#pat => st.parse_geometric_attr(&mut self.#field_ident, #lod, ::citygml::geometric::GeometryParseType::#geomtype),
});
};

add_arm(1, b"lod1Solid", "Solid");
add_arm(1, b"lod1MultiSurface", "MultiSurface");
add_arm(2, b"lod2MultiSurface", "MultiSurface");
add_arm(3, b"lod3MultiSurface", "MultiSurface");
add_arm(4, b"lod4MultiSurface", "MultiSurface");
add_arm(1, b"lod1Geometry", "Geometry");
add_arm(2, b"lod2Geometry", "Geometry");
add_arm(3, b"lod3Geometry", "Geometry");
add_arm(4, b"lod4Geometry", "Geometry");
add_arm(1, b"tin", "Triangulated");

Ok(())
} else {
Err(meta.error("unrecognized attribute"))
}
})?;
}
}

let (impl_generics, ty_generics, where_clause) = &derive_input.generics.split_for_impl();
let struct_name = &derive_input.ident;

let attr_parsing = (!attribute_arms.is_empty()).then(|| {
quote! {
st.parse_attributes(|name, value| match name {
#(#attribute_arms)*
_ => Ok(()),
})?;
}
});

Ok(quote! {
impl #impl_generics ::citygml::CityGMLElement for #struct_name #ty_generics #where_clause {
fn parse<R: std::io::BufRead>(&mut self, st: &mut ::citygml::SubTreeReader<R>) -> Result<(), ::citygml::ParseError> {
#attr_parsing

st.parse_children(|st| {
match st.current_path() {
#(#chlid_arms)*
_ => Ok(()),
}
})
}
}
})
}

fn generate_citygml_enum_model(
derive_input: &DeriveInput,
enum_data: &DataEnum,
) -> Result<TokenStream, Error> {
let mut child_arms = Vec::new();
for variant in &enum_data.variants {
if variant.fields.len() > 1 {
return Err(Error::new_spanned(
variant,
"variant must not have two or more fields",
));
}
if variant.fields.is_empty() {
continue;
}
let field = variant.fields.iter().next().unwrap();
let field_ty = &field.ty;
let variant_ident = &variant.ident;

for attr in &variant.attrs {
if !attr.path().is_ident(CITYGML_ATTR_IDENT) {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("path") {
let value = meta.value()?;
let path: LitByteStr = value.parse()?;

let arm = quote! {
#path => {
let mut v: #field_ty = Default::default();
<#field_ty as CityGMLElement>::parse(&mut v, st)?;
*self = Self::#variant_ident(v);
Ok(())
}
};
child_arms.push(arm);
}
Ok(())
})?;
}
}

let (impl_generics, ty_generics, where_clause) = &derive_input.generics.split_for_impl();
let struct_name = &derive_input.ident;

let tokens = quote! {
impl #impl_generics ::citygml::CityGMLElement for #struct_name #ty_generics #where_clause {
fn parse<R: ::std::io::BufRead>(&mut self, st: &mut ::citygml::SubTreeReader<R>) -> Result<(), ::citygml::ParseError> {
st.parse_children(|st| {
match st.current_path() {
#(#child_arms)*
_ => Ok(()),
}
})
}
}
};

Ok(tokens)
}

fn generate_citygml_model(derive_input: &DeriveInput) -> Result<TokenStream, Error> {
match &derive_input.data {
Data::Struct(data) => generate_citygml_struct_model(derive_input, data),
Data::Enum(data) => generate_citygml_enum_model(derive_input, data),
_ => Err(Error::new_spanned(
derive_input,
"target must be struct or enum",
)),
}
}

#[proc_macro_derive(CityGMLElement, attributes(citygml))]
pub fn derive_citygml_model(token: proc_macro::TokenStream) -> proc_macro::TokenStream {
match generate_citygml_model(&parse_macro_input!(token)) {
Ok(tokens) => tokens,
Err(e) => e.to_compile_error(),
}
.into()
}
94 changes: 94 additions & 0 deletions nusamai-plateau/citygml/src/geometric.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use nusamai_geometry::{MultiLineString, MultiPolygon};

#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Debug, Clone, Copy)]
pub enum GeometryParseType {
Geometry,
Solid,
MultiSurface,
MultiCurve,
Triangulated,
}

#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Debug, Clone, Copy, Default)]
pub enum GeometryType {
#[default]
Unknown,
Solid,
Surface,
Triangle,
}

#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Debug)]
pub struct GeometryRefEntry {
#[serde(rename = "type")]
pub ty: GeometryType,
pub lod: u8,
pub pos: u32,
pub len: u32,
}

pub type GeometryRef = Vec<GeometryRefEntry>;

/// Geometries in a toplevel city object and its children.
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Debug, Default)]
pub struct Geometries {
pub vertices: Vec<[f64; 3]>,
pub polygons: MultiPolygon<'static, 1, u32>,
pub linestrings: MultiLineString<'static, 1, u32>,
}

/// Store for collecting vertices and polygons from GML.
#[derive(Default)]
pub struct GeometryCollector {
pub vertices: indexmap::IndexSet<[u64; 3]>,
pub multi_polygon: MultiPolygon<'static, 1, u32>,
pub multi_linestring: MultiLineString<'static, 1, u32>,
}

impl GeometryCollector {
pub fn add_exterior_ring(&mut self, iter: impl Iterator<Item = [f64; 3]>) -> usize {
self.multi_polygon.add_exterior(iter.map(|v| {
// ...
let vbits = [v[0].to_bits(), v[1].to_bits(), v[2].to_bits()];
let (index, _) = self.vertices.insert_full(vbits);
[index as u32]
}));

self.multi_polygon.len() - 1
}

pub fn add_interior_ring(&mut self, iter: impl Iterator<Item = [f64; 3]>) {
self.multi_polygon.add_interior(iter.map(|v| {
// ...
let vbits = [v[0].to_bits(), v[1].to_bits(), v[2].to_bits()];
let (index, _) = self.vertices.insert_full(vbits);
[index as u32]
}));
}

pub fn to_geometries(&self) -> Geometries {
let mut vertices = Vec::with_capacity(self.vertices.len());
for vbits in &self.vertices {
vertices.push([
f64::from_bits(vbits[0]),
f64::from_bits(vbits[1]),
f64::from_bits(vbits[2]),
]);
}
Geometries {
vertices,
polygons: self.multi_polygon.clone(),
linestrings: self.multi_linestring.clone(),
}
}

pub fn clear(&mut self) {
self.vertices.clear();
self.multi_polygon.clear();
self.multi_linestring.clear();
}
}
11 changes: 11 additions & 0 deletions nusamai-plateau/citygml/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
pub mod geometric;
pub mod model;
pub mod namespace;
pub mod parser;

pub use geometric::*;
pub use model::*;
pub use namespace::*;
pub use parser::*;

pub use macros::CityGMLElement;
Loading

0 comments on commit 57e86bb

Please sign in to comment.