diff --git a/app/src-tauri/Cargo.toml b/app/src-tauri/Cargo.toml index 23a2df7d9..b56fbf20e 100644 --- a/app/src-tauri/Cargo.toml +++ b/app/src-tauri/Cargo.toml @@ -20,12 +20,10 @@ serde = { version = "1.0", features = ["derive"] } tauri = { version = "1.5.2", features = ["dialog-all"] } nusamai-geometry = { path = "../../nusamai-geometry" } nusamai-geojson = { path = "../../nusamai-geojson" } -earcut-rs = { git = "https://github.com/MIERUNE/earcut-rs.git" } +nusamai-plateau = { path = "../../nusamai-plateau" } +citygml = {path = "../../nusamai-plateau/citygml" } quick-xml = "0.31.0" -thiserror = "1.0.50" geojson = "0.24.1" -byteorder = "1.5.0" -indexmap = "2.1.0" [features] diff --git a/app/src-tauri/src/example.rs b/app/src-tauri/src/example.rs index 0916ae946..33399eb05 100644 --- a/app/src-tauri/src/example.rs +++ b/app/src-tauri/src/example.rs @@ -1,242 +1,68 @@ //! デモ用 -//! nusamai-{geometry,geojson}/examples/citygml_polygons.rs を元にしています。 - -use byteorder::{ByteOrder, LittleEndian, WriteBytesExt}; -use indexmap::IndexSet; -use nusamai_geojson::{geojson_geometry_to_feature, nusamai_to_geojson_geometry}; -use nusamai_geometry::{Geometry, MultiPolygon3}; -use quick_xml::{ - events::Event, - name::{Namespace, ResolveResult::Bound}, - reader::NsReader, -}; +//! nusamai-geojson の exmaple/gml2geojson を元にした、暫定的な処理 +use citygml::{CityGMLElement, CityGMLReader, ParseError, SubTreeReader}; +use nusamai_geojson::toplevel_cityobj_to_geojson_features; +use nusamai_plateau::TopLevelCityObject; use std::fs; +use std::io::BufRead; use std::io::BufWriter; -use std::io::Write; -use thiserror::Error; -const GML_NS: Namespace = Namespace(b"http://www.opengis.net/gml"); -const BUILDING_NS: Namespace = Namespace(b"http://www.opengis.net/citygml/building/2.0"); -const CITYFURNITURE_NS: Namespace = Namespace(b"http://www.opengis.net/citygml/cityfurniture/2.0"); -const TRANSPORTATION_NS: Namespace = - Namespace(b"http://www.opengis.net/citygml/transportation/2.0"); -const BRIDGE_NS: Namespace = Namespace(b"http://www.opengis.net/citygml/bridge/2.0"); -const VEGETATION_NS: Namespace = Namespace(b"http://www.opengis.net/citygml/vegetation/2.0"); +fn toplevel_dispatcher( + st: &mut SubTreeReader, +) -> Result, ParseError> { + let mut cityobjs: Vec = vec![]; -#[derive(Error, Debug)] -pub enum ParseError { - #[error("XML error: {0}")] - XmlError(quick_xml::Error), -} + match st.parse_children(|st| match st.current_path() { + b"core:cityObjectMember" => { + let mut cityobj: nusamai_plateau::models::TopLevelCityObject = Default::default(); + cityobj.parse(st)?; + let geometries = st.collect_geometries(); -fn parse_polygon( - reader: &mut NsReader<&[u8]>, - mpoly: &mut MultiPolygon3, - buf: &mut Vec, -) -> Result<(), ParseError> { - let mut is_interior = false; - let mut in_poslist = false; - loop { - match reader.read_resolved_event() { - Ok((Bound(GML_NS), Event::Start(e))) => match e.local_name().as_ref() { - b"posList" => in_poslist = true, - b"exterior" => is_interior = false, - b"interior" => is_interior = true, - _ => (), - }, - Ok((Bound(GML_NS), Event::End(e))) => match e.local_name().as_ref() { - b"Polygon" => return Ok(()), - b"posList" => in_poslist = false, - _ => (), - }, - Ok((_, Event::Text(e))) => { - if !in_poslist { - continue; - } - let text = e.unescape().unwrap(); - buf.clear(); - buf.extend( - text.split_ascii_whitespace() - .map(|v| v.parse::().unwrap()), - ); - if is_interior { - mpoly.add_interior(buf.chunks_exact(3).map(|c| [c[1], c[0], c[2]])); - // lon, lat, height - } else { - mpoly.add_exterior(buf.chunks_exact(3).map(|c| [c[1], c[0], c[2]])); - // lon, lat, height - } + if let Some(root) = cityobj.into_object() { + let obj = TopLevelCityObject { root, geometries }; + cityobjs.push(obj); } - Ok(_) => (), - Err(e) => return Err(ParseError::XmlError(e)), - } - } -} -fn parse_lod_geometry( - reader: &mut NsReader<&[u8]>, - mpoly: &mut MultiPolygon3, - buf: &mut Vec, -) -> Result<(), ParseError> { - let mut depth = 0; - loop { - match reader.read_resolved_event() { - Ok((Bound(GML_NS), Event::Start(e))) => match e.local_name().as_ref() { - b"Polygon" => parse_polygon(reader, mpoly, buf)?, - _ => depth += 1, - }, - Ok((_, Event::Start(_))) => depth += 1, - Ok((_, Event::End(_))) => match depth { - 0 => return Ok(()), - _ => depth -= 1, - }, - Ok(_) => (), - Err(e) => return Err(ParseError::XmlError(e)), + Ok(()) } - } -} - -fn parse_cityobj( - reader: &mut NsReader<&[u8]>, - buf: &mut Vec, -) -> Result, ParseError> { - let mut mpoly = MultiPolygon3::new(); - let mut depth = 0; - let mut max_lod = 0; - loop { - let ev = reader.read_resolved_event(); - match ev { - Ok(( - Bound( - BUILDING_NS | CITYFURNITURE_NS | TRANSPORTATION_NS | VEGETATION_NS | BRIDGE_NS, - ), - Event::Start(e), - )) => match e.local_name().as_ref() { - b"lod4Geometry" | b"lod4MultiSurface" => { - if max_lod < 4 { - max_lod = 4; - mpoly.clear(); - } - if max_lod == 4 { - parse_lod_geometry(reader, &mut mpoly, buf)?; - } else { - depth += 1; - } - } - b"lod3Geometry" | b"lod3MultiSurface" => { - if max_lod < 3 { - max_lod = 3; - mpoly.clear(); - } - if max_lod == 3 { - parse_lod_geometry(reader, &mut mpoly, buf)?; - } else { - depth += 1; - } - } - b"lod2Geometry" | b"lod2MultiSurface" => { - if max_lod < 2 { - max_lod = 2; - mpoly.clear(); - } - if max_lod == 2 { - parse_lod_geometry(reader, &mut mpoly, buf)?; - } else { - depth += 1; - } - } - b"lod1Solid" | b"lod1MultiSurface" => { - if max_lod < 1 { - max_lod = 1; - mpoly.clear(); - } - if max_lod == 1 { - parse_lod_geometry(reader, &mut mpoly, buf)?; - } else { - depth += 1; - } - } - _ => depth += 1, - }, - Ok((_, Event::Start(_))) => depth += 1, - Ok((_, Event::End(_))) => match depth { - 0 => return Ok(mpoly), - _ => depth -= 1, - }, - Ok(_) => (), - Err(e) => return Err(ParseError::XmlError(e)), + b"gml:boundedBy" | b"app:appearanceMember" => { + st.skip_current_element()?; + Ok(()) } - } -} - -fn parse_body(reader: &mut NsReader<&[u8]>) -> Result>, ParseError> { - let mut mpolys: Vec = Vec::new(); - let mut buf: Vec = Vec::new(); - loop { - match reader.read_resolved_event() { - Ok((_, Event::Eof)) => return Ok(mpolys), - Ok(( - Bound( - BUILDING_NS | CITYFURNITURE_NS | TRANSPORTATION_NS | VEGETATION_NS | BRIDGE_NS, - ), - Event::Start(e), - )) => match e.local_name().as_ref() { - b"Building" - | b"CityFurniture" - | b"Road" - | b"Bridge" - | b"SolitaryVegetationObject" - | b"PlantCover" => mpolys.push(parse_cityobj(reader, &mut buf)?), - _ => (), - }, - Ok(_) => (), - Err(e) => return Err(ParseError::XmlError(e)), + other => Err(ParseError::SchemaViolation(format!( + "Unrecognized element {}", + String::from_utf8_lossy(other) + ))), + }) { + Ok(_) => Ok(cityobjs), + Err(e) => { + println!("Err: {:?}", e); + Err(e) } } } -fn load_citygml_file(input_path: &str) -> Vec { - // load - - let mut all_mpolys = Vec::new(); +pub fn citygml_to_geojson(input_path: &str, output_path: &str) { + let reader = std::io::BufReader::new(std::fs::File::open(input_path).unwrap()); + let mut xml_reader = quick_xml::NsReader::from_reader(reader); - let xml = fs::read_to_string(input_path).unwrap(); - let mut reader = NsReader::from_str(&xml); - reader.trim_text(true); - match parse_body(&mut reader) { - Ok(mpolys) => { - println!( - "features={features} polygons={polygons}", - features = mpolys.len(), - polygons = mpolys.iter().flatten().count() - ); - all_mpolys.extend(mpolys); - } - Err(e) => match e { - ParseError::XmlError(e) => { - println!("Error at position {}: {:?}", reader.buffer_position(), e) - } + let cityobjs = match CityGMLReader::new().start_root(&mut xml_reader) { + Ok(mut st) => match toplevel_dispatcher(&mut st) { + Ok(items) => items, + Err(e) => panic!("Err: {:?}", e), }, + Err(e) => panic!("Err: {:?}", e), }; - all_mpolys -} - -///////////////////////////////////////////// -// GeoJSON - -pub fn citygml_to_geojson(input_path: &str, output_path: &str) { - let all_mpolys = load_citygml_file(input_path); - - let geojson_features = all_mpolys + let geojson_features: Vec = cityobjs .iter() - .map(|poly| nusamai_to_geojson_geometry(&Geometry::MultiPolygon::<3, f64>(poly.to_owned()))) - .map(geojson_geometry_to_feature); + .flat_map(toplevel_cityobj_to_geojson_features) + .collect(); let geojson_feature_collection = geojson::FeatureCollection { bbox: None, - features: geojson_features.collect(), + features: geojson_features, foreign_members: None, }; let geojson = geojson::GeoJson::from(geojson_feature_collection); @@ -245,122 +71,3 @@ pub fn citygml_to_geojson(input_path: &str, output_path: &str) { let mut writer = BufWriter::new(&mut file); serde_json::to_writer(&mut writer, &geojson).unwrap(); } - -///////////////////////////////////////////// -// PLY - -const PLY_HEADER_TEMPLATE: &str = r##"ply -format binary_little_endian 1.0 -element vertex {n_verts} -property float x -property float y -property float z -element face {n_faces} -property list uchar uint vertex_indices -end_header -"##; - -// comment crs: GEOGCRS["JGD2011",DATUM["Japanese Geodetic Datum 2011",ELLIPSOID["GRS 1980",6378137,298.257222101,LENGTHUNIT["metre",1]]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],CS[ellipsoidal,2],AXIS["geodetic latitude (Lat)",north,ORDER[1],ANGLEUNIT["degree",0.0174532925199433]],AXIS["geodetic longitude (Lon)",east,ORDER[2],ANGLEUNIT["degree",0.0174532925199433]],USAGE[SCOPE["Horizontal component of 3D system."],AREA["Japan - onshore and offshore."],BBOX[17.09,122.38,46.05,157.65]],ID["EPSG",6668]] - -fn write_features(mpolys: &[MultiPolygon3], mu_lng: f64, mu_lat: f64, output_path: &str) { - use earcut_rs::{utils_3d::project3d_to_2d, Earcut}; - let mut earcutter = Earcut::new(); - let mut buf3d: Vec = Vec::new(); - let mut buf2d: Vec = Vec::new(); - let mut triangles_out: Vec = Vec::new(); - - let mut indices: Vec = Vec::new(); - let mut vertices: IndexSet<[u32; 3]> = IndexSet::new(); - - for mpoly in mpolys { - for poly in mpoly { - let num_outer = match poly.hole_indices().first() { - Some(&v) => v as usize, - None => poly.coords().len() / 3, - }; - - buf3d.clear(); - buf3d.extend(poly.coords().chunks_exact(3).flat_map(|v| { - let (lat, lng) = (v[0], v[1]); - [ - (lng - mu_lng) * (10000000. * lat.to_radians().cos() / 90.), - (lat - mu_lat) * (10000000. / 90.), - v[2], - ] - })); - - if project3d_to_2d(&buf3d, num_outer, &mut buf2d) { - // earcut - earcutter.earcut(&buf2d, poly.hole_indices(), 2, &mut triangles_out); - // indices and vertices - indices.extend(triangles_out.iter().map(|idx| { - let vbits = [ - (buf3d[*idx as usize * 3] as f32).to_bits(), - (buf3d[*idx as usize * 3 + 1] as f32).to_bits(), - (buf3d[*idx as usize * 3 + 2] as f32).to_bits(), - ]; - let (index, _) = vertices.insert_full(vbits); - index as u32 - })); - } else { - println!("WARN: polygon does not have normal"); - } - } - } - - println!("{:?} {:?}", vertices.len(), indices.len()); - let file = std::fs::File::create(output_path).unwrap(); - let mut writer = std::io::BufWriter::new(file); - - writer - .write_all( - PLY_HEADER_TEMPLATE - .replace("{n_verts}", &vertices.len().to_string()) - .replace("{n_faces}", &(indices.len() / 3).to_string()) - .as_ref(), - ) - .unwrap(); - - let mut buf = [0; 12]; - vertices.iter().for_each(|v| { - LittleEndian::write_u32_into(v, &mut buf); - writer.write_all(&buf).unwrap(); - }); - indices.chunks_exact(3).for_each(|v| { - writer.write_u8(3).unwrap(); - LittleEndian::write_u32_into(v, &mut buf); - writer.write_all(&buf).unwrap(); - }); - - writer.flush().unwrap(); -} - -pub fn citygml_to_ply(input_path: &str, output_path: &str) { - let all_mpolys = load_citygml_file(input_path); - - let (mu_lat, mu_lng) = { - let (mut mu_lat, mut mu_lng) = (0.0, 0.0); - let mut num_features = 0; - for mpoly in &all_mpolys { - let (mut feat_mu_lng, mut feat_mu_lat) = (0.0, 0.0); - let mut num_verts = 0; - for poly in mpoly { - for v in poly.coords().chunks_exact(3) { - num_verts += 1; - feat_mu_lng += v[0]; - feat_mu_lat += v[1]; - } - } - if num_verts > 0 { - num_features += 1; - mu_lat += feat_mu_lng / num_verts as f64; - mu_lng += feat_mu_lat / num_verts as f64; - } - } - (mu_lat / num_features as f64, mu_lng / num_features as f64) - }; - println!("{} {}", mu_lat, mu_lng); - - // Write to PLY - write_features(&all_mpolys, mu_lng, mu_lat, output_path); -} diff --git a/app/src-tauri/src/main.rs b/app/src-tauri/src/main.rs index c2d1a5655..9273092bb 100644 --- a/app/src-tauri/src/main.rs +++ b/app/src-tauri/src/main.rs @@ -18,9 +18,6 @@ fn convert_and_save(input_path: String, output_path: String, filetype: String) { "GeoJSON" => { example::citygml_to_geojson(&input_path, &output_path); } - "PLY" => { - example::citygml_to_ply(&input_path, &output_path); - } _ => { println!("Unknown filetype: {}", filetype); } diff --git a/app/src/lib/settings.ts b/app/src/lib/settings.ts index 69b50e8cb..80f9935d4 100644 --- a/app/src/lib/settings.ts +++ b/app/src/lib/settings.ts @@ -1,4 +1,4 @@ -const fileTypeOptions = ['GeoJSON', 'PLY']; +const fileTypeOptions = ['GeoJSON']; const crsOptions = [ { value: 'EPSG:6678', label: 'JGD2011 / Japan Plane Rectangular CS X' }, diff --git a/nusamai-geojson/Cargo.toml b/nusamai-geojson/Cargo.toml index a17281e9b..8bc5378b6 100644 --- a/nusamai-geojson/Cargo.toml +++ b/nusamai-geojson/Cargo.toml @@ -8,9 +8,10 @@ edition = "2021" [dependencies] geojson = "0.24.1" nusamai-geometry = { path = "../nusamai-geometry" } +nusamai-plateau = { path = "../nusamai-plateau" } [dev-dependencies] +citygml = {path = "../nusamai-plateau/citygml" } clap = { version = "4.4.8", features = ["derive"] } quick-xml = "0.31.0" -serde_json = "1.0.108" -thiserror = "1.0.50" +serde_json = "1.0.108" \ No newline at end of file diff --git a/nusamai-geojson/examples/citygml_polygons.rs b/nusamai-geojson/examples/citygml_polygons.rs deleted file mode 100644 index 44bf28442..000000000 --- a/nusamai-geojson/examples/citygml_polygons.rs +++ /dev/null @@ -1,256 +0,0 @@ -//! CityGMLファイル (.gml) からポリゴンを読み込んで .geojson 形式で出力するデモ -//! nusamai-geometry/examples/citygml_polygons.rs を元にしています。 -//! -//! 使用例: -//! -//! ```bash -//! cargo run --example citygml_polygons --release -- ~/path/to/PLATEAU/22203_numazu-shi_2021_citygml_4_op/udx/*/52385628_*_6697_op.gml -//! ```` -//! -//! このXMLのパース方法は本格的なパーザで使うことを意図していません。 - -use clap::Parser; -use nusamai_geojson::{geojson_geometry_to_feature, nusamai_to_geojson_geometry}; -use nusamai_geometry::{Geometry, MultiPolygon3}; -use quick_xml::{ - events::Event, - name::{Namespace, ResolveResult::Bound}, - reader::NsReader, -}; - -use std::fs; -use std::io::BufWriter; -use thiserror::Error; - -const GML_NS: Namespace = Namespace(b"http://www.opengis.net/gml"); -const BUILDING_NS: Namespace = Namespace(b"http://www.opengis.net/citygml/building/2.0"); -const CITYFURNITURE_NS: Namespace = Namespace(b"http://www.opengis.net/citygml/cityfurniture/2.0"); -const TRANSPORTATION_NS: Namespace = - Namespace(b"http://www.opengis.net/citygml/transportation/2.0"); -const BRIDGE_NS: Namespace = Namespace(b"http://www.opengis.net/citygml/bridge/2.0"); -const VEGETATION_NS: Namespace = Namespace(b"http://www.opengis.net/citygml/vegetation/2.0"); - -#[derive(Error, Debug)] -pub enum ParseError { - #[error("XML error: {0}")] - XmlError(quick_xml::Error), -} - -fn parse_polygon( - reader: &mut NsReader<&[u8]>, - mpoly: &mut MultiPolygon3, - buf: &mut Vec, -) -> Result<(), ParseError> { - let mut is_interior = false; - let mut in_poslist = false; - loop { - match reader.read_resolved_event() { - Ok((Bound(GML_NS), Event::Start(e))) => match e.local_name().as_ref() { - b"posList" => in_poslist = true, - b"exterior" => is_interior = false, - b"interior" => is_interior = true, - _ => (), - }, - Ok((Bound(GML_NS), Event::End(e))) => match e.local_name().as_ref() { - b"Polygon" => return Ok(()), - b"posList" => in_poslist = false, - _ => (), - }, - Ok((_, Event::Text(e))) => { - if !in_poslist { - continue; - } - let text = e.unescape().unwrap(); - buf.clear(); - buf.extend( - text.split_ascii_whitespace() - .map(|v| v.parse::().unwrap()), - ); - if is_interior { - mpoly.add_interior(buf.chunks_exact(3).map(|c| [c[1], c[0], c[2]])); - // lon, lat, height - } else { - mpoly.add_exterior(buf.chunks_exact(3).map(|c| [c[1], c[0], c[2]])); - // lon, lat, height - } - } - Ok(_) => (), - Err(e) => return Err(ParseError::XmlError(e)), - } - } -} - -fn parse_lod_geometry( - reader: &mut NsReader<&[u8]>, - mpoly: &mut MultiPolygon3, - buf: &mut Vec, -) -> Result<(), ParseError> { - let mut depth = 0; - loop { - match reader.read_resolved_event() { - Ok((Bound(GML_NS), Event::Start(e))) => match e.local_name().as_ref() { - b"Polygon" => parse_polygon(reader, mpoly, buf)?, - _ => depth += 1, - }, - Ok((_, Event::Start(_))) => depth += 1, - Ok((_, Event::End(_))) => match depth { - 0 => return Ok(()), - _ => depth -= 1, - }, - Ok(_) => (), - Err(e) => return Err(ParseError::XmlError(e)), - } - } -} - -fn parse_cityobj( - reader: &mut NsReader<&[u8]>, - buf: &mut Vec, -) -> Result, ParseError> { - let mut mpoly = MultiPolygon3::new(); - let mut depth = 0; - let mut max_lod = 0; - loop { - let ev = reader.read_resolved_event(); - match ev { - Ok(( - Bound( - BUILDING_NS | CITYFURNITURE_NS | TRANSPORTATION_NS | VEGETATION_NS | BRIDGE_NS, - ), - Event::Start(e), - )) => match e.local_name().as_ref() { - b"lod4Geometry" | b"lod4MultiSurface" => { - if max_lod < 4 { - max_lod = 4; - mpoly.clear(); - } - if max_lod == 4 { - parse_lod_geometry(reader, &mut mpoly, buf)?; - } else { - depth += 1; - } - } - b"lod3Geometry" | b"lod3MultiSurface" => { - if max_lod < 3 { - max_lod = 3; - mpoly.clear(); - } - if max_lod == 3 { - parse_lod_geometry(reader, &mut mpoly, buf)?; - } else { - depth += 1; - } - } - b"lod2Geometry" | b"lod2MultiSurface" => { - if max_lod < 2 { - max_lod = 2; - mpoly.clear(); - } - if max_lod == 2 { - parse_lod_geometry(reader, &mut mpoly, buf)?; - } else { - depth += 1; - } - } - b"lod1Solid" | b"lod1MultiSurface" => { - if max_lod < 1 { - max_lod = 1; - mpoly.clear(); - } - if max_lod == 1 { - parse_lod_geometry(reader, &mut mpoly, buf)?; - } else { - depth += 1; - } - } - _ => depth += 1, - }, - Ok((_, Event::Start(_))) => depth += 1, - Ok((_, Event::End(_))) => match depth { - 0 => return Ok(mpoly), - _ => depth -= 1, - }, - Ok(_) => (), - Err(e) => return Err(ParseError::XmlError(e)), - } - } -} - -fn parse_body(reader: &mut NsReader<&[u8]>) -> Result>, ParseError> { - let mut mpolys: Vec = Vec::new(); - let mut buf: Vec = Vec::new(); - loop { - match reader.read_resolved_event() { - Ok((_, Event::Eof)) => return Ok(mpolys), - Ok(( - Bound( - BUILDING_NS | CITYFURNITURE_NS | TRANSPORTATION_NS | VEGETATION_NS | BRIDGE_NS, - ), - Event::Start(e), - )) => match e.local_name().as_ref() { - b"Building" - | b"CityFurniture" - | b"Road" - | b"Bridge" - | b"SolitaryVegetationObject" - | b"PlantCover" => mpolys.push(parse_cityobj(reader, &mut buf)?), - _ => (), - }, - Ok(_) => (), - Err(e) => return Err(ParseError::XmlError(e)), - } - } -} - -#[derive(Parser)] -struct Args { - #[clap(required = true)] - filenames: Vec, -} - -fn main() { - let args = Args::parse(); - - let mut all_mpolys = Vec::new(); - - for filename in args.filenames { - let xml = fs::read_to_string(filename).unwrap(); - let mut reader = NsReader::from_str(&xml); - reader.trim_text(true); - match parse_body(&mut reader) { - Ok(mpolys) => { - println!( - "features={features} polygons={polygons}", - features = mpolys.len(), - polygons = mpolys.iter().flatten().count() - ); - all_mpolys.extend(mpolys); - } - Err(e) => match e { - ParseError::XmlError(e) => { - println!("Error at position {}: {:?}", reader.buffer_position(), e) - } - }, - }; - } - - // NOTE: この時点で MultiPolygon にジオメトリデータが詰め込まれている状態 - // - // ここから先は geojson 形式での出力を行う。 - - let geojson_features = all_mpolys - .iter() - .map(|poly| nusamai_to_geojson_geometry(&Geometry::MultiPolygon::<3, f64>(poly.to_owned()))) - .map(geojson_geometry_to_feature); - - let geojson_feature_collection = geojson::FeatureCollection { - bbox: None, - features: geojson_features.collect(), - foreign_members: None, - }; - let geojson = geojson::GeoJson::from(geojson_feature_collection); - - let mut file = fs::File::create("out.geojson").unwrap(); - let mut writer = BufWriter::new(&mut file); - serde_json::to_writer(&mut writer, &geojson).unwrap(); -} diff --git a/nusamai-geojson/examples/gml2geojson.rs b/nusamai-geojson/examples/gml2geojson.rs new file mode 100644 index 000000000..ef98a9a9e --- /dev/null +++ b/nusamai-geojson/examples/gml2geojson.rs @@ -0,0 +1,82 @@ +//! This example converts a CityGML file to GeoJSON and outputs it to a file + +use citygml::{CityGMLElement, CityGMLReader, ParseError, SubTreeReader}; +use clap::Parser; +use nusamai_geojson::toplevel_cityobj_to_geojson_features; +use nusamai_plateau::models::CityObject; +use nusamai_plateau::TopLevelCityObject; +use std::fs; +use std::io::BufRead; +use std::io::BufWriter; + +#[derive(Parser)] +struct Args { + #[clap(required = true)] + filename: String, +} + +fn toplevel_dispatcher( + st: &mut SubTreeReader, +) -> Result, ParseError> { + let mut cityobjs: Vec = vec![]; + + match st.parse_children(|st| match st.current_path() { + b"core:cityObjectMember" => { + let mut cityobj: CityObject = Default::default(); + cityobj.parse(st)?; + let geometries = st.collect_geometries(); + + if let Some(root) = cityobj.into_object() { + let obj = TopLevelCityObject { root, geometries }; + cityobjs.push(obj); + } + + Ok(()) + } + b"gml:boundedBy" | b"app:appearanceMember" => { + st.skip_current_element()?; + Ok(()) + } + other => Err(ParseError::SchemaViolation(format!( + "Unrecognized element {}", + String::from_utf8_lossy(other) + ))), + }) { + Ok(_) => Ok(cityobjs), + Err(e) => { + println!("Err: {:?}", e); + Err(e) + } + } +} + +fn main() { + let args = Args::parse(); + + let reader = std::io::BufReader::new(std::fs::File::open(args.filename).unwrap()); + let mut xml_reader = quick_xml::NsReader::from_reader(reader); + + let cityobjs = match CityGMLReader::new().start_root(&mut xml_reader) { + Ok(mut st) => match toplevel_dispatcher(&mut st) { + Ok(items) => items, + Err(e) => panic!("Err: {:?}", e), + }, + Err(e) => panic!("Err: {:?}", e), + }; + + let geojson_features: Vec = cityobjs + .iter() + .flat_map(toplevel_cityobj_to_geojson_features) + .collect(); + + let geojson_feature_collection = geojson::FeatureCollection { + bbox: None, + features: geojson_features, + foreign_members: None, + }; + let geojson = geojson::GeoJson::from(geojson_feature_collection); + + let mut file = fs::File::create("out.geojson").unwrap(); + let mut writer = BufWriter::new(&mut file); + serde_json::to_writer(&mut writer, &geojson).unwrap(); +} diff --git a/nusamai-geojson/src/conversion.rs b/nusamai-geojson/src/conversion.rs index e18de4802..91ab761e6 100644 --- a/nusamai-geojson/src/conversion.rs +++ b/nusamai-geojson/src/conversion.rs @@ -1,255 +1,115 @@ -use nusamai_geometry::{CoordNum, Geometry, MultiPolygon, Polygon}; +use nusamai_geometry::{MultiLineString, MultiPoint, MultiPolygon, Polygon}; -/// A wrapper to convert an arbitrary "nusamai geometry" to a "geojson geometry" -// TODO: implementations for all geometry variants -pub fn nusamai_to_geojson_geometry( - geometry: &Geometry, +/// Create a GeoJSON geometry from nusamai_plateau::TopLevelCityObject's `multipolygon` geometry +pub fn multipolygon_to_geojson_geometry( + vertices: &[[f64; 3]], + mpoly: &MultiPolygon<1, u32>, ) -> geojson::Geometry { - match geometry { - Geometry::MultiPoint(geom) => multi_point_to_geojson_geometry(geom), - Geometry::LineString(geom) => linestring_to_geojson_geometry(geom), - Geometry::MultiLineString(geom) => multi_linestring_to_geojson_geometry(geom), - Geometry::Polygon(geom) => polygon_to_geojson_geometry(geom), - Geometry::MultiPolygon(geom) => multi_polygon_to_geojson_geometry(geom), - } -} - -fn multi_point_to_geojson_geometry( - mpoint: &nusamai_geometry::MultiPoint, -) -> geojson::Geometry { - let point_list: Vec = mpoint + let ring_list: Vec = mpoly .iter() - .map(|point| point.iter().map(|&t| t.to_f64().unwrap()).collect()) + .map(|poly| polygon_to_rings(vertices, &poly)) .collect(); - geojson::Geometry::new(geojson::Value::MultiPoint(point_list)) + + geojson::Geometry::new(geojson::Value::MultiPolygon(ring_list)) } -fn linestring_to_geojson_geometry( - linestring: &nusamai_geometry::LineString, -) -> geojson::Geometry { - let point_list: geojson::LineStringType = linestring - .iter() - .map(|point| point.iter().map(|&t| t.to_f64().unwrap()).collect()) +fn polygon_to_rings(vertices: &[[f64; 3]], poly: &Polygon<1, u32>) -> geojson::PolygonType { + let linestrings = std::iter::once(poly.exterior()).chain(poly.interiors()); + + let rings: Vec<_> = linestrings + .map(|ls| { + let coords: Vec<_> = ls + .iter_closed() + .map(|idx| vertices[idx[0] as usize].to_vec()) // Get the actual coord values + .collect(); + coords + }) .collect(); - geojson::Geometry::new(geojson::Value::LineString(point_list)) + + rings } -fn multi_linestring_to_geojson_geometry( - mlinestring: &nusamai_geometry::MultiLineString, +/// Create a GeoJSON geometry from nusamai_plateau::TopLevelCityObject's `multilinestring` geometry +pub fn multilinestring_to_geojson_geometry( + vertices: &[[f64; 3]], + mls: &MultiLineString<1, u32>, ) -> geojson::Geometry { - let line_list: Vec = mlinestring + let mls_coords: Vec = mls .iter() - .map(|linestring| { - linestring + .map(|ls| { + let coords: Vec<_> = ls .iter() - .map(|point| point.iter().map(|&t| t.to_f64().unwrap()).collect()) - .collect() + .map(|idx| vertices[idx[0] as usize].to_vec()) // Get the actual coord values + .collect(); + coords }) .collect(); - geojson::Geometry::new(geojson::Value::MultiLineString(line_list)) -} - -fn polygon_to_geojson_geometry( - poly: &Polygon, -) -> geojson::Geometry { - let rings = polygon_to_rings(poly); - geojson::Geometry::new(geojson::Value::Polygon(rings)) + geojson::Geometry::new(geojson::Value::MultiLineString(mls_coords)) } -fn multi_polygon_to_geojson_geometry( - mpoly: &MultiPolygon, +/// Create a GeoJSON geometry from nusamai_plateau::TopLevelCityObject's `multipoint` geometry +pub fn multipoint_to_geojson_geometry( + vertices: &[[f64; 3]], + mpoint: &MultiPoint<1, u32>, ) -> geojson::Geometry { - let ring_list: Vec = - mpoly.iter().map(|poly| polygon_to_rings(&poly)).collect(); - geojson::Geometry::new(geojson::Value::MultiPolygon(ring_list)) -} - -fn polygon_to_rings(poly: &Polygon) -> geojson::PolygonType { - let rings = std::iter::once(poly.exterior()) - .chain(poly.interiors()) - .map(|linestring| { - linestring - .iter_closed() - .map(|slice| slice.iter().map(|&t| t.to_f64().unwrap()).collect()) - .collect() - }) + let mpoint_coords: Vec = mpoint + .iter() + .map(|p| vertices[p[0] as usize].to_vec()) // Get the actual coord values .collect(); - rings + geojson::Geometry::new(geojson::Value::MultiPoint(mpoint_coords)) } #[cfg(test)] mod tests { use super::*; - use nusamai_geometry::{MultiLineString2, MultiPoint2, MultiPolygon2, Polygon2, Polygon3}; - - #[test] - fn test_multi_point_basic() { - let mut mpoint = MultiPoint2::new(); - mpoint.push(&[0., 0.]); - mpoint.push(&[1., 1.]); - mpoint.push(&[2., 2.]); - - let geojson_geometry = multi_point_to_geojson_geometry(&mpoint); - - assert!(geojson_geometry.bbox.is_none()); - assert!(geojson_geometry.foreign_members.is_none()); - - if let geojson::Value::MultiPoint(points) = geojson_geometry.value { - assert_eq!(points.len(), mpoint.len()); - assert_eq!(points[0], vec![0., 0.]); - assert_eq!(points[1], vec![1., 1.]); - assert_eq!(points[2], vec![2., 2.]); - } else { - unreachable!("The result is not a GeoJSON MultiPoint"); - }; - } - - #[test] - fn test_linestring_basic() { - let mut linestring = nusamai_geometry::LineString2::new(); - linestring.push(&[0., 0.]); - linestring.push(&[1., 1.]); - linestring.push(&[2., 2.]); - - let geojson_geometry = linestring_to_geojson_geometry(&linestring); - - assert!(geojson_geometry.bbox.is_none()); - assert!(geojson_geometry.foreign_members.is_none()); - - if let geojson::Value::LineString(points) = geojson_geometry.value { - assert_eq!(points.len(), linestring.len()); - assert_eq!(points[0], vec![0., 0.]); - assert_eq!(points[1], vec![1., 1.]); - assert_eq!(points[2], vec![2., 2.]); - } else { - unreachable!("The result is not a GeoJSON LineString"); - }; - } - - #[test] - fn test_multi_linestring_basic() { - let mut mls = MultiLineString2::new(); - mls.add_linestring(vec![[0., 0.], [1., 1.]]); - mls.add_linestring(vec![[2., 2.], [3., 3.]]); - mls.add_linestring(vec![[4., 4.], [5., 5.], [6., 6.]]); - - let geojson_geometry = multi_linestring_to_geojson_geometry(&mls); - - assert!(geojson_geometry.bbox.is_none()); - assert!(geojson_geometry.foreign_members.is_none()); - - if let geojson::Value::MultiLineString(lines) = geojson_geometry.value { - assert_eq!(lines.len(), mls.len()); - assert_eq!(lines[0], vec![[0., 0.], [1., 1.]]); - assert_eq!(lines[1], vec![[2., 2.], [3., 3.]]); - assert_eq!(lines[2], vec![[4., 4.], [5., 5.], [6., 6.]]); - } else { - unreachable!("The result is not a GeoJSON MultiLineString"); - }; - } - - #[test] - fn test_polygon_basic() { - let mut poly = Polygon2::new(); - poly.add_ring([[0., 0.], [5., 0.], [5., 5.], [0., 5.]]); - poly.add_ring([[1., 1.], [2., 1.], [2., 2.], [1., 2.]]); - poly.add_ring([[3., 3.], [4., 3.], [4., 4.], [3., 4.]]); - - let geojson_geometry = polygon_to_geojson_geometry(&poly); - - assert!(geojson_geometry.bbox.is_none()); - assert!(geojson_geometry.foreign_members.is_none()); - if let geojson::Value::Polygon(rings) = geojson_geometry.value { - assert_eq!(rings.len(), 3); - assert_eq!(rings[0].len(), 5); - assert_eq!(rings[1].len(), 5); - assert_eq!(rings[2].len(), 5); - assert_eq!( - rings[0], - vec![[0., 0.], [5., 0.], [5., 5.], [0., 5.], [0., 0.]] - ); - assert_eq!( - rings[1], - vec![[1., 1.], [2., 1.], [2., 2.], [1., 2.], [1., 1.]] - ); - assert_eq!( - rings[2], - vec![[3., 3.], [4., 3.], [4., 4.], [3., 4.], [3., 3.]] - ); - } else { - unreachable!("The result is not a GeoJSON Polygon"); - }; - } - - #[test] - fn test_polygon_basic_3d() { - let mut poly = Polygon3::new(); - poly.add_ring([[0., 0., 99.], [5., 0., 99.], [5., 5., 99.], [0., 5., 99.]]); - poly.add_ring([[1., 1., 99.], [2., 1., 99.], [2., 2., 99.], [1., 2., 99.]]); - poly.add_ring([[3., 3., 99.], [4., 3., 99.], [4., 4., 99.], [3., 4., 99.]]); - - let geojson_geometry = polygon_to_geojson_geometry(&poly); - - assert!(geojson_geometry.bbox.is_none()); - assert!(geojson_geometry.foreign_members.is_none()); - if let geojson::Value::Polygon(rings) = geojson_geometry.value { - assert_eq!(rings.len(), 3); - assert_eq!(rings[0].len(), 5); - assert_eq!(rings[1].len(), 5); - assert_eq!(rings[2].len(), 5); - assert_eq!( - rings[0], - vec![ - [0., 0., 99.], - [5., 0., 99.], - [5., 5., 99.], - [0., 5., 99.], - [0., 0., 99.] - ] - ); - assert_eq!( - rings[1], - vec![ - [1., 1., 99.], - [2., 1., 99.], - [2., 2., 99.], - [1., 2., 99.], - [1., 1., 99.] - ] - ); - assert_eq!( - rings[2], - vec![ - [3., 3., 99.], - [4., 3., 99.], - [4., 4., 99.], - [3., 4., 99.], - [3., 3., 99.] - ] - ); - } else { - unreachable!("The result is not a GeoJSON Polygon"); - }; - } #[test] - fn test_multi_polygon_basic() { - let mut mpoly = MultiPolygon2::new(); - + fn test_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., 0.], [5., 0.], [5., 5.], [0., 5.], [0., 0.]]); - mpoly.add_interior([[1., 1.], [2., 1.], [2., 2.], [1., 2.], [1., 1.]]); - mpoly.add_interior([[3., 3.], [4., 3.], [4., 4.], [3., 4.], [3., 3.]]); - + 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([[4., 0.], [7., 0.], [7., 3.], [4., 3.], [4., 0.]]); - mpoly.add_interior([[5., 1.], [6., 1.], [6., 2.], [5., 2.], [5., 1.]]); - + mpoly.add_exterior([[12], [13], [14], [15], [12]]); + mpoly.add_interior([[16], [17], [18], [19], [16]]); // 3rd polygon - mpoly.add_exterior([[4., 0.], [7., 0.], [7., 3.], [4., 3.], [4., 0.]]); - mpoly.add_interior([[5., 1.], [6., 1.], [6., 2.], [5., 2.], [5., 1.]]); + mpoly.add_exterior([[20], [21], [22], [23], [20]]); - let geojson_geometry = multi_polygon_to_geojson_geometry(&mpoly); + let geojson_geometry = multipolygon_to_geojson_geometry(&vertices, &mpoly); assert!(geojson_geometry.bbox.is_none()); assert!(geojson_geometry.foreign_members.is_none()); @@ -263,15 +123,33 @@ mod tests { assert_eq!(rings[2].len(), 5); assert_eq!( rings[0], - vec![[0., 0.], [5., 0.], [5., 5.], [0., 5.], [0., 0.]] + vec![ + [0., 0., 111.], + [5., 0., 111.], + [5., 5., 111.], + [0., 5., 111.], + [0., 0., 111.] + ] ); assert_eq!( rings[1], - vec![[1., 1.], [2., 1.], [2., 2.], [1., 2.], [1., 1.]] + vec![ + [1., 1., 111.], + [2., 1., 111.], + [2., 2., 111.], + [1., 2., 111.], + [1., 1., 111.] + ] ); assert_eq!( rings[2], - vec![[3., 3.], [4., 3.], [4., 4.], [3., 4.], [3., 3.]] + vec![ + [3., 3., 111.], + [4., 3., 111.], + [4., 4., 111.], + [3., 4., 111.], + [3., 3., 111.] + ] ); } 1 => { @@ -280,31 +158,129 @@ mod tests { assert_eq!(rings[1].len(), 5); assert_eq!( rings[0], - vec![[4., 0.], [7., 0.], [7., 3.], [4., 3.], [4., 0.]] + vec![ + [4., 0., 222.], + [7., 0., 222.], + [7., 3., 222.], + [4., 3., 222.], + [4., 0., 222.] + ] ); assert_eq!( rings[1], - vec![[5., 1.], [6., 1.], [6., 2.], [5., 2.], [5., 1.]] + vec![ + [5., 1., 222.], + [6., 1., 222.], + [6., 2., 222.], + [5., 2., 222.], + [5., 1., 222.] + ] ); } 2 => { - assert_eq!(rings.len(), 2); + assert_eq!(rings.len(), 1); assert_eq!(rings[0].len(), 5); - assert_eq!(rings[1].len(), 5); assert_eq!( rings[0], - vec![[4., 0.], [7., 0.], [7., 3.], [4., 3.], [4., 0.]] - ); - assert_eq!( - rings[1], - vec![[5., 1.], [6., 1.], [6., 2.], [5., 2.], [5., 1.]] + vec![ + [4., 0., 333.], + [7., 0., 333.], + [7., 3., 333.], + [4., 3., 333.], + [4., 0., 333.] + ] ); } - _ => unreachable!(), + _ => unreachable!("Unexpected number of polygons"), } } } else { unreachable!("The result is not a GeoJSON MultiPolygon"); }; } + + #[test] + fn test_multilinestring() { + let vertices = vec![ + // 1st linestring + [0., 0., 111.], + [1., 1., 111.], + // 2nd linestring + [2., 3., 222.], + [4., 5., 222.], + // 3rd linestring + [6., 7., 333.], + [8., 9., 333.], + [10., 11., 333.], + ]; + + let mut mls = MultiLineString::<1, u32>::new(); + mls.add_linestring([[0], [1]]); + mls.add_linestring([[2], [3]]); + mls.add_linestring([[4], [5], [6]]); + + let geojson_geometry = multilinestring_to_geojson_geometry(&vertices, &mls); + + assert!(geojson_geometry.bbox.is_none()); + assert!(geojson_geometry.foreign_members.is_none()); + if let geojson::Value::MultiLineString(lines) = geojson_geometry.value { + assert_eq!(lines.len(), mls.len()); + for (i, li) in lines.iter().enumerate() { + match i { + 0 => { + assert_eq!(li.len(), 2); + assert_eq!(li[0], [0., 0., 111.]); + assert_eq!(li[1], [1., 1., 111.]); + } + 1 => { + assert_eq!(li.len(), 2); + assert_eq!(li[0], [2., 3., 222.]); + assert_eq!(li[1], [4., 5., 222.]); + } + 2 => { + assert_eq!(li.len(), 3); + assert_eq!(li[0], [6., 7., 333.]); + assert_eq!(li[1], [8., 9., 333.]); + assert_eq!(li[2], [10., 11., 333.]); + } + _ => unreachable!("Unexpected number of lines"), + } + } + } else { + unreachable!("The result is not a GeoJSON MultiLineString"); + } + } + + #[test] + fn test_multipoint() { + let vertices = vec![[0., 0., 111.], [1., 2., 222.], [3., 4., 333.]]; + let mut mpoint = MultiPoint::<1, u32>::new(); + mpoint.push(&[0]); + mpoint.push(&[1]); + mpoint.push(&[2]); + + let geojson_geometry = multipoint_to_geojson_geometry(&vertices, &mpoint); + + assert!(geojson_geometry.bbox.is_none()); + assert!(geojson_geometry.foreign_members.is_none()); + if let geojson::Value::MultiPoint(point_list) = geojson_geometry.value { + assert_eq!(point_list.len(), mpoint.len()); + for (i, point) in point_list.iter().enumerate() { + match i { + 0 => { + assert_eq!(*point, vec![0., 0., 111.]); + } + 1 => { + assert_eq!(*point, vec![1., 2., 222.]); + } + 2 => { + assert_eq!(*point, vec![3., 4., 333.]); + } + _ => unreachable!("Unexpected number of points"), + } + } + } else { + unreachable!("The result is not a GeoJSON MultiPoint"); + } + } } diff --git a/nusamai-geojson/src/lib.rs b/nusamai-geojson/src/lib.rs index 30708637b..ee3c7877a 100644 --- a/nusamai-geojson/src/lib.rs +++ b/nusamai-geojson/src/lib.rs @@ -1,15 +1,121 @@ mod conversion; -pub use conversion::nusamai_to_geojson_geometry; - -/// An intermediate function to create a "geojson feature" from a "geojson geometry" -// TODO: Handle properties -pub fn geojson_geometry_to_feature(geojson_geom: geojson::Geometry) -> geojson::Feature { - geojson::Feature { - bbox: None, - geometry: Some(geojson_geom), - id: None, - properties: None, - foreign_members: None, +use conversion::{ + multilinestring_to_geojson_geometry, multipoint_to_geojson_geometry, + multipolygon_to_geojson_geometry, +}; +use nusamai_plateau::TopLevelCityObject; + +/// Create GeoJSON features from a TopLevelCityObject +/// Each feature for MultiPolygon, MultiLineString, and MultiPoint will be created (if it exists) +// TODO: Handle properties (`obj.root` -> `geojson::Feature.properties`) +// TODO: We may want to traverse the tree and create features for each semantic child in the future +pub fn toplevel_cityobj_to_geojson_features(obj: &TopLevelCityObject) -> Vec { + let mut geojson_features: Vec = vec![]; + + if !obj.geometries.multipolygon.is_empty() { + let mpoly_geojson_geom = multipolygon_to_geojson_geometry( + &obj.geometries.vertices, + &obj.geometries.multipolygon, + ); + let mpoly_geojson_feat = geojson::Feature { + bbox: None, + geometry: Some(mpoly_geojson_geom), + id: None, + properties: None, // TODO: from `obj.root` + foreign_members: None, + }; + geojson_features.push(mpoly_geojson_feat); + } + + if !obj.geometries.multilinestring.is_empty() { + let mls_geojson_geom = multilinestring_to_geojson_geometry( + &obj.geometries.vertices, + &obj.geometries.multilinestring, + ); + let mls_geojson_feat = geojson::Feature { + bbox: None, + geometry: Some(mls_geojson_geom), + id: None, + properties: None, // TODO: from `obj.root`` + foreign_members: None, + }; + geojson_features.push(mls_geojson_feat); + } + + if !obj.geometries.multipoint.is_empty() { + let mpoint_geojson_geom = + multipoint_to_geojson_geometry(&obj.geometries.vertices, &obj.geometries.multipoint); + let mpoint_geojson_feat = geojson::Feature { + bbox: None, + geometry: Some(mpoint_geojson_geom), + id: None, + properties: None, // TODO: from `obj.root`` + foreign_members: None, + }; + geojson_features.push(mpoint_geojson_feat); + } + + geojson_features +} + +#[cfg(test)] +mod tests { + use super::*; + use citygml::ObjectValue; + use nusamai_geometry::MultiPolygon; + + #[test] + fn test_toplevel_cityobj_multipolygon() { + let vertices: Vec<[f64; 3]> = vec![ + [0., 0., 111.], + [5., 0., 111.], + [5., 5., 111.], + [0., 5., 111.], + ]; + let mut mpoly = MultiPolygon::<'_, 1, u32>::new(); + mpoly.add_exterior([[0], [1], [2], [3], [0]]); + let geometries = citygml::Geometries { + vertices, + multipolygon: mpoly, + multilinestring: Default::default(), + multipoint: Default::default(), + }; + + let obj = TopLevelCityObject { + root: ObjectValue::String("test".to_string()), + geometries, + }; + + let geojson_features = toplevel_cityobj_to_geojson_features(&obj); + assert_eq!(geojson_features.len(), 1); + + let mpoly_geojson = geojson_features.get(0).unwrap(); + assert!(mpoly_geojson.bbox.is_none()); + assert!(mpoly_geojson.foreign_members.is_none()); + if let geojson::Value::MultiPolygon(rings_list) = + mpoly_geojson.geometry.clone().unwrap().value + { + for (i, rings) in rings_list.iter().enumerate() { + match i { + 0 => { + assert_eq!(rings.len(), 1); + assert_eq!( + rings[0], + vec![ + [0., 0., 111.], + [5., 0., 111.], + [5., 5., 111.], + [0., 5., 111.], + [0., 0., 111.] + ] + ); + } + _ => unreachable!("Unexpected number of polygons"), + } + } + } else { + unreachable!("The result is not a GeoJSON MultiPolygon"); + }; } }