1use crate::{EdgeSE2, EdgeSE3, Graph, GraphLoader, IoError, VertexSE2, VertexSE3};
2use rayon::prelude::*;
3use std::collections::HashMap;
4use std::{fs::File, io::Write, path::Path};
5
6pub struct G2oLoader;
8
9impl GraphLoader for G2oLoader {
10 fn load<P: AsRef<Path>>(path: P) -> Result<Graph, IoError> {
11 let path_ref = path.as_ref();
12 let content = std::fs::read_to_string(path_ref).map_err(|e| {
13 IoError::Io(e).log_with_source(format!("Failed to read G2O file: {:?}", path_ref))
14 })?;
15
16 Self::parse_content(&content)
17 }
18
19 fn write<P: AsRef<Path>>(graph: &Graph, path: P) -> Result<(), IoError> {
20 let path_ref = path.as_ref();
21 let mut file = File::create(path_ref).map_err(|e| {
22 IoError::Io(e).log_with_source(format!("Failed to create G2O file: {:?}", path_ref))
23 })?;
24
25 writeln!(file, "# G2O file written by Apex Solver")
27 .map_err(|e| IoError::Io(e).log_with_source("Failed to write G2O header"))?;
28 writeln!(
29 file,
30 "# Timestamp: {}",
31 chrono::Local::now().format("%Y-%m-%d %H:%M:%S")
32 )
33 .map_err(|e| IoError::Io(e).log_with_source("Failed to write G2O timestamp"))?;
34 writeln!(
35 file,
36 "# SE2 vertices: {}, SE3 vertices: {}, SE2 edges: {}, SE3 edges: {}",
37 graph.vertices_se2.len(),
38 graph.vertices_se3.len(),
39 graph.edges_se2.len(),
40 graph.edges_se3.len()
41 )
42 .map_err(|e| IoError::Io(e).log_with_source("Failed to write G2O statistics"))?;
43 writeln!(file)
44 .map_err(|e| IoError::Io(e).log_with_source("Failed to write G2O header newline"))?;
45
46 let mut se2_ids: Vec<_> = graph.vertices_se2.keys().collect();
48 se2_ids.sort();
49
50 for id in se2_ids {
51 let vertex = &graph.vertices_se2[id];
52 writeln!(
53 file,
54 "VERTEX_SE2 {} {:.17e} {:.17e} {:.17e}",
55 vertex.id,
56 vertex.x(),
57 vertex.y(),
58 vertex.theta()
59 )
60 .map_err(|e| {
61 IoError::Io(e).log_with_source(format!("Failed to write SE2 vertex {}", vertex.id))
62 })?;
63 }
64
65 let mut se3_ids: Vec<_> = graph.vertices_se3.keys().collect();
67 se3_ids.sort();
68
69 for id in se3_ids {
70 let vertex = &graph.vertices_se3[id];
71 let trans = vertex.translation();
72 let quat = vertex.rotation();
73 writeln!(
74 file,
75 "VERTEX_SE3:QUAT {} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e}",
76 vertex.id, trans.x, trans.y, trans.z, quat.i, quat.j, quat.k, quat.w
77 )
78 .map_err(|e| {
79 IoError::Io(e).log_with_source(format!("Failed to write SE3 vertex {}", vertex.id))
80 })?;
81 }
82
83 for edge in &graph.edges_se2 {
85 let meas = &edge.measurement;
86 let info = &edge.information;
87
88 writeln!(
90 file,
91 "EDGE_SE2 {} {} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e}",
92 edge.from,
93 edge.to,
94 meas.x(),
95 meas.y(),
96 meas.angle(),
97 info[(0, 0)],
98 info[(0, 1)],
99 info[(1, 1)],
100 info[(2, 2)],
101 info[(0, 2)],
102 info[(1, 2)]
103 )
104 .map_err(|e| {
105 IoError::Io(e).log_with_source(format!(
106 "Failed to write SE2 edge {} -> {}",
107 edge.from, edge.to
108 ))
109 })?;
110 }
111
112 for edge in &graph.edges_se3 {
114 let trans = edge.measurement.translation();
115 let quat = edge.measurement.rotation_quaternion();
116 let info = &edge.information;
117
118 write!(
120 file,
121 "EDGE_SE3:QUAT {} {} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e} {:.17e}",
122 edge.from, edge.to, trans.x, trans.y, trans.z, quat.i, quat.j, quat.k, quat.w
123 )
124 .map_err(|e| {
125 IoError::Io(e).log_with_source(format!(
126 "Failed to write SE3 edge {} -> {}",
127 edge.from, edge.to
128 ))
129 })?;
130
131 for i in 0..6 {
133 for j in i..6 {
134 write!(file, " {:.17e}", info[(i, j)]).map_err(|e| {
135 IoError::Io(e).log_with_source(format!(
136 "Failed to write SE3 edge {} -> {} information matrix",
137 edge.from, edge.to
138 ))
139 })?;
140 }
141 }
142 writeln!(file).map_err(|e| {
143 IoError::Io(e).log_with_source(format!(
144 "Failed to write SE3 edge {} -> {} newline",
145 edge.from, edge.to
146 ))
147 })?;
148 }
149
150 Ok(())
151 }
152}
153
154impl G2oLoader {
155 fn parse_content(content: &str) -> Result<Graph, IoError> {
157 let lines: Vec<&str> = content.lines().collect();
158 let minimum_lines_for_parallel = 1000;
159
160 let estimated_vertices = lines.len() / 4;
162 let estimated_edges = estimated_vertices * 3;
163 let mut graph = Graph {
164 vertices_se2: HashMap::with_capacity(estimated_vertices),
165 vertices_se3: HashMap::with_capacity(estimated_vertices),
166 edges_se2: Vec::with_capacity(estimated_edges),
167 edges_se3: Vec::with_capacity(estimated_edges),
168 };
169
170 if lines.len() > minimum_lines_for_parallel {
172 Self::parse_parallel(&lines, &mut graph)?;
173 } else {
174 Self::parse_sequential(&lines, &mut graph)?;
175 }
176
177 Ok(graph)
178 }
179
180 fn parse_sequential(lines: &[&str], graph: &mut Graph) -> Result<(), IoError> {
182 for (line_num, line) in lines.iter().enumerate() {
183 Self::parse_line(line, line_num + 1, graph)?;
184 }
185 Ok(())
186 }
187
188 fn parse_parallel(lines: &[&str], graph: &mut Graph) -> Result<(), IoError> {
190 let results: Result<Vec<_>, IoError> = lines
192 .par_iter()
193 .enumerate()
194 .map(|(line_num, line)| Self::parse_line_to_enum(line, line_num + 1))
195 .collect();
196
197 let parsed_items = results?;
198
199 for item in parsed_items.into_iter().flatten() {
201 match item {
202 ParsedItem::VertexSE2(vertex) => {
203 let id = vertex.id;
204 if graph.vertices_se2.insert(id, vertex).is_some() {
205 return Err(IoError::DuplicateVertex { id });
206 }
207 }
208 ParsedItem::VertexSE3(vertex) => {
209 let id = vertex.id;
210 if graph.vertices_se3.insert(id, vertex).is_some() {
211 return Err(IoError::DuplicateVertex { id });
212 }
213 }
214 ParsedItem::EdgeSE2(edge) => {
215 graph.edges_se2.push(edge);
216 }
217 ParsedItem::EdgeSE3(edge) => {
218 graph.edges_se3.push(*edge);
219 }
220 }
221 }
222
223 Ok(())
224 }
225
226 fn parse_line(line: &str, line_num: usize, graph: &mut Graph) -> Result<(), IoError> {
228 let line = line.trim();
229
230 if line.is_empty() || line.starts_with('#') {
232 return Ok(());
233 }
234
235 let parts: Vec<&str> = line.split_whitespace().collect();
236 if parts.is_empty() {
237 return Ok(());
238 }
239
240 match parts[0] {
241 "VERTEX_SE2" => {
242 let vertex = Self::parse_vertex_se2(&parts, line_num)?;
243 let id = vertex.id;
244 if graph.vertices_se2.insert(id, vertex).is_some() {
245 return Err(IoError::DuplicateVertex { id });
246 }
247 }
248 "VERTEX_SE3:QUAT" => {
249 let vertex = Self::parse_vertex_se3(&parts, line_num)?;
250 let id = vertex.id;
251 if graph.vertices_se3.insert(id, vertex).is_some() {
252 return Err(IoError::DuplicateVertex { id });
253 }
254 }
255 "EDGE_SE2" => {
256 let edge = Self::parse_edge_se2(&parts, line_num)?;
257 graph.edges_se2.push(edge);
258 }
259 "EDGE_SE3:QUAT" => {
260 let edge = Self::parse_edge_se3(&parts, line_num)?;
261 graph.edges_se3.push(edge);
262 }
263 _ => {
264 }
266 }
267
268 Ok(())
269 }
270
271 fn parse_line_to_enum(line: &str, line_num: usize) -> Result<Option<ParsedItem>, IoError> {
273 let line = line.trim();
274
275 if line.is_empty() || line.starts_with('#') {
277 return Ok(None);
278 }
279
280 let parts: Vec<&str> = line.split_whitespace().collect();
281 if parts.is_empty() {
282 return Ok(None);
283 }
284
285 let item = match parts[0] {
286 "VERTEX_SE2" => Some(ParsedItem::VertexSE2(Self::parse_vertex_se2(
287 &parts, line_num,
288 )?)),
289 "VERTEX_SE3:QUAT" => Some(ParsedItem::VertexSE3(Self::parse_vertex_se3(
290 &parts, line_num,
291 )?)),
292 "EDGE_SE2" => Some(ParsedItem::EdgeSE2(Self::parse_edge_se2(&parts, line_num)?)),
293 "EDGE_SE3:QUAT" => Some(ParsedItem::EdgeSE3(Box::new(Self::parse_edge_se3(
294 &parts, line_num,
295 )?))),
296 _ => None, };
298
299 Ok(item)
300 }
301
302 pub fn parse_vertex_se2(parts: &[&str], line_num: usize) -> Result<VertexSE2, IoError> {
304 if parts.len() < 5 {
305 return Err(IoError::MissingFields { line: line_num });
306 }
307
308 let id = parts[1]
309 .parse::<usize>()
310 .map_err(|_| IoError::InvalidNumber {
311 line: line_num,
312 value: parts[1].to_string(),
313 })?;
314
315 let x = parts[2]
316 .parse::<f64>()
317 .map_err(|_| IoError::InvalidNumber {
318 line: line_num,
319 value: parts[2].to_string(),
320 })?;
321
322 let y = parts[3]
323 .parse::<f64>()
324 .map_err(|_| IoError::InvalidNumber {
325 line: line_num,
326 value: parts[3].to_string(),
327 })?;
328
329 let theta = parts[4]
330 .parse::<f64>()
331 .map_err(|_| IoError::InvalidNumber {
332 line: line_num,
333 value: parts[4].to_string(),
334 })?;
335
336 Ok(VertexSE2::new(id, x, y, theta))
337 }
338
339 pub fn parse_vertex_se3(parts: &[&str], line_num: usize) -> Result<VertexSE3, IoError> {
341 if parts.len() < 9 {
342 return Err(IoError::MissingFields { line: line_num });
343 }
344
345 let id = parts[1]
346 .parse::<usize>()
347 .map_err(|_| IoError::InvalidNumber {
348 line: line_num,
349 value: parts[1].to_string(),
350 })?;
351
352 let x = parts[2]
353 .parse::<f64>()
354 .map_err(|_| IoError::InvalidNumber {
355 line: line_num,
356 value: parts[2].to_string(),
357 })?;
358
359 let y = parts[3]
360 .parse::<f64>()
361 .map_err(|_| IoError::InvalidNumber {
362 line: line_num,
363 value: parts[3].to_string(),
364 })?;
365
366 let z = parts[4]
367 .parse::<f64>()
368 .map_err(|_| IoError::InvalidNumber {
369 line: line_num,
370 value: parts[4].to_string(),
371 })?;
372
373 let qx = parts[5]
374 .parse::<f64>()
375 .map_err(|_| IoError::InvalidNumber {
376 line: line_num,
377 value: parts[5].to_string(),
378 })?;
379
380 let qy = parts[6]
381 .parse::<f64>()
382 .map_err(|_| IoError::InvalidNumber {
383 line: line_num,
384 value: parts[6].to_string(),
385 })?;
386
387 let qz = parts[7]
388 .parse::<f64>()
389 .map_err(|_| IoError::InvalidNumber {
390 line: line_num,
391 value: parts[7].to_string(),
392 })?;
393
394 let qw = parts[8]
395 .parse::<f64>()
396 .map_err(|_| IoError::InvalidNumber {
397 line: line_num,
398 value: parts[8].to_string(),
399 })?;
400
401 let translation = nalgebra::Vector3::new(x, y, z);
402 let quaternion = nalgebra::Quaternion::new(qw, qx, qy, qz);
403
404 let quat_norm = (qw * qw + qx * qx + qy * qy + qz * qz).sqrt();
406 if (quat_norm - 1.0).abs() > 0.01 {
407 return Err(IoError::InvalidQuaternion {
408 line: line_num,
409 norm: quat_norm,
410 });
411 }
412
413 let quaternion = quaternion.normalize();
415
416 Ok(VertexSE3::from_translation_quaternion(
417 id,
418 translation,
419 quaternion,
420 ))
421 }
422
423 fn parse_edge_se2(parts: &[&str], line_num: usize) -> Result<EdgeSE2, IoError> {
425 if parts.len() < 12 {
426 return Err(IoError::MissingFields { line: line_num });
427 }
428
429 let from = parts[1]
430 .parse::<usize>()
431 .map_err(|_| IoError::InvalidNumber {
432 line: line_num,
433 value: parts[1].to_string(),
434 })?;
435
436 let to = parts[2]
437 .parse::<usize>()
438 .map_err(|_| IoError::InvalidNumber {
439 line: line_num,
440 value: parts[2].to_string(),
441 })?;
442
443 let dx = parts[3]
445 .parse::<f64>()
446 .map_err(|_| IoError::InvalidNumber {
447 line: line_num,
448 value: parts[3].to_string(),
449 })?;
450 let dy = parts[4]
451 .parse::<f64>()
452 .map_err(|_| IoError::InvalidNumber {
453 line: line_num,
454 value: parts[4].to_string(),
455 })?;
456 let dtheta = parts[5]
457 .parse::<f64>()
458 .map_err(|_| IoError::InvalidNumber {
459 line: line_num,
460 value: parts[5].to_string(),
461 })?;
462
463 let info_values: Result<Vec<f64>, _> =
465 parts[6..12].iter().map(|s| s.parse::<f64>()).collect();
466
467 let info_values = info_values.map_err(|_| IoError::Parse {
468 line: line_num,
469 message: "Invalid information matrix values".to_string(),
470 })?;
471
472 let information = nalgebra::Matrix3::new(
473 info_values[0],
474 info_values[1],
475 info_values[2],
476 info_values[1],
477 info_values[3],
478 info_values[4],
479 info_values[2],
480 info_values[4],
481 info_values[5],
482 );
483
484 Ok(EdgeSE2::new(from, to, dx, dy, dtheta, information))
485 }
486
487 fn parse_edge_se3(parts: &[&str], line_num: usize) -> Result<EdgeSE3, IoError> {
489 if parts.len() < 10 {
491 return Err(IoError::MissingFields { line: line_num });
492 }
493
494 let from = parts[1]
496 .parse::<usize>()
497 .map_err(|_| IoError::InvalidNumber {
498 line: line_num,
499 value: parts[1].to_string(),
500 })?;
501
502 let to = parts[2]
503 .parse::<usize>()
504 .map_err(|_| IoError::InvalidNumber {
505 line: line_num,
506 value: parts[2].to_string(),
507 })?;
508
509 let tx = parts[3]
511 .parse::<f64>()
512 .map_err(|_| IoError::InvalidNumber {
513 line: line_num,
514 value: parts[3].to_string(),
515 })?;
516
517 let ty = parts[4]
518 .parse::<f64>()
519 .map_err(|_| IoError::InvalidNumber {
520 line: line_num,
521 value: parts[4].to_string(),
522 })?;
523
524 let tz = parts[5]
525 .parse::<f64>()
526 .map_err(|_| IoError::InvalidNumber {
527 line: line_num,
528 value: parts[5].to_string(),
529 })?;
530
531 let translation = nalgebra::Vector3::new(tx, ty, tz);
532
533 let qx = parts[6]
535 .parse::<f64>()
536 .map_err(|_| IoError::InvalidNumber {
537 line: line_num,
538 value: parts[6].to_string(),
539 })?;
540
541 let qy = parts[7]
542 .parse::<f64>()
543 .map_err(|_| IoError::InvalidNumber {
544 line: line_num,
545 value: parts[7].to_string(),
546 })?;
547
548 let qz = parts[8]
549 .parse::<f64>()
550 .map_err(|_| IoError::InvalidNumber {
551 line: line_num,
552 value: parts[8].to_string(),
553 })?;
554
555 let qw = parts[9]
556 .parse::<f64>()
557 .map_err(|_| IoError::InvalidNumber {
558 line: line_num,
559 value: parts[9].to_string(),
560 })?;
561
562 let rotation =
563 nalgebra::UnitQuaternion::from_quaternion(nalgebra::Quaternion::new(qw, qx, qy, qz));
564
565 let info_values: Result<Vec<f64>, _> =
567 parts[10..31].iter().map(|s| s.parse::<f64>()).collect();
568
569 let info_values = info_values.map_err(|_| IoError::Parse {
570 line: line_num,
571 message: "Invalid information matrix values".to_string(),
572 })?;
573
574 let information = nalgebra::Matrix6::new(
575 info_values[0],
576 info_values[1],
577 info_values[2],
578 info_values[3],
579 info_values[4],
580 info_values[5],
581 info_values[1],
582 info_values[6],
583 info_values[7],
584 info_values[8],
585 info_values[9],
586 info_values[10],
587 info_values[2],
588 info_values[7],
589 info_values[11],
590 info_values[12],
591 info_values[13],
592 info_values[14],
593 info_values[3],
594 info_values[8],
595 info_values[12],
596 info_values[15],
597 info_values[16],
598 info_values[17],
599 info_values[4],
600 info_values[9],
601 info_values[13],
602 info_values[16],
603 info_values[18],
604 info_values[19],
605 info_values[5],
606 info_values[10],
607 info_values[14],
608 info_values[17],
609 info_values[19],
610 info_values[20],
611 );
612
613 Ok(EdgeSE3::new(from, to, translation, rotation, information))
614 }
615}
616
617enum ParsedItem {
619 VertexSE2(VertexSE2),
620 VertexSE3(VertexSE3),
621 EdgeSE2(EdgeSE2),
622 EdgeSE3(Box<EdgeSE3>),
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628 use nalgebra::{Matrix3, Matrix6, UnitQuaternion, Vector3};
629 use std::io::Write;
630 use tempfile::NamedTempFile;
631
632 type TestResult = Result<(), Box<dyn std::error::Error>>;
633
634 #[test]
635 fn test_parse_vertex_se2() -> TestResult {
636 let parts = vec!["VERTEX_SE2", "0", "1.0", "2.0", "0.5"];
637 let vertex = G2oLoader::parse_vertex_se2(&parts, 1)?;
638
639 assert_eq!(vertex.id(), 0);
640 assert_eq!(vertex.x(), 1.0);
641 assert_eq!(vertex.y(), 2.0);
642 assert_eq!(vertex.theta(), 0.5);
643
644 Ok(())
645 }
646
647 #[test]
648 fn test_parse_vertex_se3() -> TestResult {
649 let parts = vec![
650 "VERTEX_SE3:QUAT",
651 "1",
652 "1.0",
653 "2.0",
654 "3.0",
655 "0.0",
656 "0.0",
657 "0.0",
658 "1.0",
659 ];
660 let vertex = G2oLoader::parse_vertex_se3(&parts, 1)?;
661
662 assert_eq!(vertex.id(), 1);
663 assert_eq!(vertex.translation(), nalgebra::Vector3::new(1.0, 2.0, 3.0));
664 assert!(vertex.rotation().quaternion().w > 0.99); Ok(())
667 }
668
669 #[test]
670 fn test_error_handling() {
671 let parts = vec!["VERTEX_SE2", "invalid", "1.0", "2.0", "0.5"];
673 let result = G2oLoader::parse_vertex_se2(&parts, 1);
674 assert!(matches!(result, Err(IoError::InvalidNumber { .. })));
675
676 let parts = vec!["VERTEX_SE2", "0"];
678 let result = G2oLoader::parse_vertex_se2(&parts, 1);
679 assert!(matches!(result, Err(IoError::MissingFields { .. })));
680 }
681
682 #[test]
683 fn test_write_se2_graph_round_trip() -> TestResult {
684 let mut graph = Graph::new();
685 graph
686 .vertices_se2
687 .insert(0, VertexSE2::new(0, 1.0, 2.0, 0.5));
688 graph
689 .vertices_se2
690 .insert(1, VertexSE2::new(1, 3.0, 4.0, 1.0));
691 let info = Matrix3::new(500.0, 0.0, 0.0, 0.0, 500.0, 0.0, 0.0, 0.0, 200.0);
692 graph
693 .edges_se2
694 .push(EdgeSE2::new(0, 1, 0.5, 0.3, 0.1, info));
695
696 let f = NamedTempFile::new()?;
697 G2oLoader::write(&graph, f.path())?;
698 let loaded = G2oLoader::load(f.path())?;
699
700 assert_eq!(loaded.vertices_se2.len(), 2);
701 assert_eq!(loaded.edges_se2.len(), 1);
702 let v0 = &loaded.vertices_se2[&0];
703 assert!((v0.x() - 1.0).abs() < 1e-10);
704 assert!((v0.y() - 2.0).abs() < 1e-10);
705 let e = &loaded.edges_se2[0];
706 assert_eq!(e.from, 0);
707 assert_eq!(e.to, 1);
708 assert!((e.information[(0, 0)] - 500.0).abs() < 1e-10);
709 Ok(())
710 }
711
712 #[test]
713 fn test_write_se3_graph_round_trip() -> TestResult {
714 let trans = Vector3::new(1.0, 2.0, 3.0);
715 let rot = UnitQuaternion::identity();
716 let mut graph = Graph::new();
717 graph.vertices_se3.insert(0, VertexSE3::new(0, trans, rot));
718 graph
719 .vertices_se3
720 .insert(1, VertexSE3::new(1, Vector3::zeros(), rot));
721 graph
722 .edges_se3
723 .push(EdgeSE3::new(0, 1, trans, rot, Matrix6::identity()));
724
725 let f = NamedTempFile::new()?;
726 G2oLoader::write(&graph, f.path())?;
727 let loaded = G2oLoader::load(f.path())?;
728
729 assert_eq!(loaded.vertices_se3.len(), 2);
730 assert_eq!(loaded.edges_se3.len(), 1);
731 let v0 = &loaded.vertices_se3[&0];
732 assert!((v0.x() - 1.0).abs() < 1e-10);
733 assert!((v0.y() - 2.0).abs() < 1e-10);
734 assert!((v0.z() - 3.0).abs() < 1e-10);
735 let e = &loaded.edges_se3[0];
736 assert!((e.information[(0, 0)] - 1.0).abs() < 1e-10);
737 Ok(())
738 }
739
740 #[test]
741 fn test_write_mixed_graph_round_trip() -> TestResult {
742 let mut graph = Graph::new();
743 graph
744 .vertices_se2
745 .insert(0, VertexSE2::new(0, 1.0, 0.0, 0.0));
746 graph.vertices_se3.insert(
747 1,
748 VertexSE3::new(1, Vector3::new(0.0, 0.0, 1.0), UnitQuaternion::identity()),
749 );
750
751 let f = NamedTempFile::new()?;
752 G2oLoader::write(&graph, f.path())?;
753 let loaded = G2oLoader::load(f.path())?;
754
755 assert_eq!(loaded.vertices_se2.len(), 1);
756 assert_eq!(loaded.vertices_se3.len(), 1);
757 Ok(())
758 }
759
760 #[test]
761 fn test_write_empty_graph() -> TestResult {
762 let graph = Graph::new();
763 let f = NamedTempFile::new()?;
764 G2oLoader::write(&graph, f.path())?;
765 let loaded = G2oLoader::load(f.path())?;
766 assert_eq!(loaded.vertices_se2.len(), 0);
767 assert_eq!(loaded.vertices_se3.len(), 0);
768 assert_eq!(loaded.edges_se2.len(), 0);
769 assert_eq!(loaded.edges_se3.len(), 0);
770 Ok(())
771 }
772
773 #[test]
774 fn test_load_nonexistent_file() {
775 let result = G2oLoader::load("/nonexistent/path/file.g2o");
776 assert!(result.is_err(), "loading a missing file should return Err");
777 }
778
779 #[test]
780 fn test_parse_vertex_se3_invalid_quaternion_norm() -> TestResult {
781 let mut f = NamedTempFile::new()?;
783 writeln!(f, "VERTEX_SE3:QUAT 0 0.0 0.0 0.0 0.0 0.0 0.0 0.1")?;
784 f.flush()?;
785 let result = G2oLoader::load(f.path());
786 assert!(
787 matches!(result, Err(IoError::InvalidQuaternion { .. })),
788 "far-from-unit quaternion should return InvalidQuaternion"
789 );
790 Ok(())
791 }
792
793 #[test]
794 fn test_parse_edge_se2_information_matrix() -> TestResult {
795 let mut f = NamedTempFile::new()?;
797 writeln!(f, "VERTEX_SE2 0 0.0 0.0 0.0")?;
798 writeln!(f, "VERTEX_SE2 1 1.0 0.0 0.0")?;
799 writeln!(f, "EDGE_SE2 0 1 1.0 0.0 0.0 500.0 0.0 0.0 300.0 0.0 200.0")?;
800 f.flush()?;
801 let graph = G2oLoader::load(f.path())?;
802 assert_eq!(graph.edges_se2.len(), 1);
803 let e = &graph.edges_se2[0];
804 assert_eq!(e.from, 0);
805 assert_eq!(e.to, 1);
806 assert!(
807 (e.information[(0, 0)] - 500.0).abs() < 1e-10,
808 "i11={}",
809 e.information[(0, 0)]
810 );
811 assert!(
812 (e.information[(1, 1)] - 300.0).abs() < 1e-10,
813 "i22={}",
814 e.information[(1, 1)]
815 );
816 assert!(
817 (e.information[(2, 2)] - 200.0).abs() < 1e-10,
818 "i33={}",
819 e.information[(2, 2)]
820 );
821 Ok(())
822 }
823
824 #[test]
825 fn test_parse_edge_se3_information_matrix() -> TestResult {
826 let mut f = NamedTempFile::new()?;
828 writeln!(f, "VERTEX_SE3:QUAT 0 0.0 0.0 0.0 0.0 0.0 0.0 1.0")?;
829 writeln!(f, "VERTEX_SE3:QUAT 1 1.0 0.0 0.0 0.0 0.0 0.0 1.0")?;
830 let info_vals = "100.0 0.0 0.0 0.0 0.0 0.0 100.0 0.0 0.0 0.0 0.0 100.0 0.0 0.0 0.0 100.0 0.0 0.0 100.0 0.0 100.0";
832 writeln!(
833 f,
834 "EDGE_SE3:QUAT 0 1 1.0 0.0 0.0 0.0 0.0 0.0 1.0 {}",
835 info_vals
836 )?;
837 f.flush()?;
838 let graph = G2oLoader::load(f.path())?;
839 assert_eq!(graph.edges_se3.len(), 1);
840 let e = &graph.edges_se3[0];
841 assert!((e.information[(0, 0)] - 100.0).abs() < 1e-10);
842 assert!((e.information[(1, 1)] - 100.0).abs() < 1e-10);
843 Ok(())
844 }
845
846 #[test]
851 fn test_parse_vertex_se2_invalid_x() -> TestResult {
852 let mut f = NamedTempFile::new()?;
853 writeln!(f, "VERTEX_SE2 0 bad 2.0 0.5")?;
854 f.flush()?;
855 let result = G2oLoader::load(f.path());
856 assert!(
857 matches!(result, Err(IoError::InvalidNumber { .. })),
858 "invalid x in VERTEX_SE2 should return InvalidNumber"
859 );
860 Ok(())
861 }
862
863 #[test]
864 fn test_parse_vertex_se2_invalid_y() -> TestResult {
865 let mut f = NamedTempFile::new()?;
866 writeln!(f, "VERTEX_SE2 0 1.0 bad 0.5")?;
867 f.flush()?;
868 let result = G2oLoader::load(f.path());
869 assert!(
870 matches!(result, Err(IoError::InvalidNumber { .. })),
871 "invalid y in VERTEX_SE2 should return InvalidNumber"
872 );
873 Ok(())
874 }
875
876 #[test]
877 fn test_parse_vertex_se2_invalid_theta() -> TestResult {
878 let mut f = NamedTempFile::new()?;
879 writeln!(f, "VERTEX_SE2 0 1.0 2.0 bad")?;
880 f.flush()?;
881 let result = G2oLoader::load(f.path());
882 assert!(
883 matches!(result, Err(IoError::InvalidNumber { .. })),
884 "invalid theta in VERTEX_SE2 should return InvalidNumber"
885 );
886 Ok(())
887 }
888
889 #[test]
890 fn test_parse_vertex_se2_missing_fields() -> TestResult {
891 let mut f = NamedTempFile::new()?;
892 writeln!(f, "VERTEX_SE2 0 1.0")?; f.flush()?;
894 let result = G2oLoader::load(f.path());
895 assert!(
896 matches!(result, Err(IoError::MissingFields { .. })),
897 "VERTEX_SE2 with too few fields should return MissingFields"
898 );
899 Ok(())
900 }
901
902 #[test]
903 fn test_parse_duplicate_vertex_se2() -> TestResult {
904 let mut f = NamedTempFile::new()?;
905 writeln!(f, "VERTEX_SE2 3 1.0 2.0 0.0")?;
906 writeln!(f, "VERTEX_SE2 3 3.0 4.0 0.0")?;
907 f.flush()?;
908 let result = G2oLoader::load(f.path());
909 assert!(
910 matches!(result, Err(IoError::DuplicateVertex { id: 3 })),
911 "duplicate VERTEX_SE2 ID should return DuplicateVertex"
912 );
913 Ok(())
914 }
915
916 #[test]
921 fn test_parse_vertex_se3_missing_fields() -> TestResult {
922 let mut f = NamedTempFile::new()?;
923 writeln!(f, "VERTEX_SE3:QUAT 0 1.0 2.0")?; f.flush()?;
925 let result = G2oLoader::load(f.path());
926 assert!(
927 matches!(result, Err(IoError::MissingFields { .. })),
928 "VERTEX_SE3:QUAT with too few fields should return MissingFields"
929 );
930 Ok(())
931 }
932
933 #[test]
934 fn test_parse_vertex_se3_invalid_translation() -> TestResult {
935 let mut f = NamedTempFile::new()?;
936 writeln!(f, "VERTEX_SE3:QUAT 0 bad 2.0 3.0 0.0 0.0 0.0 1.0")?;
937 f.flush()?;
938 let result = G2oLoader::load(f.path());
939 assert!(
940 matches!(result, Err(IoError::InvalidNumber { .. })),
941 "invalid translation in VERTEX_SE3:QUAT should return InvalidNumber"
942 );
943 Ok(())
944 }
945
946 #[test]
947 fn test_parse_vertex_se3_invalid_quaternion_field() -> TestResult {
948 let mut f = NamedTempFile::new()?;
949 writeln!(f, "VERTEX_SE3:QUAT 0 1.0 2.0 3.0 bad 0.0 0.0 1.0")?;
950 f.flush()?;
951 let result = G2oLoader::load(f.path());
952 assert!(
953 matches!(result, Err(IoError::InvalidNumber { .. })),
954 "invalid quaternion field should return InvalidNumber"
955 );
956 Ok(())
957 }
958
959 #[test]
960 fn test_parse_duplicate_vertex_se3() -> TestResult {
961 let mut f = NamedTempFile::new()?;
962 writeln!(f, "VERTEX_SE3:QUAT 7 1.0 0.0 0.0 0.0 0.0 0.0 1.0")?;
963 writeln!(f, "VERTEX_SE3:QUAT 7 2.0 0.0 0.0 0.0 0.0 0.0 1.0")?;
964 f.flush()?;
965 let result = G2oLoader::load(f.path());
966 assert!(
967 matches!(result, Err(IoError::DuplicateVertex { id: 7 })),
968 "duplicate VERTEX_SE3:QUAT ID should return DuplicateVertex"
969 );
970 Ok(())
971 }
972
973 #[test]
974 fn test_parse_vertex_se3_invalid_id() -> TestResult {
975 let mut f = NamedTempFile::new()?;
976 writeln!(f, "VERTEX_SE3:QUAT bad 1.0 2.0 3.0 0.0 0.0 0.0 1.0")?;
977 f.flush()?;
978 let result = G2oLoader::load(f.path());
979 assert!(
980 matches!(result, Err(IoError::InvalidNumber { .. })),
981 "invalid id in VERTEX_SE3:QUAT should return InvalidNumber"
982 );
983 Ok(())
984 }
985
986 #[test]
987 fn test_parse_vertex_se3_invalid_y() -> TestResult {
988 let mut f = NamedTempFile::new()?;
989 writeln!(f, "VERTEX_SE3:QUAT 0 1.0 bad 3.0 0.0 0.0 0.0 1.0")?;
990 f.flush()?;
991 let result = G2oLoader::load(f.path());
992 assert!(
993 matches!(result, Err(IoError::InvalidNumber { .. })),
994 "invalid y in VERTEX_SE3:QUAT should return InvalidNumber"
995 );
996 Ok(())
997 }
998
999 #[test]
1000 fn test_parse_vertex_se3_invalid_z() -> TestResult {
1001 let mut f = NamedTempFile::new()?;
1002 writeln!(f, "VERTEX_SE3:QUAT 0 1.0 2.0 bad 0.0 0.0 0.0 1.0")?;
1003 f.flush()?;
1004 let result = G2oLoader::load(f.path());
1005 assert!(
1006 matches!(result, Err(IoError::InvalidNumber { .. })),
1007 "invalid z in VERTEX_SE3:QUAT should return InvalidNumber"
1008 );
1009 Ok(())
1010 }
1011
1012 #[test]
1013 fn test_parse_vertex_se3_invalid_qy() -> TestResult {
1014 let mut f = NamedTempFile::new()?;
1015 writeln!(f, "VERTEX_SE3:QUAT 0 1.0 2.0 3.0 0.0 bad 0.0 1.0")?;
1016 f.flush()?;
1017 let result = G2oLoader::load(f.path());
1018 assert!(
1019 matches!(result, Err(IoError::InvalidNumber { .. })),
1020 "invalid qy in VERTEX_SE3:QUAT should return InvalidNumber"
1021 );
1022 Ok(())
1023 }
1024
1025 #[test]
1026 fn test_parse_vertex_se3_invalid_qz() -> TestResult {
1027 let mut f = NamedTempFile::new()?;
1028 writeln!(f, "VERTEX_SE3:QUAT 0 1.0 2.0 3.0 0.0 0.0 bad 1.0")?;
1029 f.flush()?;
1030 let result = G2oLoader::load(f.path());
1031 assert!(
1032 matches!(result, Err(IoError::InvalidNumber { .. })),
1033 "invalid qz in VERTEX_SE3:QUAT should return InvalidNumber"
1034 );
1035 Ok(())
1036 }
1037
1038 #[test]
1039 fn test_parse_vertex_se3_invalid_qw() -> TestResult {
1040 let mut f = NamedTempFile::new()?;
1041 writeln!(f, "VERTEX_SE3:QUAT 0 1.0 2.0 3.0 0.0 0.0 0.0 bad")?;
1042 f.flush()?;
1043 let result = G2oLoader::load(f.path());
1044 assert!(
1045 matches!(result, Err(IoError::InvalidNumber { .. })),
1046 "invalid qw in VERTEX_SE3:QUAT should return InvalidNumber"
1047 );
1048 Ok(())
1049 }
1050
1051 #[test]
1056 fn test_parse_edge_se2_missing_fields() -> TestResult {
1057 let mut f = NamedTempFile::new()?;
1058 writeln!(f, "EDGE_SE2 0 1 1.0 0.0")?; f.flush()?;
1060 let result = G2oLoader::load(f.path());
1061 assert!(
1062 matches!(result, Err(IoError::MissingFields { .. })),
1063 "EDGE_SE2 with too few fields should return MissingFields"
1064 );
1065 Ok(())
1066 }
1067
1068 #[test]
1069 fn test_parse_edge_se2_invalid_from_id() -> TestResult {
1070 let mut f = NamedTempFile::new()?;
1071 writeln!(
1072 f,
1073 "EDGE_SE2 bad 1 1.0 0.0 0.0 500.0 0.0 0.0 500.0 0.0 200.0"
1074 )?;
1075 f.flush()?;
1076 let result = G2oLoader::load(f.path());
1077 assert!(
1078 matches!(result, Err(IoError::InvalidNumber { .. })),
1079 "invalid from-ID in EDGE_SE2 should return InvalidNumber"
1080 );
1081 Ok(())
1082 }
1083
1084 #[test]
1085 fn test_parse_edge_se2_invalid_measurement() -> TestResult {
1086 let mut f = NamedTempFile::new()?;
1087 writeln!(f, "EDGE_SE2 0 1 bad 0.0 0.0 500.0 0.0 0.0 500.0 0.0 200.0")?;
1088 f.flush()?;
1089 let result = G2oLoader::load(f.path());
1090 assert!(
1091 result.is_err(),
1092 "invalid measurement in EDGE_SE2 should return error"
1093 );
1094 Ok(())
1095 }
1096
1097 #[test]
1098 fn test_parse_edge_se2_invalid_to_id() -> TestResult {
1099 let mut f = NamedTempFile::new()?;
1100 writeln!(
1101 f,
1102 "EDGE_SE2 0 bad 1.0 0.0 0.0 500.0 0.0 0.0 500.0 0.0 200.0"
1103 )?;
1104 f.flush()?;
1105 let result = G2oLoader::load(f.path());
1106 assert!(
1107 matches!(result, Err(IoError::InvalidNumber { .. })),
1108 "invalid to-ID in EDGE_SE2 should return InvalidNumber"
1109 );
1110 Ok(())
1111 }
1112
1113 #[test]
1114 fn test_parse_edge_se2_invalid_dy() -> TestResult {
1115 let mut f = NamedTempFile::new()?;
1116 writeln!(f, "EDGE_SE2 0 1 1.0 bad 0.0 500.0 0.0 0.0 500.0 0.0 200.0")?;
1117 f.flush()?;
1118 let result = G2oLoader::load(f.path());
1119 assert!(
1120 result.is_err(),
1121 "invalid dy in EDGE_SE2 should return error"
1122 );
1123 Ok(())
1124 }
1125
1126 #[test]
1127 fn test_parse_edge_se2_invalid_dtheta() -> TestResult {
1128 let mut f = NamedTempFile::new()?;
1129 writeln!(f, "EDGE_SE2 0 1 1.0 0.0 bad 500.0 0.0 0.0 500.0 0.0 200.0")?;
1130 f.flush()?;
1131 let result = G2oLoader::load(f.path());
1132 assert!(
1133 result.is_err(),
1134 "invalid dtheta in EDGE_SE2 should return error"
1135 );
1136 Ok(())
1137 }
1138
1139 #[test]
1140 fn test_parse_edge_se2_invalid_info_matrix() -> TestResult {
1141 let mut f = NamedTempFile::new()?;
1142 writeln!(f, "EDGE_SE2 0 1 1.0 0.0 0.0 bad 0.0 0.0 500.0 0.0 200.0")?;
1143 f.flush()?;
1144 let result = G2oLoader::load(f.path());
1145 assert!(
1146 result.is_err(),
1147 "invalid info-matrix value in EDGE_SE2 should return error"
1148 );
1149 Ok(())
1150 }
1151
1152 #[test]
1157 fn test_parse_edge_se3_missing_fields() -> TestResult {
1158 let mut f = NamedTempFile::new()?;
1159 writeln!(f, "EDGE_SE3:QUAT 0 1 1.0 0.0")?; f.flush()?;
1161 let result = G2oLoader::load(f.path());
1162 assert!(
1163 matches!(result, Err(IoError::MissingFields { .. })),
1164 "EDGE_SE3:QUAT with too few fields should return MissingFields"
1165 );
1166 Ok(())
1167 }
1168
1169 #[test]
1170 fn test_parse_edge_se3_invalid_translation() -> TestResult {
1171 let mut f = NamedTempFile::new()?;
1172 writeln!(f, "EDGE_SE3:QUAT 0 1 bad 0.0 0.0 0.0 0.0 0.0 1.0")?;
1174 f.flush()?;
1175 let result = G2oLoader::load(f.path());
1176 assert!(
1177 matches!(result, Err(IoError::InvalidNumber { .. })),
1178 "invalid translation in EDGE_SE3:QUAT should return InvalidNumber"
1179 );
1180 Ok(())
1181 }
1182
1183 #[test]
1184 fn test_parse_edge_se3_invalid_quaternion_field() -> TestResult {
1185 let mut f = NamedTempFile::new()?;
1186 let info_vals =
1188 "1.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 1.0";
1189 writeln!(
1190 f,
1191 "EDGE_SE3:QUAT 0 1 1.0 0.0 0.0 0.0 0.0 bad 1.0 {}",
1192 info_vals
1193 )?;
1194 f.flush()?;
1195 let result = G2oLoader::load(f.path());
1196 assert!(
1197 matches!(result, Err(IoError::InvalidNumber { .. })),
1198 "invalid quaternion field in EDGE_SE3:QUAT should return InvalidNumber"
1199 );
1200 Ok(())
1201 }
1202
1203 #[test]
1204 fn test_parse_edge_se3_invalid_from_id() -> TestResult {
1205 let mut f = NamedTempFile::new()?;
1206 let info_vals =
1207 "1.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 1.0";
1208 writeln!(
1209 f,
1210 "EDGE_SE3:QUAT bad 1 1.0 0.0 0.0 0.0 0.0 0.0 1.0 {}",
1211 info_vals
1212 )?;
1213 f.flush()?;
1214 let result = G2oLoader::load(f.path());
1215 assert!(
1216 matches!(result, Err(IoError::InvalidNumber { .. })),
1217 "invalid from-id in EDGE_SE3:QUAT should return InvalidNumber"
1218 );
1219 Ok(())
1220 }
1221
1222 #[test]
1223 fn test_parse_edge_se3_invalid_to_id() -> TestResult {
1224 let mut f = NamedTempFile::new()?;
1225 let info_vals =
1226 "1.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 1.0";
1227 writeln!(
1228 f,
1229 "EDGE_SE3:QUAT 0 bad 1.0 0.0 0.0 0.0 0.0 0.0 1.0 {}",
1230 info_vals
1231 )?;
1232 f.flush()?;
1233 let result = G2oLoader::load(f.path());
1234 assert!(
1235 matches!(result, Err(IoError::InvalidNumber { .. })),
1236 "invalid to-id in EDGE_SE3:QUAT should return InvalidNumber"
1237 );
1238 Ok(())
1239 }
1240
1241 #[test]
1242 fn test_parse_edge_se3_invalid_ty() -> TestResult {
1243 let mut f = NamedTempFile::new()?;
1244 writeln!(f, "EDGE_SE3:QUAT 0 1 1.0 bad 0.0 0.0 0.0 0.0 1.0")?;
1245 f.flush()?;
1246 let result = G2oLoader::load(f.path());
1247 assert!(
1248 matches!(result, Err(IoError::InvalidNumber { .. })),
1249 "invalid ty in EDGE_SE3:QUAT should return InvalidNumber"
1250 );
1251 Ok(())
1252 }
1253
1254 #[test]
1255 fn test_parse_edge_se3_invalid_tz() -> TestResult {
1256 let mut f = NamedTempFile::new()?;
1257 writeln!(f, "EDGE_SE3:QUAT 0 1 1.0 0.0 bad 0.0 0.0 0.0 1.0")?;
1258 f.flush()?;
1259 let result = G2oLoader::load(f.path());
1260 assert!(
1261 matches!(result, Err(IoError::InvalidNumber { .. })),
1262 "invalid tz in EDGE_SE3:QUAT should return InvalidNumber"
1263 );
1264 Ok(())
1265 }
1266
1267 #[test]
1268 fn test_parse_edge_se3_invalid_qx() -> TestResult {
1269 let mut f = NamedTempFile::new()?;
1270 writeln!(f, "EDGE_SE3:QUAT 0 1 1.0 0.0 0.0 bad 0.0 0.0 1.0")?;
1271 f.flush()?;
1272 let result = G2oLoader::load(f.path());
1273 assert!(
1274 matches!(result, Err(IoError::InvalidNumber { .. })),
1275 "invalid qx in EDGE_SE3:QUAT should return InvalidNumber"
1276 );
1277 Ok(())
1278 }
1279
1280 #[test]
1281 fn test_parse_edge_se3_invalid_qy() -> TestResult {
1282 let mut f = NamedTempFile::new()?;
1283 writeln!(f, "EDGE_SE3:QUAT 0 1 1.0 0.0 0.0 0.0 bad 0.0 1.0")?;
1284 f.flush()?;
1285 let result = G2oLoader::load(f.path());
1286 assert!(
1287 matches!(result, Err(IoError::InvalidNumber { .. })),
1288 "invalid qy in EDGE_SE3:QUAT should return InvalidNumber"
1289 );
1290 Ok(())
1291 }
1292
1293 #[test]
1294 fn test_parse_edge_se3_invalid_qw() -> TestResult {
1295 let mut f = NamedTempFile::new()?;
1296 let info_vals =
1297 "1.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 1.0";
1298 writeln!(
1299 f,
1300 "EDGE_SE3:QUAT 0 1 1.0 0.0 0.0 0.0 0.0 0.0 bad {}",
1301 info_vals
1302 )?;
1303 f.flush()?;
1304 let result = G2oLoader::load(f.path());
1305 assert!(
1306 matches!(result, Err(IoError::InvalidNumber { .. })),
1307 "invalid qw in EDGE_SE3:QUAT should return InvalidNumber"
1308 );
1309 Ok(())
1310 }
1311
1312 #[test]
1313 fn test_parse_edge_se3_invalid_info_matrix() -> TestResult {
1314 let mut f = NamedTempFile::new()?;
1315 let info_vals = "bad 0.0 0.0 0.0 0.0 0.0 100.0 0.0 0.0 0.0 0.0 100.0 0.0 0.0 0.0 100.0 0.0 0.0 100.0 0.0 100.0";
1316 writeln!(
1317 f,
1318 "EDGE_SE3:QUAT 0 1 1.0 0.0 0.0 0.0 0.0 0.0 1.0 {}",
1319 info_vals
1320 )?;
1321 f.flush()?;
1322 let result = G2oLoader::load(f.path());
1323 assert!(
1324 result.is_err(),
1325 "invalid info-matrix values in EDGE_SE3:QUAT should return error"
1326 );
1327 Ok(())
1328 }
1329
1330 #[test]
1335 fn test_parse_comment_and_empty_lines_ignored() -> TestResult {
1336 let mut f = NamedTempFile::new()?;
1337 writeln!(f, "# This is a comment")?;
1338 writeln!(f, "VERTEX_SE2 0 1.0 2.0 0.0")?;
1339 writeln!(f)?; writeln!(f, "VERTEX_SE2 1 3.0 4.0 0.0")?;
1341 f.flush()?;
1342 let graph = G2oLoader::load(f.path())?;
1343 assert_eq!(
1344 graph.vertices_se2.len(),
1345 2,
1346 "comments and empty lines should be ignored"
1347 );
1348 Ok(())
1349 }
1350
1351 #[test]
1352 fn test_parse_unknown_token_ignored() -> TestResult {
1353 let mut f = NamedTempFile::new()?;
1354 writeln!(f, "FIX 0")?; writeln!(f, "VERTEX_SE2 0 0.0 0.0 0.0")?;
1356 f.flush()?;
1357 let graph = G2oLoader::load(f.path())?;
1358 assert_eq!(graph.vertices_se2.len(), 1);
1359 Ok(())
1360 }
1361
1362 #[test]
1367 fn test_parse_parallel_large_se2_file() -> TestResult {
1368 let mut f = NamedTempFile::new()?;
1369 for i in 0..1001usize {
1370 writeln!(f, "VERTEX_SE2 {} {} {} 0.0", i, i as f64, i as f64)?;
1371 }
1372 f.flush()?;
1373 let graph = G2oLoader::load(f.path())?;
1374 assert_eq!(graph.vertices_se2.len(), 1001);
1375 Ok(())
1376 }
1377
1378 #[test]
1379 fn test_parse_parallel_with_se2_edges() -> TestResult {
1380 let mut f = NamedTempFile::new()?;
1381 for i in 0..1001usize {
1382 writeln!(f, "VERTEX_SE2 {} 0.0 0.0 0.0", i)?;
1383 }
1384 writeln!(f, "EDGE_SE2 0 1 1.0 0.0 0.0 500.0 0.0 0.0 500.0 0.0 200.0")?;
1385 f.flush()?;
1386 let graph = G2oLoader::load(f.path())?;
1387 assert_eq!(graph.vertices_se2.len(), 1001);
1388 assert_eq!(graph.edges_se2.len(), 1);
1389 Ok(())
1390 }
1391
1392 #[test]
1393 fn test_parse_parallel_with_se3_vertices() -> TestResult {
1394 let mut f = NamedTempFile::new()?;
1395 for i in 0..1001usize {
1396 writeln!(
1397 f,
1398 "VERTEX_SE3:QUAT {} 0.0 0.0 {} 0.0 0.0 0.0 1.0",
1399 i, i as f64
1400 )?;
1401 }
1402 f.flush()?;
1403 let graph = G2oLoader::load(f.path())?;
1404 assert_eq!(graph.vertices_se3.len(), 1001);
1405 Ok(())
1406 }
1407
1408 #[test]
1409 fn test_parse_parallel_with_se3_edges() -> TestResult {
1410 let mut f = NamedTempFile::new()?;
1411 for i in 0..1001usize {
1412 writeln!(f, "VERTEX_SE3:QUAT {} 0.0 0.0 0.0 0.0 0.0 0.0 1.0", i)?;
1413 }
1414 let info_vals =
1415 "1.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 1.0";
1416 writeln!(
1417 f,
1418 "EDGE_SE3:QUAT 0 1 1.0 0.0 0.0 0.0 0.0 0.0 1.0 {}",
1419 info_vals
1420 )?;
1421 f.flush()?;
1422 let graph = G2oLoader::load(f.path())?;
1423 assert_eq!(graph.vertices_se3.len(), 1001);
1424 assert_eq!(graph.edges_se3.len(), 1);
1425 Ok(())
1426 }
1427
1428 #[test]
1429 fn test_parse_parallel_duplicate_vertex_returns_error() -> TestResult {
1430 let mut f = NamedTempFile::new()?;
1431 for i in 0..1000usize {
1432 writeln!(f, "VERTEX_SE2 {} 0.0 0.0 0.0", i)?;
1433 }
1434 writeln!(f, "VERTEX_SE2 0 9.0 9.0 0.0")?; f.flush()?;
1436 let result = G2oLoader::load(f.path());
1437 assert!(
1438 matches!(result, Err(IoError::DuplicateVertex { id: 0 })),
1439 "duplicate vertex in parallel parse should return DuplicateVertex"
1440 );
1441 Ok(())
1442 }
1443
1444 #[test]
1445 fn test_parse_parallel_comment_and_empty_lines_ignored() -> TestResult {
1446 let mut f = NamedTempFile::new()?;
1447 writeln!(f, "# parallel parse comment test")?;
1448 for i in 0..1000usize {
1449 writeln!(f, "VERTEX_SE2 {} 0.0 0.0 0.0", i)?;
1450 }
1451 writeln!(f)?; f.flush()?;
1453 let graph = G2oLoader::load(f.path())?;
1454 assert_eq!(graph.vertices_se2.len(), 1000);
1455 Ok(())
1456 }
1457}