diff --git a/nusamai-citygml/src/geometry.rs b/nusamai-citygml/src/geometry.rs index 6cb0c7b12..853ba9916 100644 --- a/nusamai-citygml/src/geometry.rs +++ b/nusamai-citygml/src/geometry.rs @@ -3,6 +3,9 @@ use nusamai_projection::crs::*; use crate::LocalId; +/// 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, @@ -84,6 +87,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>, @@ -122,7 +126,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([ @@ -132,8 +136,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/parser.rs b/nusamai-citygml/src/parser.rs index ae29adb7d..00a11edab 100644 --- a/nusamai-citygml/src/parser.rs +++ b/nusamai-citygml/src/parser.rs @@ -319,9 +319,9 @@ impl<'b, R: BufRead> SubTreeReader<'_, 'b, R> { self.state.context.id_to_integer_id(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) } /// Expect a geometric attribute of CityGML @@ -519,14 +519,18 @@ 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(self.state.context.id_to_integer_id(id)); - break; + } + if localname.as_ref() == b"srsName" { + geometry_crs_uri = + Some(String::from_utf8_lossy(attr.value.as_ref()).to_string()); } } @@ -595,6 +599,8 @@ impl<'b, R: BufRead> SubTreeReader<'_, 'b, R> { end: poly_end as u32, }); } + + self.state.geometry_collector.geometry_crs_uri = geometry_crs_uri; } } Ok(Event::End(_)) => break, diff --git a/nusamai-citygml/src/values.rs b/nusamai-citygml/src/values.rs index acc8f135e..c8c7cedba 100644 --- a/nusamai-citygml/src/values.rs +++ b/nusamai-citygml/src/values.rs @@ -630,13 +630,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| { match st.current_path() { diff --git a/nusamai-plateau/examples/parse_and_compress.rs b/nusamai-plateau/examples/parse_and_compress.rs index 8fe83f901..717c0bcd8 100644 --- a/nusamai-plateau/examples/parse_and_compress.rs +++ b/nusamai-plateau/examples/parse_and_compress.rs @@ -21,7 +21,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/tests/common/mod.rs b/nusamai-plateau/tests/common/mod.rs index 7b556f632..30d860adc 100644 --- a/nusamai-plateau/tests/common/mod.rs +++ b/nusamai-plateau/tests/common/mod.rs @@ -18,7 +18,7 @@ fn toplevel_dispatcher( b"core:cityObjectMember" => { let mut cityobj: TopLevelCityObject = Default::default(); cityobj.parse(st)?; - let geometries = st.collect_geometries(); + let geometries = st.collect_geometries(None); cityobjs.push(CityObject { cityobj, geometries, diff --git a/nusamai/src/sink/obj/obj_writer.rs b/nusamai/src/sink/obj/obj_writer.rs index eb878d7e3..efe6c8e0f 100644 --- a/nusamai/src/sink/obj/obj_writer.rs +++ b/nusamai/src/sink/obj/obj_writer.rs @@ -47,7 +47,6 @@ fn write_obj( mesh_data.push((feature_id, mesh, vertex_offset, uv_offset)); } - let mut obj_writer = BufWriter::new(File::create(obj_path)?); writeln!(obj_writer, "mtllib {}.mtl", file_name)?; diff --git a/nusamai/src/source/citygml.rs b/nusamai/src/source/citygml.rs index c8537394a..c20021cb2 100644 --- a/nusamai/src/source/citygml.rs +++ b/nusamai/src/source/citygml.rs @@ -88,6 +88,7 @@ fn toplevel_dispatcher( ) -> Result<(), ParseError> { let mut entities = Vec::new(); let mut global_appearances = AppearanceStore::default(); + let mut envelope = Envelope::default(); st.parse_children(|st| { if feedback.is_canceled() { @@ -100,14 +101,13 @@ fn toplevel_dispatcher( Ok(()) } b"gml:boundedBy/gml:Envelope" => { - let mut envelope = Envelope::default(); envelope.parse(st)?; Ok(()) } b"core:cityObjectMember" => { let mut cityobj: models::TopLevelCityObject = Default::default(); cityobj.parse(st)?; - let geometry_store = st.collect_geometries(); + let geometry_store = st.collect_geometries(envelope.crs_uri.clone()); if let Some(root) = cityobj.into_object() { let entity = Entity { diff --git a/nusamai/src/transformer/transform/projection.rs b/nusamai/src/transformer/transform/projection.rs index dc00d1d77..0b45b4e77 100644 --- a/nusamai/src/transformer/transform/projection.rs +++ b/nusamai/src/transformer/transform/projection.rs @@ -24,140 +24,23 @@ impl Transform for ProjectionTransform { }; match input_epsg { - EPSG_JGD2011_GEOGRAPHIC_3D => match self.output_epsg { - EPSG_JGD2011_GEOGRAPHIC_3D => { - let mut geom_store = entity.geometry_store.write().unwrap(); - geom_store.vertices.iter_mut().for_each(|v| { - // Swap x and y (lat, lng -> lng, lat) - (v[0], v[1], v[2]) = (v[1], v[0], v[2]); - }); - geom_store.epsg = self.output_epsg; - } - EPSG_WGS84_GEOGRAPHIC_3D => { - let mut geom_store = entity.geometry_store.write().unwrap(); - geom_store.vertices.iter_mut().for_each(|v| { - // Swap x and y (lat, lng -> lng, lat) - let (lng, lat, height) = (v[1], v[0], v[2]); - // JGD2011 to WGS 84 (elevation to ellipsoidal height) - (v[0], v[1], v[2]) = self.jgd2wgs.convert(lng, lat, height); - }); - geom_store.epsg = self.output_epsg; - } - EPSG_WEB_MERCATOR => { - let mut geom_store = entity.geometry_store.write().unwrap(); - geom_store.vertices.iter_mut().for_each(|v| { - // Swap x and y (lat, lng -> lng, lat) - let (lng, lat) = (v[1], v[0]); - // LngLat to Web Mercator - (v[0], v[1]) = - nusamai_mvt::webmercator::lnglat_to_web_mercator_meters(lng, lat) - }); - geom_store.epsg = self.output_epsg; - } - EPSG_JGD2011_JPRECT_I_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_II_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_III_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_IV_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_V_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_VI_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_VII_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_VIII_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_IX_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_X_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_XI_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_XII_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_XIII_JGD2011_HEIGHT => { - // To Japan Plane Rectangular CS + JGD2011 (vertical) height - let proj = self.jpr_zone_proj.as_ref().unwrap(); - let mut geom_store = entity.geometry_store.write().unwrap(); - geom_store.vertices.iter_mut().for_each(|v| { - let (lng, lat) = (v[1], v[0]); - // Change x and y; keep the height - // TODO: error handling - (v[0], v[1], _) = proj.project_forward(lng, lat, 0.).unwrap(); - }); - geom_store.epsg = self.output_epsg; - } - EPSG_JGD2011_JPRECT_I - | EPSG_JGD2011_JPRECT_II - | EPSG_JGD2011_JPRECT_III - | EPSG_JGD2011_JPRECT_IV - | EPSG_JGD2011_JPRECT_V - | EPSG_JGD2011_JPRECT_VI - | EPSG_JGD2011_JPRECT_VII - | EPSG_JGD2011_JPRECT_VIII - | EPSG_JGD2011_JPRECT_IX - | EPSG_JGD2011_JPRECT_X - | EPSG_JGD2011_JPRECT_XI - | EPSG_JGD2011_JPRECT_XII - | EPSG_JGD2011_JPRECT_XIII - | EPSG_JGD2011_JPRECT_XIV - | EPSG_JGD2011_JPRECT_XV - | EPSG_JGD2011_JPRECT_XVI - | EPSG_JGD2011_JPRECT_XVII - | EPSG_JGD2011_JPRECT_XVIII - | EPSG_JGD2011_JPRECT_XIX => { - // To Japan Plane Rectangular CS - let proj = self.jpr_zone_proj.as_ref().unwrap(); - let mut geom_store = entity.geometry_store.write().unwrap(); - geom_store.vertices.iter_mut().for_each(|v| { - let (lng, lat) = (v[1], v[0]); - // Change x and y; keep the height - // TODO: error handling - (v[0], v[1], _) = proj.project_forward(lng, lat, 0.).unwrap(); - }); - geom_store.epsg = self.output_epsg; - } - _ => { - panic!("Unsupported output CRS: {}", self.output_epsg); - } - }, - EPSG_WGS84_GEOGRAPHIC_3D => match self.output_epsg { - EPSG_WGS84_GEOGRAPHIC_3D => { - // Do nothing - } - EPSG_JGD2011_JPRECT_I_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_II_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_III_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_IV_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_V_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_VI_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_VII_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_VIII_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_IX_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_X_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_XI_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_XII_JGD2011_HEIGHT - | EPSG_JGD2011_JPRECT_XIII_JGD2011_HEIGHT => { - // TODO: implement - unimplemented!("WGS84 to EPSG:{} not supported yet", self.output_epsg); - } - EPSG_JGD2011_JPRECT_I - | EPSG_JGD2011_JPRECT_II - | EPSG_JGD2011_JPRECT_III - | EPSG_JGD2011_JPRECT_IV - | EPSG_JGD2011_JPRECT_V - | EPSG_JGD2011_JPRECT_VI - | EPSG_JGD2011_JPRECT_VII - | EPSG_JGD2011_JPRECT_VIII - | EPSG_JGD2011_JPRECT_IX - | EPSG_JGD2011_JPRECT_X - | EPSG_JGD2011_JPRECT_XI - | EPSG_JGD2011_JPRECT_XII - | EPSG_JGD2011_JPRECT_XIII - | EPSG_JGD2011_JPRECT_XIV - | EPSG_JGD2011_JPRECT_XV - | EPSG_JGD2011_JPRECT_XVI - | EPSG_JGD2011_JPRECT_XVII - | EPSG_JGD2011_JPRECT_XVIII - | EPSG_JGD2011_JPRECT_XIX => { - // TODO: implement - unimplemented!("WGS84 to EPSG:{} not supported yet", self.output_epsg); - } - _ => { - panic!("Unsupported output CRS: {}", self.output_epsg); - } - }, + EPSG_JGD2011_GEOGRAPHIC_3D => self.transform_from_jgd2011(&entity, None), + EPSG_WGS84_GEOGRAPHIC_3D => self.transform_from_wgs84(&entity, None), + EPSG_JGD2011_JPRECT_I_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_II_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_III_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_IV_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_V_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_VI_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_VII_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_VIII_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_IX_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_X_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_XI_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_XII_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_XIII_JGD2011_HEIGHT => { + self.transform_from_jgd2011(&entity, Some(input_epsg)); + } _ => { panic!("Unsupported input CRS: {}", input_epsg); } @@ -182,4 +65,170 @@ impl ProjectionTransform { jpr_zone_proj, } } + + fn rectangular_to_lnglat(x: f64, y: f64, height: f64, input_epsg: EpsgCode) -> (f64, f64, f64) { + let zone = JPRZone::from_epsg(input_epsg).unwrap(); + let proj = zone.projection(); + let (lng, lat, height) = proj.project_inverse(x, y, height).unwrap(); + (lng, lat, height) + } + + fn transform_from_jgd2011(&mut self, entity: &Entity, rectangular: Option) { + match self.output_epsg { + EPSG_JGD2011_GEOGRAPHIC_3D => { + let mut geom_store = entity.geometry_store.write().unwrap(); + geom_store.vertices.iter_mut().for_each(|v| { + // Swap x and y (lat, lng -> lng, lat) + (v[0], v[1], v[2]) = (v[1], v[0], v[2]); + if let Some(input_epsg) = rectangular { + (v[0], v[1], v[2]) = + Self::rectangular_to_lnglat(v[0], v[1], v[2], input_epsg); + }; + }); + geom_store.epsg = self.output_epsg; + } + EPSG_WGS84_GEOGRAPHIC_3D => { + let mut geom_store = entity.geometry_store.write().unwrap(); + geom_store.vertices.iter_mut().for_each(|v| { + // Swap x and y (lat, lng -> lng, lat) + (v[0], v[1], v[2]) = (v[1], v[0], v[2]); + if let Some(input_epsg) = rectangular { + (v[0], v[1], v[2]) = + Self::rectangular_to_lnglat(v[0], v[1], v[2], input_epsg); + }; + // JGD2011 to WGS 84 (elevation to ellipsoidal height) + (v[0], v[1], v[2]) = self.jgd2wgs.convert(v[0], v[1], v[2]); + }); + geom_store.epsg = self.output_epsg; + } + EPSG_WEB_MERCATOR => { + let mut geom_store = entity.geometry_store.write().unwrap(); + geom_store.vertices.iter_mut().for_each(|v| { + // Swap x and y (lat, lng -> lng, lat) + let (lng, lat) = (v[1], v[0]); + if let Some(input_epsg) = rectangular { + (v[0], v[1], v[2]) = + Self::rectangular_to_lnglat(v[0], v[1], v[2], input_epsg); + }; + // LngLat to Web Mercator + (v[0], v[1]) = nusamai_mvt::webmercator::lnglat_to_web_mercator_meters(lng, lat) + }); + geom_store.epsg = self.output_epsg; + } + EPSG_JGD2011_JPRECT_I_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_II_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_III_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_IV_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_V_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_VI_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_VII_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_VIII_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_IX_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_X_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_XI_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_XII_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_XIII_JGD2011_HEIGHT => { + // To Japan Plane Rectangular CS + JGD2011 (vertical) height + let proj = self.jpr_zone_proj.as_ref().unwrap(); + let mut geom_store = entity.geometry_store.write().unwrap(); + geom_store.vertices.iter_mut().for_each(|v| { + let (lng, lat) = (v[1], v[0]); + if let Some(input_epsg) = rectangular { + (v[0], v[1], v[2]) = + Self::rectangular_to_lnglat(v[0], v[1], v[2], input_epsg); + }; + // Change x and y; keep the height + // TODO: error handling + (v[0], v[1], _) = proj.project_forward(lng, lat, 0.).unwrap(); + }); + geom_store.epsg = self.output_epsg; + } + EPSG_JGD2011_JPRECT_I + | EPSG_JGD2011_JPRECT_II + | EPSG_JGD2011_JPRECT_III + | EPSG_JGD2011_JPRECT_IV + | EPSG_JGD2011_JPRECT_V + | EPSG_JGD2011_JPRECT_VI + | EPSG_JGD2011_JPRECT_VII + | EPSG_JGD2011_JPRECT_VIII + | EPSG_JGD2011_JPRECT_IX + | EPSG_JGD2011_JPRECT_X + | EPSG_JGD2011_JPRECT_XI + | EPSG_JGD2011_JPRECT_XII + | EPSG_JGD2011_JPRECT_XIII + | EPSG_JGD2011_JPRECT_XIV + | EPSG_JGD2011_JPRECT_XV + | EPSG_JGD2011_JPRECT_XVI + | EPSG_JGD2011_JPRECT_XVII + | EPSG_JGD2011_JPRECT_XVIII + | EPSG_JGD2011_JPRECT_XIX => { + // To Japan Plane Rectangular CS + let proj = self.jpr_zone_proj.as_ref().unwrap(); + let mut geom_store = entity.geometry_store.write().unwrap(); + geom_store.vertices.iter_mut().for_each(|v| { + let (lng, lat) = (v[1], v[0]); + if let Some(input_epsg) = rectangular { + (v[0], v[1], v[2]) = + Self::rectangular_to_lnglat(v[0], v[1], v[2], input_epsg); + }; + // Change x and y; keep the height + // TODO: error handling + (v[0], v[1], _) = proj.project_forward(lng, lat, 0.).unwrap(); + }); + geom_store.epsg = self.output_epsg; + } + _ => { + panic!("Unsupported output CRS: {}", self.output_epsg); + } + }; + } + + fn transform_from_wgs84(&mut self, _entity: &Entity, _rectangular: Option) { + match self.output_epsg { + EPSG_WGS84_GEOGRAPHIC_3D => { + // Do nothing + } + EPSG_JGD2011_JPRECT_I_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_II_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_III_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_IV_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_V_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_VI_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_VII_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_VIII_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_IX_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_X_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_XI_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_XII_JGD2011_HEIGHT + | EPSG_JGD2011_JPRECT_XIII_JGD2011_HEIGHT => { + // TODO: implement + unimplemented!("WGS84 to EPSG:{} not supported yet", self.output_epsg); + } + EPSG_JGD2011_JPRECT_I + | EPSG_JGD2011_JPRECT_II + | EPSG_JGD2011_JPRECT_III + | EPSG_JGD2011_JPRECT_IV + | EPSG_JGD2011_JPRECT_V + | EPSG_JGD2011_JPRECT_VI + | EPSG_JGD2011_JPRECT_VII + | EPSG_JGD2011_JPRECT_VIII + | EPSG_JGD2011_JPRECT_IX + | EPSG_JGD2011_JPRECT_X + | EPSG_JGD2011_JPRECT_XI + | EPSG_JGD2011_JPRECT_XII + | EPSG_JGD2011_JPRECT_XIII + | EPSG_JGD2011_JPRECT_XIV + | EPSG_JGD2011_JPRECT_XV + | EPSG_JGD2011_JPRECT_XVI + | EPSG_JGD2011_JPRECT_XVII + | EPSG_JGD2011_JPRECT_XVIII + | EPSG_JGD2011_JPRECT_XIX => { + // TODO: implement + unimplemented!("WGS84 to EPSG:{} not supported yet", self.output_epsg); + } + _ => { + panic!("Unsupported output CRS: {}", self.output_epsg); + } + } + } }