diff --git a/Cargo.toml b/Cargo.toml index 6d8ace0fb..eef0957bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,37 +6,13 @@ members = [ resolver = "2" [workspace.package] -authors = ["MIERUNE Inc. ", "Re:Earth Flow Contributors"] -edition = "2021" -license = "MIT" -repository = "https://github.com/reearth/plateau-gis-converter" -rust-version = "1.81" -version = "0.1.0" +authors = ["MIERUNE Inc. "] +version = "0.0.0-alpha.0" -[profile.dev] -opt-level = 0 - -[profile.debug-fast] -debug = true -incremental = false -inherits = "release" -panic = "unwind" -strip = "none" +[profile.dev.package."*"] +opt-level = 3 [profile.release-lto] codegen-units = 8 inherits = "release" lto = "fat" - -[workspace.dependencies] -ahash = "0.8.11" -chrono = "0.4.38" -indexmap = "2.5.0" -log = "0.4.22" -once_cell = "1.20.1" -quick-xml = "0.36.2" -regex = "1.11.0" -serde = "1.0.210" -serde_json = "1.0.128" -thiserror = "1.0.64" -url = "2.5.2" diff --git a/nusamai-citygml/Cargo.toml b/nusamai-citygml/Cargo.toml index e64770b66..2d727c85a 100644 --- a/nusamai-citygml/Cargo.toml +++ b/nusamai-citygml/Cargo.toml @@ -1,29 +1,24 @@ [package] +edition = "2021" name = "nusamai-citygml" - -authors.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true version.workspace = true [features] default = ["serde"] -serde = ["dep:serde", "nusamai-geometry/serde", "serde_json"] +serde = ["dep:serde", "flatgeom/serde", "serde_json"] [dependencies] -ahash.workspace = true -chrono = { workspace = true, features = ["serde"] } -indexmap = { workspace = true, features = ["serde"] } -log.workspace = true +ahash = "0.8.11" +chrono = { version = "0.4.35", features = ["serde"], default-features = false } +flatgeom = "0.0.2" +indexmap = { version = "2.2.6", features = ["serde"] } +log = "0.4.21" macros = { path = "./macros" } -nusamai-geometry = { path = "../nusamai-geometry", features = ["serde"] } nusamai-projection = { path = "../nusamai-projection" } -once_cell.workspace = true -quick-xml.workspace = true -regex.workspace = true -serde = { workspace = true, features = ["derive"], optional = true } -serde_json = { workspace = true, features = ["indexmap"], optional = true } -thiserror.workspace = true -url = { workspace = true, features = ["serde"] } +once_cell = "1.20.2" +quick-xml = "0.36.2" +regex = "1.11.0" +serde = { version = "1.0", features = ["derive"], optional = true } +serde_json = { version = "1.0.115", features = ["indexmap"], optional = true } +thiserror = "1.0" +url = { version = "2.5.0", features = ["serde"] } diff --git a/nusamai-citygml/macros/Cargo.toml b/nusamai-citygml/macros/Cargo.toml index 4ce7d0a1b..820e1db82 100644 --- a/nusamai-citygml/macros/Cargo.toml +++ b/nusamai-citygml/macros/Cargo.toml @@ -1,12 +1,7 @@ [package] name = "macros" - -authors.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true -version.workspace = true +version = "0.1.0" +edition = "2021" [lib] proc-macro = true diff --git a/nusamai-citygml/macros/src/derive.rs b/nusamai-citygml/macros/src/derive.rs index bf6e36a9f..129437c15 100644 --- a/nusamai-citygml/macros/src/derive.rs +++ b/nusamai-citygml/macros/src/derive.rs @@ -131,11 +131,10 @@ fn generate_citygml_impl_for_struct( c.extend(name); let path = LitByteStr::new(&c, prefix.span()); let geomtype = format_ident!("{}", geomtype); - let name = std::str::from_utf8(&c).unwrap(); let hash = hash(&c); child_arms.push(quote! { - (#hash, #path) => st.parse_geometric_attr(#lod, #name, ::nusamai_citygml::geometry::GeometryParseType::#geomtype), + (#hash, #path) => st.parse_geometric_attr(&mut self.#field_ident, #lod, ::nusamai_citygml::geometry::GeometryParseType::#geomtype), }); }; diff --git a/nusamai-citygml/src/geometry.rs b/nusamai-citygml/src/geometry.rs index 9f4fa09d6..6c4d81dae 100644 --- a/nusamai-citygml/src/geometry.rs +++ b/nusamai-citygml/src/geometry.rs @@ -1,9 +1,12 @@ -use nusamai_geometry::{MultiLineString, MultiPoint, MultiPolygon}; +use flatgeom::{MultiLineString, MultiPoint, MultiPolygon}; use nusamai_projection::crs::*; use crate::LocalId; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// URI prefix for EPSG codes +const CRS_URI_EPSG_PREFIX: &str = "http://www.opengis.net/def/crs/EPSG/0/"; + +#[derive(Debug, Clone, Copy)] pub enum GeometryParseType { Geometry, Solid, @@ -98,6 +101,7 @@ pub struct SurfaceSpan { #[derive(Default)] pub(crate) struct GeometryCollector { pub vertices: indexmap::IndexSet<[u64; 3], ahash::RandomState>, + pub geometry_crs_uri: Option, pub multipolygon: MultiPolygon<'static, u32>, pub multilinestring: MultiLineString<'static, u32>, pub multipoint: MultiPoint<'static, u32>, @@ -139,7 +143,7 @@ impl GeometryCollector { })); } - pub fn into_geometries(self) -> GeometryStore { + pub fn into_geometries(self, envelope_crs_uri: Option) -> GeometryStore { let mut vertices = Vec::with_capacity(self.vertices.len()); for vbits in &self.vertices { vertices.push([ @@ -149,8 +153,21 @@ impl GeometryCollector { ]); } + let crs_uri = envelope_crs_uri.unwrap_or(self.geometry_crs_uri.unwrap_or_default()); + + let epsg = if crs_uri.starts_with(CRS_URI_EPSG_PREFIX) { + if let Some(stripped) = crs_uri.strip_prefix(CRS_URI_EPSG_PREFIX) { + stripped.parse::().ok() + } else { + None + } + } else { + None + } + .unwrap_or(EPSG_JGD2011_GEOGRAPHIC_3D); + GeometryStore { - epsg: EPSG_JGD2011_GEOGRAPHIC_3D, + epsg, vertices, multipolygon: self.multipolygon, multilinestring: self.multilinestring, diff --git a/nusamai-citygml/src/namespace.rs b/nusamai-citygml/src/namespace.rs index d87cfed5b..7d9b58617 100644 --- a/nusamai-citygml/src/namespace.rs +++ b/nusamai-citygml/src/namespace.rs @@ -163,8 +163,9 @@ mod tests { "#; let mut reader = NsReader::from_str(data); - reader.config_mut().trim_text(true); - reader.config_mut().expand_empty_elements = true; + let config = reader.config_mut(); + config.trim_text(true); + config.expand_empty_elements = true; loop { match reader.read_resolved_event() { Ok((ns, Event::Start(ref e))) => { diff --git a/nusamai-citygml/src/parser.rs b/nusamai-citygml/src/parser.rs index f24ae1678..f9f3d983f 100644 --- a/nusamai-citygml/src/parser.rs +++ b/nusamai-citygml/src/parser.rs @@ -6,6 +6,7 @@ use quick_xml::{ name::{Namespace, ResolveResult::Bound}, NsReader, }; + use regex::Regex; use thiserror::Error; use url::Url; @@ -25,7 +26,6 @@ static PROPERTY_PATTERN: Lazy = Lazy::new(|| Regex::new(r"([a-zA-Z0-9:_]+)\[([^\]]+)\]").unwrap()); static PROPERTY_KEY_VALUE_PATTERN: Lazy = Lazy::new(|| Regex::new(r"([a-zA-Z0-9:_]+)=([a-zA-Z0-9_\-]+)").unwrap()); - #[derive(Error, Debug)] pub enum ParseError { #[error("Broken XML: {0}")] @@ -124,8 +124,9 @@ impl<'a> CityGmlReader<'a> { &'a mut self, reader: &'b mut quick_xml::NsReader, ) -> Result, ParseError> { - reader.config_mut().trim_text(true); - reader.config_mut().expand_empty_elements = true; + let config = reader.config_mut(); + config.trim_text(true); + config.expand_empty_elements = true; let state = &mut self.state; let geomrefs = &mut self.geomrefs; @@ -277,7 +278,10 @@ impl<'b, R: BufRead> SubTreeReader<'_, 'b, R> { pub fn skip_current_element(&mut self) -> Result<(), ParseError> { let Some(start) = &self.state.current_start else { - panic!("skip_current_element() must be called immediately after encountering a new starting tag."); + panic!( + "skip_current_element() must be called immediately after encountering a new \ + starting tag." + ); }; self.reader .read_to_end_into(start.name(), &mut self.state.buf1)?; @@ -347,13 +351,13 @@ impl<'b, R: BufRead> SubTreeReader<'_, 'b, R> { &mut self.state.context } - pub fn id(&mut self, id: String) -> LocalId { + pub fn id_to_integer_id(&mut self, id: String) -> LocalId { LocalId::from(id) } - pub fn collect_geometries(&mut self) -> GeometryStore { + pub fn collect_geometries(&mut self, envelope_crs_uri: Option) -> GeometryStore { let collector = std::mem::take(&mut self.state.geometry_collector); - collector.into_geometries() + collector.into_geometries(envelope_crs_uri) } pub fn extract_feature_id_and_type(&self) -> (Option, Option) { @@ -384,18 +388,21 @@ impl<'b, R: BufRead> SubTreeReader<'_, 'b, R> { #[inline(never)] pub fn parse_geometric_attr( &mut self, + geomref: &mut GeometryRefs, lod: u8, - _feature_type: &str, geomtype: GeometryParseType, ) -> Result<(), ParseError> { use GeometryParseType::*; let (feature_id, feature_type) = self.extract_feature_id_and_type(); + match geomtype { - Solid => self.parse_solid_prop(lod, feature_id, feature_type)?, - MultiSurface => self.parse_multi_surface_prop(lod, feature_id, feature_type)?, - Surface => self.parse_surface_prop(lod, feature_id, feature_type)?, // FIXME - Geometry => self.parse_geometry_prop(lod, feature_id, feature_type)?, // FIXME: not only surfaces - Triangulated => self.parse_triangulated_prop(lod, feature_id, feature_type)?, + Solid => self.parse_solid_prop(geomref, lod, feature_id, feature_type)?, + MultiSurface => { + self.parse_multi_surface_prop(geomref, lod, feature_id, feature_type)? + } + Surface => self.parse_surface_prop(geomref, lod, feature_id, feature_type)?, // FIXME + Geometry => self.parse_geometry_prop(geomref, lod, feature_id, feature_type)?, // FIXME: not only surfaces + Triangulated => self.parse_triangulated_prop(geomref, lod, feature_id, feature_type)?, // FIXME Point => todo!(), // FIXME MultiPoint => todo!(), // FIXME MultiCurve => { @@ -404,14 +411,17 @@ impl<'b, R: BufRead> SubTreeReader<'_, 'b, R> { return Ok(()); } // FIXME } + self.state .path_buf .truncate(self.state.path_stack_indices.pop().unwrap()); + Ok(()) } fn parse_multi_surface_prop( &mut self, + geomrefs: &mut GeometryRefs, lod: u8, feature_id: Option, feature_type: Option, @@ -450,7 +460,7 @@ impl<'b, R: BufRead> SubTreeReader<'_, 'b, R> { }; let poly_end = self.state.geometry_collector.multipolygon.len(); - self.geomrefs.push(GeometryRef { + geomrefs.push(GeometryRef { ty: geomtype, lod, pos: poly_begin as u32, @@ -488,6 +498,7 @@ impl<'b, R: BufRead> SubTreeReader<'_, 'b, R> { fn parse_surface_prop( &mut self, + geomrefs: &mut GeometryRefs, lod: u8, feature_id: Option, feature_type: Option, @@ -495,21 +506,24 @@ impl<'b, R: BufRead> SubTreeReader<'_, 'b, R> { let poly_begin = self.state.geometry_collector.multipolygon.len(); let (surface_id, _) = self.parse_surface()?; let poly_end = self.state.geometry_collector.multipolygon.len(); - self.geomrefs.push(GeometryRef { - ty: GeometryType::Surface, - lod, - pos: poly_begin as u32, - len: (poly_end - poly_begin) as u32, - id: surface_id, - solid_ids: Vec::new(), - feature_id, - feature_type, - }); + if poly_end - poly_begin > 0 { + geomrefs.push(GeometryRef { + ty: GeometryType::Surface, + lod, + pos: poly_begin as u32, + len: (poly_end - poly_begin) as u32, + id: surface_id, + solid_ids: Vec::new(), + feature_id, + feature_type, + }); + } Ok(()) } fn parse_solid_prop( &mut self, + geomrefs: &mut GeometryRefs, lod: u8, feature_id: Option, feature_type: Option, @@ -524,21 +538,24 @@ impl<'b, R: BufRead> SubTreeReader<'_, 'b, R> { } let poly_end = self.state.geometry_collector.multipolygon.len(); - self.geomrefs.push(GeometryRef { - ty: GeometryType::Solid, - lod, - pos: poly_begin as u32, - len: (poly_end - poly_begin) as u32, - id: surface_id, - solid_ids, - feature_id, - feature_type, - }); + if poly_end - poly_begin > 0 { + geomrefs.push(GeometryRef { + ty: GeometryType::Solid, + lod, + pos: poly_begin as u32, + len: (poly_end - poly_begin) as u32, + id: surface_id, + solid_ids, + feature_id, + feature_type, + }); + } Ok(()) } fn parse_multi_geometry( &mut self, + geomrefs: &mut GeometryRefs, lod: u8, feature_id: Option, feature_type: Option, @@ -553,6 +570,7 @@ impl<'b, R: BufRead> SubTreeReader<'_, 'b, R> { (Bound(GML31_NS), b"geometryMember") => { inside_member = true; self.parse_geometry_prop( + geomrefs, lod, feature_id.clone(), feature_type.clone(), @@ -587,6 +605,7 @@ impl<'b, R: BufRead> SubTreeReader<'_, 'b, R> { fn parse_geometry_prop( &mut self, + geomrefs: &mut GeometryRefs, lod: u8, feature_id: Option, feature_type: Option, @@ -597,20 +616,25 @@ impl<'b, R: BufRead> SubTreeReader<'_, 'b, R> { Ok(Event::Start(start)) => { let (nsres, localname) = self.reader.resolve_element(start.name()); let poly_begin = self.state.geometry_collector.multipolygon.len(); + let mut geometry_crs_uri = None; - // surface id for attr in start.attributes().flatten() { let (nsres, localname) = self.reader.resolve_attribute(attr.key); + // surface id if nsres == Bound(GML31_NS) && localname.as_ref() == b"id" { let id = String::from_utf8_lossy(attr.value.as_ref()).to_string(); surface_id = Some(LocalId::from(id)); - break; + } + if localname.as_ref() == b"srsName" { + geometry_crs_uri = + Some(String::from_utf8_lossy(attr.value.as_ref()).to_string()); } } let geomtype = match (nsres, localname.as_ref()) { (Bound(GML31_NS), b"MultiGeometry") => { self.parse_multi_geometry( + geomrefs, lod, feature_id.clone(), feature_type.clone(), @@ -670,27 +694,31 @@ impl<'b, R: BufRead> SubTreeReader<'_, 'b, R> { }; let poly_end = self.state.geometry_collector.multipolygon.len(); - self.geomrefs.push(GeometryRef { - ty: geomtype, - lod, - pos: poly_begin as u32, - len: (poly_end - poly_begin) as u32, - id: surface_id.clone(), - solid_ids: Vec::new(), - feature_id: feature_id.clone(), - feature_type: feature_type.clone(), - }); + if poly_end - poly_begin > 0 { + geomrefs.push(GeometryRef { + ty: geomtype, + lod, + pos: poly_begin as u32, + len: (poly_end - poly_begin) as u32, + id: surface_id.clone(), + solid_ids: Vec::new(), + feature_id: feature_id.clone(), + feature_type: feature_type.clone(), + }); + + // record a partial surface span + if let Some(id) = surface_id.clone() { + self.state + .geometry_collector + .surface_spans + .push(SurfaceSpan { + id, + start: poly_begin as u32, + end: poly_end as u32, + }); + } - // record a partial surface span - if let Some(id) = surface_id.clone() { - self.state - .geometry_collector - .surface_spans - .push(SurfaceSpan { - id, - start: poly_begin as u32, - end: poly_end as u32, - }); + self.state.geometry_collector.geometry_crs_uri = geometry_crs_uri; } } Ok(Event::End(_)) => break, @@ -708,6 +736,7 @@ impl<'b, R: BufRead> SubTreeReader<'_, 'b, R> { fn parse_triangulated_prop( &mut self, + geomrefs: &mut GeometryRefs, lod: u8, feature_id: Option, feature_type: Option, @@ -743,16 +772,18 @@ impl<'b, R: BufRead> SubTreeReader<'_, 'b, R> { } let poly_end = self.state.geometry_collector.multipolygon.len(); - self.geomrefs.push(GeometryRef { - ty: GeometryType::Triangle, - lod, - pos: poly_begin as u32, - len: (poly_end - poly_begin) as u32, - id: None, - solid_ids: Vec::new(), - feature_id, - feature_type, - }); + if poly_end - poly_begin > 0 { + geomrefs.push(GeometryRef { + ty: GeometryType::Triangle, + lod, + pos: poly_begin as u32, + len: (poly_end - poly_begin) as u32, + id: None, + solid_ids: Vec::new(), + feature_id, + feature_type, + }); + } Ok(()) } @@ -1029,6 +1060,7 @@ impl<'b, R: BufRead> SubTreeReader<'_, 'b, R> { "Unexpected text content".into(), )); } + // parse coordinate sequence self.state.fp_buf.clear(); for s in text.unescape().unwrap().split_ascii_whitespace() { diff --git a/nusamai-citygml/src/values.rs b/nusamai-citygml/src/values.rs index 4380bb19e..bbe7e74cd 100644 --- a/nusamai-citygml/src/values.rs +++ b/nusamai-citygml/src/values.rs @@ -367,6 +367,7 @@ impl LocalId { pub fn new>(s: S) -> Self { Self(s.as_ref().to_string()) } + pub fn value(&self) -> String { self.0.clone() } @@ -381,11 +382,17 @@ impl From for LocalId { impl CityGmlElement for LocalId { #[inline(never)] fn parse(&mut self, st: &mut SubTreeReader) -> Result<(), ParseError> { - let s = { st.parse_text()? }; - { + let s = st.parse_text()?; + if let Some(id) = s.strip_prefix('#') { + let s = id.to_string(); *self = LocalId::from(s.to_string()); - }; - Ok(()) + Ok(()) + } else { + Err(ParseError::InvalidValue(format!( + "Expected a reference starts with '#' but got {}", + s + ))) + } } #[inline(never)] @@ -401,7 +408,14 @@ impl CityGmlElement for LocalId { impl CityGmlAttribute for LocalId { fn parse_attribute_value(value: &str, _st: &mut ParseContext) -> Result { let s = value; - Ok(Self::from(s.to_string())) + if let Some(id) = s.strip_prefix('#') { + Ok(Self::from(id.to_string())) + } else { + Err(ParseError::InvalidValue(format!( + "Expected a reference starts with '#' but got {}", + s + ))) + } } } @@ -621,13 +635,18 @@ impl CityGmlElement for Box { pub struct Envelope { lower_corner: Point, upper_corner: Point, - // TODO: crs_uri: Option, + pub crs_uri: Option, } impl CityGmlElement for Envelope { #[inline(never)] fn parse(&mut self, st: &mut SubTreeReader) -> Result<(), ParseError> { - // TODO: parse CRS URI + st.parse_attributes(|k, v, _| { + if k == b"@srsName" { + self.crs_uri = Some(String::from_utf8_lossy(v).into()); + } + Ok(()) + })?; st.parse_children(|st| { let current_path: &[u8] = &st.current_path(); diff --git a/nusamai-czml/Cargo.toml b/nusamai-czml/Cargo.toml index a141d5740..b92c37615 100644 --- a/nusamai-czml/Cargo.toml +++ b/nusamai-czml/Cargo.toml @@ -1,18 +1,13 @@ [package] +edition = "2021" name = "nusamai-czml" - -authors.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true -version.workspace = true +version = "0.1.0" [dependencies] -chrono = { workspace = true, features = ["serde"] } -nusamai-geometry = { path = "../nusamai-geometry" } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true, features = ["float_roundtrip"] } +chrono = { version = "0.4.35", features = ["serde"] } +flatgeom = "0.0.2" +serde = { version = "1.0.197", features = ["derive"] } +serde_json = { version = "1.0.115", features = ["float_roundtrip"] } [dev-dependencies] glob = "0.3.1" diff --git a/nusamai-czml/src/conversion.rs b/nusamai-czml/src/conversion.rs index d7bc27739..3da20a34d 100644 --- a/nusamai-czml/src/conversion.rs +++ b/nusamai-czml/src/conversion.rs @@ -1,4 +1,4 @@ -use nusamai_geometry::{MultiPolygon, Polygon}; +use flatgeom::{MultiPolygon, Polygon}; use crate::{ models::CzmlPolygon, PositionList, PositionListOfLists, PositionListOfListsProperties, diff --git a/nusamai-geojson/Cargo.toml b/nusamai-geojson/Cargo.toml new file mode 100644 index 000000000..899927842 --- /dev/null +++ b/nusamai-geojson/Cargo.toml @@ -0,0 +1,11 @@ +[package] +edition = "2021" +name = "nusamai-geojson" +version = "0.1.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +flatgeom = "0.0.2" +geojson = "0.24.1" +serde_json = { version = "1.0.115", features = ["indexmap"] } diff --git a/nusamai-geojson/src/conversion.rs b/nusamai-geojson/src/conversion.rs new file mode 100644 index 000000000..569535c71 --- /dev/null +++ b/nusamai-geojson/src/conversion.rs @@ -0,0 +1,646 @@ +use flatgeom::{ + Coord, LineString, LineString3, MultiLineString, MultiLineString3, MultiPoint, MultiPoint3, + MultiPolygon, Polygon, +}; + +/// Create a GeoJSON Polygon from `flatgeom::MultiPolygon`. +pub fn polygon_to_value(poly: &Polygon<[f64; D]>) -> geojson::Value { + polygon_to_value_with_mapping(poly, |c| c.to_vec()) +} + +/// Create a GeoJSON Polygon from vertices and indices. +pub fn indexed_polygon_to_value(vertices: &[[f64; 3]], poly_idx: &Polygon) -> geojson::Value { + polygon_to_value_with_mapping(poly_idx, |idx| vertices[idx as usize].to_vec()) +} + +/// Create a GeoJSON Polygon from `flatgeom::Polygon` with a mapping function. +pub fn polygon_to_value_with_mapping( + poly: &Polygon, + mapping: impl Fn(T) -> Vec, +) -> geojson::Value { + let coords: geojson::PolygonType = poly + .rings() + .map(|ls| { + ls.iter_closed() + .map(&mapping) // Get the actual coord values + .collect() + }) + .collect::>(); + geojson::Value::Polygon(coords) +} + +/// Create a GeoJSON MultiPolygon from `flatgeom::MultiPolygon`. +pub fn multipolygon_to_value(mpoly: &MultiPolygon<[f64; D]>) -> geojson::Value { + multipolygon_to_value_with_mapping(mpoly, |c| c.to_vec()) +} + +/// Create a GeoJSON MultiPolygon from vertices and indices. +pub fn indexed_multipolygon_to_value( + vertices: &[[f64; 3]], + mpoly_idx: &MultiPolygon, +) -> geojson::Value { + multipolygon_to_value_with_mapping(mpoly_idx, |idx| vertices[idx as usize].to_vec()) +} + +/// Create a GeoJSON MultiPolygon from `flatgeom::MultiPolygon` with a mapping function. +pub fn multipolygon_to_value_with_mapping( + mpoly: &MultiPolygon, + mapping: impl Fn(T) -> Vec, +) -> geojson::Value { + let coords: Vec = mpoly + .iter() + .map(|poly| { + poly.rings() + .map(|ls| { + ls.iter_closed() + .map(&mapping) // Get the actual coord values + .collect() + }) + .collect::>() + }) + .collect(); + geojson::Value::MultiPolygon(coords) +} + +/// Create a GeoJSON LineString from `flatgeom::LineString`. +pub fn linestring_to_value(ls: &LineString3) -> geojson::Value { + linestring_to_value_with_mapping(ls, |c| c.to_vec()) +} + +/// Create a GeoJSON LineString from vertices and indices. +pub fn indexed_linestring_to_value( + vertices: &[[f64; 3]], + ls_idx: &LineString, +) -> geojson::Value { + linestring_to_value_with_mapping(ls_idx, |idx| vertices[idx as usize].to_vec()) +} + +/// Create a GeoJSON LineString from `flatgeom::LineString` with a mapping function. +pub fn linestring_to_value_with_mapping( + ls: &LineString, + mapping: impl Fn(T) -> Vec, +) -> geojson::Value { + let coords = ls.iter().map(&mapping).collect(); // Get the actual coord values .collect() + geojson::Value::LineString(coords) +} + +/// Create a GeoJSON MultiLineString from `flatgeom::MultiLineString`. +pub fn multilinestring_to_value(mls: &MultiLineString3) -> geojson::Value { + multilinestring_to_value_with_mapping(mls, |c| c.to_vec()) +} + +/// Create a GeoJSON MultiLineString from vertices and indices. +pub fn indexed_multilinestring_to_value( + vertices: &[[f64; 3]], + mls_idx: &MultiLineString, +) -> geojson::Value { + multilinestring_to_value_with_mapping(mls_idx, |idx| vertices[idx as usize].to_vec()) +} + +/// Create a GeoJSON MultiLineString from `flatgeom::MultiLineString` with a mapping function. +pub fn multilinestring_to_value_with_mapping( + mls: &MultiLineString, + mapping: impl Fn(T) -> Vec, +) -> geojson::Value { + let coords = mls + .iter() + .map(|ls_idx| { + ls_idx + .iter() + .map(&mapping) // Get the actual coord values + .collect() + }) + .collect(); + geojson::Value::MultiLineString(coords) +} + +/// Create a GeoJSON Point from `flatgeom::Point`. +pub fn point_to_value(point: &[f64; 3]) -> geojson::Value { + point_to_value_with_mapping(*point, |c| c.to_vec()) +} + +/// Create a GeoJSON Point from vertices and indices. +pub fn indexed_point_to_value(vertices: &[[f64; 3]], point_idx: u32) -> geojson::Value { + point_to_value_with_mapping(point_idx, |idx| vertices[idx as usize].to_vec()) +} + +/// Create a GeoJSON Point from `flatgeom::Point` with a mapping function. +pub fn point_to_value_with_mapping( + point: T, + mapping: impl Fn(T) -> Vec, +) -> geojson::Value { + geojson::Value::Point(mapping(point)) +} + +/// Create a GeoJSON MultiPoint from `flatgeom::Point`. +pub fn multipoint_to_value(mpoint: &MultiPoint3) -> geojson::Value { + multipoint_to_value_with_mapping(mpoint, |c| c.to_vec()) +} + +/// Create a GeoJSON MultiPoint from vertices and indices. +pub fn indexed_multipoint_to_value( + vertices: &[[f64; 3]], + mpoint_idx: &MultiPoint, +) -> geojson::Value { + multipoint_to_value_with_mapping(mpoint_idx, |idx| vertices[idx as usize].to_vec()) +} + +/// Create a GeoJSON MultiPoint from `flatgeom::MultiPoint` with a mapping function. +pub fn multipoint_to_value_with_mapping( + mpoint: &MultiPoint, + mapping: impl Fn(T) -> Vec, +) -> geojson::Value { + let coords = mpoint + .iter() + .map(&mapping) // Get the actual coord values + .collect(); + geojson::Value::MultiPoint(coords) +} + +#[cfg(test)] +mod tests { + use flatgeom::{MultiPolygon3, Polygon3}; + + use super::*; + + #[test] + fn test_polygon() { + let mut poly = Polygon3::new(); + // polygon + poly.add_ring([ + [10., 10., 0.], + [10., 20., 0.], + [20., 20., 0.], + [20., 10., 0.], // not closed + ]); + poly.add_ring([ + [15., 15., 0.], + [18., 10., 0.], + [18., 18., 0.], + [15., 18., 0.], + ]); + let value = polygon_to_value(&poly); + let geojson::Value::Polygon(poly) = value else { + panic!("The result is not a GeoJSON Polygon"); + }; + assert_eq!( + poly, + vec![ + vec![ + vec![10., 10., 0.], + vec![10., 20., 0.], + vec![20., 20., 0.], + vec![20., 10., 0.], + vec![10., 10., 0.], + ], + vec![ + vec![15., 15., 0.], + vec![18., 10., 0.], + vec![18., 18., 0.], + vec![15., 18., 0.], + vec![15., 15., 0.], + ], + ], + ); + } + + #[test] + fn test_indexed_polygon() { + 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.], + ]; + + let mut poly = Polygon::::new(); + // 1st polygon + poly.add_ring([0, 1, 2, 3, 0]); + poly.add_ring([4, 5, 6, 7, 4]); + poly.add_ring([8, 9, 10, 11, 8]); + + let value = indexed_polygon_to_value(&vertices, &poly); + + let geojson::Value::Polygon(rings) = value else { + panic!("The result is not a GeoJSON Polygon") + }; + + assert_eq!( + rings, + vec![ + vec![ + [0., 0., 111.], + [5., 0., 111.], + [5., 5., 111.], + [0., 5., 111.], + [0., 0., 111.] + ], + vec![ + [1., 1., 111.], + [2., 1., 111.], + [2., 2., 111.], + [1., 2., 111.], + [1., 1., 111.] + ], + vec![ + [3., 3., 111.], + [4., 3., 111.], + [4., 4., 111.], + [3., 4., 111.], + [3., 3., 111.] + ] + ] + ); + } + + #[test] + fn test_multipolygon() { + let mut mpoly = MultiPolygon3::new(); + // 1st polygon + mpoly.add_exterior([ + [0., 0., 0.], + [0., 10., 0.], + [10., 10., 0.], + [10., 0., 0.], + [0., 0., 0.], // closed + ]); + // polygon + mpoly.add_exterior([ + [10., 10., 0.], + [10., 20., 0.], + [20., 20., 0.], + [20., 10., 0.], // not closed + ]); + mpoly.add_interior([ + [15., 15., 0.], + [18., 10., 0.], + [18., 18., 0.], + [15., 18., 0.], + ]); + let value = multipolygon_to_value(&mpoly); + let geojson::Value::MultiPolygon(mpoly) = value else { + panic!("The result is not a GeoJSON MultiPolygon"); + }; + assert_eq!( + mpoly, + vec![ + vec![vec![ + vec![0., 0., 0.], + vec![0., 10., 0.], + vec![10., 10., 0.], + vec![10., 0., 0.], + vec![0., 0., 0.], + ]], + vec![ + vec![ + vec![10., 10., 0.], + vec![10., 20., 0.], + vec![20., 20., 0.], + vec![20., 10., 0.], + vec![10., 10., 0.], + ], + vec![ + vec![15., 15., 0.], + vec![18., 10., 0.], + vec![18., 18., 0.], + vec![15., 18., 0.], + vec![15., 15., 0.], + ], + ], + ], + ); + } + + #[test] + fn test_indexed_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::::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 value = indexed_multipolygon_to_value(&vertices, &mpoly); + + if let geojson::Value::MultiPolygon(rings_list) = value { + for (i, rings) in rings_list.iter().enumerate() { + match i { + 0 => { + 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., 111.], + [5., 0., 111.], + [5., 5., 111.], + [0., 5., 111.], + [0., 0., 111.] + ] + ); + assert_eq!( + rings[1], + vec![ + [1., 1., 111.], + [2., 1., 111.], + [2., 2., 111.], + [1., 2., 111.], + [1., 1., 111.] + ] + ); + assert_eq!( + rings[2], + vec![ + [3., 3., 111.], + [4., 3., 111.], + [4., 4., 111.], + [3., 4., 111.], + [3., 3., 111.] + ] + ); + } + 1 => { + assert_eq!(rings.len(), 2); + assert_eq!(rings[0].len(), 5); + assert_eq!(rings[1].len(), 5); + assert_eq!( + rings[0], + vec![ + [4., 0., 222.], + [7., 0., 222.], + [7., 3., 222.], + [4., 3., 222.], + [4., 0., 222.] + ] + ); + assert_eq!( + rings[1], + vec![ + [5., 1., 222.], + [6., 1., 222.], + [6., 2., 222.], + [5., 2., 222.], + [5., 1., 222.] + ] + ); + } + 2 => { + assert_eq!(rings.len(), 1); + assert_eq!(rings[0].len(), 5); + assert_eq!( + rings[0], + vec![ + [4., 0., 333.], + [7., 0., 333.], + [7., 3., 333.], + [4., 3., 333.], + [4., 0., 333.] + ] + ); + } + _ => unreachable!("Unexpected number of polygons"), + } + } + } else { + unreachable!("The result is not a GeoJSON MultiPolygon"); + }; + } + + #[test] + fn test_linestring() { + let mut ls = LineString3::new(); + ls.push([11., 12., 13.]); + ls.push([21., 22., 23.]); + ls.push([31., 32., 33.]); + + let value = linestring_to_value(&ls); + let geojson::Value::LineString(ls) = value else { + panic!("The result is not a GeoJSON LineString"); + }; + assert_eq!( + ls, + vec![ + vec![11., 12., 13.], + vec![21., 22., 23.], + vec![31., 32., 33.], + ], + ); + } + + #[test] + fn test_indexed_linestring() { + let vertices = vec![[0., 0., 111.], [1., 1., 111.]]; + + let mut ls = LineString::::new(); + ls.push(0); + ls.push(1); + + let value = indexed_linestring_to_value(&vertices, &ls); + let geojson::Value::LineString(ls) = value else { + unreachable!(); + }; + assert_eq!(ls, vec![vec![0., 0., 111.], vec![1., 1., 111.]]); + } + + #[test] + fn test_multilinestring() { + let mut mls = MultiLineString3::new(); + mls.add_linestring([[11., 12., 13.], [21., 22., 23.], [31., 32., 33.]]); + mls.add_linestring([[111., 112., 113.], [121., 122., 123.], [131., 132., 133.]]); + + let value = multilinestring_to_value(&mls); + let geojson::Value::MultiLineString(mls) = value else { + panic!("The result is not a GeoJSON MultiPolygon"); + }; + assert_eq!( + mls, + vec![ + vec![ + vec![11., 12., 13.], + vec![21., 22., 23.], + vec![31., 32., 33.], + ], + vec![ + vec![111., 112., 113.], + vec![121., 122., 123.], + vec![131., 132., 133.], + ], + ] + ); + } + + #[test] + fn test_indexed_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::::new(); + mls.add_linestring([0, 1]); + mls.add_linestring([2, 3]); + mls.add_linestring([4, 5, 6]); + + let value = indexed_multilinestring_to_value(&vertices, &mls); + + if let geojson::Value::MultiLineString(lines) = 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_point() { + let point = [11., 12., 13.]; + let value = point_to_value(&point); + let geojson::Value::Point(point) = value else { + panic!("The result is not a GeoJSON MultiPolygon"); + }; + assert_eq!(point, vec![11., 12., 13.],); + } + + #[test] + fn test_indexed_point() { + let vertices = vec![[0., 0., 111.], [1., 2., 222.], [3., 4., 333.]]; + let point_idx = 1; + + let value = indexed_point_to_value(&vertices, point_idx); + + let geojson::Value::Point(point) = value else { + unreachable!(); + }; + assert_eq!(point, [1., 2., 222.]); + } + + #[test] + fn test_multipoint() { + let mut mpoint = MultiPoint3::new(); + mpoint.push([11., 12., 13.]); + mpoint.push([21., 22., 23.]); + mpoint.push([31., 32., 33.]); + + let value = multipoint_to_value(&mpoint); + let geojson::Value::MultiPoint(mpoint) = value else { + panic!("The result is not a GeoJSON MultiPolygon"); + }; + assert_eq!( + mpoint, + vec![ + vec![11., 12., 13.], + vec![21., 22., 23.], + vec![31., 32., 33.], + ] + ); + } + + #[test] + fn test_indexed_multipoint() { + let vertices = vec![[0., 0., 111.], [1., 2., 222.], [3., 4., 333.]]; + let mut mpoint = MultiPoint::::new(); + mpoint.push(0); + mpoint.push(1); + mpoint.push(2); + + let value = indexed_multipoint_to_value(&vertices, &mpoint); + + if let geojson::Value::MultiPoint(point_list) = 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 new file mode 100644 index 000000000..11eb2fc0c --- /dev/null +++ b/nusamai-geojson/src/lib.rs @@ -0,0 +1 @@ +pub mod conversion; diff --git a/nusamai-geometry/Cargo.toml b/nusamai-geometry/Cargo.toml deleted file mode 100644 index edf43af20..000000000 --- a/nusamai-geometry/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "nusamai-geometry" - -authors.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true -version.workspace = true - -[dependencies] -num-traits = "0.2.19" -serde = { workspace = true, features = ["derive"], optional = true } - -[dev-dependencies] -geo-types = "0.7.13" -geojson = "0.24.1" -indexmap.workspace = true -serde = { workspace = true, features = ["derive"] } -serde_json.workspace = true -thiserror.workspace = true diff --git a/nusamai-geometry/README.md b/nusamai-geometry/README.md deleted file mode 100644 index c727f5aea..000000000 --- a/nusamai-geometry/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# nusamai-geometry - -A compact, zero-copy geometry representation. - -This library avoids using jagged arrays (i.e. Vector of Vector of ...) to represent MultiPolygon/Polygon/etc. - -## Visual examples of the data structure - -### LineString - -![LineString](./docs/01_linestring.png) - -### Polygon - -![Polygon](./docs/02_polygon.png) - -### Polygon with a hole - -![Polygon with a hole](./docs/03_polygon_with_a_hole.png) - -### Polygon with multiple holes - -![Polygon with multiple holes](./docs/04_polygon_with_multiple_holes.png) - -### MultiPolygon - -![MultiPolygon](./docs/05_multipolygon.png) - -### MultiPolygon with holes - -![MultiPolygon with holes](./docs/06_multipolygon_with_holes.png) - -### Multiple polygons, multiple holes - -![Multiple polygons, multiple holes](./docs/07_multipolygon_multiple_holes.png) \ No newline at end of file diff --git a/nusamai-geometry/docs/01_linestring.png b/nusamai-geometry/docs/01_linestring.png deleted file mode 100644 index 72c0189aa..000000000 Binary files a/nusamai-geometry/docs/01_linestring.png and /dev/null differ diff --git a/nusamai-geometry/docs/02_polygon.png b/nusamai-geometry/docs/02_polygon.png deleted file mode 100644 index dd8c4f996..000000000 Binary files a/nusamai-geometry/docs/02_polygon.png and /dev/null differ diff --git a/nusamai-geometry/docs/03_polygon_with_a_hole.png b/nusamai-geometry/docs/03_polygon_with_a_hole.png deleted file mode 100644 index 60cd453eb..000000000 Binary files a/nusamai-geometry/docs/03_polygon_with_a_hole.png and /dev/null differ diff --git a/nusamai-geometry/docs/04_polygon_with_multiple_holes.png b/nusamai-geometry/docs/04_polygon_with_multiple_holes.png deleted file mode 100644 index 472492073..000000000 Binary files a/nusamai-geometry/docs/04_polygon_with_multiple_holes.png and /dev/null differ diff --git a/nusamai-geometry/docs/05_multipolygon.png b/nusamai-geometry/docs/05_multipolygon.png deleted file mode 100644 index b402d8f1c..000000000 Binary files a/nusamai-geometry/docs/05_multipolygon.png and /dev/null differ diff --git a/nusamai-geometry/docs/06_multipolygon_with_holes.png b/nusamai-geometry/docs/06_multipolygon_with_holes.png deleted file mode 100644 index 2a3d00085..000000000 Binary files a/nusamai-geometry/docs/06_multipolygon_with_holes.png and /dev/null differ diff --git a/nusamai-geometry/docs/07_multipolygon_multiple_holes.png b/nusamai-geometry/docs/07_multipolygon_multiple_holes.png deleted file mode 100644 index 064c2abb0..000000000 Binary files a/nusamai-geometry/docs/07_multipolygon_multiple_holes.png and /dev/null differ diff --git a/nusamai-geometry/docs/nusamai-geometry.key b/nusamai-geometry/docs/nusamai-geometry.key deleted file mode 100644 index 5399553a2..000000000 Binary files a/nusamai-geometry/docs/nusamai-geometry.key and /dev/null differ diff --git a/nusamai-geometry/src/compact/linestring.rs b/nusamai-geometry/src/compact/linestring.rs deleted file mode 100644 index 90b92218b..000000000 --- a/nusamai-geometry/src/compact/linestring.rs +++ /dev/null @@ -1,383 +0,0 @@ -use crate::{Coord, Coord2d}; -use std::{borrow::Cow, hash::Hash}; - -/// Computer-friendly LineString -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, Default)] -pub struct LineString<'a, T: Coord> { - /// Coordinates of all points - /// - /// e.g. `[x0, y0, z0, x1, y1, z1, ...]` - coords: Cow<'a, [T]>, -} - -pub type LineString2<'a, C = f64> = LineString<'a, [C; 2]>; -pub type LineString3<'a, C = f64> = LineString<'a, [C; 3]>; - -impl PartialEq for LineString2<'_, f64> { - fn eq(&self, other: &Self) -> bool { - self.coords == other.coords - } -} - -impl PartialEq for LineString3<'_, f64> { - fn eq(&self, other: &Self) -> bool { - self.coords == other.coords - } -} - -impl Hash for LineString2<'_, f64> { - fn hash(&self, state: &mut H) { - self.coords - .iter() - .for_each(|c| c.iter().for_each(|a| a.to_bits().hash(state))); - } -} - -impl Hash for LineString3<'_, f64> { - fn hash(&self, state: &mut H) { - self.coords - .iter() - .for_each(|c| c.iter().for_each(|a| a.to_bits().hash(state))); - } -} - -impl<'a, T: Coord> LineString<'a, T> { - /// Creates an empty LineString. - pub fn new() -> Self { - Self { - coords: Cow::Borrowed(&[]), - } - } - - pub fn from_raw(coords: Cow<'a, [T]>) -> Self { - Self { coords } - } - - pub fn raw_coords(&self) -> &[T] { - self.as_ref() - } - - /// Returns iterator over the all points in the LineString. - pub fn iter(&self) -> Iter { - Iter { - slice: &self.coords, - pos: 0, - close: false, - } - } - - /// Returns iterator over the all points with the start point repeated. - pub fn iter_closed(&self) -> Iter { - Iter { - slice: &self.coords, - pos: 0, - close: true, - } - } - - /// Returns the number of points in the LineString. - pub fn len(&self) -> usize { - self.coords.len() - } - - /// Returns `true` if the LineString is empty. - pub fn is_empty(&self) -> bool { - self.coords.is_empty() - } - - /// Appends a point to the LineString. - pub fn push(&mut self, coord: T) { - self.coords.to_mut().push(coord); - } - - /// Removes all points from the LineString. - pub fn clear(&mut self) { - self.coords.to_mut().clear(); - } - - /// Create a new LineString by applying the given transformation to all coordinates. - pub fn transform(&self, f: impl Fn(&T) -> T2) -> LineString { - LineString { - coords: self.coords.iter().map(f).collect(), - } - } - - /// Applies the given transformation to all coordinates in the LineString. - pub fn transform_inplace(&mut self, mut f: impl FnMut(&T) -> T) { - self.coords.to_mut().iter_mut().for_each(|c| { - *c = f(c); - }); - } - - /// Reverses the coordinates in the LineString. - pub fn reverse_inplace(&mut self) { - let len = self.coords.len(); - if len > 0 { - let data = self.coords.to_mut(); - for i in 0..data.len() / 2 { - data.swap(i, len - (i + 1)); - } - } - } - - /// Reverses the winding order of the coordinates in the ring, preserving the first coordinate. - pub fn reverse_ring_inplace(&mut self) { - let len = self.coords.len(); - if len > 1 { - let data = self.coords.to_mut(); - for i in 1..(data.len() + 1) / 2 { - data.swap(i, len - i); - } - } - } -} - -// 2-dimensional only -impl<'a, T: Coord2d> LineString<'a, T> { - /// Returns true if the ring is counter-clockwise. - pub fn is_ccw(&self) -> bool { - self.signed_ring_area() > 0.0 - } - - /// Returns true if the ring is clockwise. - pub fn is_cw(&self) -> bool { - self.signed_ring_area() < 0.0 - } - - /// Calculates the area of this LineString as a ring. - pub fn ring_area(&self) -> f64 { - self.signed_ring_area().abs() - } - - /// Calculates the signed area of this LineString as a ring. - pub fn signed_ring_area(&self) -> f64 { - if self.is_empty() { - return 0.0; - } - let mut area = 0.0; - let mut ring_iter = self.iter_closed(); - let mut prev = ring_iter.next().unwrap().xy(); - // shoelace formula - for coord in ring_iter { - let xy = coord.xy(); - area += (prev.0 * xy.1) - (prev.1 * xy.0); - prev = xy; - } - area / 2.0 - } -} - -impl AsRef<[T]> for LineString<'_, T> { - fn as_ref(&self) -> &[T] { - self.coords.as_ref() - } -} - -impl<'a, T: Coord> IntoIterator for &'a LineString<'_, T> { - type Item = T; - type IntoIter = Iter<'a, T>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -pub struct Iter<'a, T: Coord> { - slice: &'a [T], - pos: usize, - close: bool, -} - -impl<'a, T: Coord> Iterator for Iter<'a, T> { - type Item = T; - - fn next(&mut self) -> Option { - let v = if self.pos < self.slice.len() { - Some(self.slice[self.pos].clone()) - } else if self.close && !self.slice.is_empty() && self.pos == self.slice.len() { - Some(self.slice[0].clone()) - } else { - None - }; - self.pos += 1; - v - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_line_basic() { - let mut line: LineString2 = LineString2::from_raw( - (0..4) - .map(|v| [v as f64, v as f64]) - .collect::>() - .into(), - ); - assert_eq!(line.len(), 4); - assert!(!line.is_empty()); - for (i, coord) in line.iter().enumerate() { - match i { - 0 => assert_eq!(coord, [0., 0.]), - 1 => assert_eq!(coord, [1., 1.]), - 2 => assert_eq!(coord, [2., 2.]), - 3 => assert_eq!(coord, [3., 3.]), - _ => unreachable!(), - } - } - - line.clear(); - assert_eq!(line.len(), 0); - assert!(line.is_empty()); - - line.push([0., 1.]); - assert_eq!(line.len(), 1); - for _c in &line {} - } - - #[test] - fn test_line_empty() { - let line: LineString2 = LineString2::new(); - assert_eq!(line.len(), 0); - assert!(line.is_empty()); - assert_eq!(line.iter().count(), 0); - } - - #[test] - fn test_line_empty_iter_closed() { - let line: LineString2 = LineString2::new(); - assert_eq!(line.iter_closed().count(), 0); - } - - #[test] - fn test_line_close() { - let line: LineString2 = LineString2::from_raw( - (0..3) - .map(|v| [v as f64, v as f64]) - .collect::>() - .into(), - ); - assert_eq!(line.len(), 3); - assert!(!line.is_empty()); - for (i, coord) in line.iter_closed().enumerate() { - match i { - 0 => assert_eq!(coord, [0., 0.]), - 1 => assert_eq!(coord, [1., 1.]), - 2 => assert_eq!(coord, [2., 2.]), - 3 => assert_eq!(coord, [0., 0.]), - _ => unreachable!(), - } - } - } - - #[test] - fn test_transform() { - { - let line = LineString2::from_raw(vec![[0., 0.], [5., 0.], [5., 5.], [0., 5.]].into()); - let new_line = line.transform(|[x, y]| [x + 2., y + 1.]); - assert_eq!( - new_line.raw_coords(), - [[2., 1.], [7., 1.], [7., 6.], [2., 6.]] - ); - } - - { - let mut line = - LineString2::from_raw([[0., 0.], [5., 0.], [5., 5.], [0., 5.]][..].into()); - line.transform_inplace(|[x, y]| [x + 2., y + 1.]); - assert_eq!(line.raw_coords(), [[2., 1.], [7., 1.], [7., 6.], [2., 6.]]); - } - } - - #[test] - fn test_winding_order() { - let line = - LineString2::from_raw(vec![[0.0, 0.0], [3.0, 0.0], [3.0, 3.0], [0.0, 3.0]].into()); - assert!(line.is_ccw()); - assert!(!line.is_cw()); - - let line = - LineString2::from_raw(vec![[0.0, 0.0], [0.0, 3.0], [3.0, 3.0], [3.0, 0.0]].into()); - assert!(!line.is_ccw()); - assert!(line.is_cw()); - - let line = LineString2::from_raw(vec![[0.0, 0.0], [0.0, 0.0]].into()); - assert!(!line.is_ccw()); - assert!(!line.is_cw()); - } - - #[test] - fn test_reverse() { - let mut line = LineString2::from_raw( - vec![ - [0.0, 0.0], - [3.0, 0.0], - [3.0, 1.0], - [3.0, 3.0], - [1.0, 3.0], - [0.0, 3.0], - ] - .into(), - ); - line.reverse_inplace(); - assert_eq!( - line.raw_coords(), - vec![ - [0.0, 3.0], - [1.0, 3.0], - [3.0, 3.0], - [3.0, 1.0], - [3.0, 0.0], - [0.0, 0.0] - ] - ); - - let mut line = LineString2::from_raw( - vec![[0.0, 0.0], [3.0, 0.0], [6.0, 0.0], [6.0, 3.0], [3.0, 3.0]].into(), - ); - line.reverse_inplace(); - assert_eq!( - line.raw_coords(), - vec![[3.0, 3.0], [6.0, 3.0], [6.0, 0.0], [3.0, 0.0], [0.0, 0.0]] - ); - } - - #[test] - fn test_ring_reverse() { - let mut line = LineString2::from_raw( - vec![ - [0.0, 0.0], - [3.0, 0.0], - [3.0, 1.0], - [3.0, 3.0], - [1.0, 3.0], - [0.0, 3.0], - ] - .into(), - ); - line.reverse_ring_inplace(); - assert_eq!( - line.raw_coords(), - vec![ - [0.0, 0.0], - [0.0, 3.0], - [1.0, 3.0], - [3.0, 3.0], - [3.0, 1.0], - [3.0, 0.0] - ] - ); - - let mut line = LineString2::from_raw( - vec![[0.0, 0.0], [3.0, 0.0], [6.0, 0.0], [6.0, 3.0], [3.0, 3.0]].into(), - ); - line.reverse_ring_inplace(); - assert_eq!( - line.raw_coords(), - vec![[0.0, 0.0], [3.0, 3.0], [6.0, 3.0], [6.0, 0.0], [3.0, 0.0]] - ); - } -} diff --git a/nusamai-geometry/src/compact/mod.rs b/nusamai-geometry/src/compact/mod.rs deleted file mode 100644 index e190ff301..000000000 --- a/nusamai-geometry/src/compact/mod.rs +++ /dev/null @@ -1,82 +0,0 @@ -mod linestring; -mod multi_linestring; -mod multi_point; -mod multi_polygon; -mod polygon; - -pub use linestring::{LineString, LineString2, LineString3}; -pub use multi_linestring::{MultiLineString, MultiLineString2, MultiLineString3}; -pub use multi_point::{MultiPoint, MultiPoint2, MultiPoint3}; -pub use multi_polygon::{MultiPolygon, MultiPolygon2, MultiPolygon3}; -pub use polygon::{Polygon, Polygon2, Polygon3}; - -use num_traits::ToPrimitive; - -pub trait Coord: Clone + PartialEq {} -pub trait CoordNum: ToPrimitive + PartialEq + Clone {} - -impl Coord for [N; D] {} -impl Coord for N {} -impl CoordNum for f32 {} -impl CoordNum for f64 {} -impl CoordNum for u8 {} -impl CoordNum for i8 {} -impl CoordNum for u16 {} -impl CoordNum for i16 {} -impl CoordNum for u32 {} -impl CoordNum for i32 {} -impl CoordNum for u64 {} -impl CoordNum for i64 {} - -pub trait Coord2d: Coord { - fn xy(&self) -> (f64, f64); -} - -impl Coord2d for [N; 2] { - fn xy(&self) -> (f64, f64) { - (self[0].to_f64().unwrap(), self[1].to_f64().unwrap()) - } -} - -/// Computer-friendly Geometry -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(tag = "type") -)] -#[derive(Debug, Clone)] -pub enum Geometry<'a, T: Coord> { - MultiPoint(MultiPoint<'a, T>), - LineString(LineString<'a, T>), - MultiLineString(MultiLineString<'a, T>), - Polygon(Polygon<'a, T>), - MultiPolygon(MultiPolygon<'a, T>), -} - -pub type Geometry2<'a, C = f64> = Geometry<'a, [C; 2]>; -pub type Geometry3<'a, C = f64> = Geometry<'a, [C; 3]>; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_coord_num_trait() { - // 2D LineString with floating point numbers - let a: LineString2 = LineString::from_raw(vec![[1.2, 2.3], [3.4, 4.5]].into()); - assert_eq!(a.len(), 2); - // Can also be used to store integer values (e.g., vertex indices) - let b: LineString = LineString::from_raw(vec![1, 2, 3].into()); - assert_eq!(b.len(), 3); - } - - #[test] - fn coord2d() { - let v = [1, 2]; - assert_eq!(v.xy(), (1.0, 2.0)); - - let a: LineString2 = - LineString::from_raw(vec![[0.0, 0.0], [1.0, 1.0], [-2.0, 3.0]].into()); - assert_eq!(a.ring_area(), 2.5); - } -} diff --git a/nusamai-geometry/src/compact/multi_linestring.rs b/nusamai-geometry/src/compact/multi_linestring.rs deleted file mode 100644 index 6068e5299..000000000 --- a/nusamai-geometry/src/compact/multi_linestring.rs +++ /dev/null @@ -1,286 +0,0 @@ -use std::{borrow::Cow, ops::Range}; - -use super::{linestring::LineString, Coord}; - -/// Computer-friendly MultiString -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, Default, PartialEq)] -pub struct MultiLineString<'a, T: Coord> { - /// All coordinates of all LineStrings - /// - /// e.g. `[x0, y0, z0, x1, y1, z1, ...]` - all_coords: Cow<'a, [T]>, - - /// A sequence of indices of all_coords from which each linestring starts - /// (the first linestring always starts from 0 so it is omitted) - /// - /// e.g. `[5, 12, 23]` - coords_spans: Cow<'a, [u32]>, -} - -pub type MultiLineString2<'a, C = f64> = MultiLineString<'a, [C; 2]>; -pub type MultiLineString3<'a, C = f64> = MultiLineString<'a, [C; 3]>; - -impl<'a, T: Coord> MultiLineString<'a, T> { - /// Creates an empty MultiLineString. - pub fn new() -> Self { - Self { - all_coords: Cow::Borrowed(&[]), - coords_spans: Cow::Borrowed(&[]), - } - } - - pub fn from_raw(all_coords: Cow<'a, [T]>, coords_spans: Cow<'a, [u32]>) -> Self { - // Check if all span values are within range and monotonically increasing - if let Some(&last) = coords_spans.last() { - let len = (all_coords.len()) as u32; - if last >= len || coords_spans.windows(2).any(|a| a[0] >= a[1]) { - panic!("invalid coords_spans") - } - } - Self { - all_coords, - coords_spans, - } - } - - pub fn from_raw_unchecked(all_coords: Cow<'a, [T]>, coords_spans: Cow<'a, [u32]>) -> Self { - Self { - all_coords, - coords_spans, - } - } - - /// Returns iterator over the linestrings. - pub fn iter(&self) -> Iter { - Iter { - ls: self, - pos: 0, - end: self.len(), - } - } - - /// Returns iterator over the linestrings in the given range. - pub fn iter_range(&self, range: Range) -> Iter { - Iter { - ls: self, - pos: range.start, - end: range.end, - } - } - - /// Returns the number of linestrings. - pub fn len(&self) -> usize { - if self.all_coords.is_empty() { - 0 - } else { - self.coords_spans.len() + 1 - } - } - - /// Returns `true` if the MultiLineString is empty. - pub fn is_empty(&self) -> bool { - self.all_coords.is_empty() - } - - /// Removes all linestrings. - pub fn clear(&mut self) { - self.all_coords.to_mut().clear(); - self.coords_spans.to_mut().clear(); - } - - /// Add a linestring to the MultiLineString. - pub fn add_linestring>(&mut self, iter: I) { - if !self.all_coords.is_empty() { - self.coords_spans - .to_mut() - .push((self.all_coords.len()) as u32); - } - self.all_coords.to_mut().extend(iter); - } - - /// Create a new MultiLineString by applying the given transformation to all coordinates. - pub fn transform(&self, f: impl Fn(&T) -> T2) -> MultiLineString { - MultiLineString { - all_coords: self.all_coords.iter().map(f).collect(), - coords_spans: self.coords_spans.clone(), - } - } - - /// Applies the given transformation to all coordinates in the MultiLineString. - pub fn transform_inplace(&mut self, mut f: impl FnMut(&T) -> T) { - self.all_coords.to_mut().iter_mut().for_each(|c| { - *c = f(c); - }); - } -} - -impl<'a, T: Coord> IntoIterator for &'a MultiLineString<'_, T> { - type Item = LineString<'a, T>; - type IntoIter = Iter<'a, T>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -pub struct Iter<'a, T: Coord> { - ls: &'a MultiLineString<'a, T>, - pos: usize, - end: usize, -} - -impl<'a, T: Coord> Iterator for Iter<'a, T> { - type Item = LineString<'a, T>; - - fn next(&mut self) -> Option { - if self.ls.is_empty() { - return None; - } - if self.pos < self.end { - let start = match self.pos { - 0 => 0, - _ => self.ls.coords_spans[self.pos - 1] as usize, - }; - let end = if self.pos < self.ls.coords_spans.len() { - self.ls.coords_spans[self.pos] as usize - } else { - self.ls.all_coords.len() - }; - let coords = &self.ls.all_coords[start..end]; - self.pos += 1; - Some(LineString::from_raw(coords.into())) - } else { - None - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_mline_basic() { - let mut mline = MultiLineString2::from_raw( - (0..7) - .map(|v| [(v * 2) as f64, (v * 2 + 1) as f64]) - .collect::>() - .into(), - vec![3, 5].into(), - ); - assert_eq!(mline.len(), 3); - assert_eq!(mline.iter().count(), 3); - for (i, line) in mline.iter().enumerate() { - match i { - 0 => assert_eq!(line.raw_coords(), &[[0., 1.], [2., 3.], [4., 5.]]), - 1 => assert_eq!(line.raw_coords(), &[[6., 7.], [8., 9.]]), - 2 => assert_eq!(line.raw_coords(), &[[10., 11.], [12., 13.]]), - _ => unreachable!(), - } - } - mline.clear(); - assert_eq!(mline.len(), 0); - } - - #[test] - fn test_mline_one_linestring() { - let mline = MultiLineString2::from_raw_unchecked( - Cow::Borrowed(&[[1., 2.], [3., 4.]]), - Cow::Borrowed(&[]), - ); - assert_eq!(mline.len(), 1); - assert!(!mline.is_empty()); - for (i, _line) in mline.iter().enumerate() { - match i { - 0 => assert_eq!(_line.raw_coords(), &[[1., 2.], [3., 4.]]), - _ => unreachable!(), - } - } - } - - #[test] - fn test_mline_empty() { - let mline = MultiLineString2::::new(); - assert_eq!(mline.len(), 0); - assert!(mline.is_empty()); - assert_eq!(mline.iter().count(), 0); - } - - #[test] - fn test_mline_add_linestring() { - let mut mline = MultiLineString2::new(); - assert_eq!(mline.len(), 0); - - mline.add_linestring(vec![[0., 0.], [1., 1.], [2., 2.]]); - assert_eq!(mline.len(), 1); - - mline.add_linestring(vec![[3., 3.], [4., 4.], [5., 5.]]); - assert_eq!(mline.len(), 2); - - mline.add_linestring(vec![[6., 6.], [7., 7.], [8., 8.], [9., 9.]]); - assert_eq!(mline.len(), 3); - - mline.add_linestring(vec![[1., 1.], [2., 2.], [3., 3.], [4., 4.]]); - assert_eq!(mline.len(), 4); - - for (i, line) in mline.iter().enumerate() { - match i { - 0 => assert_eq!(line.raw_coords(), &[[0., 0.], [1., 1.], [2., 2.]]), - 1 => assert_eq!(line.raw_coords(), &[[3., 3.], [4., 4.], [5., 5.]]), - 2 => assert_eq!(line.raw_coords(), &[[6., 6.], [7., 7.], [8., 8.], [9., 9.]]), - 3 => assert_eq!(line.raw_coords(), &[[1., 1.], [2., 2.], [3., 3.], [4., 4.]]), - _ => unreachable!(), - } - } - - for (i, line) in mline.iter_range(1..3).enumerate() { - match i { - 0 => assert_eq!(line.raw_coords(), &[[3., 3.], [4., 4.], [5., 5.]]), - 1 => assert_eq!(line.raw_coords(), &[[6., 6.], [7., 7.], [8., 8.], [9., 9.]]), - _ => unreachable!(), - } - } - } - - #[test] - fn test_transform() { - { - let mut mlines: MultiLineString2 = MultiLineString2::new(); - mlines.add_linestring([[0., 0.], [5., 0.], [5., 5.], [0., 5.]]); - let new_mlines = mlines.transform(|[x, y]| [x + 2., y + 1.]); - assert_eq!( - new_mlines.iter().next().unwrap().raw_coords(), - [[2., 1.], [7., 1.], [7., 6.], [2., 6.]] - ); - } - - { - let mut mlines = MultiLineString2::new(); - mlines.add_linestring([[0., 0.], [5., 0.], [5., 5.], [0., 5.]]); - mlines.transform_inplace(|[x, y]| [x + 2., y + 1.]); - assert_eq!( - mlines.iter().next().unwrap().raw_coords(), - [[2., 1.], [7., 1.], [7., 6.], [2., 6.]] - ); - } - } - - #[test] - #[should_panic(expected = "invalid coords_spans")] - fn test_mline_invalid_coords_spans_1() { - let all_coords: Vec<[f64; 2]> = (0..=2).map(|i| [i as f64, i as f64]).collect(); - let coords_spans: Vec = vec![99]; // out of `all_coords` range - let _polygon: MultiLineString2 = - MultiLineString2::from_raw(all_coords.into(), coords_spans.into()); - } - - #[test] - #[should_panic(expected = "invalid coords_spans")] - fn test_mline_invalid_coords_spans_2() { - let all_coords: Vec<[f64; 2]> = vec![]; - let coords_spans: Vec = vec![0]; // out of `all_coords` range - let _polygon: MultiLineString2 = - MultiLineString2::from_raw(all_coords.into(), coords_spans.into()); - } -} diff --git a/nusamai-geometry/src/compact/multi_point.rs b/nusamai-geometry/src/compact/multi_point.rs deleted file mode 100644 index e26a5a373..000000000 --- a/nusamai-geometry/src/compact/multi_point.rs +++ /dev/null @@ -1,205 +0,0 @@ -use crate::Coord; -use std::borrow::Cow; -use std::ops::Range; - -/// Computer-friendly MultiPoint -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, Default, PartialEq)] -pub struct MultiPoint<'a, T: Coord> { - /// Coordinates of all points - /// - /// e.g. `[x0, y0, z0, x1, y1, z1, ...]` - coords: Cow<'a, [T]>, -} - -pub type MultiPoint2<'a, C = f64> = MultiPoint<'a, [C; 2]>; -pub type MultiPoint3<'a, C = f64> = MultiPoint<'a, [C; 3]>; - -impl<'a, T: Coord> MultiPoint<'a, T> { - /// Creates an empty MultiPoint. - pub fn new() -> Self { - Self { - coords: Cow::Borrowed(&[]), - } - } - - pub fn from_raw(coords: Cow<'a, [T]>) -> Self { - Self { coords } - } - - pub fn raw_coords(&self) -> &[T] { - self.as_ref() - } - - /// Returns iterator over the all points. - pub fn iter(&self) -> Iter { - Iter { - slice: &self.coords, - pos: 0, - end: self.coords.len(), - } - } - - /// Returns iterator over the points in the given range. - pub fn iter_range(&self, range: Range) -> Iter { - Iter { - slice: &self.coords, - pos: range.start, - end: range.end, - } - } - - /// Returns the point at the given index. - pub fn get(&self, index: usize) -> Option { - self.coords.get(index).cloned() - } - - /// Returns the number of points in the LineString. - pub fn len(&self) -> usize { - self.coords.len() - } - - /// Returns `true` if the MultiPoint is empty. - pub fn is_empty(&self) -> bool { - self.coords.is_empty() - } - - /// Appends a coordinate to the MultiPoint. - pub fn push(&mut self, coord: T) { - self.coords.to_mut().push(coord); - } - - /// Removes all points from the LineString. - pub fn clear(&mut self) { - self.coords.to_mut().clear(); - } - - /// Create a new MultiPoint by applying the given transformation to all coordinates. - pub fn transform(&self, f: impl Fn(&T) -> T2) -> MultiPoint { - MultiPoint { - coords: self.coords.iter().map(f).collect(), - } - } - - /// Applies the given transformation to all coordinates in the MultiPoint. - pub fn transform_inplace(&mut self, mut f: impl FnMut(&T) -> T) { - self.coords.to_mut().iter_mut().for_each(|c| { - *c = f(c); - }); - } -} - -impl AsRef<[T]> for MultiPoint<'_, T> { - fn as_ref(&self) -> &[T] { - self.coords.as_ref() - } -} - -impl<'a, T: Coord> IntoIterator for &'a MultiPoint<'_, T> { - type Item = T; - type IntoIter = Iter<'a, T>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -pub struct Iter<'a, T: Coord> { - slice: &'a [T], - pos: usize, - end: usize, -} - -impl<'a, T: Coord> Iterator for Iter<'a, T> { - type Item = T; - - fn next(&mut self) -> Option { - let v = if self.pos < self.end { - Some(self.slice[self.pos].clone()) - } else { - None - }; - self.pos += 1; - v - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_mpoints_basic() { - let mut mpoints = MultiPoint2::new(); - assert!(mpoints.is_empty()); - assert_eq!(mpoints.len(), 0); - - mpoints.push([0.0, 0.0]); - assert!(!mpoints.is_empty()); - assert_eq!(mpoints.len(), 1); - - mpoints.push([1.0, 1.0]); - mpoints.push([2.0, 2.0]); - mpoints.push([3.0, 3.0]); - assert_eq!(mpoints.get(0), Some([0.0, 0.0])); - assert_eq!(mpoints.get(1), Some([1.0, 1.0])); - assert_eq!(mpoints.get(2), Some([2.0, 2.0])); - assert_eq!(mpoints.get(3), Some([3.0, 3.0])); - assert_eq!(mpoints.len(), 4); - assert_eq!(mpoints.iter().count(), 4); - assert_eq!((&mpoints).into_iter().count(), 4); - - for (i, point) in mpoints.iter().enumerate() { - match i { - 0 => assert_eq!(point, [0.0, 0.0]), - 1 => assert_eq!(point, [1.0, 1.0]), - 2 => assert_eq!(point, [2.0, 2.0]), - 3 => assert_eq!(point, [3.0, 3.0]), - _ => unreachable!(), - } - } - - for (i, point) in mpoints.iter_range(1..3).enumerate() { - match i { - 0 => assert_eq!(point, [1.0, 1.0]), - 1 => assert_eq!(point, [2.0, 2.0]), - _ => unreachable!(), - } - } - - mpoints.clear(); - assert!(mpoints.is_empty()); - assert_eq!(mpoints.len(), 0); - assert_eq!(mpoints.iter().count(), 0); - } - - #[test] - fn test_mpoints_from_raw() { - let mpoints = MultiPoint2::from_raw([[1.2, 2.1], [3.4, 4.3]][..].into()); - assert_eq!(mpoints.as_ref(), [[1.2, 2.1], [3.4, 4.3]]); - assert_eq!(mpoints.raw_coords(), [[1.2, 2.1], [3.4, 4.3]]); - } - - #[test] - fn test_transform() { - { - let mpoints = - MultiPoint2::from_raw([[0., 0.], [5., 0.], [5., 5.], [0., 5.]][..].into()); - let new_mpoints = mpoints.transform(|[x, y]| [x + 2., y + 1.]); - assert_eq!( - new_mpoints.raw_coords(), - [[2., 1.], [7., 1.], [7., 6.], [2., 6.]] - ); - } - - { - let mut mpoints = - MultiPoint2::from_raw([[0., 0.], [5., 0.], [5., 5.], [0., 5.]][..].into()); - mpoints.transform_inplace(|[x, y]| [x + 2., y + 1.]); - assert_eq!( - mpoints.raw_coords(), - [[2., 1.], [7., 1.], [7., 6.], [2., 6.]] - ); - } - } -} diff --git a/nusamai-geometry/src/compact/multi_polygon.rs b/nusamai-geometry/src/compact/multi_polygon.rs deleted file mode 100644 index 60c76cd90..000000000 --- a/nusamai-geometry/src/compact/multi_polygon.rs +++ /dev/null @@ -1,608 +0,0 @@ -use std::{borrow::Cow, ops::Range}; - -use super::{polygon::Polygon, Coord}; - -/// Computer-friendly MultiPolygon -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, Default, PartialEq)] -pub struct MultiPolygon<'a, T: Coord> { - /// All coordinates of all polygons - /// - /// e.g. `[x0, y0, z0, x1, y1, z1, ...]` - all_coords: Cow<'a, [T]>, - - /// A sequence of indices of all_coords from which each polygon starts - /// (the first polygon always starts from 0 so it is omitted) - coords_spans: Cow<'a, [u32]>, - - /// All hole_indices (See `Polygon`) of all polygons - all_hole_indices: Cow<'a, [u32]>, - - /// A sequence of indices of all_hole_indices from which each polygon starts - /// (the first polygon always starts from 0 so it is omitted) - holes_spans: Cow<'a, [u32]>, -} - -pub type MultiPolygon2<'a, C = f64> = MultiPolygon<'a, [C; 2]>; -pub type MultiPolygon3<'a, C = f64> = MultiPolygon<'a, [C; 3]>; - -impl<'a, T: Coord> MultiPolygon<'a, T> { - /// Creates an empty MultiPolygon. - pub fn new() -> Self { - Self { - all_coords: Cow::Borrowed(&[]), - coords_spans: Cow::Borrowed(&[]), - all_hole_indices: Cow::Borrowed(&[]), - holes_spans: Cow::Borrowed(&[]), - } - } - - /// Create a new polygon from the given raw data, checking for validity. - pub fn from_raw( - all_coords: Cow<'a, [T]>, - coords_spans: Cow<'a, [u32]>, - all_hole_indices: Cow<'a, [u32]>, - holes_spans: Cow<'a, [u32]>, - ) -> Self { - if coords_spans.len() != holes_spans.len() { - panic!("coords_spans and holes_spans must have the same length"); - } - - // check multipolygon - let mut cs_start = 0; - let mut hs_start = 0; - coords_spans - .iter() - .zip(holes_spans.iter()) - .chain(Some(( - &((all_coords.len()) as u32), - &((all_hole_indices.len()) as u32), - ))) - .for_each(|(&cs_end, &hs_end)| { - if cs_start > cs_end { - panic!("invalid coords_spans"); - } - if hs_start > hs_end { - panic!("invalid holes_spans"); - } - // check polygon - let coords = &all_coords[cs_start as usize..cs_end as usize]; - let hole_indices = &all_hole_indices[hs_start as usize..hs_end as usize]; - if let Some(&last) = hole_indices.last() { - let len = (coords.len()) as u32; - if last >= len || hole_indices.windows(2).any(|a| a[0] >= a[1]) { - panic!("invalid hole_indices") - } - } - (cs_start, hs_start) = (cs_end, hs_end); - }); - - Self { - all_coords, - coords_spans, - holes_spans, - all_hole_indices, - } - } - - /// Creates a new multipolygon from the given raw data, without validity check. - pub fn from_raw_unchecked( - all_coords: Cow<'a, [T]>, - coords_spans: Cow<'a, [u32]>, - holes_spans: Cow<'a, [u32]>, - all_hole_indices: Cow<'a, [u32]>, - ) -> Self { - Self { - all_coords, - coords_spans, - holes_spans, - all_hole_indices, - } - } - - /// Returns the number of polygons in the multipolygon. - pub fn len(&self) -> usize { - match self.coords_spans.len() { - 0 => match self.all_coords.len() { - 0 => 0, - _ => 1, - }, - len => len + 1, - } - } - - /// Returns `true` if the multipolygon contains no polygons. - pub fn is_empty(&self) -> bool { - self.all_coords.is_empty() - } - - /// Returns an iterator over the polygons - pub fn iter(&self) -> Iter { - Iter { - mpoly: self, - pos: 0, - end: self.len(), - } - } - - /// Returns an iterator over the polygons in the given range. - pub fn iter_range(&self, range: Range) -> Iter { - Iter { - mpoly: self, - pos: range.start, - end: range.end, - } - } - - /// Returns the polygon at the given index. - pub fn get(&'a self, index: usize) -> Polygon<'a, T> { - let len = self.len(); - let (c_start, c_end, h_start, h_end) = match index { - index if index >= len => { - panic!( - "index out of bounds: {} polygon(s) but index is {}", - len, index - ); - } - 0 => ( - 0, - self.coords_spans - .first() - .map_or(self.all_coords.len(), |&i| i as usize), - 0, - self.holes_spans - .first() - .map_or(self.all_hole_indices.len(), |&i| i as usize), - ), - index if index == len - 1 => ( - self.coords_spans[index - 1] as usize, - self.all_coords.len(), - self.holes_spans[index - 1] as usize, - self.all_hole_indices.len(), - ), - _ => ( - self.coords_spans[index - 1] as usize, - self.coords_spans[index] as usize, - self.holes_spans[index - 1] as usize, - self.holes_spans[index] as usize, - ), - }; - Polygon::from_raw_unchecked( - (&self.all_coords[c_start..c_end]).into(), - (&self.all_hole_indices[h_start..h_end]).into(), - ) - } - - /// Clears the multipolygon, removing all polygons. - pub fn clear(&mut self) { - self.all_coords.to_mut().clear(); - self.all_hole_indices.to_mut().clear(); - self.coords_spans.to_mut().clear(); - self.holes_spans.to_mut().clear(); - } - - /// Adds a polygon to the multipolygon. - pub fn push(&mut self, poly: &Polygon) { - self.add_exterior(&poly.exterior()); - for hole in poly.interiors() { - self.add_interior(&hole); - } - } - - /// Adds a polygon with the given exterior ring. - pub fn add_exterior>(&mut self, iter: I) { - if !self.all_coords.is_empty() { - self.coords_spans - .to_mut() - .push((self.all_coords.len()) as u32); - self.holes_spans - .to_mut() - .push(self.all_hole_indices.len() as u32); - } - - let head = self.all_coords.len(); - self.all_coords.to_mut().extend(iter); - - // remove closing point if exists - let tail = self.all_coords.len(); - if tail > head + 2 && self.all_coords[head..head + 1] == self.all_coords[tail - 1..] { - self.all_coords.to_mut().truncate(tail - 1); - } - } - - /// Adds an interior ring to the last polygon. - pub fn add_interior>(&mut self, iter: I) { - self.all_hole_indices - .to_mut() - .push((self.all_coords.len()) as u32 - *self.coords_spans.last().unwrap_or(&0)); - - let head = self.all_coords.len(); - self.all_coords.to_mut().extend(iter); - - // remove closing point if exists - let tail = self.all_coords.len(); - if tail > head + 2 && self.all_coords[head..head + 1] == self.all_coords[tail - 1..] { - self.all_coords.to_mut().truncate(tail - 1); - } - } - - /// Create a new MultiPolygon by applying the given transformation to all coordinates. - pub fn transform(&self, f: impl Fn(&T) -> T2) -> MultiPolygon { - MultiPolygon { - all_coords: self.all_coords.iter().map(f).collect(), - coords_spans: self.coords_spans.clone(), - all_hole_indices: self.all_hole_indices.clone(), - holes_spans: self.holes_spans.clone(), - } - } - - /// Applies the given transformation to all coordinates in the MultiPolygon. - pub fn transform_inplace(&mut self, mut f: impl FnMut(&T) -> T) { - self.all_coords.to_mut().iter_mut().for_each(|c| { - *c = f(c); - }); - } -} - -impl<'a, T: Coord> IntoIterator for &'a MultiPolygon<'_, T> { - type Item = Polygon<'a, T>; - type IntoIter = Iter<'a, T>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -pub struct Iter<'a, T: Coord> { - mpoly: &'a MultiPolygon<'a, T>, - pos: usize, - end: usize, -} - -impl<'a, T: Coord> Iterator for Iter<'a, T> { - type Item = Polygon<'a, T>; - - fn next(&mut self) -> Option { - if self.pos < self.end { - // TODO: optimize - let poly = self.mpoly.get(self.pos); - self.pos += 1; - Some(poly) - } else { - None - } - } -} - -#[cfg(test)] -mod tests { - use super::{super::polygon::Polygon2, *}; - - #[test] - fn test_mpoly_add() { - let mut mpoly = MultiPolygon2::new(); - assert_eq!(mpoly.len(), 0); - assert!(mpoly.is_empty()); - assert_eq!(mpoly.iter().count(), 0); - - // 1st polygon - let mut poly1 = Polygon2::new(); - poly1.add_ring([[0., 0.], [5., 0.], [5., 5.], [0., 5.]]); // exterior - poly1.add_ring([[1., 1.], [2., 1.], [2., 2.], [1., 2.]]); // interior - poly1.add_ring([[3., 3.], [4., 3.], [4., 4.], [3., 4.]]); // interior - mpoly.push(&poly1); - assert!(!mpoly.is_empty()); - assert_eq!(mpoly.len(), 1); - - // 2nd polygon - let mut poly2 = Polygon2::new(); - poly2.add_ring([[4., 0.], [7., 0.], [7., 3.], [4., 3.]]); // exterior - poly2.add_ring([[5., 1.], [6., 1.], [6., 2.], [5., 2.]]); // interior - mpoly.push(&poly2); - assert_eq!(mpoly.len(), 2); - - // 3rd polygon - let mut poly3 = Polygon2::new(); - poly3.add_ring([[4., 0.], [7., 0.], [7., 3.], [4., 3.]]); // exterior - mpoly.push(&poly3); - assert_eq!(mpoly.len(), 3); - - for (i, poly) in mpoly.iter().enumerate() { - match i { - 0 => { - assert_eq!( - poly.exterior().iter().collect::>(), - [[0., 0.], [5., 0.], [5., 5.], [0., 5.]] - ); - assert_eq!(poly.interiors().count(), 2); - assert_eq!( - poly.interiors() - .map(|r| r.iter().collect::>()) - .collect::>(), - [ - [[1., 1.], [2., 1.], [2., 2.], [1., 2.]], - [[3., 3.], [4., 3.], [4., 4.], [3., 4.]], - ] - ); - } - 1 => { - assert_eq!( - poly.exterior().iter().collect::>(), - [[4., 0.], [7., 0.], [7., 3.], [4., 3.]] - ); - assert_eq!(poly.interiors().count(), 1); - assert_eq!( - poly.interiors() - .map(|r| r.iter().collect::>()) - .collect::>(), - [[[5., 1.], [6., 1.], [6., 2.], [5., 2.]]] - ); - } - 2 => { - assert_eq!( - poly.exterior().iter().collect::>(), - [[4., 0.], [7., 0.], [7., 3.], [4., 3.]] - ); - assert_eq!(poly.interiors().count(), 0); - } - _ => unreachable!(), - } - } - } - - #[test] - fn test_mpoly_append() { - let mut mpoly = MultiPolygon2::new(); - assert_eq!(mpoly.len(), 0); - assert!(mpoly.is_empty()); - assert_eq!(mpoly.iter().count(), 0); - - // 1st polygon - mpoly.add_exterior([[0., 0.], [5., 0.], [5., 5.], [0., 5.], [0., 0.]]); - assert_eq!(mpoly.len(), 1); - mpoly.add_interior([[1., 1.], [2., 1.], [2., 2.], [1., 2.], [1., 1.]]); - assert_eq!(mpoly.len(), 1); - mpoly.add_interior([[3., 3.], [4., 3.], [4., 4.], [3., 4.], [3., 3.]]); - assert_eq!(mpoly.len(), 1); - assert!(!mpoly.is_empty()); - for (i, poly) in mpoly.iter().enumerate() { - match i { - 0 => assert_eq!(poly.exterior().len(), 4), - _ => unreachable!(), - } - } - - // 2nd polygon - mpoly.add_exterior([[4., 0.], [7., 0.], [7., 3.], [4., 3.], [4., 0.]]); - assert_eq!(mpoly.len(), 2); - mpoly.add_interior([[5., 1.], [6., 1.], [6., 2.], [5., 2.], [5., 1.]]); - assert_eq!(mpoly.len(), 2); - - // 3rd polygon - mpoly.add_exterior([[4., 0.], [7., 0.], [7., 3.], [4., 3.], [4., 0.]]); - assert_eq!(mpoly.len(), 3); - - for (i, poly) in mpoly.iter().enumerate() { - match i { - 0 => assert_eq!(poly.interiors().count(), 2), - 1 => assert_eq!(poly.interiors().count(), 1), - 2 => assert_eq!(poly.interiors().count(), 0), - _ => unreachable!(), - } - } - - for (i, poly) in mpoly.iter_range(0..1).enumerate() { - match i { - 0 => assert_eq!(poly.interiors().count(), 2), - _ => unreachable!(), - } - } - - for (i, poly) in mpoly.iter_range(1..2).enumerate() { - match i { - 0 => assert_eq!(poly.interiors().count(), 1), - _ => unreachable!(), - } - } - - let mut found = false; - for (i, poly) in mpoly.iter_range(2..3).enumerate() { - match i { - 0 => { - assert_eq!(poly.interiors().count(), 0); - found = true; - } - _ => unreachable!(), - } - } - assert!(found); - - mpoly.clear(); - assert_eq!(mpoly.len(), 0); - assert!(mpoly.is_empty()); - } - - #[test] - fn test_mpoly_empty_exterior() { - let mut mpoly = MultiPolygon2::new(); - - // Start the 1st polygon with an empty exterior - mpoly.add_interior([[5., 1.], [6., 1.], [6., 2.], [5., 2.], [5., 1.]]); - assert_eq!(mpoly.get(0).exterior().len(), 0); - assert_eq!(mpoly.len(), 1); - // Start the 2nd polygon - mpoly.add_exterior([[4., 0.], [7., 0.], [7., 3.], [4., 3.], [4., 0.]]); - assert_eq!(mpoly.len(), 2); - } - - #[test] - fn test_from_raw_valid() { - // checked - let _mpoly = MultiPolygon2::from_raw( - [ - [0.0, 0.0], - [5.0, 0.0], - [5.0, 5.0], - [0.0, 5.0], - [1.0, 1.0], - [2.0, 1.0], - [2.0, 2.0], - [1.0, 2.0], - [3.0, 3.0], - [4.0, 3.0], - [4.0, 4.0], - [3.0, 4.0], - [4.0, 0.0], - [7.0, 0.0], - [7.0, 3.0], - [4.0, 3.0], - [5.0, 1.0], - [6.0, 1.0], - [6.0, 2.0], - [5.0, 2.0], - [4.0, 0.0], - [7.0, 0.0], - [7.0, 3.0], - [4.0, 3.0], - [5.0, 1.0], - [6.0, 1.0], - [6.0, 2.0], - [5.0, 2.0], - ][..] - .into(), - [12, 20][..].into(), - [4, 8, 4, 4][..].into(), - [2, 3][..].into(), - ); - - // unchecked - let _mpoly = MultiPolygon2::from_raw_unchecked( - [ - [0.0, 0.0], - [5.0, 0.0], - [5.0, 5.0], - [0.0, 5.0], - [1.0, 1.0], - [2.0, 1.0], - [2.0, 2.0], - [1.0, 2.0], - [3.0, 3.0], - [4.0, 3.0], - [4.0, 4.0], - [3.0, 4.0], - [4.0, 0.0], - [7.0, 0.0], - [7.0, 3.0], - [4.0, 3.0], - [5.0, 1.0], - [6.0, 1.0], - [6.0, 2.0], - [5.0, 2.0], - [4.0, 0.0], - [7.0, 0.0], - [7.0, 3.0], - [4.0, 3.0], - [5.0, 1.0], - [6.0, 1.0], - [6.0, 2.0], - [5.0, 2.0], - ][..] - .into(), - [12, 20][..].into(), - [4, 8, 4, 4][..].into(), - [2, 3][..].into(), - ); - } - - #[test] - fn test_transform() { - { - let mut mpoly = MultiPolygon2::new(); - mpoly.add_exterior([[0., 0.], [5., 0.], [5., 5.], [0., 5.]]); - let new_mpoly = mpoly.transform(|[x, y]| [x + 2., y + 1.]); - assert_eq!( - new_mpoly.get(0).raw_coords(), - [[2., 1.], [7., 1.], [7., 6.], [2., 6.]] - ); - } - - { - let mut mpoly = MultiPolygon2::new(); - mpoly.add_exterior([[0., 0.], [5., 0.], [5., 5.], [0., 5.]]); - mpoly.transform_inplace(|[x, y]| [x + 2., y + 1.]); - assert_eq!( - mpoly.get(0).raw_coords(), - [[2., 1.], [7., 1.], [7., 6.], [2., 6.]] - ); - } - } - - #[test] - #[should_panic] - fn test_from_raw_invalid_1() { - let _mpoly = MultiPolygon2::from_raw( - [[0.0, 0.0], [5.0, 0.0], [5.0, 5.0], [0.0, 1.0]][..].into(), - [2][..].into(), - [1, 99][..].into(), // invalid - [1][..].into(), - ); - } - - #[test] - #[should_panic] - fn test_from_raw_invalid_2() { - let _mpoly = MultiPolygon2::from_raw( - [[0.0, 0.0], [5.0, 0.0], [5.0, 5.0], [0.0, 1.0]][..].into(), - [99][..].into(), // invalid - [1, 1][..].into(), - [1][..].into(), - ); - } - - #[test] - #[should_panic] - fn test_from_raw_invalid_3() { - let _mpoly = MultiPolygon2::from_raw( - [[0.0, 0.0], [5.0, 0.0], [5.0, 5.0], [0.0, 1.0]][..].into(), - [2][..].into(), - [1, 1][..].into(), - [99][..].into(), // invalid - ); - } - - #[test] - #[should_panic] - fn test_from_raw_invalid_4() { - // checked - let _mpoly = MultiPolygon2::from_raw( - [[0.0, 0.0], [5.0, 0.0], [5.0, 5.0], [0.0, 1.0]][..].into(), - [1, 2][..].into(), - [][..].into(), - [0][..].into(), // coords_spans.len() != holes_spans.len() - ); - } - - #[test] - #[should_panic] - fn test_from_raw_invalid_5() { - // checked - let _mpoly = MultiPolygon2::from_raw( - [[0.0, 0.0], [5.0, 0.0], [5.0, 5.0], [0.0, 1.0]][..].into(), - [2, 1][..].into(), // not increasing - [][..].into(), - [0, 0][..].into(), - ); - } - - #[test] - #[should_panic] - fn test_from_raw_invalid_6() { - // checked - let _mpoly = MultiPolygon2::from_raw( - [[0., 0.], [1., 1.], [2., 2.], [3., 3.], [4., 4.], [5., 5.]][..].into(), - [2, 4][..].into(), - [1, 1][..].into(), - [1, 0][..].into(), // not increasing - ); - } -} diff --git a/nusamai-geometry/src/compact/polygon.rs b/nusamai-geometry/src/compact/polygon.rs deleted file mode 100644 index fbaeb5a1a..000000000 --- a/nusamai-geometry/src/compact/polygon.rs +++ /dev/null @@ -1,368 +0,0 @@ -use std::{borrow::Cow, hash::Hash}; - -use crate::Coord2d; - -use super::{linestring::LineString, Coord}; - -/// Computer-friendly Polygon -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, Default)] -pub struct Polygon<'a, T: Coord> { - /// Coordinates - coords: Cow<'a, [T]>, - - /// A sequence of indices from which each hole starts - hole_indices: Cow<'a, [u32]>, -} - -pub type Polygon3<'a, C = f64> = Polygon<'a, [C; 3]>; -pub type Polygon2<'a, C = f64> = Polygon<'a, [C; 2]>; - -impl PartialEq for Polygon3<'_, f64> { - fn eq(&self, other: &Self) -> bool { - self.exterior() == other.exterior() - && self.interiors().zip(other.interiors()).all(|(a, b)| a == b) - } -} - -impl PartialEq for Polygon2<'_, f64> { - fn eq(&self, other: &Self) -> bool { - self.exterior() == other.exterior() - && self.interiors().zip(other.interiors()).all(|(a, b)| a == b) - } -} - -impl Hash for Polygon2<'_, f64> { - fn hash(&self, state: &mut H) { - self.exterior().hash(state); - for interior in self.interiors() { - interior.hash(state); - } - } -} - -impl Hash for Polygon3<'_, f64> { - fn hash(&self, state: &mut H) { - self.exterior().hash(state); - for interior in self.interiors() { - interior.hash(state); - } - } -} - -impl Eq for Polygon3<'_, f64> {} -impl Eq for Polygon2<'_, f64> {} - -impl<'a, T: Coord> Polygon<'a, T> { - /// Creates an empty Polygon. - pub fn new() -> Self { - Self { - coords: Cow::Borrowed(&[]), - hole_indices: Cow::Borrowed(&[]), - } - } - - pub fn from_raw(coords: Cow<'a, [T]>, hole_indices: Cow<'a, [u32]>) -> Self { - // Check if all span values are within range and monotonically increasing - if let Some(&last) = hole_indices.last() { - let len = (coords.len()) as u32; - if last >= len || hole_indices.windows(2).any(|a| a[0] >= a[1]) { - panic!("invalid hole_indices") - } - } - Self { - coords, - hole_indices, - } - } - - pub fn from_raw_unchecked(coords: Cow<'a, [T]>, hole_indices: Cow<'a, [u32]>) -> Self { - Self { - coords, - hole_indices, - } - } - - pub fn raw_coords(&self) -> &[T] { - self.coords.as_ref() - } - - /// A sequence of indices from which each hole starts - pub fn hole_indices(&self) -> &[u32] { - self.hole_indices.as_ref() - } - - /// Returns the exterior ring of the polygon. - pub fn exterior(&self) -> LineString { - LineString::from_raw(if self.hole_indices.is_empty() { - self.coords[..].into() - } else { - self.coords[..self.hole_indices[0] as usize].into() - }) - } - - /// Returns an iterator over the interior rings of the polygon. - pub fn interiors(&self) -> Iter { - Iter { poly: self, pos: 1 } - } - - /// Returns an iterator over the exterior and interior rings of the polygon. - pub fn rings(&self) -> Iter { - Iter { poly: self, pos: 0 } - } - - /// Remove all rings from the polygon. - pub fn clear(&mut self) { - self.coords.to_mut().clear(); - self.hole_indices.to_mut().clear(); - } - - /// Adds an exterior or interior ring to the polygon. - pub fn add_ring>(&mut self, iter: I) { - if !self.coords.is_empty() { - self.hole_indices.to_mut().push((self.coords.len()) as u32); - } - let head = self.coords.len(); - self.coords.to_mut().extend(iter); - - // remove closing point if exists - let tail = self.coords.len(); - if tail > head + 2 && self.coords[head..head + 1] == self.coords[tail - 1..] { - self.coords.to_mut().truncate(tail - 1); - } - } - - /// Create a new Polygon by applying the given transformation to all coordinates. - pub fn transform(self, f: impl Fn(&T) -> T2) -> Polygon<'a, T2> { - Polygon { - coords: self.coords.iter().map(f).collect(), - hole_indices: self.hole_indices.clone(), - } - } - - /// Applies the given transformation to all coordinates in the Polygon. - pub fn transform_inplace(&mut self, mut f: impl FnMut(&T) -> T) { - self.coords.to_mut().iter_mut().for_each(|c| { - *c = f(c); - }); - } -} - -// 2-dimensional only -impl<'a, T: Coord2d> Polygon<'a, T> { - pub fn area(&self) -> f64 { - let mut area = 0.0; - area += self.exterior().ring_area(); - for interior in self.interiors() { - area -= interior.ring_area(); - } - area - } -} - -pub struct Iter<'a, T: Coord> { - poly: &'a Polygon<'a, T>, - pos: usize, -} - -impl<'a, T: Coord> Iterator for Iter<'a, T> { - type Item = LineString<'a, T>; - - fn next(&mut self) -> Option { - if self.pos < self.poly.hole_indices.len() + 1 { - let start = if self.pos == 0 { - 0 - } else { - self.poly.hole_indices[self.pos - 1] as usize - }; - - let end = if self.pos == self.poly.hole_indices.len() { - self.poly.coords.len() - } else { - self.poly.hole_indices[self.pos] as usize - }; - - let line = LineString::from_raw(self.poly.coords[start..end].into()); - self.pos += 1; - Some(line) - } else { - None - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_polygon_empty() { - let polygon = Polygon2::::new(); - assert_eq!(polygon.exterior().len(), 0); - assert_eq!(polygon.interiors().count(), 0); - } - - #[test] - fn test_polygon_no_interior() { - let coords: Vec<_> = (0..=10).map(|i| [i as f64, i as f64]).collect(); - let hole_indices: Vec = vec![]; - let polygon: Polygon2 = - Polygon2::from_raw_unchecked(coords.into(), hole_indices.into()); - - assert_eq!(polygon.exterior().len(), 11); - assert_eq!(polygon.interiors().count(), 0); - } - - #[test] - fn test_polygon_one_interior() { - let coords: Vec<_> = (0..=10).map(|i| [i as f64, i as f64]).collect(); - let hole_indices: Vec = vec![4]; - let polygon: Polygon2 = - Polygon2::from_raw_unchecked(coords.into(), hole_indices.into()); - - assert_eq!(polygon.exterior().len(), 4); - assert_eq!(polygon.interiors().count(), 1); - for (i, interior) in polygon.interiors().enumerate() { - match i { - 0 => assert_eq!(interior.len(), 7), - _ => unreachable!(), - } - } - } - - #[test] - fn test_polygon_multiple_interiors() { - let coords: Vec<_> = (0..=10).map(|i| [i as f64, i as f64]).collect(); - let hole_indices: Vec = vec![4, 7]; - let polygon: Polygon2 = - Polygon2::from_raw_unchecked(coords.into(), hole_indices.into()); - - assert_eq!(polygon.exterior().len(), 4); - assert_eq!(polygon.interiors().count(), 2); - for (i, interior) in polygon.interiors().enumerate() { - match i { - 0 => assert_eq!(interior.len(), 3), - 1 => assert_eq!(interior.len(), 4), - _ => unreachable!(), - } - } - } - - #[test] - fn test_polygon_clear() { - let coords: Vec<_> = (0..=10).map(|i| [i as f64, i as f64]).collect(); - let hole_indices: Vec = vec![4, 7]; - let mut polygon: Polygon2 = - Polygon2::from_raw_unchecked(coords.into(), hole_indices.into()); - - assert_eq!(polygon.exterior().len(), 4); - assert_eq!(polygon.interiors().count(), 2); - - polygon.clear(); - assert_eq!(polygon.exterior().len(), 0); - assert_eq!(polygon.interiors().count(), 0); - } - - #[test] - fn test_polygon_add_ring() { - let mut polygon = Polygon2::new(); - assert_eq!(polygon.exterior().len(), 0); - assert_eq!(polygon.interiors().count(), 0); - polygon.add_ring([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]); - assert_eq!(polygon.exterior().len(), 3); - assert_eq!(polygon.interiors().count(), 0); - polygon.add_ring([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]); - assert_eq!(polygon.exterior().len(), 3); - assert_eq!(polygon.interiors().count(), 1); - - let mut polygon = Polygon2::new(); - polygon.add_ring([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [0.0, 0.0]]); - assert_eq!(polygon.exterior().len(), 3); - assert_eq!(polygon.interiors().count(), 0); - } - - #[test] - fn test_transform() { - { - let mut poly: Polygon2 = Polygon2::new(); - poly.add_ring([[0., 0.], [5., 0.], [5., 5.], [0., 5.]]); - let new_poly = poly.transform(|[x, y]| [x + 2., y + 1.]); - assert_eq!( - new_poly.exterior().raw_coords(), - [[2., 1.], [7., 1.], [7., 6.], [2., 6.]] - ); - } - - { - let mut poly = Polygon2::new(); - poly.add_ring([[0., 0.], [5., 0.], [5., 5.], [0., 5.]]); - poly.transform_inplace(|[x, y]| [x + 2., y + 1.]); - assert_eq!( - poly.exterior().raw_coords(), - [[2., 1.], [7., 1.], [7., 6.], [2., 6.]] - ); - } - } - - /// Currently, it does not check whether the exterior is valid (have at least three vertices) or not - #[test] - fn test_polygon_invalid_exterior() { - let coords: Vec<_> = (0..1).map(|i| [i as f64, i as f64]).collect(); - let hole_indices: Vec = vec![]; - let polygon: Polygon2 = Polygon2::from_raw(coords.into(), hole_indices.into()); - - assert_eq!(polygon.exterior().len(), 1); // only one vertex - assert_eq!(polygon.interiors().count(), 0); - } - - /// Currently, it does not check whether the interior is valid (have at least three vertices) or not - /// (It could be a "Steiner point", as we see in earcut) - #[test] - fn test_polygon_invalid_interior() { - let all_coords: Vec<_> = (0..=10).map(|i| [i as f64, i as f64]).collect(); - let hole_indices: Vec = vec![10]; // only one vertex - let polygon: Polygon2 = Polygon2::from_raw(all_coords.into(), hole_indices.into()); - - assert_eq!(polygon.exterior().len(), 10); - assert_eq!(polygon.interiors().count(), 1); - for (i, interior) in polygon.interiors().enumerate() { - match i { - 0 => assert_eq!(interior.len(), 1), // only one vertex - _ => unreachable!(), - } - } - } - - #[test] - #[should_panic] - fn test_polygon_invalid_hole_indices_1() { - let coords: Vec<_> = (0..=2).map(|i| [i as f64, i as f64]).collect(); - let hole_indices: Vec = vec![3]; // out of `all_coords` range - let _polygon: Polygon2 = Polygon2::from_raw(coords.into(), hole_indices.into()); - } - - #[test] - #[should_panic] - fn test_polygon_invalid_hole_indices_2() { - let coords: Vec<_> = (0..15).map(|i| [i as f64, i as f64]).collect(); - let hole_indices: Vec = vec![6, 3]; // not monotonically increasing - let _polygon: Polygon2 = Polygon2::from_raw(coords.into(), hole_indices.into()); - } - - #[test] - fn test_area() { - let mut polygon = Polygon2::new(); - assert_eq!(polygon.area(), 0.0); - polygon.add_ring([[0.0, 0.0], [3.0, 0.0], [3.0, 3.0], [0.0, 3.0]]); - assert_eq!(polygon.area(), 9.0); - polygon.add_ring([[1.0, 1.0], [1.0, 2.0], [2.0, 2.0], [2.0, 1.0]]); - assert_eq!(polygon.area(), 8.0); - - // winding order should not matter - let mut polygon = Polygon2::new(); - polygon.add_ring([[0.0, 0.0], [0.0, 3.0], [3.0, 3.0], [3.0, 0.0]]); - assert_eq!(polygon.area(), 9.0); - polygon.add_ring([[1.0, 1.0], [2.0, 1.0], [2.0, 2.0], [1.0, 2.0]]); - assert_eq!(polygon.area(), 8.0); - } -} diff --git a/nusamai-geometry/src/lib.rs b/nusamai-geometry/src/lib.rs deleted file mode 100644 index c6d439d28..000000000 --- a/nusamai-geometry/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod compact; - -pub use compact::*; diff --git a/nusamai-geometry/tests/georust_interop.rs b/nusamai-geometry/tests/georust_interop.rs deleted file mode 100644 index 2580a6b61..000000000 --- a/nusamai-geometry/tests/georust_interop.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! Testing mutual conversion between geo_types and our MultiPolygon -//! -//! Efficiency of the conversion is not considered. -//! -//! TODO: Implement these conversions as the 'geo-interop' feature. - -use geo_types::{Coord, LineString, MultiPolygon, Polygon}; -use geojson::GeoJson; -use nusamai_geometry::{LineString2, MultiPolygon2}; - -/// Convert GeoRust MultiPolygon to MultiPolygon -fn georust_to_compact(multipolygon: &MultiPolygon) -> MultiPolygon2 { - let MultiPolygon(multipolygon) = &multipolygon; - let mut mpoly = MultiPolygon2::new(); - - for polygon in multipolygon { - let LineString(exterior) = &polygon.exterior(); - mpoly.add_exterior(exterior.iter().map(|c| [c.x, c.y])); - for LineString(interior) in polygon.interiors() { - mpoly.add_interior(interior.iter().map(|c| [c.x, c.y])); - } - } - mpoly -} - -/// Convert MultiPolygon to GeoRust MultiPolygon -fn compact_to_georust(mpoly: &MultiPolygon2) -> MultiPolygon { - fn _coords_to_linestring(coords: &LineString2) -> LineString { - LineString::new( - coords - .iter_closed() - .map(|a| Coord { x: a[0], y: a[1] }) - .collect(), - ) - } - - let polygons = mpoly - .iter() - .map(|poly| { - let exterior = _coords_to_linestring(&poly.exterior()); - let interiors = poly - .interiors() - .map(|interior| _coords_to_linestring(&interior)) - .collect(); - Polygon::new(exterior, interiors) - }) - .collect(); - - MultiPolygon(polygons) -} - -#[test] -fn test_georust_multipolygon_interop() { - let geojson_str = r#" - { - "type": "MultiPolygon", - "coordinates": [ - [ - [[102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 2.0]] - ], - [ - [[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]], - [[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]] - ], - [ - [[102.0, 0.0], [103.0, 0.0], [103.0, 1.0], [102.0, 1.0], [102.0, 0.0]], - [[102.2, 0.2], [102.4, 0.2], [102.4, 0.4], [102.2, 0.4], [102.2, 0.2]], - [[102.6, 0.6], [102.8, 0.6], [102.8, 0.8], [102.6, 0.8], [102.6, 0.6]] - ], - [ - [[101.0, 1.0], [102.0, 1.0], [102.0, 2.0], [101.0, 2.0], [101.0, 1.0]], - [[101.2, 1.2], [101.4, 1.2], [101.4, 1.4], [101.2, 1.4], [101.2, 1.2]], - [[101.6, 1.6], [101.8, 1.6], [101.8, 1.8], [101.6, 1.6]] - ] - ] - } - "#; - - // Load GeoJSON - let Ok(GeoJson::Geometry(geometry)) = geojson_str.parse::() else { - panic!("failed to parse GeoJSON"); - }; - - // GeoJSON -> GeoRust MultiPolygon - let Ok(mpoly): Result = geometry.value.try_into() else { - panic!("failed to convert GeoJSON to MultiPolygon"); - }; - - // GeoRust MultiPolygon -> MultiPolygon - let compact_mpoly = georust_to_compact(&mpoly); - - // MultiPolygon -> GeoRust MultiPolygon - let mpoly_again = compact_to_georust(&compact_mpoly); - - // Check equality - assert_eq!(mpoly, mpoly_again); -} diff --git a/nusamai-geometry/tests/serde.rs b/nusamai-geometry/tests/serde.rs deleted file mode 100644 index ea98cdffb..000000000 --- a/nusamai-geometry/tests/serde.rs +++ /dev/null @@ -1,30 +0,0 @@ -#[cfg(feature = "serde")] -mod tests { - use nusamai_geometry::{ - Geometry2, LineString3, MultiLineString2, MultiPoint3, MultiPolygon2, Polygon2, - }; - - #[derive(serde::Serialize, serde::Deserialize)] - struct MyStruct { - mpoly: MultiPolygon2<'static>, - poly: Polygon2<'static>, - line: LineString3<'static>, - mline: MultiLineString2<'static>, - mpoint: MultiPoint3<'static>, - geom: Geometry2<'static>, - } - - #[test] - fn test_serde_serialize_deserialize() { - let m = MyStruct { - mpoly: Default::default(), - poly: Default::default(), - mline: Default::default(), - line: Default::default(), - mpoint: Default::default(), - geom: Geometry2::MultiPoint(Default::default()), - }; - let serialized = serde_json::to_string(&m).unwrap(); - let _: MyStruct = serde_json::from_str(&serialized).unwrap(); - } -} diff --git a/nusamai-gltf/Cargo.toml b/nusamai-gltf/Cargo.toml index 6988bd6c2..8f0f9f2f5 100644 --- a/nusamai-gltf/Cargo.toml +++ b/nusamai-gltf/Cargo.toml @@ -1,25 +1,14 @@ [package] +edition = "2021" name = "nusamai-gltf" - -authors.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true -version.workspace = true +version = "0.1.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] byteorder = "1.5.0" -clap = { version = "4.5.4", features = ["derive"] } -earcut = "0.4" -indexmap.workspace = true -nusamai-geometry = { path = "../nusamai-geometry" } nusamai-gltf-json = { "path" = "nusamai-gltf-json" } -quick-xml.workspace = true -serde_json.workspace = true -thiserror.workspace = true +serde_json = "1.0.115" [dev-dependencies] glob = "0.3.1" diff --git a/nusamai-gltf/examples/make_gltf.rs b/nusamai-gltf/examples/make_gltf.rs index 644c06774..703e2dca7 100644 --- a/nusamai-gltf/examples/make_gltf.rs +++ b/nusamai-gltf/examples/make_gltf.rs @@ -19,7 +19,10 @@ fn main() -> io::Result<()> { byte_length, ..Default::default() }; - buffer.uri = "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=".to_string().into(); + buffer.uri = "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/\ + AAAAAAAAAAAAAAAAAACAPwAAAAA=" + .to_string() + .into(); let buffer_view1 = BufferView { name: None, diff --git a/nusamai-gltf/nusamai-gltf-json/Cargo.toml b/nusamai-gltf/nusamai-gltf-json/Cargo.toml index 6f3c0f888..77a3e8b7b 100644 --- a/nusamai-gltf/nusamai-gltf-json/Cargo.toml +++ b/nusamai-gltf/nusamai-gltf-json/Cargo.toml @@ -6,10 +6,10 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true, features = ["indexmap", "float_roundtrip"] } -serde_repr = "0.1.19" -cesiumtiles = { git = "https://github.com/reearth/cesiumtiles-rs.git" } +serde = { version = "1.0.197", features = ["derive"] } +serde_json = { version = "1.0.115", features = ["float_roundtrip"] } +serde_repr = "0.1.18" +cesiumtiles = { git = "https://github.com/MIERUNE/cesiumtiles-rs.git" } [dev-dependencies] glob = "0.3.1" diff --git a/nusamai-gpkg/Cargo.toml b/nusamai-gpkg/Cargo.toml index d3c570225..eac16d93a 100644 --- a/nusamai-gpkg/Cargo.toml +++ b/nusamai-gpkg/Cargo.toml @@ -1,19 +1,14 @@ [package] +edition = "2021" name = "nusamai-gpkg" - -authors.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true -version.workspace = true +version = "0.1.0" [dependencies] -indexmap.workspace = true -nusamai-geometry = { path = "../nusamai-geometry" } -sqlx = { version = "0.8.1", features = ["runtime-tokio", "sqlite"] } -thiserror.workspace = true -url.workspace = true +flatgeom = "0.0.2" +indexmap = "2.2.6" +sqlx = { version = "0.8.0", features = ["runtime-tokio", "sqlite"] } +thiserror = "1.0.58" +url = "2.5.0" [dev-dependencies] tokio = { version = "1.36", features = ["full"] } diff --git a/nusamai-gpkg/src/geometry.rs b/nusamai-gpkg/src/geometry.rs index 232aa1736..9805c07ea 100644 --- a/nusamai-gpkg/src/geometry.rs +++ b/nusamai-gpkg/src/geometry.rs @@ -4,7 +4,7 @@ use std::io::Write; -use nusamai_geometry::{Coord, MultiPolygon, Polygon}; +use flatgeom::{Coord, MultiPolygon, Polygon}; #[repr(u8)] pub enum WkbByteOrder { diff --git a/nusamai-gpkg/src/handler.rs b/nusamai-gpkg/src/handler.rs index 5c1372f54..c15eabb71 100644 --- a/nusamai-gpkg/src/handler.rs +++ b/nusamai-gpkg/src/handler.rs @@ -142,9 +142,12 @@ impl GpkgHandler { pub async fn gpkg_geometry_columns( &self, ) -> Result, GpkgError> { - let result = sqlx::query("SELECT table_name, column_name, geometry_type_name, srs_id, z, m FROM gpkg_geometry_columns;") - .fetch_all(&self.pool) - .await?; + let result = sqlx::query( + "SELECT table_name, column_name, geometry_type_name, srs_id, z, m FROM \ + gpkg_geometry_columns;", + ) + .fetch_all(&self.pool) + .await?; let rows = result .iter() @@ -213,7 +216,8 @@ impl<'c> GpkgTransaction<'c> { // Add the table to `gpkg_contents` sqlx::query( - "INSERT INTO gpkg_contents (table_name, data_type, identifier, srs_id) VALUES (?, ?, ?, ?);", + "INSERT INTO gpkg_contents (table_name, data_type, identifier, srs_id) VALUES (?, ?, \ + ?, ?);", ) .bind(table_info.name.as_str()) .bind(if table_info.has_geometry { @@ -229,13 +233,17 @@ impl<'c> GpkgTransaction<'c> { // Add the table to `gpkg_geometry_columns` if table_info.has_geometry { sqlx::query( - "INSERT INTO gpkg_geometry_columns (table_name, column_name, geometry_type_name, srs_id, z, m) VALUES (?, ?, ?, ?, ?, ?);" - ).bind(table_info.name.as_str()) + "INSERT INTO gpkg_geometry_columns (table_name, column_name, geometry_type_name, \ + srs_id, z, m) VALUES (?, ?, ?, ?, ?, ?);", + ) + .bind(table_info.name.as_str()) .bind("geometry") .bind("MULTIPOLYGON") // Fixed for now - TODO: Change according to the data .bind(srs_id) .bind(1) - .bind(0).execute(&mut *executor).await?; + .bind(0) + .execute(&mut *executor) + .await?; } // TODO: add MIME type to `gpkg_data_columns` @@ -319,13 +327,15 @@ impl<'c> GpkgTransaction<'c> { (min_x, min_y, max_x, max_y): (f64, f64, f64, f64), ) -> Result<(), GpkgError> { let executor = self.tx.acquire().await.unwrap(); - let query = sqlx::query("UPDATE gpkg_contents SET min_x = ?, min_y = ?, max_x = ?, max_y = ? WHERE table_name = ?;" -) - .bind(min_x) - .bind(min_y) - .bind(max_x) - .bind(max_y) - .bind(table_name); + let query = sqlx::query( + "UPDATE gpkg_contents SET min_x = ?, min_y = ?, max_x = ?, max_y = ? WHERE table_name \ + = ?;", + ) + .bind(min_x) + .bind(min_y) + .bind(max_x) + .bind(max_y) + .bind(table_name); query.execute(&mut *executor).await?; Ok(()) } diff --git a/nusamai-gpkg/src/sql/srs.sql b/nusamai-gpkg/src/sql/srs.sql index 168e539ff..2d2a85524 100644 --- a/nusamai-gpkg/src/sql/srs.sql +++ b/nusamai-gpkg/src/sql/srs.sql @@ -54,6 +54,25 @@ AXIS ["Ellipsoidal height (h)",up,LENGTHUNIT["metre",1,ID["EPSG",9001]]], ID ["EPSG",4979]]' ); +-- Web Mercator (WGS 84 / Pseudo-Mercator) +-- cf. https://epsg.org/crs_3857/Web_Mercator.html +INSERT INTO + gpkg_spatial_ref_sys ( + srs_name, + srs_id, + organization, + organization_coordsys_id, + definition + ) +VALUES + ( + 'WGS 84 / Pseudo-Mercator', + 3857, + 'EPSG', + 3857, + 'PROJCRS["WGS 84 / Pseudo-Mercator",BASEGEOGCRS["WGS 84",ENSEMBLE["World Geodetic System 1984 ensemble",MEMBER["World Geodetic System 1984 (Transit)"],MEMBER["World Geodetic System 1984 (G730)"],MEMBER["World Geodetic System 1984 (G873)"],MEMBER["World Geodetic System 1984 (G1150)"],MEMBER["World Geodetic System 1984 (G1674)"],MEMBER["World Geodetic System 1984 (G1762)"],MEMBER["World Geodetic System 1984 (G2139)"],ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]],ENSEMBLEACCURACY[2.0]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],ID["EPSG",4326]],CONVERSION["Popular Visualisation Pseudo-Mercator",METHOD["Popular Visualisation Pseudo Mercator",ID["EPSG",1024]],PARAMETER["Latitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8801]],PARAMETER["Longitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8802]],PARAMETER["False easting",0,LENGTHUNIT["metre",1],ID["EPSG",8806]],PARAMETER["False northing",0,LENGTHUNIT["metre",1],ID["EPSG",8807]]],CS[Cartesian,2],AXIS["easting (X)",east,ORDER[1],LENGTHUNIT["metre",1]],AXIS["northing (Y)",north,ORDER[2],LENGTHUNIT["metre",1]],USAGE[SCOPE["Web mapping and visualisation."],AREA["World between 85.06°S and 85.06°N."],BBOX[-85.06,-180,85.06,180]],ID["EPSG",3857]]' + ); + -- Japan Plane Rectangular CS + JGD2011 (vertical) height -- cf. https://epsg.org/crs_10162/JGD2011-Japan-Plane-Rectangular-CS-I-JGD2011-vertical-height.html, etc. INSERT INTO diff --git a/nusamai-kml/Cargo.toml b/nusamai-kml/Cargo.toml index ebeeb18ab..86cf5b306 100644 --- a/nusamai-kml/Cargo.toml +++ b/nusamai-kml/Cargo.toml @@ -1,15 +1,10 @@ [package] +edition = "2021" name = "nusamai-kml" - -authors.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true -version.workspace = true +version = "0.1.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +flatgeom = "0.0.2" kml = "0.8.5" -nusamai-geometry = { path = "../nusamai-geometry" } diff --git a/nusamai-kml/src/conversion.rs b/nusamai-kml/src/conversion.rs index 6312ca3c8..62c69536a 100644 --- a/nusamai-kml/src/conversion.rs +++ b/nusamai-kml/src/conversion.rs @@ -1,12 +1,10 @@ use std::{collections::HashMap, vec}; +use flatgeom::{Coord, MultiPoint, MultiPoint3, MultiPolygon, MultiPolygon3, Polygon, Polygon3}; use kml::types::{ AltitudeMode, Coord as KmlCoord, Geometry, LinearRing, MultiGeometry, Point, Polygon as KmlPolygon, }; -use nusamai_geometry::{ - Coord, MultiPoint, MultiPoint3, MultiPolygon, MultiPolygon3, Polygon, Polygon3, -}; pub fn multipolygon_to_kml(mpoly: &MultiPolygon3) -> Vec { multipolygon_to_kml_with_mapping(mpoly, |c| c) @@ -84,7 +82,7 @@ fn polygon_to_kml_inner_boundary_with_mapping( .collect() } -/// Create a kml::MultiGeometry with Polygon from `nusamai_geometry::MultiPoint` with a mapping function. +/// Create a kml::MultiGeometry with Polygon from `flatgeom::MultiPoint` with a mapping function. pub fn polygon_to_kml_with_mapping( poly: &Polygon, mapping: impl Fn(T) -> [f64; 3], @@ -92,7 +90,7 @@ pub fn polygon_to_kml_with_mapping( vec![polygon_to_kml_polygon_with_mapping(poly, mapping)] } -/// Create a kml::MultiGeometry from a nusamai_geometry::MultiPolygon +/// Create a kml::MultiGeometry from a flatgeom::MultiPolygon pub fn polygon_to_kml(poly: &Polygon3) -> Vec { polygon_to_kml_with_mapping(poly, |c| c) } @@ -102,7 +100,7 @@ pub fn indexed_polygon_to_kml(vertices: &[[f64; 3]], poly_idx: &Polygon) -> polygon_to_kml_with_mapping(poly_idx, |idx| vertices[idx as usize]) } -/// Create a kml::MultiGeometry with Points from `nusamai_geometry::MultiPoint` with a mapping function. +/// Create a kml::MultiGeometry with Points from `flatgeom::MultiPoint` with a mapping function. pub fn multipoint_to_kml_with_mapping( mpoint: &MultiPoint, mapping: impl Fn(T) -> [f64; 3], @@ -125,7 +123,7 @@ pub fn indexed_multipoint_to_kml( multipoint_to_kml_with_mapping(mpoint_idx, |idx| vertices[idx as usize]) } -/// Create a kml::MultiGeometry from a nusamai_geometry::MultiPoint +/// Create a kml::MultiGeometry from a flatgeom::MultiPoint pub fn multipoint_to_kml(mpoint: &MultiPoint3) -> MultiGeometry { multipoint_to_kml_with_mapping(mpoint, |c| c) } diff --git a/nusamai-mvt/Cargo.toml b/nusamai-mvt/Cargo.toml index df2d54bcc..559f6aee8 100644 --- a/nusamai-mvt/Cargo.toml +++ b/nusamai-mvt/Cargo.toml @@ -1,17 +1,12 @@ [package] +edition = "2021" name = "nusamai-mvt" - -authors.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true -version.workspace = true +version = "0.1.0" [dependencies] -ahash.workspace = true -indexmap.workspace = true -prost = "0.13.1" +ahash = "0.8.11" +indexmap = "2.2.6" +prost = "0.13.3" [build-dependencies] -prost-build = "0.13.1" +prost-build = "0.13.3" diff --git a/nusamai-mvt/src/webmercator.rs b/nusamai-mvt/src/webmercator.rs index a809c865b..e623a1008 100644 --- a/nusamai-mvt/src/webmercator.rs +++ b/nusamai-mvt/src/webmercator.rs @@ -1,8 +1,11 @@ //! Web Mercator projection utilities. -use std::f64::consts::FRAC_PI_2; +use std::f64::consts::{FRAC_PI_2, TAU}; -/// Converts geographic coordinate (lng, lat) to Web Mercator coordinate (mx, my). +const A: f64 = 6378137.; +const CIRCUMFERENCE: f64 = A * TAU; + +/// Converts geographic coordinate (lng, lat) to Web Mercator coordinate (mx, my) normalized. /// /// The range of (mx, my) is [0.0, 0.0]-[1.0, 1.0] (same as Mapbox/MapLibre API, etc.) pub fn lnglat_to_web_mercator(lng: f64, lat: f64) -> (f64, f64) { @@ -12,7 +15,7 @@ pub fn lnglat_to_web_mercator(lng: f64, lat: f64) -> (f64, f64) { (mx, my) } -/// Converts Web Mercator coordinate (mx, my) to geographic coordinate (lng, lat). +/// Converts Web Mercator coordinate (mx, my) normalized to geographic coordinate (lng, lat). /// /// The range of (mx, my) is [0.0, 0.0]-[1.0, 1.0] (same as Mapbox/MapLibre API, etc.) pub fn web_mercator_to_lnglat(mx: f64, my: f64) -> (f64, f64) { @@ -22,12 +25,30 @@ pub fn web_mercator_to_lnglat(mx: f64, my: f64) -> (f64, f64) { (lng, lat) } +/// Converts geographic coordinate (lng, lat) to Web Mercator coordinate (mx, my) in meters. +/// +/// The range of (mx, my) is [-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244] +pub fn lnglat_to_web_mercator_meters(lng: f64, lat: f64) -> (f64, f64) { + let mx = lng / 360.0 * CIRCUMFERENCE; + let my = ((90.0 + lat).to_radians() / 2.0).tan().ln() * A; + (mx, my) +} + +/// Converts Web Mercator coordinate (mx, my) in meters to geographic coordinate (lng, lat). +/// +/// The range of (mx, my) is [-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244] +pub fn web_mercator_meters_to_lnglat(mx: f64, my: f64) -> (f64, f64) { + let lng = mx / CIRCUMFERENCE * 360.0; + let lat = (2.0 * (my / A).exp().atan()).to_degrees() - 90.0; + (lng, lat) +} + #[cfg(test)] mod tests { use super::*; #[test] - fn roundtrip() { + fn roundtrip_normalized() { { let (lng, lat) = (136.08, 37.39); let (mx, my) = lnglat_to_web_mercator(lng, lat); @@ -44,6 +65,24 @@ mod tests { } } + #[test] + fn roundtrip_in_meters() { + { + let (lng, lat) = (136.08, 37.39); + let (mx, my) = lnglat_to_web_mercator_meters(lng, lat); + let (lng2, lat2) = web_mercator_meters_to_lnglat(mx, my); + assert!((lng - lng2).abs() < 1e-9); + assert!((lat - lat2).abs() < 1e-9); + } + { + let (lng, lat) = (0.3, 0.2); + let (mx, my) = lnglat_to_web_mercator_meters(lng, lat); + let (lng2, lat2) = web_mercator_meters_to_lnglat(mx, my); + assert!((lng - lng2).abs() < 1e-9); + assert!((lat - lat2).abs() < 1e-9); + } + } + #[test] fn null_island() { // https://en.wikipedia.org/wiki/Null_Island @@ -53,4 +92,24 @@ mod tests { assert!((mx - 0.5).abs() < 1e-10); assert!((my - 0.5).abs() < 1e-10); } + + #[test] + fn null_island_in_meters() { + // https://en.wikipedia.org/wiki/Null_Island + // (lng: 0, lat: 0) -> (mx: 0.5, my: 0.5) + let (lng, lat) = (0., 0.); + let (mx, my) = lnglat_to_web_mercator_meters(lng, lat); + println!("{}, {}", mx, my); + assert!((mx - 0.0).abs() < 1e-9); + assert!((my - 0.0).abs() < 1e-9); + } + + #[test] + fn bound_in_meters() { + let (lng, lat) = (180., 85.0511287798066); + let (mx, my) = lnglat_to_web_mercator_meters(lng, lat); + println!("{}", CIRCUMFERENCE / 2.); + assert!((mx - CIRCUMFERENCE / 2.).abs() < 1e-7); + assert!((my - CIRCUMFERENCE / 2.).abs() < 1e-7); + } } diff --git a/nusamai-plateau/Cargo.toml b/nusamai-plateau/Cargo.toml index 56ff5e1b5..39a28c655 100644 --- a/nusamai-plateau/Cargo.toml +++ b/nusamai-plateau/Cargo.toml @@ -1,11 +1,6 @@ [package] +edition = "2021" name = "nusamai-plateau" - -authors.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true version.workspace = true [features] @@ -13,22 +8,22 @@ default = ["serde"] serde = ["dep:serde"] [dependencies] -chrono = { workspace = true, features = ["serde"] } -hashbrown = { version = "0.14.5", features = ["serde"] } -indexmap.workspace = true -log.workspace = true +chrono = { version = "0.4.35", features = ["serde"], default-features = false } +flatgeom = "0.0.2" +hashbrown = { version = "0.15.0", features = ["serde"] } +indexmap = "2.2.6" +log = "0.4.21" nusamai-citygml = { path = "../nusamai-citygml", features = ["serde"] } -nusamai-geometry = { path = "../nusamai-geometry" } -once_cell.workspace = true -quick-xml.workspace = true -serde = { workspace = true, features = ["derive", "rc"], optional = true } -serde_json.workspace = true -stretto = "0.8.4" -url.workspace = true +once_cell = "1.20.2" +quick-xml = "0.36.2" +serde = { version = "1.0.197", features = ["derive", "rc"], optional = true } +stretto = "0.8.3" +url = "2.5.0" [dev-dependencies] bincode = { version = "2.0.0-rc.3", default-features = false, features = ["serde", "std"] } -clap = { version = "4.5.16", features = ["derive"] } -lz4_flex = "0.11.3" -serde = { workspace = true, features = ["derive"] } -zstd = { version = "0.13.2", features = ["zdict_builder"] } +clap = { version = "4.5", features = ["derive"] } +lz4_flex = "0.11.2" +serde = { version = "1.0.197", features = ["derive"] } +serde_json = "1.0.115" +zstd = { version = "0.13.0", features = ["zdict_builder"] } diff --git a/nusamai-plateau/examples/parse_and_compress.rs b/nusamai-plateau/examples/parse_and_compress.rs index 357101d59..23e5701bd 100644 --- a/nusamai-plateau/examples/parse_and_compress.rs +++ b/nusamai-plateau/examples/parse_and_compress.rs @@ -23,7 +23,7 @@ fn example_toplevel_dispatcher( b"core:cityObjectMember" => { let mut cityobj: nusamai_plateau::models::TopLevelCityObject = Default::default(); cityobj.parse(st)?; - let geometries = st.collect_geometries(); + let geometries = st.collect_geometries(None); if let Some(root) = cityobj.into_object() { let obj = self::TopLevelCityObject { root, geometries }; diff --git a/nusamai-plateau/src/appearance.rs b/nusamai-plateau/src/appearance.rs index 0de311c46..093ade283 100644 --- a/nusamai-plateau/src/appearance.rs +++ b/nusamai-plateau/src/appearance.rs @@ -2,9 +2,9 @@ use std::hash::{Hash, Hasher}; +use flatgeom::LineString2; use hashbrown::HashMap; use nusamai_citygml::{appearance::TextureAssociation, Color, LocalId, SurfaceSpan}; -use nusamai_geometry::LineString2; use url::Url; use crate::models::appearance::{self, ParameterizedTexture, SurfaceDataProperty, X3DMaterial}; diff --git a/nusamai-plateau/src/codelist/xml.rs b/nusamai-plateau/src/codelist/xml.rs index e093ed10d..93b38ff98 100644 --- a/nusamai-plateau/src/codelist/xml.rs +++ b/nusamai-plateau/src/codelist/xml.rs @@ -88,7 +88,7 @@ fn parse_definition( )); } Ok(_) => {} - Err(e) => return Err(ParseError::XmlError(e)), + Err(e) => return Err(e.into()), } } @@ -107,8 +107,10 @@ pub fn parse_dictionary( src_reader: R, ) -> Result, ParseError> { let mut reader = quick_xml::NsReader::from_reader(src_reader); - reader.config_mut().trim_text(true); - reader.config_mut().expand_empty_elements = true; + let config = reader.config_mut(); + config.trim_text(true); + config.expand_empty_elements = true; + let mut depth = 0; let mut buf = Vec::new(); let mut buf2 = Vec::new(); diff --git a/nusamai-plateau/src/entity.rs b/nusamai-plateau/src/entity.rs index c01c27939..297102727 100644 --- a/nusamai-plateau/src/entity.rs +++ b/nusamai-plateau/src/entity.rs @@ -5,7 +5,7 @@ use nusamai_citygml::{geometry::GeometryStore, object::Value, GeometryRefs}; use crate::appearance::AppearanceStore; /// City objects, features, objects or data -#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct Entity { /// GML id pub id: String, @@ -26,8 +26,7 @@ pub struct Entity { pub geometry_refs: GeometryRefs, } - -#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct BoundedBy { pub id: String, pub geometry_refs: GeometryRefs, diff --git a/nusamai-plateau/src/lib.rs b/nusamai-plateau/src/lib.rs index e8c48e3dd..bf817d719 100644 --- a/nusamai-plateau/src/lib.rs +++ b/nusamai-plateau/src/lib.rs @@ -2,6 +2,5 @@ pub mod appearance; pub mod codelist; mod entity; pub mod models; - pub use entity::BoundedBy; pub use entity::Entity; diff --git a/nusamai-plateau/src/models/building.rs b/nusamai-plateau/src/models/building.rs index 6d4c2b3a9..1ede14991 100644 --- a/nusamai-plateau/src/models/building.rs +++ b/nusamai-plateau/src/models/building.rs @@ -1,6 +1,7 @@ use nusamai_citygml::{ citygml_feature, citygml_property, CityGmlElement, Code, GYear, Length, MeasureOrNullList, }; + use once_cell::sync::Lazy; use crate::BoundedBy; diff --git a/nusamai-plateau/src/models/iur/uro/underground_building.rs b/nusamai-plateau/src/models/iur/uro/underground_building.rs index c70430dea..56b3a056a 100644 --- a/nusamai-plateau/src/models/iur/uro/underground_building.rs +++ b/nusamai-plateau/src/models/iur/uro/underground_building.rs @@ -1,10 +1,7 @@ use nusamai_citygml::{citygml_feature, CityGmlElement, Code, GYear, Length, MeasureOrNullList}; use once_cell::sync::Lazy; -use crate::{ - models::{building as bldg, core, iur::uro}, - BoundedBy, -}; +use crate::models::{building as bldg, core, iur::uro, BoundedBy}; #[citygml_feature(name = "uro:UndergroundBuilding")] pub struct UndergroundBuilding { diff --git a/nusamai-plateau/tests/common/mod.rs b/nusamai-plateau/tests/common/mod.rs index 7b556f632..13b3f768d 100644 --- a/nusamai-plateau/tests/common/mod.rs +++ b/nusamai-plateau/tests/common/mod.rs @@ -14,33 +14,36 @@ fn toplevel_dispatcher( ) -> Result, ParseError> { let mut cityobjs = Vec::new(); - match st.parse_children(|st| match st.current_path() { - b"core:cityObjectMember" => { - let mut cityobj: TopLevelCityObject = Default::default(); - cityobj.parse(st)?; - let geometries = st.collect_geometries(); - cityobjs.push(CityObject { - cityobj, - geometries, - }); - Ok(()) + match st.parse_children(|st| { + let current_path: &[u8] = &st.current_path(); + match current_path { + b"core:cityObjectMember" => { + let mut cityobj: TopLevelCityObject = Default::default(); + cityobj.parse(st)?; + let geometries = st.collect_geometries(None); + cityobjs.push(CityObject { + cityobj, + geometries, + }); + Ok(()) + } + b"gml:boundedBy" => { + st.skip_current_element()?; + Ok(()) + } + b"app:appearanceMember" => { + let mut app: AppearanceProperty = Default::default(); + app.parse(st)?; + let AppearanceProperty::Appearance(_app) = app else { + unreachable!(); + }; + Ok(()) + } + other => Err(ParseError::SchemaViolation(format!( + "Unrecognized element {}", + String::from_utf8_lossy(other) + ))), } - b"gml:boundedBy" => { - st.skip_current_element()?; - Ok(()) - } - b"app:appearanceMember" => { - let mut app: AppearanceProperty = Default::default(); - app.parse(st)?; - let AppearanceProperty::Appearance(_app) = app else { - unreachable!(); - }; - Ok(()) - } - other => Err(ParseError::SchemaViolation(format!( - "Unrecognized element {}", - String::from_utf8_lossy(other) - ))), }) { Ok(_) => Ok(cityobjs), Err(e) => { diff --git a/nusamai-plateau/tests/load_examples.rs b/nusamai-plateau/tests/load_examples.rs new file mode 100644 index 000000000..4ac99ff8c --- /dev/null +++ b/nusamai-plateau/tests/load_examples.rs @@ -0,0 +1,1012 @@ +pub mod common; + +use common::{load_cityobjs, load_cityobjs_from_zstd}; +use nusamai_citygml::{Code, Date, Measure}; +use nusamai_plateau::models::{relief, uro, TopLevelCityObject}; + +// #[test] +// fn load_area_example() { +// let cityobjs = load_cityobjs("./tests/data/plateau-3_0/udx/area/523846_area_6697.gml"); +// assert_eq!(cityobjs.len(), 4); +// let TopLevelCityObject::Zone(zone) = &cityobjs.first().unwrap().cityobj else { +// panic!("Not a Zone"); +// }; + +// assert_eq!( +// zone.function, +// vec![Code::new("港湾区域".into(), "0201".into())] +// ); +// assert_eq!(zone.urf_valid_from, Date::from_ymd_opt(1, 1, 1)); +// assert_eq!( +// zone.valid_from_type, +// Code::new("決定".into(), "1".into()).into() +// ); +// } + +// #[test] +// fn load_bridge_example() { +// { +// let cityobjs = +// load_cityobjs("./tests/data/plateau-3_0/udx/brid/dorokyo_51324378_brid_6697.gml"); +// assert_eq!(cityobjs.len(), 1); +// let TopLevelCityObject::Bridge(bridge) = &cityobjs.first().unwrap().cityobj else { +// panic!("Expected a bridge"); +// }; + +// assert_eq!( +// bridge.class, +// Some(Code::new("アーチ橋".to_string(), "03".to_string())) +// ); +// assert_eq!( +// bridge.function, +// vec![Code::new("道路橋".to_string(), "01".to_string())] +// ); +// assert_eq!(bridge.year_of_construction, Some("1962".to_string())); +// assert_eq!(bridge.is_movable, Some(false)); +// assert_eq!( +// bridge.outer_bridge_construction[0].function, +// vec![Code::new("アーチ".to_string(), "04".to_string())] +// ); +// } + +// { +// let cityobjs = +// load_cityobjs("./tests/data/plateau-3_0/udx/brid/hodokyo_51324378_brid_6697.gml"); +// assert_eq!(cityobjs.len(), 1); +// let TopLevelCityObject::Bridge(bridge) = &cityobjs.first().unwrap().cityobj else { +// panic!("Expected a bridge"); +// }; + +// assert_eq!( +// bridge.function, +// vec![Code::new("横断歩道橋".to_string(), "07".to_string())] +// ); +// assert_eq!(bridge.year_of_construction, Some("1968".to_string())); +// assert_eq!( +// bridge +// .brid_risk_assessment_attribute +// .as_ref() +// .unwrap() +// .risk_type +// .as_ref() +// .unwrap() +// .value(), +// "判定区分Ⅰ(健全)" +// ); +// } + +// { +// let cityobjs = +// load_cityobjs("./tests/data/plateau-3_0/udx/brid/pedeck_53360690_brid_6697.gml"); +// assert_eq!(cityobjs.len(), 1); +// let TopLevelCityObject::Bridge(bridge) = &cityobjs.first().unwrap().cityobj else { +// panic!("Expected a bridge"); +// }; + +// assert_eq!( +// bridge.function, +// vec![Code::new( +// "ペデストリアンデッキ".to_string(), +// "08".to_string() +// )] +// ); +// assert_eq!( +// bridge +// .brid_structure_attribute +// .as_ref() +// .unwrap() +// .length +// .as_ref() +// .unwrap() +// .value(), +// 776.0 +// ); +// } +// } + +#[test] +fn load_building_lod4_example() { + let cityobjs = load_cityobjs_from_zstd( + "./tests/data/tokyo23-ku/udx/bldg/53393680_bldg_6697_lod4.2_op.gml.zst", + ); + + let mut multipolygons = 0; + let mut buildings = 0; + let mut cityobjectgroups = 0; + + assert_eq!(cityobjs.len(), 1527); + + for cityobj in cityobjs { + multipolygons += cityobj.geometries.multipolygon.len(); + match cityobj.cityobj { + TopLevelCityObject::Building(_building) => { + buildings += 1; + } + TopLevelCityObject::CityObjectGroup(_group) => { + cityobjectgroups += 1; + } + _ => {} + } + } + + assert_eq!(buildings, 1485); + assert_eq!(cityobjectgroups, 42); + assert_eq!(multipolygons, 197633); +} + +#[test] +fn load_cityfurniture_example() { + let cityobjs = load_cityobjs("./tests/data/kawasaki-shi/udx/frn/53391597_frn_6697_op.gml"); + assert_eq!(cityobjs.len(), 28); + let TopLevelCityObject::CityFurniture(frn) = &cityobjs.first().unwrap().cityobj else { + panic!("Not a CityFurniture"); + }; + + assert_eq!(frn.function, vec![Code::new("柱".into(), "4800".into())]); + assert_eq!( + frn.frn_data_quality_attribute.as_ref().unwrap().src_scale, + vec![Code::new("地図情報レベル500".into(), "3".into(),)] + ); +} + +// #[test] +// fn load_generics_example() { +// let cityobjs = load_cityobjs("./tests/data/plateau-3_0/udx/gen/53392565_gen_6697.gml"); +// assert_eq!(cityobjs.len(), 4); +// let TopLevelCityObject::GenericCityObject(_gen) = &cityobjs.first().unwrap().cityobj else { +// panic!("Not a GenericCityObject"); +// }; +// } + +#[test] +fn load_landslide_example() { + let cityobjs = load_cityobjs("./tests/data/numazu-shi/udx/lsld/523857_lsld_6668_op.gml"); + assert_eq!(cityobjs.len(), 81); + let TopLevelCityObject::SedimentDisasterProneArea(lsld) = &cityobjs.first().unwrap().cityobj + else { + panic!("expected SedimentDisasterProneArea"); + }; + assert_eq!(lsld.location, Some("沼津市下香貫八重".into())); + assert_eq!(lsld.disaster_type.as_ref().unwrap().code(), "1"); + assert_eq!(lsld.area_type.as_ref().unwrap().code(), "2"); + assert_eq!(lsld.zone_number.as_ref().unwrap(), "103-Ⅰ-0648"); + assert_eq!(lsld.status.as_ref().unwrap().code(), "0"); +} + +#[test] +fn load_landuse_example() { + let cityobjs = load_cityobjs("./tests/data/numazu-shi/udx/luse/523836_luse_6668_op.gml"); + assert_eq!(cityobjs.len(), 225); + let TopLevelCityObject::LandUse(landuse) = &cityobjs.first().unwrap().cityobj else { + panic!("Not a Landuse"); + }; + + assert_eq!( + landuse.land_use_detail_attribute[0].prefecture, + Some(Code::new("静岡県".into(), "22".into())) + ); + assert_eq!( + landuse.land_use_detail_attribute[0].city, + Some(Code::new("静岡県沼津市".into(), "22203".into())) + ); +} + +// #[test] +// fn load_other_construction_example() { +// { +// let cityobjs = load_cityobjs("./tests/data/plateau-3_0/udx/cons/52384697_cons_6697.gml"); +// assert_eq!(cityobjs.len(), 1); +// let TopLevelCityObject::OtherConstruction(cons) = &cityobjs.first().unwrap().cityobj else { +// panic!("must be OtherConstruction"); +// }; +// let uro::DmAttributeProperty::DmGeometricAttribute(_) = cons.cons_dm_attribute[0] else { +// panic!("must be DmGeometricAttribute"); +// }; +// } + +// { +// let cityobjs = load_cityobjs("./tests/data/plateau-3_0/udx/cons/52384698_cons_6697_1.gml"); +// assert_eq!(cityobjs.len(), 1); +// let TopLevelCityObject::OtherConstruction(cons) = &cityobjs.first().unwrap().cityobj else { +// panic!("must be OtherConstruction"); +// }; +// let uro::DmAttributeProperty::DmGeometricAttribute(_) = cons.cons_dm_attribute[0] else { +// panic!("must be DmGeometricAttribute"); +// }; +// } + +// { +// let cityobjs = load_cityobjs("./tests/data/plateau-3_0/udx/cons/52384698_cons_6697_2.gml"); +// assert_eq!(cityobjs.len(), 1); +// let TopLevelCityObject::OtherConstruction(cons) = &cityobjs.first().unwrap().cityobj else { +// panic!("must be OtherConstruction"); +// }; +// let uro::DmAttributeProperty::DmGeometricAttribute(_) = cons.cons_dm_attribute[0] else { +// panic!("must be DmGeometricAttribute"); +// }; +// } + +// { +// let cityobjs = load_cityobjs("./tests/data/plateau-3_0/udx/cons/53394695_cons_6697.gml"); +// assert_eq!(cityobjs.len(), 1); +// let TopLevelCityObject::OtherConstruction(cons) = &cityobjs.first().unwrap().cityobj else { +// panic!("must be OtherConstruction"); +// }; +// let uro::DmAttributeProperty::DmGeometricAttribute(_) = cons.cons_dm_attribute[0] else { +// panic!("must be DmGeometricAttribute"); +// }; +// } + +// { +// let cityobjs = load_cityobjs("./tests/data/plateau-3_0/udx/cons/53395603_cons_6697.gml"); +// assert_eq!(cityobjs.len(), 1); +// let TopLevelCityObject::OtherConstruction(cons) = &cityobjs.first().unwrap().cityobj else { +// panic!("must be OtherConstruction"); +// }; +// let uro::DmAttributeProperty::DmGeometricAttribute(_) = cons.cons_dm_attribute[0] else { +// panic!("must be DmGeometricAttribute"); +// }; +// } + +// { +// let cityobjs = load_cityobjs("./tests/data/plateau-3_0/udx/cons/56403133_cons_6697.gml"); +// assert_eq!(cityobjs.len(), 1); +// let TopLevelCityObject::OtherConstruction(cons) = &cityobjs.first().unwrap().cityobj else { +// panic!("must be OtherConstruction"); +// }; +// let uro::DmAttributeProperty::DmGeometricAttribute(_) = cons.cons_dm_attribute[0] else { +// panic!("must be DmGeometricAttribute"); +// }; +// assert_eq!( +// cons.cons_base_attribute.as_ref().unwrap().admin_type, +// Some(Code::new("北陸地方整備局".into(), "23".into())) +// ); +// assert_eq!( +// cons.cons_base_attribute.as_ref().unwrap().administorator, +// Some("信濃川河川事務所".into()) +// ) +// } +// } + +#[test] +fn load_dem_example() { + let cityobjs = load_cityobjs("./tests/data/yokosuka-shi/udx/dem/523965_dem_6697_05_op.gml"); + assert_eq!(cityobjs.len(), 2); + let TopLevelCityObject::ReliefFeature(dem) = &cityobjs.first().unwrap().cityobj else { + panic!("Not a ReliefFeature"); + }; + + let relief::ReliefComponentProperty::TINRelief(tin) = &dem.relief_component[0] else { + panic!("Unexpected relief component type"); + }; + assert_eq!(tin.lod, Some(1)); + + assert_eq!(cityobjs[0].geometries.epsg, 6697); + assert_eq!( + cityobjs + .iter() + .map(|o| o.geometries.multipolygon.len()) + .sum::(), + 1066 + ); +} + +#[test] +fn load_road_example() { + let cityobjs = load_cityobjs("./tests/data/numazu-shi/udx/tran/52385608_tran_6697_op.gml"); + assert_eq!(cityobjs.len(), 549); + let TopLevelCityObject::Road(road) = &cityobjs.first().unwrap().cityobj else { + panic!("Not a Road"); + }; + + assert_eq!( + road.function, + vec![Code::new("都道府県道".into(), "3".into(),)] + ); + assert_eq!( + road.usage, + vec![ + Code::new("緊急輸送道路(第三次緊急輸送道路)".into(), "3".into()), + Code::new("避難路/避難道路".into(), "5".into()), + ] + ); + assert_eq!( + road.traffic_area.first().unwrap().function, + vec![Code::new("歩道".into(), "2020".into())] + ); + assert_eq!( + road.auxiliary_traffic_area.first().unwrap().function, + vec![Code::new("歩道部の段差".into(), "2000".into())] + ); + assert_eq!( + road.road_structure_attribute[0].width, + Some(Measure::new(22.0)), + ); + assert_eq!( + road.traffic_volume_attribute[0].weekday12hour_traffic_volume, + Some(8170), + ); +} + +// #[test] +// fn load_railway_example() { +// let cityobjs = load_cityobjs("./tests/data/plateau-3_0/udx/rwy/53395527_rwy_6697.gml"); +// assert_eq!(cityobjs.len(), 4); +// let TopLevelCityObject::Railway(railway) = &cityobjs.first().unwrap().cityobj else { +// panic!("Not a Railway"); +// }; + +// assert_eq!( +// railway.id, +// "rwy_f087faa5-f548-4188-aa2e-03c7a5f2d3b9".to_string() +// ); + +// assert_eq!( +// railway.name, +// vec![Code::new("東北線".into(), "東北線".into())] +// ); +// assert_eq!(railway.traffic_area.len(), 7); +// assert_eq!( +// railway.traffic_area.first().unwrap().function, +// vec![Code::new("軌道中心線".to_string(), "8000".to_string())] +// ); +// assert_eq!(railway.auxiliary_traffic_area.len(), 1); +// } + +// #[test] +// fn load_track_example() { +// let cityobjs = load_cityobjs("./tests/data/plateau-3_0/udx/trk/53361601_trk_6697.gml"); +// assert_eq!(cityobjs.len(), 125); +// let TopLevelCityObject::Track(track) = &cityobjs.first().unwrap().cityobj else { +// panic!("Not a Track"); +// }; + +// assert_eq!(track.function, vec![Code::new("徒歩道".into(), "1".into())]); +// assert_eq!( +// track +// .tran_data_quality_attribute +// .as_ref() +// .unwrap() +// .geometry_src_desc, +// vec![Code::new("既成図数値化".into(), "6".into())] +// ); +// assert_eq!( +// track.auxiliary_traffic_area.first().unwrap().function, +// vec![Code::new("島".into(), "3000".into())] +// ); +// assert_eq!( +// track.track_attribute.as_ref().unwrap().admin_type, +// Some(Code::new("市区町村".into(), "3".into())) +// ); +// } + +// #[test] +// fn load_square_example() { +// let cityobjs = load_cityobjs("./tests/data/plateau-3_0/udx/squr/53360690_squr_6697.gml"); +// assert_eq!(cityobjs.len(), 1); +// let TopLevelCityObject::Square(square) = &cityobjs.first().unwrap().cityobj else { +// panic!("Not a Square"); +// }; + +// assert_eq!( +// square.class, +// Some(Code::new("その他".into(), "1090".into())) +// ); +// assert_eq!( +// square.function, +// vec![Code::new("駅前広場".into(), "1".into())] +// ); +// assert_eq!(square.traffic_area.len(), 9); +// assert_eq!(square.auxiliary_traffic_area.len(), 3); +// assert_eq!( +// square.traffic_area.first().unwrap().function, +// vec![Code::new("歩道部".into(), "2000".into())] +// ); +// assert_eq!( +// square.auxiliary_traffic_area.first().unwrap().function, +// vec![Code::new("島".into(), "3000".into())] +// ); +// } + +// #[test] +// fn load_waterway_example() { +// let cityobjs = load_cityobjs("./tests/data/plateau-3_0/udx/wwy/52397630_wwy_6697.gml"); +// assert_eq!(cityobjs.len(), 1); +// let TopLevelCityObject::Waterway(square) = &cityobjs.first().unwrap().cityobj else { +// panic!("Not a Waterway"); +// }; + +// assert_eq!( +// square.function, +// vec![Code::new("法定航路".into(), "01".into())] +// ); +// assert_eq!( +// square.waterway_detail_attribute.as_ref().unwrap().route_id, +// Some("002".into()) +// ) +// } + +// #[test] +// fn load_tunnel_example() { +// let cityobjs = load_cityobjs("./tests/data/plateau-3_0/udx/tun/53361613_tun_6697.gml"); +// assert_eq!(cityobjs.len(), 1); +// let TopLevelCityObject::Tunnel(tunnel) = &cityobjs.first().unwrap().cityobj else { +// panic!("Not a Tunnel"); +// }; + +// assert_eq!(tunnel.class, Some(Code::new("交通".into(), "1000".into()))); +// assert_eq!( +// tunnel.function, +// vec![Code::new("道路用トンネル".into(), "1010".into())] +// ); +// assert_eq!(tunnel.year_of_construction, Some("1989".into())); +// assert_eq!( +// tunnel.outer_tunnel_installation[0].function, +// vec![Code::new("その他".into(), "90".into())] +// ); +// assert_eq!( +// tunnel.outer_tunnel_installation[0].function, +// vec![Code::new("その他".into(), "90".into())] +// ); +// } + +// #[test] +// fn load_underground_building_example() { +// let cityobjs = load_cityobjs("./tests/data/plateau-3_0/udx/ubld/51324378_ubld_6697.gml"); +// assert_eq!(cityobjs.len(), 3); +// let TopLevelCityObject::UndergroundBuilding(ubld) = &cityobjs.first().unwrap().cityobj else { +// panic!("Not a UndergroundBuilding"); +// }; +// assert_eq!(ubld.interior_room.len(), 2); +// let room = &ubld.interior_room[1]; +// assert_eq!(room.room_installation.len(), 3); +// } + +#[test] +fn load_urf_example() { + let cityobjs = load_cityobjs("./tests/data/takeo-shi/udx/urf/493060_urf_6668_op.gml"); + assert_eq!(cityobjs.len(), 140); + + let cityobjs = load_cityobjs("./tests/data/numazu-shi/udx/urf/523857_urf_6668_op.gml"); + assert_eq!(cityobjs.len(), 47); + + let cityobjs = load_cityobjs("./tests/data/tokyo23-ku/udx/urf/533957_urf_6668_op.gml"); + assert_eq!(cityobjs.len(), 38); +} + +// #[test] +// fn load_utility_network_example() { +// { +// let cityobjs = load_cityobjs("./tests/data/plateau-3_0/udx/unf/gas_53403039_unf_6697.gml"); +// assert_eq!(cityobjs.len(), 7); +// let TopLevelCityObject::OilGasChemicalsPipe(pipe) = &cityobjs[0].cityobj else { +// panic!("expected OilGasChemicalsPipe"); +// }; +// assert_eq!(pipe.function, vec![Code::new("管路".into(), "5500".into())]); +// let TopLevelCityObject::Appurtenance(appur) = &cityobjs[1].cityobj else { +// panic!("expected Appurtenance"); +// }; +// assert_eq!( +// appur.function, +// vec![Code::new("ハンドホール".into(), "5620".into())] +// ); +// let TopLevelCityObject::Handhole(hole) = &cityobjs[5].cityobj else { +// panic!("expected Handhole"); +// }; +// assert_eq!( +// hole.function, +// vec![Code::new("ハンドホール".into(), "5620".into())] +// ); +// } + +// { +// let cityobjs = load_cityobjs("./tests/data/plateau-3_0/udx/unf/elec_53403039_unf_6697.gml"); +// assert_eq!(cityobjs.len(), 2); +// let TopLevelCityObject::Duct(_duct) = &cityobjs[0].cityobj else { +// panic!("unexpected cityobj"); +// }; +// let TopLevelCityObject::ElectricityCable(_cable) = &cityobjs[1].cityobj else { +// panic!("unexpected cityobj"); +// }; +// } + +// { +// let cityobjs = +// load_cityobjs("./tests/data/plateau-3_0/udx/unf/sewer_53403039_unf_6697.gml"); +// assert_eq!(cityobjs.len(), 6); +// let TopLevelCityObject::SewerPipe(_) = &cityobjs[0].cityobj else { +// panic!("expected SewerPipe"); +// }; +// let TopLevelCityObject::Manhole(_) = &cityobjs[1].cityobj else { +// panic!("expected Manhole"); +// }; +// } + +// { +// let cityobjs = +// load_cityobjs("./tests/data/plateau-3_0/udx/unf/water_53403039_unf_6697.gml"); +// assert_eq!(cityobjs.len(), 7); +// let TopLevelCityObject::Appurtenance(_) = &cityobjs[0].cityobj else { +// panic!("expected Appurtenance"); +// }; +// let TopLevelCityObject::WaterPipe(_) = &cityobjs[1].cityobj else { +// panic!("expected WaterPipe"); +// }; +// } +// } + +// #[test] +// fn load_vegetation_example() { +// let cityobjs = load_cityobjs("./tests/data/plateau-3_0/udx/veg/52385628_veg_6697_op.gml"); +// assert_eq!(cityobjs.len(), 28); +// let TopLevelCityObject::PlantCover(veg) = &cityobjs[0].cityobj else { +// panic!("expected PlantCover"); +// }; +// assert_eq!(veg.average_height.as_ref().unwrap().value(), 0.5); +// let dq = veg.vegetation_data_quality_attribute.as_ref().unwrap(); +// assert_eq!(dq.appearance_src_desc.first().unwrap().code(), "4"); + +// let TopLevelCityObject::SolitaryVegetationObject(veg) = &cityobjs[9].cityobj else { +// panic!("expected SolitaryVegetationObject"); +// }; +// assert_eq!(veg.height.as_ref().unwrap().value(), 12.5); +// let dq = veg.vegetation_data_quality_attribute.as_ref().unwrap(); +// assert_eq!(dq.appearance_src_desc.first().unwrap().code(), "4"); +// } + +// #[test] +// fn load_waterbody_example() { +// let cityobjs = load_cityobjs("./tests/data/plateau-3_0/udx/wtr/55370156_wtr_6697.gml"); +// assert_eq!(cityobjs.len(), 1); +// let TopLevelCityObject::WaterBody(waterbody) = &cityobjs.first().unwrap().cityobj else { +// panic!("expected WaterBody"); +// }; + +// assert_eq!( +// waterbody.class, +// Some(Code::new( +// "river / stream(河川/小川)".into(), +// "1030".into() +// )) +// ); +// } + +#[test] +fn load_flood_example() { + { + let cityobjs = load_cityobjs("./tests/data/numazu-shi/udx/fld/52385721_fld_6697_l1_op.gml"); + assert_eq!(cityobjs.len(), 3); + let TopLevelCityObject::WaterBody(waterbody) = &cityobjs.first().unwrap().cityobj else { + panic!("expected SedimentDisasterProneArea"); + }; + let uro::FloodingRiskAttributeProperty::RiverFloodingRiskAttribute(flood) = + waterbody.flooding_risk_attribute.first().unwrap() + else { + panic!("expected WaterBodyRiverFloodingRiskAttribute"); + }; + assert_eq!(flood.admin_type.as_ref().unwrap().code(), "1"); + assert_eq!(flood.scale.as_ref().unwrap().code(), "L1"); + } + + { + let cityobjs = load_cityobjs("./tests/data/numazu-shi/udx/tnm/523855_tnm_6697_op.gml"); + assert_eq!(cityobjs.len(), 3); + let TopLevelCityObject::WaterBody(_waterbody) = &cityobjs.first().unwrap().cityobj else { + panic!("expected SedimentDisasterProneArea"); + }; + } +} + +#[test] +fn load_urf_kodo_example() { + let cityobjs = load_cityobjs("./tests/data/kawasaki-shi/udx/urf/533915_urf_6668_kodo_op.gml"); + assert_eq!(cityobjs.len(), 1); + let TopLevelCityObject::HeightControlDistrict(hcd) = &cityobjs.first().unwrap().cityobj else { + panic!("Not a HeightControlDistrict"); + }; + + assert_eq!( + hcd.function, + vec![Code::new("高度地区".to_string(), "18".to_string(),)] + ); + + assert_eq!( + hcd.urf_valid_from, + Option::Some(Date::from_ymd_opt(2009, 11, 11).unwrap()) + ); + + assert_eq!( + hcd.valid_from_type, + Some(Code::new("変更".to_string(), "3".to_string())) + ); +} + +#[test] +fn load_urf_kuiki_example() { + let cityobjs = load_cityobjs("./tests/data/kawasaki-shi/udx/urf/533915_urf_6668_kuiki_op.gml"); + assert_eq!(cityobjs.len(), 2); + let TopLevelCityObject::AreaClassification(acf) = &cityobjs.first().unwrap().cityobj else { + panic!("Not a AreaClassification"); + }; + + assert_eq!( + acf.function, + vec![Code::new("市街化区域".to_string(), "22".to_string(),)] + ); + + assert_eq!( + acf.urf_valid_from, + Option::Some(Date::from_ymd_opt(2009, 9, 18).unwrap()) + ); + + assert_eq!( + acf.valid_from_type, + Some(Code::new("変更".to_string(), "3".to_string())) + ); +} + +#[test] +fn load_urf_rinko_example() { + let cityobjs = load_cityobjs("./tests/data/kawasaki-shi/udx/urf/533915_urf_6668_rinko_op.gml"); + assert_eq!(cityobjs.len(), 2); + let TopLevelCityObject::PortZone(pz) = &cityobjs.first().unwrap().cityobj else { + panic!("Not a PortZone"); + }; + + assert_eq!( + pz.function, + vec![Code::new("臨港地区".to_string(), "30".to_string(),)] + ); + + assert_eq!( + pz.urf_valid_from, + Option::Some(Date::from_ymd_opt(2009, 9, 18).unwrap()) + ); + + assert_eq!( + pz.valid_from_type, + Some(Code::new("変更".to_string(), "3".to_string())) + ); +} + +#[test] +fn load_urf_yoto_example() { + let cityobjs = load_cityobjs("./tests/data/kawasaki-shi/udx/urf/533915_urf_6668_yoto_op.gml"); + assert_eq!(cityobjs.len(), 4); + let TopLevelCityObject::UseDistrict(ud) = &cityobjs.first().unwrap().cityobj else { + panic!("Not a UseDistrict"); + }; + + assert_eq!( + ud.function, + vec![Code::new("工業専用地域".to_string(), "13".to_string(),)] + ); + + assert_eq!( + ud.urf_valid_from, + Option::Some(Date::from_ymd_opt(2009, 11, 11).unwrap()) + ); + + assert_eq!( + ud.valid_from_type, + Some(Code::new("変更".to_string(), "3".to_string())) + ); +} + +#[test] +fn load_urf_boka_example() { + let cityobjs = load_cityobjs("./tests/data/kawasaki-shi/udx/urf/533916_urf_6668_boka_op.gml"); + assert_eq!(cityobjs.len(), 2); + let TopLevelCityObject::FirePreventionDistrict(fpd) = &cityobjs.first().unwrap().cityobj else { + panic!("Not a FirePreventionDistrict"); + }; + + assert_eq!( + fpd.function, + vec![Code::new("準防火地域".to_string(), "25".to_string(),)] + ); + + assert_eq!( + fpd.urf_valid_from, + Option::Some(Date::from_ymd_opt(2009, 9, 18).unwrap()) + ); + + assert_eq!( + fpd.valid_from_type, + Some(Code::new("変更".to_string(), "3".to_string())) + ); +} + +#[test] +fn load_urf_seisan_example() { + let cityobjs = load_cityobjs("./tests/data/kawasaki-shi/udx/urf/533923_urf_6668_seisan_op.gml"); + assert_eq!(cityobjs.len(), 27); + let TopLevelCityObject::ProductiveGreenZone(pgz) = &cityobjs.first().unwrap().cityobj else { + panic!("Not a ProductiveGreenZone"); + }; + + assert_eq!( + pgz.function, + vec![Code::new("生産緑地地区".to_string(), "38".to_string(),)] + ); + + assert_eq!( + pgz.urf_valid_from, + Option::Some(Date::from_ymd_opt(2010, 12, 21).unwrap()) + ); + + assert_eq!( + pgz.valid_from_type, + Some(Code::new("変更".to_string(), "3".to_string())) + ); +} + +#[test] +fn load_urf_tokuryoku_example() { + let cityobjs = + load_cityobjs("./tests/data/kawasaki-shi/udx/urf/533923_urf_6668_tokuryoku_op.gml"); + assert_eq!(cityobjs.len(), 7); + let TopLevelCityObject::SpecialGreenSpaceConservationDistrict(sgcd) = + &cityobjs.first().unwrap().cityobj + else { + panic!("Not a SpecialGreenSpaceConservationDistrict"); + }; + + assert_eq!( + sgcd.function, + vec![Code::new("特別緑地保存地区".to_string(), "35".to_string(),)] + ); + + assert_eq!( + sgcd.urf_valid_from, + Option::Some(Date::from_ymd_opt(2022, 4, 7).unwrap()) + ); + + assert_eq!( + sgcd.valid_from_type, + Some(Code::new("変更".to_string(), "3".to_string())) + ); +} +#[test] +fn load_urf_chusha_example() { + let cityobjs = load_cityobjs("./tests/data/kawasaki-shi/udx/urf/533925_urf_6668_chusha_op.gml"); + assert_eq!(cityobjs.len(), 1); + let TopLevelCityObject::ParkingPlaceDevelopmentZone(ppdz) = &cityobjs.first().unwrap().cityobj + else { + panic!("Not a ParkingPlaceDevelopmentZone"); + }; + + assert_eq!( + ppdz.function, + vec![Code::new("駐車場整備地区".to_string(), "29".to_string(),)] + ); + + assert_eq!( + ppdz.urf_valid_from, + Option::Some(Date::from_ymd_opt(1969, 3, 13).unwrap()) + ); + + assert_eq!( + ppdz.valid_from_type, + Some(Code::new("決定".to_string(), "1".to_string())) + ); +} + +#[test] +fn load_urf_tokuyoto_example() { + let cityobjs = + load_cityobjs("./tests/data/kawasaki-shi/udx/urf/533925_urf_6668_tokuyoto_op.gml"); + assert_eq!(cityobjs.len(), 4); + let TopLevelCityObject::SpecialUseDistrict(sud) = &cityobjs.first().unwrap().cityobj else { + panic!("Not a SpecialUseDistrict"); + }; + + assert_eq!( + sud.function, + vec![Code::new("特別用途地区".to_string(), "14".to_string(),)] + ); + + assert_eq!( + sud.urf_valid_from, + Option::Some(Date::from_ymd_opt(2005, 10, 7).unwrap()) + ); + + assert_eq!( + sud.valid_from_type, + Some(Code::new("変更".to_string(), "3".to_string())) + ); +} + +#[test] +fn load_urf_toshikeikaku_example() { + let cityobjs = load_cityobjs("./tests/data/sapporo-shi/udx/urf/644131_urf_6668_op.gml"); + assert_eq!(cityobjs.len(), 12); + let TopLevelCityObject::UrbanPlanningArea(upa) = &cityobjs.first().unwrap().cityobj else { + panic!("Not a UrbanPlanningArea"); + }; + + assert_eq!( + upa.function, + vec![Code::new("都市計画区域".to_string(), "21".to_string(),)] + ); + + assert_eq!( + upa.urf_valid_from, + Option::Some(Date::from_ymd_opt(2022, 3, 23).unwrap()) + ); + + assert_eq!( + upa.valid_from_type, + Some(Code::new("変更".to_string(), "3".to_string())) + ); +} + +#[test] +fn load_urf_huchi_example() { + let cityobjs = load_cityobjs("./tests/data/sendai-shi/udx/urf/574026_urf_6668_huchi_op.gml"); + assert_eq!(cityobjs.len(), 1); + let TopLevelCityObject::ScenicDistrict(sd) = &cityobjs.first().unwrap().cityobj else { + panic!("Not a ScenicDistrict"); + }; + + assert_eq!( + sd.function, + vec![Code::new("風致地区".to_string(), "28".to_string(),)] + ); + + assert_eq!( + sd.prefecture, + Some(Code::new("宮城県".to_string(), "04".to_string())) + ); + + assert_eq!( + sd.city, + Some(Code::new("宮城県仙台市".to_string(), "04100".to_string())) + ); +} + +#[test] +fn load_urf_kodoriyou_example() { + let cityobjs = + load_cityobjs("./tests/data/sendai-shi/udx/urf/574027_urf_6668_kodoriyou_op.gml"); + assert_eq!(cityobjs.len(), 3); + let TopLevelCityObject::HighLevelUseDistrict(hlud) = &cityobjs.first().unwrap().cityobj else { + panic!("Not a HighLevelUseDistrict"); + }; + + assert_eq!( + hlud.function, + vec![Code::new("高度利用地区".to_string(), "19".to_string(),)] + ); + + assert_eq!( + hlud.urf_valid_from, + Option::Some(Date::from_ymd_opt(2013, 3, 8).unwrap()) + ); + + assert_eq!( + hlud.valid_from_type, + Some(Code::new("変更".to_string(), "3".to_string())) + ); + + assert_eq!(hlud.custodian, Some("仙台市".to_string())); + + assert_eq!( + hlud.prefecture, + Some(Code::new("宮城県".to_string(), "04".to_string())) + ); + + assert_eq!( + hlud.city, + Some(Code::new("宮城県仙台市".to_string(), "04100".to_string())) + ); +} + +#[test] +fn load_urf_keikan_example() { + let cityobjs = load_cityobjs("./tests/data/sendai-shi/udx/urf/574036_urf_6668_keikan_op.gml"); + assert_eq!(cityobjs.len(), 2); + let TopLevelCityObject::LandscapeZone(lz) = &cityobjs.first().unwrap().cityobj else { + panic!("Not a LandscapeZone"); + }; + + assert_eq!( + lz.function, + vec![Code::new("景観地区".to_string(), "27".to_string(),)] + ); + + assert_eq!( + lz.urf_valid_from, + Option::Some(Date::from_ymd_opt(2011, 12, 16).unwrap()) + ); + + assert_eq!( + lz.valid_from_type, + Some(Code::new("変更".to_string(), "3".to_string())) + ); + + assert_eq!(lz.custodian, Some("仙台市".to_string())); + + assert_eq!( + lz.prefecture, + Some(Code::new("宮城県".to_string(), "04".to_string())) + ); + + assert_eq!( + lz.city, + Some(Code::new("宮城県仙台市".to_string(), "04100".to_string())) + ); +} + +#[test] +fn load_urf_tosisai_example() { + let cityobjs = load_cityobjs("./tests/data/sendai-shi/udx/urf/574036_urf_6668_tosisai_op.gml"); + assert_eq!(cityobjs.len(), 1); + let TopLevelCityObject::SpecialUrbanRenaissanceDistrict(surd) = + &cityobjs.first().unwrap().cityobj + else { + panic!("Not a SpecialUrbanRenaissanceDistrict"); + }; + + assert_eq!( + surd.function, + vec![Code::new("都市再生特別地区".to_string(), "21".to_string(),)] + ); + + assert_eq!( + surd.urf_valid_from, + Option::Some(Date::from_ymd_opt(2020, 9, 16).unwrap()) + ); + + assert_eq!( + surd.valid_from_type, + Some(Code::new("変更".to_string(), "3".to_string())) + ); + + assert_eq!(surd.custodian, Some("仙台市".to_string())); + + assert_eq!( + surd.prefecture, + Some(Code::new("宮城県".to_string(), "04".to_string())) + ); + + assert_eq!( + surd.city, + Some(Code::new("宮城県仙台市".to_string(), "04100".to_string())) + ); +} + +#[test] +fn load_urf_sigaidev_example() { + let cityobjs = load_cityobjs("./tests/data/kofu-shi/udx/urf/533834_urf_6668_sigaidev_op.gml"); + assert_eq!(cityobjs.len(), 27); + let TopLevelCityObject::UrbanDevelopmentProject(udp) = &cityobjs.first().unwrap().cityobj + else { + panic!("Not a UrbanDevelopmentProject"); + }; + + assert_eq!( + udp.function, + vec![Code::new("工業地域".to_string(), "12".to_string(),)] + ); + + assert_eq!( + udp.urf_valid_from, + Option::Some(Date::from_ymd_opt(1, 1, 1).unwrap()) + ); + + assert_eq!( + udp.valid_from_type, + Some(Code::new("不明".to_string(), "4".to_string())) + ); + + assert_eq!( + udp.prefecture, + Some(Code::new("山梨県".to_string(), "19".to_string())) + ); + + assert_eq!( + udp.city, + Some(Code::new("山梨県甲府市".to_string(), "19201".to_string())) + ); +} diff --git a/nusamai-projection/Cargo.toml b/nusamai-projection/Cargo.toml index 9e5bed6af..fc03e57f6 100644 --- a/nusamai-projection/Cargo.toml +++ b/nusamai-projection/Cargo.toml @@ -1,13 +1,8 @@ [package] +edition = "2021" name = "nusamai-projection" - -authors.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true -version.workspace = true +version = "0.1.0" [dependencies] japan-geoid = "0.4.0" -thiserror.workspace = true +thiserror = "1.0.58" diff --git a/nusamai-projection/src/cartesian.rs b/nusamai-projection/src/cartesian.rs index 19ad56429..622269106 100644 --- a/nusamai-projection/src/cartesian.rs +++ b/nusamai-projection/src/cartesian.rs @@ -68,6 +68,99 @@ pub fn geocentric_to_geodetic(ellips: &Ellipsoid, x: f64, y: f64, z: f64) -> (f6 (lam.to_degrees(), phi.to_degrees(), h) } +// /// Convert from geocentric to geodetic coordinate system. +// pub fn geocentric_to_geodetic(ellips: &Ellipsoid, x: f64, y: f64, z: f64) -> (f64, f64, f64) { +// // Ported from OJ +// +// let (lam, p_div_a, z_div_a) = { +// let ra = 1. / ellips.a(); +// let x_div_a = x * ra; +// let y_div_a = y * ra; +// let z_div_a = z * ra; +// let lam = f64::atan2(y_div_a, x_div_a); +// +// // Perpendicular distance from point to Z-axis (HM eq. 5-28) +// let p_div_a = (x_div_a * x_div_a + y_div_a * y_div_a).sqrt(); +// +// (lam, p_div_a, z_div_a) +// }; +// +// // Non-optimized version: +// // theta = atan2(z * ellips.a(), p * ellips.b()); +// // c = cos(theta); +// // s = sin(theta); +// let b_div_a = 1. - ellips.f(); +// let (c, s) = { +// let p_div_a_b_div_a = p_div_a * b_div_a; +// let norm = (z_div_a * z_div_a + p_div_a_b_div_a * p_div_a_b_div_a).sqrt(); +// if norm != 0.0 { +// let inv_norm = 1.0 / norm; +// let c = p_div_a_b_div_a * inv_norm; +// let s = z_div_a * inv_norm; +// (c, s) +// } else { +// (1., 0.) +// } +// }; +// +// let e2s = ellips.e_sq() / (1.0 - ellips.e_sq()); +// let y_phi = z_div_a + e2s * b_div_a * s * s * s; +// let x_phi = p_div_a - ellips.e_sq() * c * c * c; +// let norm_phi = (y_phi * y_phi + x_phi * x_phi).sqrt(); +// +// let (mut cosphi, mut sinphi) = if norm_phi != 0. { +// let inv_norm_phi = 1.0 / norm_phi; +// (x_phi * inv_norm_phi, y_phi * inv_norm_phi) +// } else { +// (1., 0.) +// }; +// let phi = if x_phi <= 0. { +// // this happen on non-sphere ellipsoid when x,y,z is very close to 0 +// // there is no single solution to the cart->geodetic conversion in +// // that case, clamp to -90/90 deg and avoid a discontinuous boundary +// // near the poles +// cosphi = 0.; +// sinphi = if z >= 0. { 1. } else { -1. }; +// if z >= 0. { +// FRAC_2_PI +// } else { +// -FRAC_2_PI +// } +// } else { +// f64::atan2(y_phi, x_phi) +// }; +// +// fn geocentric_radius(a: f64, b_div_a: f64, cosphi: f64, sinphi: f64) -> f64 { +// // Non-optimized version: +// // const double b = a * b_div_a; +// // return hypot(a * a * cosphi, b * b * sinphi) / +// // hypot(a * cosphi, b * sinphi); +// let cosphi_sq = cosphi * cosphi; +// let sinphi_sq = sinphi * sinphi; +// let b_div_a_sq = b_div_a * b_div_a; +// let b_div_a_sq_mul_sinphi_sq = b_div_a_sq * sinphi_sq; +// a * ((cosphi_sq + b_div_a_sq * b_div_a_sq_mul_sinphi_sq) +// / (cosphi_sq + b_div_a_sq_mul_sinphi_sq)) +// .sqrt() +// } +// +// let height = if cosphi < 1e-6 { +// // poleward of 89.99994 deg, we avoid division by zero +// // by computing the height as the cartesian z value +// // minus the geocentric radius of the Earth at the given latitude +// z.abs() - geocentric_radius(ellips.a(), b_div_a, cosphi, sinphi) +// } else { +// let n = if ellips.e_sq() == 0.0 { +// ellips.a() // optimization for sphere (e=0) +// } else { +// ellips.a() / (1. - ellips.e_sq() * sinphi * sinphi).sqrt() +// }; +// ellips.a() * p_div_a / cosphi - n +// }; +// +// (lam.to_degrees(), phi.to_degrees(), height) +// } + #[cfg(test)] mod tests { use super::*; diff --git a/nusamai-projection/src/crs.rs b/nusamai-projection/src/crs.rs index 056dba0a5..e067f1f1a 100644 --- a/nusamai-projection/src/crs.rs +++ b/nusamai-projection/src/crs.rs @@ -4,6 +4,9 @@ pub const EPSG_WGS84_GEOGRAPHIC_2D: EpsgCode = 4326; pub const EPSG_WGS84_GEOGRAPHIC_3D: EpsgCode = 4979; pub const EPSG_WGS84_GEOCENTRIC: EpsgCode = 4978; +// Web Mercator +pub const EPSG_WEB_MERCATOR: EpsgCode = 3857; + /// JGD2011 pub const EPSG_JGD2011_GEOGRAPHIC_2D: EpsgCode = 6668; diff --git a/nusamai-projection/src/vshift.rs b/nusamai-projection/src/vshift.rs index 83413261d..208546cf0 100644 --- a/nusamai-projection/src/vshift.rs +++ b/nusamai-projection/src/vshift.rs @@ -32,7 +32,6 @@ impl Clone for Jgd2011ToWgs84 { Self::new() } } - #[cfg(test)] mod tests { use super::*; diff --git a/nusamai-shapefile/Cargo.toml b/nusamai-shapefile/Cargo.toml index f8fc1e178..c76126870 100644 --- a/nusamai-shapefile/Cargo.toml +++ b/nusamai-shapefile/Cargo.toml @@ -1,13 +1,8 @@ [package] +edition = "2021" name = "nusamai-shapefile" - -authors.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true -version.workspace = true +version = "0.1.0" [dependencies] -nusamai-geometry = { path = "../nusamai-geometry" } +flatgeom = "0.0.2" shapefile = "0.6.0" diff --git a/nusamai-shapefile/src/conversion.rs b/nusamai-shapefile/src/conversion.rs index 1e32b31a5..ecb91deef 100644 --- a/nusamai-shapefile/src/conversion.rs +++ b/nusamai-shapefile/src/conversion.rs @@ -1,10 +1,10 @@ -use nusamai_geometry::{ +use flatgeom::{ Coord, MultiLineString, MultiLineString3, MultiPoint, MultiPoint3, MultiPolygon, MultiPolygon3, Polygon, Polygon3, }; use shapefile::NO_DATA; -/// Create a Shapefile Polygon from `nusamai_geometry::Polygon`. +/// Create a Shapefile Polygon from `flatgeom::Polygon`. pub fn polygon_to_shape(poly: &Polygon3) -> shapefile::PolygonZ { polygon_to_shape_with_mapping(poly, |c| c) } @@ -17,7 +17,7 @@ pub fn indexed_polygon_to_shape( polygon_to_shape_with_mapping(poly_idx, |idx| vertices[idx as usize]) } -/// Create a Shapefile Polygon from `nusamai_geometry::Polygon` with a mapping function. +/// Create a Shapefile Polygon from `flatgeom::Polygon` with a mapping function. pub fn polygon_to_shape_with_mapping( poly: &Polygon, mapping: impl Fn(T) -> [f64; 3], @@ -26,7 +26,7 @@ pub fn polygon_to_shape_with_mapping( shapefile::PolygonZ::with_rings(all_rings) } -/// Create a Shapefile Polygon from `nusamai_geometry::MultiPolygon`. +/// Create a Shapefile Polygon from `flatgeom::MultiPolygon`. pub fn multipolygon_to_shape(mpoly: &MultiPolygon3) -> shapefile::PolygonZ { multipolygon_to_shape_with_mapping(mpoly, |c| c) } @@ -39,7 +39,7 @@ pub fn indexed_multipolygon_to_shape( multipolygon_to_shape_with_mapping(mpoly_idx, |idx| vertices[idx as usize]) } -/// Create a Shapefile Polygon from `nusamai_geometry::MultiPolygon` with a mapping function. +/// Create a Shapefile Polygon from `flatgeom::MultiPolygon` with a mapping function. pub fn multipolygon_to_shape_with_mapping( mpoly: &MultiPolygon, mapping: impl Fn(T) -> [f64; 3], @@ -80,7 +80,7 @@ fn polygon_to_shape_rings_with_mapping( all_rings } -/// Create a Shapefile PolylineZ from `nusamai_geometry::MultiLineString`. +/// Create a Shapefile PolylineZ from `flatgeom::MultiLineString`. pub fn multilinestring_to_shape(mls: &MultiLineString3) -> shapefile::PolylineZ { multilinestring_to_shape_with_mapping(mls, |c| c) } @@ -93,7 +93,7 @@ pub fn indexed_multilinestring_to_shape( multilinestring_to_shape_with_mapping(mls_idx, |idx| vertices[idx as usize]) } -/// Create a Shapefile PolylineZ from `nusamai_geometry::MultiLineString` with a mapping function. +/// Create a Shapefile PolylineZ from `flatgeom::MultiLineString` with a mapping function. pub fn multilinestring_to_shape_with_mapping( mls: &MultiLineString, mapping: impl Fn(T) -> [f64; 3], @@ -111,7 +111,7 @@ pub fn multilinestring_to_shape_with_mapping( shapefile::PolylineZ::with_parts(parts) } -/// Create a Shapefile MultiPointZ from `nusamai_geometry::MultiPoint`. +/// Create a Shapefile MultiPointZ from `flatgeom::MultiPoint`. pub fn multipoint_to_shape(mpoint: &MultiPoint3) -> shapefile::MultipointZ { multipoint_to_shape_with_mapping(mpoint, |c| c) } @@ -124,7 +124,7 @@ pub fn indexed_multipoint_to_shape( multipoint_to_shape_with_mapping(mpoint_idx, |idx| vertices[idx as usize]) } -/// Create a Shapefile MultiPointZ from `nusamai_geometry::MultiPoint` with a mapping function. +/// Create a Shapefile MultiPointZ from `flatgeom::MultiPoint` with a mapping function. pub fn multipoint_to_shape_with_mapping( mpoint: &MultiPoint, mapping: impl Fn(T) -> [f64; 3],