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