1414
1515use rclrs:: { IntoPrimitiveOptions , Node , PrimitiveOptions } ;
1616use ros_env:: {
17+ geometry_msgs:: msg:: Pose ,
1718 nav_msgs:: msg:: OccupancyGrid ,
1819 rmf_layered_map_msgs:: msg:: { MapRegionPatch , MapRegionUpdate } ,
1920 rmf_prototype_msgs:: msg:: Region ,
@@ -33,6 +34,7 @@ enum DynamicUpdateType {
3334struct DynamicObservation {
3435 source_id : String ,
3536 map_name : String ,
37+ frame_id : String ,
3638 update_type : DynamicUpdateType ,
3739 occupancy_value : i8 ,
3840 regions : Vec < Region > ,
@@ -80,7 +82,7 @@ impl LayeredMap {
8082 }
8183
8284 pub fn ingest_region_update ( & mut self , update : MapRegionUpdate , now_nsec : i128 ) -> bool {
83- if update . source . header . stamp . sec == 0 && update. source . header . stamp . nanosec == 0 {
85+ if source_validation_error ( & update, self . static_frame_id ( ) ) . is_some ( ) {
8486 return false ;
8587 }
8688
@@ -110,6 +112,8 @@ impl LayeredMap {
110112 }
111113
112114 let default_ttl_sec = update. source . default_ttl_sec ;
115+ let frame_id = update. source . header . frame_id ;
116+ let robot_pose = update. source . robot_pose ;
113117 let mut patches = update. patches ;
114118 patches. sort_by_key ( |patch| match patch. update_type {
115119 MapRegionPatch :: UPDATE_CLEAR => 0 ,
@@ -128,6 +132,7 @@ impl LayeredMap {
128132 . regions
129133 . into_iter ( )
130134 . filter ( |region| region_validation_error ( region) . is_none ( ) )
135+ . map ( |region| transform_region ( region, & robot_pose) )
131136 . collect ( ) ;
132137 if regions. is_empty ( ) {
133138 continue ;
@@ -155,6 +160,7 @@ impl LayeredMap {
155160 self . dynamic_observations . push ( DynamicObservation {
156161 source_id : key. source_id . clone ( ) ,
157162 map_name : key. map_name . clone ( ) ,
163+ frame_id : frame_id. clone ( ) ,
158164 update_type,
159165 occupancy_value,
160166 regions,
@@ -187,25 +193,30 @@ impl LayeredMap {
187193 pub fn compose ( & self ) -> Option < OccupancyGrid > {
188194 let mut composed = self . static_grid . clone ( ) ?;
189195 normalize_grid_data ( & mut composed) ;
196+ let frame_id = composed. header . frame_id . clone ( ) ;
190197
191198 for observation in self
192199 . dynamic_observations
193200 . iter ( )
194- . filter ( |obs| obs. update_type == DynamicUpdateType :: Clear )
201+ . filter ( |obs| obs. frame_id == frame_id && obs . update_type == DynamicUpdateType :: Clear )
195202 {
196203 rasterize_observation ( & mut composed, observation) ;
197204 }
198205
199- for observation in self
200- . dynamic_observations
201- . iter ( )
202- . filter ( |obs| obs. update_type == DynamicUpdateType :: Obstacle )
203- {
206+ for observation in self . dynamic_observations . iter ( ) . filter ( |obs| {
207+ obs. frame_id == frame_id && obs. update_type == DynamicUpdateType :: Obstacle
208+ } ) {
204209 rasterize_observation ( & mut composed, observation) ;
205210 }
206211
207212 Some ( composed)
208213 }
214+
215+ fn static_frame_id ( & self ) -> Option < & str > {
216+ self . static_grid
217+ . as_ref ( )
218+ . map ( |grid| grid. header . frame_id . as_str ( ) )
219+ }
209220}
210221
211222impl Default for LayeredMap {
@@ -269,7 +280,7 @@ impl LayeredMapServer {
269280 }
270281
271282 fn handle_region_update ( & mut self , msg : MapRegionUpdate ) {
272- log_region_update_errors ( & self . node , & msg) ;
283+ log_region_update_errors ( & self . node , & msg, self . map . static_frame_id ( ) ) ;
273284 let now_nsec = self . now_nsec ( ) ;
274285 if self . map . ingest_region_update ( msg, now_nsec) {
275286 rclrs:: log!(
@@ -386,7 +397,16 @@ fn region_update_qos(topic: &str) -> PrimitiveOptions<'_> {
386397 topic. keep_last ( MAP_QOS_DEPTH ) . reliable ( )
387398}
388399
389- fn log_region_update_errors ( node : & Node , update : & MapRegionUpdate ) {
400+ fn log_region_update_errors ( node : & Node , update : & MapRegionUpdate , expected_frame : Option < & str > ) {
401+ if let Some ( error) = source_validation_error ( update, expected_frame) {
402+ rclrs:: log_error!(
403+ node. logger( ) ,
404+ "Ignoring map region update from '{}': {}" ,
405+ update. source. source_id,
406+ error
407+ ) ;
408+ }
409+
390410 for patch in & update. patches {
391411 if !matches ! (
392412 patch. update_type,
@@ -408,6 +428,56 @@ fn log_region_update_errors(node: &Node, update: &MapRegionUpdate) {
408428 }
409429}
410430
431+ fn source_validation_error (
432+ update : & MapRegionUpdate ,
433+ expected_frame : Option < & str > ,
434+ ) -> Option < String > {
435+ let stamp = & update. source . header . stamp ;
436+ if stamp. sec == 0 && stamp. nanosec == 0 {
437+ return Some ( "source timestamp must be non-zero" . to_string ( ) ) ;
438+ }
439+
440+ let frame_id = update. source . header . frame_id . as_str ( ) ;
441+ if frame_id. is_empty ( ) {
442+ return Some ( "source frame must not be empty" . to_string ( ) ) ;
443+ }
444+
445+ if let Some ( expected) = expected_frame {
446+ if !expected. is_empty ( ) && expected != frame_id {
447+ return Some ( format ! (
448+ "source frame '{}' does not match map frame '{}'" ,
449+ frame_id, expected
450+ ) ) ;
451+ }
452+ }
453+
454+ let pose = & update. source . robot_pose ;
455+ if ![
456+ pose. position . x ,
457+ pose. position . y ,
458+ pose. position . z ,
459+ pose. orientation . x ,
460+ pose. orientation . y ,
461+ pose. orientation . z ,
462+ pose. orientation . w ,
463+ ]
464+ . into_iter ( )
465+ . all ( f64:: is_finite)
466+ {
467+ return Some ( "source pose must contain finite values" . to_string ( ) ) ;
468+ }
469+
470+ let orientation_norm = pose. orientation . x * pose. orientation . x
471+ + pose. orientation . y * pose. orientation . y
472+ + pose. orientation . z * pose. orientation . z
473+ + pose. orientation . w * pose. orientation . w ;
474+ if orientation_norm <= f64:: EPSILON {
475+ return Some ( "source pose must contain a valid orientation" . to_string ( ) ) ;
476+ }
477+
478+ None
479+ }
480+
411481fn region_validation_error ( region : & Region ) -> Option < String > {
412482 if region. points . len ( ) < 2 {
413483 return Some ( "region must contain at least one x/y point pair" . to_string ( ) ) ;
@@ -432,13 +502,49 @@ fn region_validation_error(region: &Region) -> Option<String> {
432502 }
433503}
434504
435- fn is_supported_region_hint ( hint : u8 ) -> bool {
505+ fn is_rasterizable_region_hint ( hint : u8 ) -> bool {
436506 matches ! (
437507 hint,
438- Region :: HINT_POINT | Region :: HINT_AXIS_ALIGNED_RECTANGLE
508+ Region :: HINT_POINT | Region :: HINT_AXIS_ALIGNED_RECTANGLE | Region :: HINT_CONVEX_POLYGON
439509 )
440510}
441511
512+ fn transform_region ( mut region : Region , pose : & Pose ) -> Region {
513+ let points = point_pairs ( & region) ;
514+ let points = if region. hint == Region :: HINT_AXIS_ALIGNED_RECTANGLE {
515+ let ( min_x, min_y, max_x, max_y) = bounds ( & points) ;
516+ region. hint = Region :: HINT_CONVEX_POLYGON ;
517+ vec ! [
518+ ( min_x, min_y) ,
519+ ( max_x, min_y) ,
520+ ( max_x, max_y) ,
521+ ( min_x, max_y) ,
522+ ]
523+ } else {
524+ points
525+ } ;
526+
527+ let ( cos_yaw, sin_yaw) = planar_rotation ( pose) ;
528+ region. points = points
529+ . into_iter ( )
530+ . flat_map ( |( x, y) | {
531+ let map_x = pose. position . x + cos_yaw * x - sin_yaw * y;
532+ let map_y = pose. position . y + sin_yaw * x + cos_yaw * y;
533+ [ map_x as f32 , map_y as f32 ]
534+ } )
535+ . collect ( ) ;
536+ region
537+ }
538+
539+ fn planar_rotation ( pose : & Pose ) -> ( f64 , f64 ) {
540+ let q = & pose. orientation ;
541+ let norm = q. x * q. x + q. y * q. y + q. z * q. z + q. w * q. w ;
542+ let sin_yaw = 2.0 * ( q. w * q. z + q. x * q. y ) / norm;
543+ let cos_yaw = 1.0 - 2.0 * ( q. y * q. y + q. z * q. z ) / norm;
544+ let yaw = sin_yaw. atan2 ( cos_yaw) ;
545+ ( yaw. cos ( ) , yaw. sin ( ) )
546+ }
547+
442548fn normalize_grid_data ( grid : & mut OccupancyGrid ) {
443549 let Some ( expected_len) = grid_len ( grid) else {
444550 grid. data . clear ( ) ;
@@ -477,7 +583,7 @@ fn rasterized_indices(grid: &OccupancyGrid, region: &Region) -> Vec<usize> {
477583 return Vec :: new ( ) ;
478584 }
479585
480- if !is_supported_region_hint ( region. hint ) {
586+ if !is_rasterizable_region_hint ( region. hint ) {
481587 return Vec :: new ( ) ;
482588 }
483589
@@ -666,6 +772,10 @@ mod tests {
666772
667773 fn static_grid ( width : u32 , height : u32 , value : i8 ) -> OccupancyGrid {
668774 OccupancyGrid {
775+ header : Header {
776+ frame_id : "map" . to_string ( ) ,
777+ ..Default :: default ( )
778+ } ,
669779 info : MapMetaData {
670780 resolution : 1.0 ,
671781 width,
@@ -705,6 +815,7 @@ mod tests {
705815 ..Default :: default ( )
706816 } ;
707817 source. header . stamp . sec = 1 ;
818+ source. robot_pose . orientation . w = 1.0 ;
708819 source
709820 }
710821
@@ -785,6 +896,24 @@ mod tests {
785896 assert_eq ! ( composed. data[ 0 ] , 0 ) ;
786897 }
787898
899+ #[ test]
900+ fn robot_local_regions_are_transformed_into_the_map_frame ( ) {
901+ let mut map = LayeredMap :: default ( ) ;
902+ map. set_static_map ( static_grid ( 5 , 5 , 0 ) ) ;
903+
904+ let mut update = update ( MapRegionPatch :: UPDATE_OBSTACLE , vec ! [ point( 1.0 , 0.0 ) ] ) ;
905+ update. source . robot_pose . position . x = 2.25 ;
906+ update. source . robot_pose . position . y = 1.25 ;
907+ update. source . robot_pose . orientation . z = std:: f64:: consts:: FRAC_1_SQRT_2 ;
908+ update. source . robot_pose . orientation . w = std:: f64:: consts:: FRAC_1_SQRT_2 ;
909+
910+ assert ! ( map. ingest_region_update( update, 0 ) ) ;
911+
912+ let composed = map. compose ( ) . unwrap ( ) ;
913+ assert_eq ! ( composed. data[ 2 * 5 + 2 ] , 100 ) ;
914+ assert_eq ! ( composed. data[ 0 ] , 0 ) ;
915+ }
916+
788917 #[ test]
789918 fn out_of_bounds_regions_do_not_touch_the_grid ( ) {
790919 let mut map = LayeredMap :: default ( ) ;
@@ -852,6 +981,18 @@ mod tests {
852981 assert_eq ! ( map. dynamic_observation_count( ) , 0 ) ;
853982 }
854983
984+ #[ test]
985+ fn updates_in_a_different_global_frame_are_rejected ( ) {
986+ let mut map = LayeredMap :: default ( ) ;
987+ map. set_static_map ( static_grid ( 3 , 3 , 0 ) ) ;
988+
989+ let mut update = update ( MapRegionPatch :: UPDATE_OBSTACLE , vec ! [ point( 1.0 , 1.0 ) ] ) ;
990+ update. source . header . frame_id = "odom" . to_string ( ) ;
991+
992+ assert ! ( !map. ingest_region_update( update, NANOS_PER_SECOND ) ) ;
993+ assert_eq ! ( map. dynamic_observation_count( ) , 0 ) ;
994+ }
995+
855996 #[ test]
856997 fn expired_observations_are_removed ( ) {
857998 let mut map = LayeredMap :: default ( ) ;
0 commit comments