Visualizer.cpp
Go to the documentation of this file.
1 /*
2  * Visualizer.cpp
3  *
4  * Created on: May 8, 2012
5  * Author: sdries
6  */
7 
8 #include "Visualizer.h"
9 #include "default_values.h"
10 
11 using namespace std;
12 
13 Visualizer::Visualizer() : tf_listener_(0) {
14 
15  color_mapping_["red"] = Color(1,0,0);
16  color_mapping_["green"] = Color(0,1,0);
17  color_mapping_["blue"] = Color(0,0,1);
18  color_mapping_["white"] = Color(1,1,1);
19  color_mapping_["black"] = Color(0,0,0);
20  color_mapping_["orange"] = Color(1,0.65,0);
21  color_mapping_["pink"] = Color(1,0.08,0.58);
22  color_mapping_["gold"] = Color(1,0.84,0);
23  color_mapping_["grey"] = Color(0.41,0.41,0.41);
24  color_mapping_["gray"] = Color(0.41,0.41,0.41);
25  color_mapping_["cyan"] = Color(0,1,1);
26  color_mapping_["yellow"] = Color(1,1,0);
27  color_mapping_["magenta"] = Color(1,0,1);
28 
29 }
30 
32 
33 }
34 
35 /*
36  * Function that creates markers
37  */
38 bool Visualizer::createMarkers(const std_msgs::Header& header, long ID,
39  const vector<wire_msgs::Property>& props,
40  vector<visualization_msgs::Marker>& markers_out,
41  const std::string& frame_id = "" ) {
42 
43  // Pose according to evidence
44  tf::Stamped<tf::Pose> ev_pose;
45  ev_pose.setRotation(tf::Quaternion(0, 0, 0, 1));
46  ev_pose.frame_id_ = header.frame_id;
47  ev_pose.stamp_ = ros::Time();
48 
49  // Store class label and marker text
50  string marker_text = "", class_label = "", color = "";
51 
52  // Variances
53  double cov_xx = 0;
54  double cov_yy = 0;
55  double cov_zz = 0;
56 
57  // Iterate over all the propoerties
58  bool pos_found = false;
59  for (vector<wire_msgs::Property>::const_iterator it_prop = props.begin(); it_prop != props.end(); ++it_prop) {
60 
61  pbl::PDF* pdf = pbl::msgToPDF(it_prop->pdf);
62 
63  // Position
64  if (it_prop->attribute == "position") {
65  const pbl::Gaussian* gauss = getBestGaussian(*pdf);
66  if (gauss) {
67  const pbl::Vector& mean = gauss->getMean();
68 
69  cov_xx = gauss->getCovariance()(0, 0);
70  cov_yy = gauss->getCovariance()(1, 1);
71  cov_zz = gauss->getCovariance()(2, 2);
72 
73  if (gauss->dimensions() == 2) {
74  ev_pose.setOrigin(tf::Point(mean(0), mean(1), 0));
75  pos_found = true;
76  } else if (gauss->dimensions() == 3) {
77  pos_found = true;
78  ev_pose.setOrigin(tf::Point(mean(0), mean(1), mean(2)));
79  } else {
80  ROS_WARN("World evidence: position attribute has %d dimensions, Visualizer cannot deal with this.",
81  gauss->dimensions());
82  }
83  }
84  }
85  // Orientation
86  else if (it_prop->attribute == "orientation") {
87  if (pdf->dimensions() == 4) {
88  const pbl::Gaussian* gauss = getBestGaussian(*pdf);
89  if (gauss) {
90  const pbl::Vector& mean = gauss->getMean();
91  ev_pose.setRotation(tf::Quaternion(mean(0), mean(1), mean(2), mean(3)));
92  }
93  } else {
94  ROS_WARN("Orientation attribute has %d dimensions, must be 4 (X Y Z W).", pdf->dimensions());
95  }
96  }
97  // Class label
98  else if (it_prop->attribute == "class_label") {
99  pdf->getExpectedValue(class_label);
100  }
101  // Color
102  else if (it_prop->attribute == "color") {
103  pdf->getExpectedValue(color);
104  }
105 
106  // Check if the current property represents an attribute that has to be added to the text label
107  for (map<string,bool>::const_iterator it = attribute_map_.begin(); it != attribute_map_.end(); ++it) {
108  if (it_prop->attribute == it->first && it->second) {
109  string txt = "";
110  // Only consider pmfs otherwise the getExpectedValue function fails
111  if (pdf->type() == pbl::PDF::DISCRETE) {
112  pdf->getExpectedValue(txt);
113  // If marker text not empty add ',' for ease of reading
114  if (marker_text != "") {
115  txt = ", " + txt;
116  }
117  marker_text += txt;
118  }
119  }
120  }
121 
122 
123  delete pdf;
124 
125  } // End iterate over properties
126 
127  // A position is always needed
128  if (!pos_found) {
129  return false;
130  }
131 
132  // Default marker must be defined
134 
135  // Create markers belonging to the objects class
136  string map_key = "default";
137  visualization_msgs::Marker marker;
138  visualization_msgs::Marker text_marker;
139  if (object_class_to_marker_map_.find(class_label) != object_class_to_marker_map_.end()) {
140  map_key = class_label;
141  }
142  marker = object_class_to_marker_map_[map_key].shape_marker;
143  text_marker = object_class_to_marker_map_[map_key].text_marker;
144 
145  // If the object has a color, use color of the object as marker color
146  if (color != "" && color_mapping_.find(color) != color_mapping_.end()) {
147  marker.color.r = color_mapping_[color].r;
148  marker.color.g = color_mapping_[color].g;
149  marker.color.b = color_mapping_[color].b;
150  }
151 
152 
153  // Transform to frame
154  tf::Stamped<tf::Pose> ev_pose_TRANSFORMED;
155  if (frame_id != "" && tf_listener_ != 0) {
156  try{
157  tf_listener_->transformPose(frame_id, ev_pose, ev_pose_TRANSFORMED);
158  marker.header.frame_id = frame_id;
159  text_marker.header.frame_id = frame_id;
160  } catch (tf::TransformException ex){
161  ROS_ERROR("[VISUALIZER] %s",ex.what());
162  ev_pose_TRANSFORMED = ev_pose;
163  marker.header.frame_id = header.frame_id;
164  text_marker.header.frame_id = header.frame_id;
165  }
166  } else {
167  ev_pose_TRANSFORMED = ev_pose;
168  marker.header.frame_id = header.frame_id;
169  text_marker.header.frame_id = header.frame_id;
170  }
171 
172  // Update shape marker position to transformed position and add marker to array
173  marker.pose.position.x = ev_pose_TRANSFORMED.getOrigin().getX();
174  marker.pose.position.y = ev_pose_TRANSFORMED.getOrigin().getY();
175  marker.pose.position.z = ev_pose_TRANSFORMED.getOrigin().getZ();
176  marker.header.stamp = header.stamp;
177  marker.id = 3 * ID;
178  markers_out.push_back(marker);
179 
180  // Only if text must be shown
181  if (object_class_to_marker_map_[map_key].show_text) {
182  // Update text marker and add to array
183  text_marker.pose.position.x += ev_pose_TRANSFORMED.getOrigin().getX();
184  text_marker.pose.position.y += ev_pose_TRANSFORMED.getOrigin().getY();
185  text_marker.pose.position.z += ev_pose_TRANSFORMED.getOrigin().getZ();
186  text_marker.header.stamp = header.stamp;
187  text_marker.id = 3 * ID + 1;
188 
189  // Add ID separately since this not necessarily is an attribute
190  if (attribute_map_.find("ID") != attribute_map_.end() && attribute_map_["ID"]) {
191  stringstream ss;
192  ss << ID;
193  string id_text = " (" + ss.str() + ")";
194  marker_text += id_text;
195  }
196 
197  // Set the text label and add the marker
198  text_marker.text = marker_text;
199  markers_out.push_back(text_marker);
200  }
201 
202  // Covariance marker
203  if (object_class_to_marker_map_[map_key].show_cov) {
204  visualization_msgs::Marker cov_marker = marker;
205  cov_marker.color.a *= 0.5;
206  cov_marker.scale.x *= (1 + sqrt(cov_xx) * 3);
207  cov_marker.scale.y *= (1 + sqrt(cov_yy) * 3);
208  cov_marker.scale.z *= (1 + sqrt(cov_zz) * 3);
209  cov_marker.id = 3 * ID + 2;
210  markers_out.push_back(cov_marker);
211  }
212 
213  return true;
214 }
215 
216 
217 /*
218  * Set the tf listener
219  */
221  tf_listener_ = tf_listener;
222 }
223 
224 /*
225  * Get the most probable Gaussian from a pdf
226  */
227 const pbl::Gaussian* Visualizer::getBestGaussian(const pbl::PDF& pdf, double min_weight) {
228  if (pdf.type() == pbl::PDF::GAUSSIAN) {
229  return pbl::PDFtoGaussian(pdf);
230  } else if (pdf.type() == pbl::PDF::MIXTURE) {
231  const pbl::Mixture* mix = pbl::PDFtoMixture(pdf);
232 
233  if (mix){
234  const pbl::Gaussian* G_best = 0;
235  double w_best = min_weight;
236  for(int i = 0; i < mix->components(); ++i) {
237  const pbl::PDF& pdf = mix->getComponent(i);
238  const pbl::Gaussian* G = pbl::PDFtoGaussian(pdf);
239  double w = mix->getWeight(i);
240  if (G && w > w_best) {
241  G_best = G;
242  w_best = w;
243  }
244  }
245  return G_best;
246  }
247  }
248 
249  return 0;
250 }
251 
252 
253 /*
254  * Get double
255  */
256 void Visualizer::getStructValue(XmlRpc::XmlRpcValue& s, const std::string& name, double& d, double default_value) {
257  XmlRpc::XmlRpcValue& v = s[name];
258 
260  d = (double)v;
261  return;
262  }
263 
265  d = (int)v;
266  return;
267  }
268 
269  d = default_value;
270 }
271 
272 /*
273  * Get float
274  */
275 void Visualizer::getStructValue(XmlRpc::XmlRpcValue& s, const std::string& name, float& f, float default_value) {
276  double d;
277  getStructValue(s, name, d, default_value);
278  f = (float)d;
279 }
280 
281 /*
282  * Get bool
283  */
284 void Visualizer::getStructValue(XmlRpc::XmlRpcValue& s, const std::string& name, bool& b, bool default_value) {
285  XmlRpc::XmlRpcValue& v = s[name];
286 
288  b = (bool)v;
289  return;
290  }
291 
292  b = default_value;
293 }
294 
295 /*
296  * Get string
297  */
298 void Visualizer::getStructValue(XmlRpc::XmlRpcValue& s, const std::string& name, std::string& str, const std::string& default_value) {
299  XmlRpc::XmlRpcValue& v = s[name];
300 
302  str = (std::string)v;
303  return;
304  }
305 
306  str = default_value;
307 }
308 
309 
310 bool Visualizer::setParameters(ros::NodeHandle& n, const string& ns) {
311 
312  bool param = false, att = false;
313  param = getMarkerParameters(n, ns);
314  att = getAttributeSettings(n, ns);
315 
316  return (param && att);
317 }
318 
319 
320 
321 /*
322  * Get marker parameters from parameter servers
323  */
325 
326  // Get marker parameters from the parameter server
327  XmlRpc::XmlRpcValue marker_params;
328  if (!n.getParam(ns, marker_params)) {
329  ROS_ERROR("No global marker parameters given. (namespace: %s)", n.getNamespace().c_str());
330  return false;
331  }
332 
333  // Check if the marker parameters are of type array
334  if (marker_params.getType() != XmlRpc::XmlRpcValue::TypeArray) {
335  ROS_ERROR("Malformed marker parameter specification. (namespace: %s)", n.getNamespace().c_str());
336  return false;
337  }
338 
339  // Iterate over the marker parameters in array
340  for(int i = 0; i < marker_params.size(); ++i) {
341 
342  // Get current value
343  XmlRpc::XmlRpcValue& v = marker_params[i];
344 
345  // Check type
347 
348  // Get class
349  string class_name = "";
350  getStructValue(v, "class", class_name, "");
351 
352  // If no class defined, no marker can be added
353  if (class_name == "") {
354  ROS_WARN("No class defined, skipping parameters");
355  }
356  // The first class must be default, since the default marker defines values that are not defined
357  else if (i==0 && class_name != "default") {
358  ROS_ERROR("Make sure to first specify default in YAML file");
359  return false;
360  }
361  // Add the default marker
362  else if (i==0 && class_name == "default") {
363 
364  // Get al properties
365  MarkerInfo& m = object_class_to_marker_map_[class_name];
366  m.shape_marker.action = visualization_msgs::Marker::ADD;
367  m.shape_marker.ns = ns;
374  getStructValue(v, "scale_x", m.shape_marker.scale.x, visualize::DEFAULT_SCALE_X);
375  getStructValue(v, "scale_y", m.shape_marker.scale.y, visualize::DEFAULT_SCALE_Y);
376  getStructValue(v, "scale_z", m.shape_marker.scale.z, visualize::DEFAULT_SCALE_Z);
377  getStructValue(v, "offset_x_pos", m.shape_marker.pose.position.x, visualize::DEFAULT_OFFSET_X_POS);
378  getStructValue(v, "offset_y_pos", m.shape_marker.pose.position.y, visualize::DEFAULT_OFFSET_Y_POS);
379  getStructValue(v, "offset_z_pos", m.shape_marker.pose.position.z, visualize::DEFAULT_OFFSET_Z_POS);
380  getStructValue(v, "offset_x_rot", m.shape_marker.pose.orientation.x, visualize::DEFAULT_OFFSET_X_ROT);
381  getStructValue(v, "offset_y_rot", m.shape_marker.pose.orientation.y, visualize::DEFAULT_OFFSET_Y_ROT);
382  getStructValue(v, "offset_z_rot", m.shape_marker.pose.orientation.z, visualize::DEFAULT_OFFSET_Z_ROT);
383  getStructValue(v, "offset_w_rot", m.shape_marker.pose.orientation.w, visualize::DEFAULT_OFFSET_W_ROT);
384  getStructValue(v, "mesh_resource", m.shape_marker.mesh_resource, "");
385  double duration = 0;
386  getStructValue(v, "duration", duration, visualize::DEFAULT_MARKER_LIFETIME);
387  m.shape_marker.lifetime = ros::Duration(duration);
388  m.text_marker.lifetime = ros::Duration(duration);
389 
390  // Set geomatric shape
391  setMarkerType(v, m);
392 
393  ROS_DEBUG("Added marker with class default to %s", ns.c_str());
394  }
395  // Add marker of any class
396  else {
397 
398  // Add class to the map if not defined use default values
399  MarkerInfo& m = object_class_to_marker_map_[class_name];
400  m.shape_marker.action = visualization_msgs::Marker::ADD;
401  m.shape_marker.ns = ns;
402  getStructValue(v, "show_text", m.show_text, object_class_to_marker_map_["default"].show_text);
403  getStructValue(v, "show_cov", m.show_cov, object_class_to_marker_map_["default"].show_cov);
404  getStructValue(v, "r", m.shape_marker.color.r, object_class_to_marker_map_["default"].shape_marker.color.r);
405  getStructValue(v, "g", m.shape_marker.color.g, object_class_to_marker_map_["default"].shape_marker.color.g);
406  getStructValue(v, "b", m.shape_marker.color.b, object_class_to_marker_map_["default"].shape_marker.color.b);
407  getStructValue(v, "alpha", m.shape_marker.color.a, object_class_to_marker_map_["default"].shape_marker.color.a);
408  getStructValue(v, "scale_x", m.shape_marker.scale.x, object_class_to_marker_map_["default"].shape_marker.scale.x);
409  getStructValue(v, "scale_y", m.shape_marker.scale.y, object_class_to_marker_map_["default"].shape_marker.scale.y);
410  getStructValue(v, "scale_z", m.shape_marker.scale.z, object_class_to_marker_map_["default"].shape_marker.scale.z);
411  getStructValue(v, "offset_x_pos", m.shape_marker.pose.position.x, object_class_to_marker_map_["default"].shape_marker.pose.position.x);
412  getStructValue(v, "offset_y_pos", m.shape_marker.pose.position.y, object_class_to_marker_map_["default"].shape_marker.pose.position.y);
413  getStructValue(v, "offset_z_pos", m.shape_marker.pose.position.z, object_class_to_marker_map_["default"].shape_marker.pose.position.z);
414  getStructValue(v, "offset_x_rot", m.shape_marker.pose.orientation.x, object_class_to_marker_map_["default"].shape_marker.pose.orientation.x);
415  getStructValue(v, "offset_y_rot", m.shape_marker.pose.orientation.y, object_class_to_marker_map_["default"].shape_marker.pose.orientation.y);
416  getStructValue(v, "offset_z_rot", m.shape_marker.pose.orientation.z, object_class_to_marker_map_["default"].shape_marker.pose.orientation.z);
417  getStructValue(v, "offset_w_rot", m.shape_marker.pose.orientation.w, object_class_to_marker_map_["default"].shape_marker.pose.orientation.w);
418  double duration = object_class_to_marker_map_["default"].shape_marker.lifetime.toSec();
419  getStructValue(v, "duration", duration, duration);
420  m.shape_marker.lifetime = ros::Duration(duration);
421  m.text_marker.lifetime = ros::Duration(duration);
422 
423  // Set appropriate marker geometry
424  setMarkerType(v, m);
425 
426  ROS_DEBUG("Added marker with class %s to %s", class_name.c_str(), ns.c_str());
427 
428  }
429  } else {
430  ROS_WARN("Parameter specification not a list (parameter ignored)!");
431  }
432  }
433 
434  // Check if text marker is defined
435  if (object_class_to_marker_map_.find("text") == object_class_to_marker_map_.end()) {
436  ROS_ERROR("Marker with class text must be defined!");
437  return false;
438  }
439 
440  // Set text markers for all classes with text marker properties
441  visualization_msgs::Marker default_text_marker = object_class_to_marker_map_["text"].shape_marker;
443  it->second.text_marker = default_text_marker;
444  it->second.text_marker.type = visualization_msgs::Marker::TEXT_VIEW_FACING;
445  }
446 
447  return true;
448 }
449 
450 
451 /*
452  * Set appropriate marker type
453  */
455 
456  // Check if a mesh is defined
457  if (m.shape_marker.mesh_resource != "") {
458  m.shape_marker.type = visualization_msgs::Marker::MESH_RESOURCE;
459  ROS_INFO("mesh_resource = %s", m.shape_marker.mesh_resource.c_str());
460  }
461  // If no mesh defined, get geometry type or use default
462  else {
463 
464  // Get marker type from parameter server
465  string marker_type;
466  getStructValue(v, "type", marker_type, visualize::DEFAULT_TYPE);
467 
468  // Convert type to lower case only
469  std::transform(marker_type.begin(), marker_type.end(), marker_type.begin(), ::tolower);
470 
471  // Set appropriate type
472  if (marker_type == "cube") {
473  m.shape_marker.type = visualization_msgs::Marker::CUBE;
474  } else if (marker_type == "cylinder") {
475  m.shape_marker.type = visualization_msgs::Marker::CYLINDER;
476  } else if (marker_type == "arrow") {
477  m.shape_marker.type = visualization_msgs::Marker::ARROW;
478  } else if (marker_type == "sphere") {
479  m.shape_marker.type = visualization_msgs::Marker::SPHERE;
480  } else {
481  ROS_WARN("Defined marker type %s, which is unknown. Using sphere instead.", marker_type.c_str());
482  m.shape_marker.type = visualization_msgs::Marker::SPHERE;
483  }
484  }
485 
486 }
487 
488 
489 
490 
491 /*
492  * Get attribute settings (which attributes should be present in the text labels)
493  */
495 
496  string param_name = ns + "_attributes";
497 
498  // Get marker parameters from the parameter server
499  XmlRpc::XmlRpcValue attribute_params;
500  if (!n.getParam(param_name, attribute_params)) {
501  ROS_ERROR("No global \"%s\" parameters given. (namespace: %s)", param_name.c_str(), n.getNamespace().c_str());
502  return false;
503  }
504 
505  // Check if the marker parameters are of type array
506  if (attribute_params.getType() != XmlRpc::XmlRpcValue::TypeArray) {
507  ROS_ERROR("Malformed \"attributes\" parameter specification. (namespace: %s)", n.getNamespace().c_str());
508  return false;
509  }
510 
511  // Iterate over the marker parameters in array
512  for(int i = 0; i < attribute_params.size(); ++i) {
513 
514  // Get the list
515  XmlRpc::XmlRpcValue& v = attribute_params[i];
516 
517  // Iterate over the list
518  ROS_DEBUG("Attributes for %s:", ns.c_str());
519  for (XmlRpc::XmlRpcValue::iterator it = v.begin(); it != v.end(); ++it) {
520  string att_name = static_cast<string>(it->first);
521  bool att_bool = static_cast<int>(it->second);
522  attribute_map_[att_name] = att_bool;
523  ROS_DEBUG(" %s will %s shown", att_name.c_str(), att_bool?"be":"not be");
524  }
525 
526  }
527 
528  return true;
529 }
double DEFAULT_OFFSET_X_ROT
tf::TransformListener * tf_listener_
Definition: Visualizer.h:68
d
double DEFAULT_SCALE_Y
PDF * msgToPDF(const problib::PDF &msg)
int dimensions() const
bool param(const std::string &param_name, T &param_val, const T &default_val)
std::map< std::string, bool > attribute_map_
Definition: Visualizer.h:74
PDFType type() const
double DEFAULT_OFFSET_Y_ROT
ValueStruct::iterator iterator
arma_inline const eOp< T1, eop_sqrt > sqrt(const Base< typename T1::elem_type, T1 > &A)
double DEFAULT_COLOR_G
visualization_msgs::Marker shape_marker
Definition: Visualizer.h:57
bool getAttributeSettings(ros::NodeHandle &n, const std::string &ns)
Definition: Visualizer.cpp:494
int size() const
virtual bool getExpectedValue(std::string &v) const
arma::vec Vector
bool getMarkerParameters(ros::NodeHandle &n, const std::string &ns)
Definition: Visualizer.cpp:324
int components() const
void setTFListener(tf::TransformListener *tf_listener)
Definition: Visualizer.cpp:220
virtual ~Visualizer()
Definition: Visualizer.cpp:31
std::map< std::string, MarkerInfo > object_class_to_marker_map_
Definition: Visualizer.h:71
arma_inline const Op< T1, op_mean > mean(const Base< typename T1::elem_type, T1 > &X, const uword dim=0)
Type const & getType() const
#define ROS_WARN(...)
iterator(field< oT > &in_M, const bool at_end=false)
double DEFAULT_COLOR_R
const arma::mat & getCovariance() const
visualization_msgs::Marker text_marker
Definition: Visualizer.h:58
bool setParameters(ros::NodeHandle &n, const std::string &ns)
Definition: Visualizer.cpp:310
double DEFAULT_OFFSET_Z_ROT
const pbl::Gaussian * getBestGaussian(const pbl::PDF &pdf, double min_weight=0)
Definition: Visualizer.cpp:227
const PDF & getComponent(int i) const
const_iterator(const field< oT > &in_M, const bool at_end=false)
#define ROS_INFO(...)
double DEFAULT_SCALE_Z
const arma::vec & getMean() const
std::string DEFAULT_TYPE
const std::string & getNamespace() const
double DEFAULT_OFFSET_Y_POS
double DEFAULT_OFFSET_Z_POS
void getStructValue(XmlRpc::XmlRpcValue &s, const std::string &name, float &f, float default_value)
Definition: Visualizer.cpp:275
bool createMarkers(const std_msgs::Header &header, long ID, const std::vector< wire_msgs::Property > &props, std::vector< visualization_msgs::Marker > &markers_out, const std::string &frame_id)
Definition: Visualizer.cpp:38
std::map< std::string, Color > color_mapping_
Definition: Visualizer.h:65
double DEFAULT_OFFSET_W_ROT
ros::Time stamp_
TFSIMD_FORCE_INLINE const tfScalar & w() const
std::string frame_id_
bool DEFAULT_SHOW_TEXT
void transformPose(const std::string &target_frame, const geometry_msgs::PoseStamped &stamped_in, geometry_msgs::PoseStamped &stamped_out) const
double getWeight(int i) const
double DEFAULT_OFFSET_X_POS
const Mixture * PDFtoMixture(const PDF &pdf)
bool getParam(const std::string &key, std::string &s) const
double DEFAULT_SCALE_X
double DEFAULT_COLOR_ALPHA
double DEFAULT_COLOR_B
bool DEFAULT_SHOW_COV
#define ROS_ASSERT(cond)
void setMarkerType(XmlRpc::XmlRpcValue &v, MarkerInfo &m)
Definition: Visualizer.cpp:454
#define ROS_ERROR(...)
const Gaussian * PDFtoGaussian(const PDF &pdf)
double DEFAULT_MARKER_LIFETIME
#define ROS_DEBUG(...)


wire_viz
Author(s): Sjoerd van den Dries, Jos Elfring
autogenerated on Fri Apr 16 2021 02:32:41