00001 #include <jsk_interactive_marker/interactive_marker_utils.h>
00002 #include <boost/filesystem/operations.hpp>
00003 #include <iostream>
00004 #include <stdlib.h>
00005 #include <ros/package.h>
00006 #include "urdf_parser/urdf_parser.h"
00007
00008 #include <kdl/frames_io.hpp>
00009 #include <tf_conversions/tf_kdl.h>
00010
00011 using namespace urdf;
00012 using namespace std;
00013 using namespace boost;
00014 using namespace boost::filesystem;
00015
00016
00017 namespace im_utils {
00018
00019
00020 geometry_msgs::Transform Pose2Transform( const geometry_msgs::Pose pose_msg){
00021 geometry_msgs::Transform tf_msg;
00022 tf_msg.translation.x = pose_msg.position.x;
00023 tf_msg.translation.y = pose_msg.position.y;
00024 tf_msg.translation.z = pose_msg.position.z;
00025 tf_msg.rotation = pose_msg.orientation;
00026 return tf_msg;
00027 }
00028
00029 geometry_msgs::Pose Transform2Pose( const geometry_msgs::Transform tf_msg){
00030 geometry_msgs::Pose pose_msg;
00031 pose_msg.position.x = tf_msg.translation.x;
00032 pose_msg.position.y = tf_msg.translation.y;
00033 pose_msg.position.z = tf_msg.translation.z;
00034 pose_msg.orientation = tf_msg.rotation;
00035 return pose_msg;
00036 }
00037
00038 geometry_msgs::Pose UrdfPose2Pose( const urdf::Pose pose){
00039 geometry_msgs::Pose p_msg;
00040 double x, y, z, w;
00041 pose.rotation.getQuaternion(x,y,z,w);
00042 p_msg.orientation.x = x;
00043 p_msg.orientation.y = y;
00044 p_msg.orientation.z = z;
00045 p_msg.orientation.w = w;
00046
00047 p_msg.position.x = pose.position.x;
00048 p_msg.position.y = pose.position.y;
00049 p_msg.position.z = pose.position.z;
00050
00051 return p_msg;
00052 }
00053
00054
00055 visualization_msgs::InteractiveMarkerControl makeCylinderMarkerControl(const geometry_msgs::PoseStamped &stamped, double length, double radius, const std_msgs::ColorRGBA &color, bool use_color){
00056 visualization_msgs::Marker cylinderMarker;
00057
00058 if (use_color) cylinderMarker.color = color;
00059 cylinderMarker.type = visualization_msgs::Marker::CYLINDER;
00060 cylinderMarker.scale.x = radius * 2;
00061 cylinderMarker.scale.y = radius * 2;
00062 cylinderMarker.scale.z = length;
00063 cylinderMarker.pose = stamped.pose;
00064
00065 visualization_msgs::InteractiveMarkerControl control;
00066 control.markers.push_back( cylinderMarker );
00067 control.interaction_mode = visualization_msgs::InteractiveMarkerControl::BUTTON;
00068 control.always_visible = true;
00069
00070 return control;
00071 }
00072
00073 visualization_msgs::InteractiveMarkerControl makeBoxMarkerControl(const geometry_msgs::PoseStamped &stamped, Vector3 dim, const std_msgs::ColorRGBA &color, bool use_color){
00074 visualization_msgs::Marker boxMarker;
00075
00076 fprintf(stderr, "urdfModelMarker = %f %f %f\n", dim.x, dim.y, dim.z);
00077 if (use_color) boxMarker.color = color;
00078 boxMarker.type = visualization_msgs::Marker::CUBE;
00079 boxMarker.scale.x = dim.x;
00080 boxMarker.scale.y = dim.y;
00081 boxMarker.scale.z = dim.z;
00082 boxMarker.pose = stamped.pose;
00083
00084 visualization_msgs::InteractiveMarkerControl control;
00085 control.markers.push_back( boxMarker );
00086 control.interaction_mode = visualization_msgs::InteractiveMarkerControl::BUTTON;
00087 control.always_visible = true;
00088
00089 return control;
00090 }
00091
00092 visualization_msgs::InteractiveMarkerControl makeSphereMarkerControl(const geometry_msgs::PoseStamped &stamped, double rad, const std_msgs::ColorRGBA &color, bool use_color){
00093 visualization_msgs::Marker sphereMarker;
00094
00095 if (use_color) sphereMarker.color = color;
00096 sphereMarker.type = visualization_msgs::Marker::SPHERE;
00097 sphereMarker.scale.x = rad * 2;
00098 sphereMarker.scale.y = rad * 2;
00099 sphereMarker.scale.z = rad * 2;
00100 sphereMarker.pose = stamped.pose;
00101
00102 visualization_msgs::InteractiveMarkerControl control;
00103 control.markers.push_back( sphereMarker );
00104 control.interaction_mode = visualization_msgs::InteractiveMarkerControl::BUTTON;
00105 control.always_visible = true;
00106
00107 return control;
00108 }
00109
00110
00111
00112 visualization_msgs::InteractiveMarkerControl makeMeshMarkerControl(const std::string &mesh_resource, const geometry_msgs::PoseStamped &stamped, geometry_msgs::Vector3 scale, const std_msgs::ColorRGBA &color, bool use_color){
00113 visualization_msgs::Marker meshMarker;
00114
00115 if (use_color) meshMarker.color = color;
00116 meshMarker.mesh_resource = mesh_resource;
00117 meshMarker.mesh_use_embedded_materials = !use_color;
00118 meshMarker.type = visualization_msgs::Marker::MESH_RESOURCE;
00119
00120 meshMarker.scale = scale;
00121 meshMarker.pose = stamped.pose;
00122 visualization_msgs::InteractiveMarkerControl control;
00123 control.markers.push_back( meshMarker );
00124 control.interaction_mode = visualization_msgs::InteractiveMarkerControl::BUTTON;
00125 control.always_visible = true;
00126
00127 return control;
00128 }
00129
00130 visualization_msgs::InteractiveMarkerControl makeMeshMarkerControl(const std::string &mesh_resource, const geometry_msgs::PoseStamped &stamped, geometry_msgs::Vector3 scale)
00131 {
00132 std_msgs::ColorRGBA color;
00133 color.r = 0;
00134 color.g = 0;
00135 color.b = 0;
00136 color.a = 0;
00137 return makeMeshMarkerControl(mesh_resource, stamped, scale, color, false);
00138 }
00139
00140 visualization_msgs::InteractiveMarkerControl makeMeshMarkerControl(const std::string &mesh_resource,
00141 const geometry_msgs::PoseStamped &stamped, geometry_msgs::Vector3 scale, const std_msgs::ColorRGBA &color)
00142 {
00143 return makeMeshMarkerControl(mesh_resource, stamped, scale, color, true);
00144 }
00145
00146 void addMeshLinksControl(visualization_msgs::InteractiveMarker &im, LinkConstSharedPtr link, KDL::Frame previous_frame, bool use_color, std_msgs::ColorRGBA color, double scale){
00147 addMeshLinksControl(im, link, previous_frame, use_color, color, scale, true);
00148 }
00149
00150 void addMeshLinksControl(visualization_msgs::InteractiveMarker &im, LinkConstSharedPtr link, KDL::Frame previous_frame, bool use_color, std_msgs::ColorRGBA color, double scale, bool root){
00151 if(!root && link->parent_joint){
00152 KDL::Frame parent_to_joint_frame;
00153 geometry_msgs::Pose parent_to_joint_pose = UrdfPose2Pose(link->parent_joint->parent_to_joint_origin_transform);
00154 tf::poseMsgToKDL(parent_to_joint_pose, parent_to_joint_frame);
00155 previous_frame = previous_frame * parent_to_joint_frame;
00156 }
00157
00158
00159
00160
00161
00162
00163 geometry_msgs::PoseStamped ps;
00164
00165 std::vector<VisualSharedPtr> visual_array;
00166 if(link->visual_array.size() != 0){
00167 visual_array = link->visual_array;
00168 }else if(link->visual.get() != NULL){
00169 visual_array.push_back(link->visual);
00170 }
00171 for(int i=0; i<visual_array.size(); i++){
00172 VisualSharedPtr link_visual = visual_array[i];
00173 if(link_visual.get() != NULL && link_visual->geometry.get() != NULL){
00174 visualization_msgs::InteractiveMarkerControl meshControl;
00175 if(link_visual->geometry->type == Geometry::MESH){
00176 #if ROS_VERSION_MINIMUM(1,14,0) // melodic
00177 MeshConstSharedPtr mesh = std::static_pointer_cast<const Mesh>(link_visual->geometry);
00178 #else
00179 MeshConstSharedPtr mesh = boost::static_pointer_cast<const Mesh>(link_visual->geometry);
00180 #endif
00181 string model_mesh_ = mesh->filename;
00182 model_mesh_ = getRosPathFromModelPath(model_mesh_);
00183
00184
00185 KDL::Frame pose_frame, origin_frame;
00186
00187 tf::poseMsgToKDL(UrdfPose2Pose(link_visual->origin), origin_frame);
00188 pose_frame = previous_frame * origin_frame;
00189 geometry_msgs::Pose pose;
00190 tf::poseKDLToMsg(pose_frame, pose);
00191 ps.pose = pose;
00192
00193 cout << "mesh_file:" << model_mesh_ << endl;
00194
00195 geometry_msgs::Vector3 mesh_scale;
00196 mesh_scale.x = mesh->scale.x * scale;
00197 mesh_scale.y = mesh->scale.y * scale;
00198 mesh_scale.z = mesh->scale.z * scale;
00199 if(use_color){
00200 meshControl = makeMeshMarkerControl(model_mesh_, ps, mesh_scale, color);
00201 }else{
00202 meshControl = makeMeshMarkerControl(model_mesh_, ps, mesh_scale);
00203 }
00204 }else if(link_visual->geometry->type == Geometry::CYLINDER){
00205 #if ROS_VERSION_MINIMUM(1,14,0) // melodic
00206 CylinderConstSharedPtr cylinder = std::static_pointer_cast<const Cylinder>(link_visual->geometry);
00207 #else
00208 CylinderConstSharedPtr cylinder = boost::static_pointer_cast<const Cylinder>(link_visual->geometry);
00209 #endif
00210 std::cout << "cylinder " << link->name;
00211 ps.pose = UrdfPose2Pose(link_visual->origin);
00212 double length = cylinder->length;
00213 double radius = cylinder->radius;
00214 std::cout << ", length = " << length << ", radius " << radius << std::endl;
00215 if(use_color){
00216 meshControl = makeCylinderMarkerControl(ps, length, radius, color, true);
00217 }else{
00218 meshControl = makeCylinderMarkerControl(ps, length, radius, color, true);
00219 }
00220 }else if(link_visual->geometry->type == Geometry::BOX){
00221 #if ROS_VERSION_MINIMUM(1,14,0) // melodic
00222 BoxConstSharedPtr box = std::static_pointer_cast<const Box>(link_visual->geometry);
00223 #else
00224 BoxConstSharedPtr box = boost::static_pointer_cast<const Box>(link_visual->geometry);
00225 #endif
00226 std::cout << "box " << link->name;
00227 ps.pose = UrdfPose2Pose(link_visual->origin);
00228 Vector3 dim = box->dim;
00229 std::cout << ", dim = " << dim.x << ", " << dim.y << ", " << dim.z << std::endl;
00230 if(use_color){
00231 meshControl = makeBoxMarkerControl(ps, dim, color, true);
00232 }else{
00233 meshControl = makeBoxMarkerControl(ps, dim, color, true);
00234 }
00235 }else if(link_visual->geometry->type == Geometry::SPHERE){
00236 #if ROS_VERSION_MINIMUM(1,14,0) // melodic
00237 SphereConstSharedPtr sphere = std::static_pointer_cast<const Sphere>(link_visual->geometry);
00238 #else
00239 SphereConstSharedPtr sphere = boost::static_pointer_cast<const Sphere>(link_visual->geometry);
00240 #endif
00241 ps.pose = UrdfPose2Pose(link_visual->origin);
00242 double rad = sphere->radius;
00243 if(use_color){
00244 meshControl = makeSphereMarkerControl(ps, rad, color, true);
00245 }else{
00246 meshControl = makeSphereMarkerControl(ps, rad, color, true);
00247 }
00248 }
00249 im.controls.push_back( meshControl );
00250
00251 }
00252 }
00253 for (std::vector<LinkSharedPtr>::const_iterator child = link->child_links.begin(); child != link->child_links.end(); child++){
00254 addMeshLinksControl(im, *child, previous_frame, use_color, color, scale, false);
00255 }
00256 }
00257
00258 ModelInterfaceSharedPtr getModelInterface(std::string model_file){
00259 ModelInterfaceSharedPtr model;
00260 model_file = getFilePathFromRosPath(model_file);
00261 model_file = getFullPathFromModelPath(model_file);
00262 std::string xml_string;
00263 std::fstream xml_file(model_file.c_str(), std::fstream::in);
00264 while ( xml_file.good() )
00265 {
00266 std::string line;
00267 std::getline( xml_file, line);
00268 xml_string += (line + "\n");
00269 }
00270 xml_file.close();
00271
00272 std::cout << "model_file:" << model_file << std::endl;
00273 model = parseURDF(xml_string);
00274 if (!model){
00275 std::cerr << "ERROR: Model Parsing the xml failed" << std::endl;
00276 }
00277 return model;
00278 }
00279
00280
00281 visualization_msgs::InteractiveMarker makeLinksMarker(LinkConstSharedPtr link, bool use_color, std_msgs::ColorRGBA color, geometry_msgs::PoseStamped marker_ps, geometry_msgs::Pose origin_pose)
00282 {
00283 visualization_msgs::InteractiveMarker int_marker;
00284 int_marker.header = marker_ps.header;
00285 int_marker.pose = marker_ps.pose;
00286
00287 int_marker.name = "sample";
00288 int_marker.scale = 1.0;
00289
00290 KDL::Frame origin_frame;
00291 tf::poseMsgToKDL(origin_pose, origin_frame);
00292 addMeshLinksControl(int_marker, link, origin_frame, use_color, color, 1.0);
00293 return int_marker;
00294
00295 }
00296
00297
00298
00299 visualization_msgs::InteractiveMarker makeFingerControlMarker(const char *name, geometry_msgs::PoseStamped ps){
00300 visualization_msgs::InteractiveMarker int_marker;
00301 int_marker.name = name;
00302 int_marker.header = ps.header;
00303 int_marker.pose = ps.pose;
00304 int_marker.scale = 0.5;
00305
00306 visualization_msgs::InteractiveMarkerControl control;
00307
00308
00309 control.interaction_mode = visualization_msgs::InteractiveMarkerControl::ROTATE_AXIS;
00310 control.orientation.w = 1;
00311 control.orientation.x = 0;
00312 control.orientation.y = 0;
00313 control.orientation.z = 1;
00314
00315 int_marker.controls.push_back(control);
00316
00317 return int_marker;
00318
00319 }
00320
00321
00322
00323
00324
00325
00326
00327
00328
00329
00330
00331
00332
00333
00334
00335
00336
00337
00338
00339
00340
00341
00342
00343
00344
00345
00346
00347
00348
00349
00350
00351
00352
00353
00354
00355
00356
00357
00358
00359
00360
00361
00362
00363
00364
00365
00366
00367
00368
00369
00370
00371
00372
00373
00374
00375
00376
00377
00378
00379
00380
00381
00382
00383 visualization_msgs::InteractiveMarker makeSandiaHandInteractiveMarker(geometry_msgs::PoseStamped ps, std::string hand, int finger, int link){
00384 visualization_msgs::InteractiveMarker int_marker;
00385 int_marker.header = ps.header;
00386 int_marker.pose = ps.pose;
00387
00388
00389 visualization_msgs::InteractiveMarkerControl control;
00390 std::stringstream ss;
00391
00392 ss << hand << "_f" << finger << "_" << link;
00393
00394 int_marker.name = ss.str() + "Marker";
00395 int_marker.header.frame_id = ss.str();
00396
00397 std::string frame_id = "utorso";
00398 int_marker.header.frame_id = frame_id;
00399 std::cout << ss.str() << std::endl;
00400
00401
00402 switch(link){
00403 case 0:
00404 control.markers.push_back(makeSandiaFinger0Marker(frame_id));
00405 break;
00406 case 1:
00407 control.markers.push_back(makeSandiaFinger1Marker(frame_id));
00408 break;
00409 case 2:
00410 control.markers.push_back(makeSandiaFinger2Marker(frame_id));
00411 break;
00412 default:
00413 break;
00414 }
00415 control.interaction_mode = visualization_msgs::InteractiveMarkerControl::BUTTON;
00416 int_marker.controls.push_back(control);
00417
00418 return int_marker;
00419 }
00420
00421
00422
00423 visualization_msgs::Marker makeSandiaFinger0Marker(std::string frame_id){
00424 visualization_msgs::Marker marker;
00425 marker.header.frame_id = frame_id;
00426
00427 marker.type = visualization_msgs::Marker::CYLINDER;
00428 marker.action = visualization_msgs::Marker::ADD;
00429 marker.pose.position.x = 0;
00430 marker.pose.position.y = 0;
00431 marker.pose.position.z = 0.003;
00432 marker.pose.orientation.x = 0.0;
00433 marker.pose.orientation.y = 0.0;
00434 marker.pose.orientation.z = 0.0;
00435 marker.pose.orientation.w = 1.0;
00436 marker.scale.x = 0.03;
00437 marker.scale.y = 0.03;
00438 marker.scale.z = 0.023;
00439 marker.color.a = 1.0;
00440 marker.color.r = 0.0;
00441 marker.color.g = 1.0;
00442 marker.color.b = 0.0;
00443
00444 return marker;
00445 }
00446
00447 visualization_msgs::Marker makeSandiaFinger1Marker(std::string frame_id){
00448 visualization_msgs::Marker marker;
00449 marker.header.frame_id = frame_id;
00450 marker.type = visualization_msgs::Marker::CYLINDER;
00451 marker.action = visualization_msgs::Marker::ADD;
00452 marker.pose.position.x = 0.024;
00453 marker.pose.position.y = 0;
00454 marker.pose.position.z = 0;
00455 marker.pose.orientation.x = 0.0;
00456 marker.pose.orientation.y = 1.0;
00457 marker.pose.orientation.z = 0.0;
00458 marker.pose.orientation.w = 1.0;
00459 marker.scale.x = 0.02;
00460 marker.scale.y = 0.02;
00461 marker.scale.z = 0.067;
00462 marker.color.a = 1.0;
00463 marker.color.r = 0.0;
00464 marker.color.g = 1.0;
00465 marker.color.b = 0.0;
00466
00467 return marker;
00468 }
00469
00470 visualization_msgs::Marker makeSandiaFinger2Marker(std::string frame_id){
00471 visualization_msgs::Marker marker;
00472 marker.header.frame_id = frame_id;
00473 marker.type = visualization_msgs::Marker::CYLINDER;
00474 marker.action = visualization_msgs::Marker::ADD;
00475 marker.pose.position.x = 0.024;
00476 marker.pose.position.y = 0;
00477 marker.pose.position.z = 0;
00478 marker.pose.orientation.x = 0.0;
00479 marker.pose.orientation.y = 1.0;
00480 marker.pose.orientation.z = 0.0;
00481 marker.pose.orientation.w = 1.0;
00482 marker.scale.x = 0.018;
00483 marker.scale.y = 0.018;
00484 marker.scale.z = 0.057;
00485 marker.color.a = 1.0;
00486 marker.color.r = 0.0;
00487 marker.color.g = 1.0;
00488 marker.color.b = 0.0;
00489
00490 return marker;
00491 }
00492
00493 std::string getRosPathFromModelPath(std::string path){
00494 return getRosPathFromFullPath(getFullPathFromModelPath(path));
00495 }
00496
00497 std::string getRosPathFromFullPath(std::string path){
00498 std::string ros_package_path = "";
00499 FILE* fp;
00500 char buf[1000000];
00501
00502
00503 if ((fp = popen("echo $ROS_PACKAGE_PATH", "r")) == NULL) {
00504 std::cout << "popen error" << std::endl;
00505 }
00506 while (fgets(buf, sizeof(buf), fp) != NULL) {
00507 ros_package_path += buf;
00508 }
00509 pclose(fp);
00510
00511 if( path.find("file://", 0) == 0 ){
00512 path.erase(0,7);
00513
00514
00515 size_t current = 0, found;
00516 while((found = ros_package_path.find_first_of(":", current)) != std::string::npos){
00517 std::string search_path = std::string(ros_package_path, current, found - current);
00518 current = found + 1;
00519 if(path.find(search_path, 0) == 0){
00520 path.erase(0, strlen(search_path.c_str()));
00521 break;
00522 }
00523 }
00524
00525 std::string tmp[] = {"", "jsk-ros-pkg", "jsk_model_tools"};
00526 std::set<std::string> package_blackset(tmp, tmp + sizeof(tmp) / sizeof(tmp[0]));
00527
00528 current = 0;
00529 while((found = path.find_first_of("/", current)) != std::string::npos){
00530 std::string search_path = std::string(path, current, found - current);
00531 current = found + 1;
00532 std::string package_path;
00533
00534
00535 if(package_blackset.find(search_path) != package_blackset.end()){
00536 continue;
00537 }
00538
00539 if( search_path != "" && ros::package::getPath(search_path) != ""){
00540 return "package://" + search_path + path.erase(0, current-1);
00541 }
00542 }
00543 path = "file://" + path;
00544 }
00545
00546 return path;
00547 }
00548
00549 std::string getFullPathFromModelPath(std::string path){
00550 std::string gazebo_model_path="";
00551
00552 FILE* fp;
00553 char buf[1000000];
00554
00555
00556 if ((fp = popen("echo $GAZEBO_MODEL_PATH", "r")) == NULL) {
00557 std::cout << "popen error" << std::endl;
00558 }
00559 while (fgets(buf, sizeof(buf), fp) != NULL) {
00560 gazebo_model_path += buf;
00561 }
00562 pclose(fp);
00563 if( path.find("model://", 0) == 0 ){
00564 path.erase(0,8);
00565
00566
00567
00568 size_t current = 0, found;
00569 while((found = gazebo_model_path.find_first_of(":", current)) != std::string::npos){
00570 try{
00571 std::string search_path = std::string(gazebo_model_path, current, found - current);
00572 current = found + 1;
00573 recursive_directory_iterator iter = recursive_directory_iterator(search_path);
00574 recursive_directory_iterator end = recursive_directory_iterator();
00575 for (; iter != end; ++iter) {
00576 if (is_regular_file(*iter)) {
00577 int locate = iter->path().string().find( path, 0 );
00578 if( locate != std::string::npos){
00579
00580 return "file://" + iter->path().string();
00581 }
00582 }
00583 }
00584 }catch(...){
00585 }
00586 }
00587 }
00588 return path;
00589 }
00590
00591
00592 std::string getFilePathFromRosPath( std::string rospath){
00593 std::string path = rospath;
00594 if (path.find("package://") == 0){
00595 path.erase(0, strlen("package://"));
00596 size_t pos = path.find("/");
00597 if (pos == std::string::npos){
00598 std::cout << "Could not parse package:// format" <<std::endl;
00599 return "";
00600 }
00601 std::string package = path.substr(0, pos);
00602 path.erase(0, pos);
00603 std::string package_path = ros::package::getPath(package);
00604 if (package_path.empty())
00605 {
00606 std::cout << "Package [" + package + "] does not exist" << std::endl;
00607 }
00608
00609 path = package_path + path;
00610 }
00611 return path;
00612 }
00613
00614 geometry_msgs::Pose getPose( XmlRpc::XmlRpcValue val){
00615 geometry_msgs::Pose p;
00616 if(val.hasMember("position")){
00617 XmlRpc::XmlRpcValue pos = val["position"];
00618 p.position.x = getXmlValue(pos["x"]);
00619 p.position.y = getXmlValue(pos["y"]);
00620 p.position.z = getXmlValue(pos["z"]);
00621 }else{
00622 p.position.x = p.position.y = p.position.z = 0.0;
00623 }
00624
00625 if(val.hasMember("orientation")){
00626 XmlRpc::XmlRpcValue ori = val["orientation"];
00627 p.orientation.x = getXmlValue(ori["x"]);
00628 p.orientation.y = getXmlValue(ori["y"]);
00629 p.orientation.z = getXmlValue(ori["z"]);
00630 p.orientation.w = getXmlValue(ori["w"]);
00631 }else{
00632 p.orientation.x = p.orientation.y = p.orientation.z = 0.0;
00633 p.orientation.w = 1.0;
00634 }
00635
00636 return p;
00637 }
00638
00639 double getXmlValue( XmlRpc::XmlRpcValue val ){
00640 switch(val.getType()){
00641 case XmlRpc::XmlRpcValue::TypeInt:
00642 return (double)((int)val);
00643 case XmlRpc::XmlRpcValue::TypeDouble:
00644 return (double)val;
00645 default:
00646 return 0;
00647 }
00648 }
00649
00650 }
00651
00652