Skip to content

Commit f20044d

Browse files
committed
Adding UTM conversion; tests complete show all quad works fine now after some finagling
1 parent 20bb881 commit f20044d

File tree

8 files changed

+91
-41
lines changed

8 files changed

+91
-41
lines changed

nav2_complete_coverage_msgs/action/ComputeCoveragePath.action

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ bool generate_path True
88
# The field specification to use.
99
# If providing gml file with multiple fields, specify which with gml_field_id
1010
# If using polygons, bounding polygon must be first, followed by inner cutouts
11+
# Both must specify if the data is cartesian or GPS coordinates
12+
bool frame_cartesian True
1113
bool use_gml_file False
1214
string gml_field
1315
uint16 gml_field_id 0

nav2_coverage/README.md

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,19 +64,10 @@ Walk through
6464
- RQT
6565

6666
Future
67-
- Transform GPS to UTM zone properly + back to GPS on return (https://github.com/Fields2Cover/Fields2Cover/pull/95/files)
6867
- Use coverage exceptions
69-
- transform back into proper frame
7068

7169
- BT nodes to interact with in in the Nav2 autonomy framework
7270
- A couple of utilities for the BT nodes to iterate through the swath-turn combos
7371
- Turn tester into Python API for it
7472
- A Navigator API plugin for coverage specific tasks
7573
- A BT XML to pair with the navigator + BT nodes to initial system demos
76-
77-
Q
78-
- Demo just have it follow the nav path on a coverage pattern, or something else more complex (or leave to you to determine?)
79-
--> just normal path it up
80-
- Other F2C add on features you care about? --> designed to expand or contribute back to F2C
81-
- headland pass!
82-
- inner boundary not to go through (...) can wait, firefly might do.

nav2_coverage/include/nav2_coverage/utils.hpp

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <algorithm>
2121
#include <memory>
2222

23+
#include "fields2cover.h" // NOLINT
2324
#include "rclcpp/rclcpp.hpp"
2425
#include "nav2_coverage/types.hpp"
2526
#include "nav2_util/node_utils.hpp"
@@ -78,16 +79,25 @@ inline geometry_msgs::msg::PoseStamped toMsg(const PathState & state)
7879
/**
7980
* @brief Converts swaths to coverage path message for action client
8081
* @param swaths Swaths to convert. May be ordered or unordered.
82+
* @param Field Field to use for conversion from UTM if necessary
83+
* @param header header
84+
* @param bool if the origional CRS is cartesian or not requiring conversion
8185
* @return PathComponents Info for action server to utilize
8286
*/
8387
inline nav2_complete_coverage_msgs::msg::PathComponents toCoveragePathMsg(
84-
const Swaths & swaths, const bool ordered, const std_msgs::msg::Header & header)
88+
const Swaths & raw_swaths, const F2CField & field,
89+
const bool ordered, const std_msgs::msg::Header & header, const bool is_cartesian)
8590
{
8691
nav2_complete_coverage_msgs::msg::PathComponents msg;
8792
msg.contains_turns = false;
8893
msg.swaths_ordered = ordered;
8994
msg.header = header;
90-
msg.swaths.resize(swaths.size());
95+
msg.swaths.resize(raw_swaths.size());
96+
97+
Swaths swaths = raw_swaths;
98+
if (!is_cartesian) {
99+
swaths = f2c::Transform::transformToPrevCRS(raw_swaths, field);
100+
}
91101

92102
for (unsigned int i = 0; i != swaths.size(); i++) {
93103
msg.swaths[i].start = toMsg(swaths[i].startPoint());
@@ -100,24 +110,33 @@ inline nav2_complete_coverage_msgs::msg::PathComponents toCoveragePathMsg(
100110
/**
101111
* @brief Converts full path to coverage path message for action client
102112
* @param path Full path to convert
113+
* @param Field Field to use for conversion from UTM if necessary
114+
* @param header header
115+
* @param bool if the origional CRS is cartesian or not requiring conversion
103116
* @return PathComponents Info for action server to utilize
104117
*/
105118
inline nav2_complete_coverage_msgs::msg::PathComponents toCoveragePathMsg(
106-
const Path & path, const std_msgs::msg::Header & header)
119+
const Path & raw_path, const F2CField & field,
120+
const std_msgs::msg::Header & header, const bool is_cartesian)
107121
{
108122
using f2c::types::PathSectionType;
109123
nav2_complete_coverage_msgs::msg::PathComponents msg;
110124
msg.contains_turns = true;
111125
msg.swaths_ordered = true;
112126
msg.header = header;
113127

114-
if (path.states.size() == 0) {
128+
if (raw_path.states.size() == 0) {
115129
return msg;
116130
}
117131

118132
Point curr_swath_start(0.0, 0.0);
119133
nav_msgs::msg::Path * curr_turn = nullptr;
120134

135+
Path path = raw_path;
136+
if (!is_cartesian) {
137+
path = f2c::Transform::transformToPrevCRS(raw_path, field);
138+
}
139+
121140
PathSectionType curr_state = path.states[0].type;
122141
if (curr_state == PathSectionType::SWATH) {
123142
curr_swath_start = path.states[0].point;
@@ -176,19 +195,28 @@ inline nav2_complete_coverage_msgs::msg::PathComponents toCoveragePathMsg(
176195
* @brief Converts full path to nav_msgs/path message for action client, visualization
177196
* and use in direct-sending to a controller to replace the planner server
178197
* @param path Full path to convert
198+
* @param Field Field to use for conversion from UTM if necessary
199+
* @param header header
200+
* @param bool if the origional CRS is cartesian or not requiring conversion
179201
* @return nav_msgs/Path Path
180202
*/
181203
inline nav_msgs::msg::Path toNavPathMsg(
182-
const Path & path, const std_msgs::msg::Header & header)
204+
const Path & raw_path, const F2CField & field,
205+
const std_msgs::msg::Header & header, const bool is_cartesian)
183206
{
184207
using f2c::types::PathSectionType;
185208
nav_msgs::msg::Path msg;
186209
msg.header = header;
187210

188-
if (path.states.size() == 0) {
211+
if (raw_path.states.size() == 0) {
189212
return msg;
190213
}
191214

215+
Path path = raw_path;
216+
if (!is_cartesian) {
217+
path = f2c::Transform::transformToPrevCRS(raw_path, field);
218+
}
219+
192220
msg.poses.resize(path.states.size());
193221
for (unsigned int i = 0; i != path.states.size(); i++) {
194222
msg.poses[i] = toMsg(path.states[i]);
@@ -203,7 +231,7 @@ inline nav_msgs::msg::Path toNavPathMsg(
203231
* @param goal Goal to pase
204232
* @return Field field of goal polygons
205233
*/
206-
inline Field getFieldFromGoal(
234+
inline F2CField getFieldFromGoal(
207235
typename std::shared_ptr<const typename ComputeCoveragePath::Goal> goal)
208236
{
209237
auto polygons = goal->polygons;
@@ -236,7 +264,7 @@ inline Field getFieldFromGoal(
236264
field.addRing(inner_polygon);
237265
}
238266

239-
return field;
267+
return F2CField(Fields(field));
240268
}
241269

242270
/**

nav2_coverage/src/coverage_server.cpp

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -158,21 +158,26 @@ void CoverageServer::computeCoveragePath()
158158

159159
try {
160160
// (0) Obtain field to use
161+
bool cartesian_frame = goal->frame_cartesian;
161162
Field field;
163+
F2CField master_field;
162164
std::string frame_id;
163165
if (goal->use_gml_file) {
164166
F2CFields parse_field;
165167
f2c::Parser::importGml(goal->gml_field, parse_field);
166-
// TODO(SM): replace w/ UTM conversion (and back?) transformSwaths, transformPath
167-
// Path rtn_path = f2c::Transform::transformPath(path, field, "EPSG:4258");
168-
f2c::Transform::transform(parse_field[0], "EPSG:28992");
169-
field = parse_field[goal->gml_field_id].field.getGeometry(0);
170-
frame_id = parse_field[goal->gml_field_id].coord_sys;
168+
master_field = parse_field[goal->gml_field_id];
169+
frame_id = master_field.coord_sys;
171170
} else {
172-
field = util::getFieldFromGoal(goal);
171+
master_field = util::getFieldFromGoal(goal);
172+
master_field.setCRS(goal->frame_id);
173173
frame_id = goal->frame_id;
174174
}
175175

176+
if (!cartesian_frame) {
177+
f2c::Transform::transformToUTM(master_field);
178+
}
179+
field = master_field.field.getGeometry(0);
180+
176181
RCLCPP_INFO(
177182
get_logger(),
178183
"Generating coverage path in %s frame for zone with %zu outer nodes and %zu inner polygons.",
@@ -195,19 +200,25 @@ void CoverageServer::computeCoveragePath()
195200
Swaths route = route_gen_->generateRoute(swaths, goal->route_mode);
196201

197202
// (4) Optional: Generate connection turns between ordered swaths
203+
// Converts UTM back to GPS, if necessary, for action returns
198204
if (goal->generate_path) {
199205
Path path = path_gen_->generatePath(route, goal->path_mode);
200-
result->coverage_path = util::toCoveragePathMsg(path, header);
201-
result->nav_path = util::toNavPathMsg(path, header);
206+
result->coverage_path =
207+
util::toCoveragePathMsg(path, master_field, header, cartesian_frame);
208+
result->nav_path = util::toNavPathMsg(path, master_field, header, cartesian_frame);
202209
} else {
203-
result->coverage_path = util::toCoveragePathMsg(route, true, header);
210+
result->coverage_path =
211+
util::toCoveragePathMsg(route, master_field, true, header, cartesian_frame);
204212
}
205213
} else {
206-
result->coverage_path = util::toCoveragePathMsg(swaths, false, header);
214+
result->coverage_path =
215+
util::toCoveragePathMsg(swaths, master_field, false, header, cartesian_frame);
207216
}
208217

209218
auto cycle_duration = this->now() - start_time;
210219
result->planning_time = cycle_duration;
220+
221+
// Visualization always uses cartesian coordinates for convenience
211222
visualizer_->visualize(field, field_no_headland, result, header);
212223
action_server_->succeeded_current(result);
213224
} catch (CoverageException & e) {

nav2_coverage/test/test_field.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
<gml:outerBoundaryIs>
66
<gml:LinearRing>
77
<gml:coordinates>4.26199990317851,51.7859704975047 4.26203858931428,51.7860392024232 4.26234728296244,51.7867682098064 4.26283009086539,51.7878798462305 4.26321532471923,51.7887809885397 4.26344944239078,51.7893481817958 4.25601634525234,51.7906387243889 4.25603742148301,51.7905786368844 4.25662083047068,51.7889909449083 4.25732681137421,51.7870536505356 4.25749399420598,51.7866017400346 4.26195105582634,51.7858278333044 4.26199990317851,51.7859704975047</gml:coordinates>
8+
<!-- For Testing in Cartesian -->
9+
<!-- <gml:coordinates>0.0,0.0 2.78733,7.60181 25.3394,88.3711 60.5611,211.52 88.6878,311.358 105.814,374.206 -404.753,525.721 -403.403,519.014 -365.901,341.765 -320.543,125.493 -309.789,75.0412 -3.61568,-15.8186 0.0,0.0</gml:coordinates> -->
810
</gml:LinearRing>
911
</gml:outerBoundaryIs>
1012
</gml:Polygon>

nav2_coverage/test/test_server.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ TEST(ServerTest, testServerTransactions)
9191
client_node, "compute_coverage_path");
9292

9393
auto goal_msg = nav2_complete_coverage_msgs::action::ComputeCoveragePath::Goal();
94-
goal_msg.use_gml_file = true;
94+
goal_msg.use_gml_file = true; // Use file
95+
goal_msg.frame_cartesian = false; // Using GPS coordinates
9596
goal_msg.gml_field =
9697
ament_index_cpp::get_package_share_directory("nav2_coverage") + "/test_field.xml";
9798

nav2_coverage/test/test_utils.cpp

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,20 +77,21 @@ TEST(UtilsTests, TesttoCoveragePathMsg1)
7777
std_msgs::msg::Header header_in;
7878
header_in.frame_id = "test";
7979
Swaths swaths_in;
80+
F2CField field;
8081

8182
swaths_in.push_back(Swath());
8283
swaths_in.push_back(Swath());
8384
swaths_in.push_back(Swath());
8485

85-
auto msg = util::toCoveragePathMsg(swaths_in, ordered, header_in);
86+
auto msg = util::toCoveragePathMsg(swaths_in, field, ordered, header_in, true);
8687
EXPECT_EQ(msg.header.frame_id, "test");
8788
EXPECT_EQ(msg.swaths_ordered, false);
8889
EXPECT_EQ(msg.swaths.size(), 3u);
8990
EXPECT_EQ(msg.contains_turns, false);
9091

9192
ordered = true;
9293
swaths_in = Swaths();
93-
msg = util::toCoveragePathMsg(swaths_in, ordered, header_in);
94+
msg = util::toCoveragePathMsg(swaths_in, field, ordered, header_in, true);
9495
EXPECT_EQ(msg.swaths_ordered, true);
9596
EXPECT_EQ(msg.swaths.size(), 0u);
9697
EXPECT_EQ(msg.contains_turns, false);
@@ -102,8 +103,9 @@ TEST(UtilsTests, TesttoNavPathMsg)
102103
header_in.frame_id = "test";
103104
Path path_in;
104105
path_in.states.resize(10);
106+
F2CField field;
105107

106-
auto msg = util::toNavPathMsg(path_in, header_in);
108+
auto msg = util::toNavPathMsg(path_in, field, header_in, true);
107109
EXPECT_EQ(msg.header.frame_id, "test");
108110
EXPECT_EQ(msg.poses.size(), 10u);
109111
}
@@ -113,15 +115,17 @@ TEST(UtilsTests, TesttoCoveragePathMsg2)
113115
std_msgs::msg::Header header_in;
114116
header_in.frame_id = "test";
115117
Path path_in;
116-
auto msg = util::toCoveragePathMsg(path_in, header_in);
118+
F2CField field;
119+
120+
auto msg = util::toCoveragePathMsg(path_in, field, header_in, true);
117121
EXPECT_EQ(msg.swaths.size(), 0u);
118122
EXPECT_EQ(msg.turns.size(), 0u);
119123
EXPECT_EQ(msg.contains_turns, true);
120124
EXPECT_EQ(msg.swaths_ordered, true);
121125

122126
// states are not valid (e.g. non-marked as turn or swath)
123127
path_in.states.resize(10);
124-
EXPECT_THROW(util::toCoveragePathMsg(path_in, header_in), std::runtime_error);
128+
EXPECT_THROW(util::toCoveragePathMsg(path_in, field, header_in, true), std::runtime_error);
125129

126130
// Now lets make it valid
127131
using f2c::types::PathSectionType;
@@ -136,7 +140,7 @@ TEST(UtilsTests, TesttoCoveragePathMsg2)
136140
path_in.states[8].type = PathSectionType::SWATH;
137141
path_in.states[9].type = PathSectionType::SWATH;
138142

139-
msg = util::toCoveragePathMsg(path_in, header_in);
143+
msg = util::toCoveragePathMsg(path_in, field, header_in, true);
140144
EXPECT_EQ(msg.swaths.size(), 3u);
141145
EXPECT_EQ(msg.turns.size(), 2u);
142146
EXPECT_EQ(msg.turns[0].poses.size(), 2u);
@@ -152,7 +156,7 @@ TEST(UtilsTests, TesttoCoveragePathMsg2)
152156
path_in.states[8].type = PathSectionType::TURN;
153157
path_in.states[9].type = PathSectionType::TURN;
154158

155-
msg = util::toCoveragePathMsg(path_in, header_in);
159+
msg = util::toCoveragePathMsg(path_in, field, header_in, true);
156160
EXPECT_EQ(msg.swaths.size(), 2u);
157161
EXPECT_EQ(msg.turns.size(), 3u);
158162
EXPECT_EQ(msg.turns[0].poses.size(), 2u);
@@ -174,7 +178,7 @@ TEST(UtilsTests, TestgetFieldFromGoal)
174178
// Should work now, with a trivial polygon of 3 nodes of (0, 0)
175179
goal->polygons[0].coordinates[2].axis1 = 0.0;
176180
auto field = util::getFieldFromGoal(goal);
177-
EXPECT_EQ(field.getGeometry(0).size(), 3u);
181+
EXPECT_EQ(field.field.getGeometry(0).getGeometry(0).size(), 3u);
178182

179183
// Test with inner polygons, first invalid
180184
goal->polygons.resize(2);
@@ -185,8 +189,8 @@ TEST(UtilsTests, TestgetFieldFromGoal)
185189
// Now valid
186190
goal->polygons[0].coordinates[0].axis1 = 0.0;
187191
goal->polygons[1].coordinates[0].axis1 = 1.0;
188-
field = util::getFieldFromGoal(goal);
189-
EXPECT_EQ(field.getGeometry(1).size(), 3u);
192+
auto field2 = util::getFieldFromGoal(goal);
193+
EXPECT_EQ(field2.field.getGeometry(0).getGeometry(1).size(), 3u);
190194
}
191195

192196
} // namespace nav2_coverage

nav2_coverage/test/tester.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from enum import Enum
1717

1818
from action_msgs.msg import GoalStatus
19+
from ament_index_python.packages import get_package_share_directory
1920
from lifecycle_msgs.srv import ChangeState, GetState
2021
from nav2_complete_coverage_msgs.action import ComputeCoveragePath
2122
from nav2_complete_coverage_msgs.msg import Coordinate # Coordinates
@@ -124,19 +125,29 @@ def main():
124125
goal.generate_path = True
125126

126127
goal.use_gml_file = True
127-
goal.gml_field = '/home/steve/Documents/bonsai_ws/src/Fields2Cover/data/example1.xml'
128+
goal.frame_cartesian = True
129+
goal.gml_field = get_package_share_directory('nav2_coverage') + '/test_field.xml'
128130
goal.gml_field_id = 0
129131

130-
# Cartesian coordinates corresponding to a similar XML as example1.xml
132+
# Cartesian coordinates corresponding to a similar XML as test_field.xml
131133
# test_coords = [[0.0, 0.0], [2.78733, 7.60181], [25.3394, 88.3711],
132134
# [60.5611, 211.52], [88.6878, 311.358], [105.814, 374.206],
133135
# [-404.753, 525.721], [-403.403, 519.014], [-365.901, 341.765],
134136
# [-320.543, 125.493], [-309.789, 75.0412], [-3.61568, -15.8186], [0.0, 0.0]]
137+
138+
# Same, in "EPSG:4258" frame
139+
# test_coords = [[4.26199990317851,51.7859704975047], [4.26203858931428,51.7860392024232],
140+
# [4.26234728296244,51.7867682098064], [4.26283009086539,51.7878798462305],
141+
# [4.26321532471923,51.7887809885397], [4.26344944239078,51.7893481817958],
142+
# [4.25601634525234,51.7906387243889], [4.25603742148301,51.7905786368844],
143+
# [4.25662083047068,51.7889909449083], [4.25732681137421,51.7870536505356],
144+
# [4.25749399420598,51.7866017400346], [4.26195105582634,51.7858278333044],
145+
# [4.26199990317851,51.7859704975047]]
135146
# coords = Coordinates()
136147
# for x in test_coords:
137148
# coords.coordinates.append(toMsg(x))
138149
# goal.polygons.append(coords)
139-
# goal.frame_id = "map"
150+
# goal.frame_id = "map" # "EPSG:4258"
140151

141152
# goal.headland_mode.mode = "CONSTANT"
142153
# goal.headland_mode.width = 5.0

0 commit comments

Comments
 (0)