Skip to main content

point_formats/
format.rs

1use crate::error::{Error, Result};
2use std::fmt;
3use std::path::{Path, PathBuf};
4use std::str::FromStr;
5
6/// High-level family of a format. This is used to decide when conversion
7/// requires semantic work such as meshing, rasterization, or decoding packets.
8#[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/// Declares whether a format is implemented natively in this crate.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum FormatSupport {
24    NativeReadWrite,
25    NativeReadOnly,
26    NativeWriteOnly,
27    AdapterRequired,
28    MetadataOnly,
29}
30
31/// Formats from the supplied LiDAR / point-cloud list.
32#[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    /// Infers a format from a path's extension and known compound extensions.
75    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    /// Same as [`Format::from_path`], but returns `None` instead of an error.
83    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    /// Stable lowercase name used by the CLI and diagnostics.
138    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    /// Format family.
180    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    /// Native support level in this crate.
209    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    /// Returns true when the format can be read by the built-in codecs.
277    pub const fn is_native_read(self) -> bool {
278        matches!(
279            self.support(),
280            FormatSupport::NativeReadWrite | FormatSupport::NativeReadOnly
281        )
282    }
283
284    /// Returns true when the format can be written by the built-in codecs.
285    pub const fn is_native_write(self) -> bool {
286        matches!(
287            self.support(),
288            FormatSupport::NativeReadWrite | FormatSupport::NativeWriteOnly
289        )
290    }
291
292    /// Human-readable reason when this format needs an adapter.
293    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    /// All formats represented by this crate.
311    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}