1use crate::error::{Error, Result};
2use std::fmt;
3use std::path::{Path, PathBuf};
4use std::str::FromStr;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum FormatFamily {
10 PointCloud,
11 Mesh,
12 Raster,
13 Vector,
14 Database,
15 RoboticsStream,
16 SensorRaw,
17 WebTiles,
18 VendorProject,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum FormatSupport {
24 NativeReadWrite,
25 NativeReadOnly,
26 NativeWriteOnly,
27 AdapterRequired,
28 MetadataOnly,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33#[allow(missing_docs)]
34pub enum Format {
35 Las,
36 Laz,
37 Copc,
38 E57,
39 Ply,
40 Pcd,
41 Xyz,
42 Txt,
43 Csv,
44 Pts,
45 Ptx,
46 Rcp,
47 Rcs,
48 Potree,
49 Ept,
50 GeoTiff,
51 Cog,
52 AsciiGrid,
53 NetCdf,
54 Hdf5,
55 Shapefile,
56 GeoJson,
57 Gpkg,
58 Obj,
59 Fbx,
60 Gltf,
61 Glb,
62 Stl,
63 Dxf,
64 Dwg,
65 Pcap,
66 UdpPackets,
67 VendorRaw,
68 RosBag,
69 Ros2Bag,
70 PointCloud2,
71}
72
73impl Format {
74 pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
76 let path = path.as_ref();
77 Self::from_path_opt(path).ok_or_else(|| Error::UnknownFormat {
78 path: PathBuf::from(path),
79 })
80 }
81
82 pub fn from_path_opt(path: impl AsRef<Path>) -> Option<Self> {
84 let path = path.as_ref();
85 let name = path.file_name()?.to_string_lossy().to_ascii_lowercase();
86 if name.ends_with(".copc.laz") {
87 return Some(Self::Copc);
88 }
89 if name.ends_with(".cog.tif") || name.ends_with(".cog.tiff") {
90 return Some(Self::Cog);
91 }
92 if name.ends_with(".tar.gz") {
93 return None;
94 }
95
96 let ext = path.extension()?.to_string_lossy().to_ascii_lowercase();
97 match ext.as_str() {
98 "las" => Some(Self::Las),
99 "laz" => Some(Self::Laz),
100 "copc" => Some(Self::Copc),
101 "e57" => Some(Self::E57),
102 "ply" => Some(Self::Ply),
103 "pcd" => Some(Self::Pcd),
104 "xyz" => Some(Self::Xyz),
105 "txt" => Some(Self::Txt),
106 "csv" => Some(Self::Csv),
107 "pts" => Some(Self::Pts),
108 "ptx" => Some(Self::Ptx),
109 "rcp" => Some(Self::Rcp),
110 "rcs" => Some(Self::Rcs),
111 "potree" => Some(Self::Potree),
112 "ept" => Some(Self::Ept),
113 "tif" | "tiff" => Some(Self::GeoTiff),
114 "asc" => Some(Self::AsciiGrid),
115 "nc" | "cdf" | "netcdf" => Some(Self::NetCdf),
116 "h5" | "hdf5" => Some(Self::Hdf5),
117 "shp" => Some(Self::Shapefile),
118 "geojson" | "json" => Some(Self::GeoJson),
119 "gpkg" => Some(Self::Gpkg),
120 "obj" => Some(Self::Obj),
121 "fbx" => Some(Self::Fbx),
122 "gltf" => Some(Self::Gltf),
123 "glb" => Some(Self::Glb),
124 "stl" => Some(Self::Stl),
125 "dxf" => Some(Self::Dxf),
126 "dwg" => Some(Self::Dwg),
127 "pcap" | "pcapng" => Some(Self::Pcap),
128 "udp" | "udppackets" => Some(Self::UdpPackets),
129 "raw" | "vendorraw" => Some(Self::VendorRaw),
130 "bag" => Some(Self::RosBag),
131 "db3" => Some(Self::Ros2Bag),
132 "pc2" | "pointcloud2" => Some(Self::PointCloud2),
133 _ => None,
134 }
135 }
136
137 pub const fn name(self) -> &'static str {
139 match self {
140 Self::Las => "las",
141 Self::Laz => "laz",
142 Self::Copc => "copc",
143 Self::E57 => "e57",
144 Self::Ply => "ply",
145 Self::Pcd => "pcd",
146 Self::Xyz => "xyz",
147 Self::Txt => "txt",
148 Self::Csv => "csv",
149 Self::Pts => "pts",
150 Self::Ptx => "ptx",
151 Self::Rcp => "rcp",
152 Self::Rcs => "rcs",
153 Self::Potree => "potree",
154 Self::Ept => "ept",
155 Self::GeoTiff => "geotiff",
156 Self::Cog => "cog",
157 Self::AsciiGrid => "ascii-grid",
158 Self::NetCdf => "netcdf",
159 Self::Hdf5 => "hdf5",
160 Self::Shapefile => "shapefile",
161 Self::GeoJson => "geojson",
162 Self::Gpkg => "gpkg",
163 Self::Obj => "obj",
164 Self::Fbx => "fbx",
165 Self::Gltf => "gltf",
166 Self::Glb => "glb",
167 Self::Stl => "stl",
168 Self::Dxf => "dxf",
169 Self::Dwg => "dwg",
170 Self::Pcap => "pcap",
171 Self::UdpPackets => "udp-packets",
172 Self::VendorRaw => "vendor-raw",
173 Self::RosBag => "ros-bag",
174 Self::Ros2Bag => "ros2-bag",
175 Self::PointCloud2 => "pointcloud2",
176 }
177 }
178
179 pub const fn family(self) -> FormatFamily {
181 match self {
182 Self::Las
183 | Self::Laz
184 | Self::Copc
185 | Self::E57
186 | Self::Ply
187 | Self::Pcd
188 | Self::Xyz
189 | Self::Txt
190 | Self::Csv
191 | Self::Pts
192 | Self::Ptx => FormatFamily::PointCloud,
193 Self::Obj | Self::Fbx | Self::Gltf | Self::Glb | Self::Stl | Self::Dxf | Self::Dwg => {
194 FormatFamily::Mesh
195 }
196 Self::GeoTiff | Self::Cog | Self::AsciiGrid | Self::NetCdf | Self::Hdf5 => {
197 FormatFamily::Raster
198 }
199 Self::Shapefile | Self::GeoJson => FormatFamily::Vector,
200 Self::Gpkg => FormatFamily::Database,
201 Self::RosBag | Self::Ros2Bag | Self::PointCloud2 => FormatFamily::RoboticsStream,
202 Self::Pcap | Self::UdpPackets | Self::VendorRaw => FormatFamily::SensorRaw,
203 Self::Potree | Self::Ept => FormatFamily::WebTiles,
204 Self::Rcp | Self::Rcs => FormatFamily::VendorProject,
205 }
206 }
207
208 pub const fn support(self) -> FormatSupport {
210 match self {
211 Self::Ply
212 | Self::Pcd
213 | Self::Xyz
214 | Self::Txt
215 | Self::Csv
216 | Self::Pts
217 | Self::Ptx
218 | Self::Obj
219 | Self::Stl
220 | Self::AsciiGrid => FormatSupport::NativeReadWrite,
221
222 #[cfg(feature = "las")]
223 Self::Las | Self::Laz => FormatSupport::NativeReadWrite,
224 #[cfg(feature = "copc")]
225 Self::Copc => FormatSupport::NativeReadOnly,
226 #[cfg(feature = "e57")]
227 Self::E57 => FormatSupport::NativeReadWrite,
228 #[cfg(feature = "geospatial")]
229 Self::GeoTiff | Self::Cog | Self::GeoJson => FormatSupport::NativeReadWrite,
230 #[cfg(feature = "dxf")]
231 Self::Dxf => FormatSupport::NativeReadWrite,
232 #[cfg(feature = "shapefile")]
233 Self::Shapefile => FormatSupport::NativeReadWrite,
234 #[cfg(feature = "gltf")]
235 Self::Gltf | Self::Glb => FormatSupport::NativeReadWrite,
236 #[cfg(feature = "gpkg")]
237 Self::Gpkg => FormatSupport::NativeReadWrite,
238 #[cfg(feature = "robotics")]
239 Self::RosBag | Self::Ros2Bag | Self::PointCloud2 => FormatSupport::NativeReadWrite,
240 #[cfg(feature = "sensor")]
241 Self::Pcap | Self::UdpPackets | Self::VendorRaw => FormatSupport::NativeReadWrite,
242
243 #[cfg(not(feature = "las"))]
244 Self::Las | Self::Laz => FormatSupport::AdapterRequired,
245 #[cfg(not(feature = "copc"))]
246 Self::Copc => FormatSupport::AdapterRequired,
247 #[cfg(not(feature = "e57"))]
248 Self::E57 => FormatSupport::AdapterRequired,
249 #[cfg(not(feature = "geospatial"))]
250 Self::GeoTiff | Self::Cog | Self::GeoJson => FormatSupport::AdapterRequired,
251 #[cfg(not(feature = "dxf"))]
252 Self::Dxf => FormatSupport::AdapterRequired,
253 #[cfg(not(feature = "shapefile"))]
254 Self::Shapefile => FormatSupport::AdapterRequired,
255 #[cfg(not(feature = "gltf"))]
256 Self::Gltf | Self::Glb => FormatSupport::AdapterRequired,
257 #[cfg(not(feature = "gpkg"))]
258 Self::Gpkg => FormatSupport::AdapterRequired,
259 #[cfg(not(feature = "sensor"))]
260 Self::Pcap | Self::UdpPackets | Self::VendorRaw => FormatSupport::AdapterRequired,
261
262 Self::NetCdf
263 | Self::Hdf5
264 | Self::Fbx
265 | Self::Dwg
266 | Self::Potree
267 | Self::Ept
268 | Self::Rcp
269 | Self::Rcs => FormatSupport::AdapterRequired,
270
271 #[cfg(not(feature = "robotics"))]
272 Self::RosBag | Self::Ros2Bag | Self::PointCloud2 => FormatSupport::AdapterRequired,
273 }
274 }
275
276 pub const fn is_native_read(self) -> bool {
278 matches!(
279 self.support(),
280 FormatSupport::NativeReadWrite | FormatSupport::NativeReadOnly
281 )
282 }
283
284 pub const fn is_native_write(self) -> bool {
286 matches!(
287 self.support(),
288 FormatSupport::NativeReadWrite | FormatSupport::NativeWriteOnly
289 )
290 }
291
292 pub const fn adapter_hint(self) -> &'static str {
294 match self {
295 Self::Las | Self::Laz => "use an adapter built on the `las` crate; enable LAZ through its laz/laz-parallel features when writing compressed files",
296 Self::Copc => "use an adapter built on `copc-rs` or a PDAL pipeline; COPC requires LAZ hierarchy/index handling",
297 Self::E57 => "use an adapter built on the `e57` crate; E57 can contain multiple scans, poses, images, and vendor extensions",
298 Self::GeoTiff | Self::Cog | Self::AsciiGrid => "raster products need an explicit gridding/rasterization policy and a GDAL/tiff adapter",
299 Self::NetCdf | Self::Hdf5 => "scientific containers need dataset/schema selection and a netcdf/hdf5 adapter",
300 Self::Shapefile | Self::GeoJson | Self::Gpkg => "vector/database products need an explicit feature extraction schema and GIS adapter",
301 Self::Fbx | Self::Gltf | Self::Glb | Self::Dxf | Self::Dwg => "DCC/CAD formats need a mesh/CAD adapter and may not preserve point attributes",
302 Self::Potree | Self::Ept => "web tile formats need tiling, indexing, and hierarchy generation",
303 Self::Pcap | Self::UdpPackets | Self::VendorRaw => "raw sensor data must be decoded using vendor packet calibration before point-cloud export",
304 Self::RosBag | Self::Ros2Bag | Self::PointCloud2 => "robotics streams need ROS message schemas, topic selection, frame transforms, and timestamp policy",
305 Self::Rcp | Self::Rcs => "Autodesk project formats are proprietary/vendor-specific; use vendor/export tooling or an adapter",
306 _ => "format is supported natively",
307 }
308 }
309
310 pub const ALL: &'static [Self] = &[
312 Self::Las,
313 Self::Laz,
314 Self::Copc,
315 Self::E57,
316 Self::Ply,
317 Self::Pcd,
318 Self::Xyz,
319 Self::Txt,
320 Self::Csv,
321 Self::Pts,
322 Self::Ptx,
323 Self::Rcp,
324 Self::Rcs,
325 Self::Potree,
326 Self::Ept,
327 Self::GeoTiff,
328 Self::Cog,
329 Self::AsciiGrid,
330 Self::NetCdf,
331 Self::Hdf5,
332 Self::Shapefile,
333 Self::GeoJson,
334 Self::Gpkg,
335 Self::Obj,
336 Self::Fbx,
337 Self::Gltf,
338 Self::Glb,
339 Self::Stl,
340 Self::Dxf,
341 Self::Dwg,
342 Self::Pcap,
343 Self::UdpPackets,
344 Self::VendorRaw,
345 Self::RosBag,
346 Self::Ros2Bag,
347 Self::PointCloud2,
348 ];
349}
350
351impl fmt::Display for Format {
352 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353 f.write_str(self.name())
354 }
355}
356
357impl FromStr for Format {
358 type Err = String;
359
360 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
361 let normalized = s
362 .trim()
363 .to_ascii_lowercase()
364 .replace('_', "-")
365 .replace('.', "");
366 match normalized.as_str() {
367 "las" => Ok(Self::Las),
368 "laz" => Ok(Self::Laz),
369 "copc" | "copclaz" => Ok(Self::Copc),
370 "e57" => Ok(Self::E57),
371 "ply" => Ok(Self::Ply),
372 "pcd" => Ok(Self::Pcd),
373 "xyz" => Ok(Self::Xyz),
374 "txt" => Ok(Self::Txt),
375 "csv" => Ok(Self::Csv),
376 "pts" => Ok(Self::Pts),
377 "ptx" => Ok(Self::Ptx),
378 "rcp" => Ok(Self::Rcp),
379 "rcs" => Ok(Self::Rcs),
380 "potree" => Ok(Self::Potree),
381 "ept" => Ok(Self::Ept),
382 "geotiff" | "tif" | "tiff" => Ok(Self::GeoTiff),
383 "cog" => Ok(Self::Cog),
384 "ascii-grid" | "asc" | "asciigrid" => Ok(Self::AsciiGrid),
385 "netcdf" | "nc" => Ok(Self::NetCdf),
386 "hdf5" | "h5" => Ok(Self::Hdf5),
387 "shapefile" | "shp" => Ok(Self::Shapefile),
388 "geojson" => Ok(Self::GeoJson),
389 "gpkg" | "geopackage" => Ok(Self::Gpkg),
390 "obj" => Ok(Self::Obj),
391 "fbx" => Ok(Self::Fbx),
392 "gltf" => Ok(Self::Gltf),
393 "glb" => Ok(Self::Glb),
394 "stl" => Ok(Self::Stl),
395 "dxf" => Ok(Self::Dxf),
396 "dwg" => Ok(Self::Dwg),
397 "pcap" | "pcapng" => Ok(Self::Pcap),
398 "udp" | "udp-packets" | "udppackets" => Ok(Self::UdpPackets),
399 "vendor-raw" | "vendorraw" | "raw" => Ok(Self::VendorRaw),
400 "ros-bag" | "rosbag" | "bag" => Ok(Self::RosBag),
401 "ros2-bag" | "ros2bag" | "db3" => Ok(Self::Ros2Bag),
402 "pointcloud2" | "point-cloud2" | "sensor-msgs-pointcloud2" => Ok(Self::PointCloud2),
403 _ => Err(format!("unknown format '{s}'")),
404 }
405 }
406}