static void on_param_widget_changed(BotGtkParamWidget *pw, const char *name, void *user)
{
  RendererLaser *self = (RendererLaser*) user;

  if (!self->viewer)
    return;

  self->param_scan_memory = bot_gtk_param_widget_get_int(self->pw, PARAM_SCAN_MEMORY);
  self->param_color_mode = bot_gtk_param_widget_get_enum(self->pw, PARAM_COLOR_MODE);
  self->param_z_buffer = bot_gtk_param_widget_get_bool(self->pw, PARAM_Z_BUFFER);
  self->param_big_points = bot_gtk_param_widget_get_bool(self->pw, PARAM_BIG_POINTS);
  self->param_spacial_decimate = bot_gtk_param_widget_get_bool(self->pw, PARAM_SPATIAL_DECIMATE);
  self->z_relative = bot_gtk_param_widget_get_bool(self->pw, PARAM_Z_RELATIVE);
  self->param_color_mode_z_max_z = bot_gtk_param_widget_get_double(self->pw, PARAM_COLOR_MODE_Z_MAX_Z);
  self->param_color_mode_z_min_z = bot_gtk_param_widget_get_double(self->pw, PARAM_COLOR_MODE_Z_MIN_Z);
  self->param_max_draw_z = bot_gtk_param_widget_get_double(self->pw, PARAM_MAX_DRAW_Z);
  self->param_min_draw_z = bot_gtk_param_widget_get_double(self->pw, PARAM_MIN_DRAW_Z);
  self->param_max_draw_range = bot_gtk_param_widget_get_double(self->pw, PARAM_MAX_DRAW_RANGE);
  self->param_alpha = bot_gtk_param_widget_get_double(self->pw, PARAM_ALPHA);
  self->param_max_buffer_size = bot_gtk_param_widget_get_int(self->pw, PARAM_MAX_BUFFER_SIZE);

  for (int i = 0; i < self->channels->len; i++) {
    laser_channel *lchan = g_ptr_array_index(self->channels, i);
    lchan->enabled = bot_gtk_param_widget_get_bool(pw, lchan->name);
  }

  bot_viewer_request_redraw(self->viewer);
}
static gboolean
set_look_at_smooth_func (BotViewHandler *vhandler)
{
    BotDefaultViewHandler *dvh = (BotDefaultViewHandler*) vhandler->user;

    gboolean end_timeout = TRUE;

    double elapsed_ms = (bot_timestamp_now() - dvh->viewpath_timer_start) * 1e-3;
    double time_param = elapsed_ms / dvh->viewpath_duration_ms;

    if (time_param >= 1) {
        time_param = 1;
        end_timeout = FALSE;
    }

    for (int i=0; i<3; i++) {
        dvh->eye[i]    = dvh->origin_eye[i]    + time_param * (dvh->goal_eye[i]    - dvh->origin_eye[i]);
        dvh->lookat[i] = dvh->origin_lookat[i] + time_param * (dvh->goal_lookat[i] - dvh->origin_lookat[i]);
        dvh->up[i]     = dvh->origin_up[i]     + time_param * (dvh->goal_up[i]     - dvh->origin_up[i]);
    }

    look_at_changed(dvh);
    bot_viewer_request_redraw(dvh->viewer);

    return end_timeout;
}
static int mouse_scroll  (BotViewer *viewer, BotEventHandler *ehandler,
                          const double ray_start[3], const double ray_dir[3], const GdkEventScroll *event)
{
    BotDefaultViewHandler *dvh = (BotDefaultViewHandler*) ehandler->user;

    // scrolling = zoom in and out
    if ( event->direction == GDK_SCROLL_UP) {
        zoom_ratio (dvh, 0.9);
    } else if (event->direction == GDK_SCROLL_DOWN) {
        zoom_ratio (dvh, 1.1);
    }

    // how far from XY plane?
    double dist = (ray_start[2] - dvh->lastpos[2]) / ray_dir[2];

    double dq[] = {ray_start[0] - ray_dir[0]*dist,
                   ray_start[1] - ray_dir[1]*dist,
                   ray_start[2] - ray_dir[2]*dist,
                   1
                  };

    double PM_dq[3];
    gluProject(dq[0], dq[1], dq[2],
               dvh->model_matrix, dvh->projection_matrix, dvh->viewport,
               &PM_dq[0], &PM_dq[1], &PM_dq[2]);

    window_space_pan(dvh, dq, event->x - PM_dq[0], -((dvh->viewport[3] - event->y) - PM_dq[1]), 1);

    bot_viewer_request_redraw(viewer);
    return 1;
}
static void on_lcmgl_data (const lcm_recv_buf_t *rbuf, const char *channel,
        const bot_lcmgl_data_t *_msg, void *user_data )
{
    BotLcmglRenderer *self = (BotLcmglRenderer*) user_data;

    lcmgl_channel_t *chan = g_hash_table_lookup(self->channels, _msg->name);

    if (!chan) {
        chan = (lcmgl_channel_t*) calloc(1, sizeof(lcmgl_channel_t));
        chan->enabled=1;
        //chan->backbuffer = g_ptr_array_new();
        chan->frontbuffer = g_ptr_array_new();
        g_hash_table_insert(self->channels, strdup(_msg->name), chan);
        bot_gtk_param_widget_add_booleans (self->pw,
                0, strdup(_msg->name), 1, NULL);
    }

#if 0
    int current_scene = -1;
    if (chan->backbuffer->len > 0) {
        bot_lcmgl_data_t *ld = g_ptr_array_index(chan->backbuffer, 0);
        current_scene = ld->scene;
    }

    // new scene?
    if (current_scene != _msg->scene) {

        // free objects in foreground buffer
        for (int i = 0; i < chan->frontbuffer->len; i++)
            bot_lcmgl_data_t_destroy(g_ptr_array_index(chan->frontbuffer, i));
        g_ptr_array_set_size(chan->frontbuffer, 0);

        // swap front and back buffers
        GPtrArray *tmp = chan->backbuffer;
        chan->backbuffer = chan->frontbuffer;
        chan->frontbuffer = tmp;

        bot_viewer_request_redraw( self->viewer );
    }
#endif

    for (int i = 0; i < chan->frontbuffer->len; i++)
        bot_lcmgl_data_t_destroy(g_ptr_array_index(chan->frontbuffer, i));
    g_ptr_array_set_size (chan->frontbuffer, 0);
    g_ptr_array_add(chan->frontbuffer, bot_lcmgl_data_t_copy(_msg));
    bot_viewer_request_redraw( self->viewer );
}
static void on_clear_button(GtkWidget *button, RendererLaser *self)
{
  if (!self->viewer)
    return;

  for (int i = 0; i < self->channels->len; i++) {
    laser_channel *lchan = g_ptr_array_index(self->channels, i);
    bot_ptr_circular_clear(lchan->scans);
  }
  bot_viewer_request_redraw(self->viewer);
}
static void on_param_widget_changed (BotGtkParamWidget *pw, const char *name, void *user)
{
    BotLcmglRenderer *self = (BotLcmglRenderer*) user;

    // iterate over each channel
    GList *keys = bot_g_hash_table_get_keys(self->channels);

    for (GList *kiter=keys; kiter; kiter=kiter->next) {
        lcmgl_channel_t *chan = g_hash_table_lookup(self->channels,
                                                   kiter->data);

        chan->enabled = bot_gtk_param_widget_get_bool (pw, kiter->data);
    }
    g_list_free (keys);

    bot_viewer_request_redraw(self->viewer);
}
static void on_clear_button(GtkWidget *button, BotLcmglRenderer *self)
{
    if (!self->viewer)
        return;

    // iterate over each channel
    GList *keys = bot_g_hash_table_get_keys(self->channels);
    for (GList *kiter = keys; kiter; kiter = kiter->next) {
        lcmgl_channel_t *chan = g_hash_table_lookup(self->channels, kiter->data);
        // iterate over all the messages received for this channel
        for (int i = 0; i < chan->frontbuffer->len; i++)
            bot_lcmgl_data_t_destroy(g_ptr_array_index(chan->frontbuffer, i));
        g_ptr_array_set_size(chan->frontbuffer, 0);

    }
    g_list_free(keys);

    bot_viewer_request_redraw(self->viewer);
}
static int key_press     (BotViewer *viewer, BotEventHandler *ehandler, const GdkEventKey    *event)
{
    BotDefaultViewHandler *dvh = (BotDefaultViewHandler*) ehandler->user;

    int result = TRUE;
    switch (event->keyval) {
    case GDK_Page_Up:
        zoom_ratio (dvh, 0.9);
        break;
    case GDK_Page_Down:
        zoom_ratio (dvh, 1.1);
        break;
    case GDK_leftarrow:
    case GDK_rightarrow:
    case GDK_uparrow:
    case GDK_downarrow:
    default:
        result = FALSE;
        break;
    }

    bot_viewer_request_redraw(viewer);
    return result;
}
static void frames_update_handler(BotFrames *bot_frames, const char *frame, const char * relative_to, int64_t utime,
    void *user)
{
  RendererFrames *self = (RendererFrames *) user;
  BotTrans tran;
  int activeSensorNum = bot_gtk_param_widget_get_enum(self->pw, PARAM_FRAME_SELECT);
  if (activeSensorNum > 0) {
    const char * activeSensorName = self->frameNames[activeSensorNum];
    if (strcmp(frame, activeSensorName) == 0) {
      const char * relative_to = bot_frames_get_relative_to(self->frames, activeSensorName);
      bot_frames_get_trans(self->frames, activeSensorName, relative_to, &tran);
      double rpy[3];
      bot_quat_to_roll_pitch_yaw(tran.rot_quat, rpy);
      bot_gtk_param_widget_set_double(self->pw, PARAM_X, tran.trans_vec[0]);
      bot_gtk_param_widget_set_double(self->pw, PARAM_Y, tran.trans_vec[1]);
      bot_gtk_param_widget_set_double(self->pw, PARAM_Z, tran.trans_vec[2]);
      bot_gtk_param_widget_set_double(self->pw, PARAM_ROLL, bot_to_degrees(rpy[0]));
      bot_gtk_param_widget_set_double(self->pw, PARAM_PITCH, bot_to_degrees(rpy[1]));
      bot_gtk_param_widget_set_double(self->pw, PARAM_YAW, bot_to_degrees(rpy[2]));
      self->updating = 0;
    }
  }
  bot_viewer_request_redraw(self->viewer);
}
static int mouse_motion  (BotViewer *viewer, BotEventHandler *ehandler,
                          const double ray_start[3], const double ray_dir[3], const GdkEventMotion *event)
{
    BotDefaultViewHandler *dvh = (BotDefaultViewHandler*) ehandler->user;

    double dy = event->y - dvh->last_mouse_y;
    double dx = event->x - dvh->last_mouse_x;
    double look[3];
    bot_vector_subtract_3d (dvh->lookat, dvh->eye, look);

    if (event->state & GDK_BUTTON3_MASK) {
        double xy_len = bot_vector_magnitude_2d (look);
        double init_elevation = atan2 (look[2], xy_len);

        double left[3];
        bot_vector_cross_3d (dvh->up, look, left);
        bot_vector_normalize_3d (left);

        double delevation = -dy * 0.005;
        if (delevation + init_elevation < -M_PI/2) {
            delevation = -M_PI/2 - init_elevation;
        }
        if (delevation + init_elevation > M_PI/8) {
            delevation = M_PI/8 - init_elevation;
        }

        double q[4];
        bot_angle_axis_to_quat(-delevation, left, q);
        double newlook[3];
        memcpy (newlook, look, sizeof (newlook));
        bot_quat_rotate (q, newlook);

        double dazimuth = -dx * 0.01;
        double zaxis[] = { 0, 0, 1 };
        bot_angle_axis_to_quat (dazimuth, zaxis, q);
        bot_quat_rotate (q, newlook);
        bot_quat_rotate (q, left);

        bot_vector_subtract_3d (dvh->lookat, newlook, dvh->eye);
        bot_vector_cross_3d (newlook, left, dvh->up);
        look_at_changed(dvh);
    }
    else if (event->state & GDK_BUTTON2_MASK) {
        double init_eye_dist = bot_vector_magnitude_3d (look);

        double ded = pow (10, dy * 0.01);
        double eye_dist = init_eye_dist * ded;
        if (eye_dist > EYE_MAX_DIST)
            eye_dist = EYE_MAX_DIST;
        else if (eye_dist < EYE_MIN_DIST)
            eye_dist = EYE_MIN_DIST;

        double le[3];
        memcpy (le, look, sizeof (le));
        bot_vector_normalize_3d (le);
        bot_vector_scale_3d (le, eye_dist);

        bot_vector_subtract_3d (dvh->lookat, le, dvh->eye);
        look_at_changed(dvh);
    }
    else if (event->state & GDK_BUTTON1_MASK) {

        double dx = event->x - dvh->last_mouse_x;
        double dy = event->y - dvh->last_mouse_y;

        window_space_pan(dvh, dvh->manipulation_point, dx, dy, 1);
    }
    dvh->last_mouse_x = event->x;
    dvh->last_mouse_y = event->y;

    bot_viewer_request_redraw(viewer);
    return 1;
}
static void on_laser(const lcm_recv_buf_t *rbuf, const char *channel, const bot_core_planar_lidar_t *msg, void *user)
{
  RendererLaser *self = (RendererLaser*) user;
  g_assert(self);

  /* get the laser channel object based on channel name */
  laser_channel *lchan = g_hash_table_lookup(self->channels_hash, channel);
  if (lchan == NULL) {
    /* allocate and initialize laser channel structure */
    lchan = (laser_channel*) calloc(1, sizeof(laser_channel));
    g_assert(lchan);
    lchan->enabled = 1;
    lchan->name = bot_param_get_planar_lidar_name_from_lcm_channel(self->bot_param, channel);
    lchan->channel = strdup(channel);
    lchan->projector = laser_projector_new(self->bot_param, self->bot_frames, lchan->name, 1);

    char param_prefix[1024];
    bot_param_get_planar_lidar_prefix(NULL, lchan->name, param_prefix, sizeof(param_prefix));

    char color_key[1024];
    sprintf(color_key, "%s.viewer_color", param_prefix);
    double color[3];
    int color_size = bot_param_get_double_array(self->bot_param, color_key, color, 3);
    if (color_size != 3) {
      ERR("Error: Missing or funny color for planar LIDAR "
      "configuration key: '%s'\n", color_key);
      lchan->color[0] = 1;
      lchan->color[1] = 1;
      lchan->color[2] = 1;
    }
    else {
      lchan->color[0] = color[0];
      lchan->color[1] = color[1];
      lchan->color[2] = color[2];
    }

    lchan->scans = bot_ptr_circular_new(self->param_max_buffer_size, laser_scan_destroy, NULL);
    g_assert(lchan->scans);

    /* add laser channel to hash table and array */
    g_hash_table_insert(self->channels_hash, lchan->channel, lchan);
    g_ptr_array_add(self->channels, lchan);

    /* add check box */
    if (self->viewer)
      bot_gtk_param_widget_add_booleans(self->pw, 0, lchan->name, lchan->enabled, NULL);
  }

  /* TODO: Optimization - allocate space for local points from a
   circular buffer instead of calling calloc for each scan */

  laser_projected_scan *lscan = laser_create_projected_scan_from_planar_lidar(lchan->projector, msg,
      bot_frames_get_root_name(self->bot_frames));
  if (lscan == NULL)
    return; //probably didn't have a pose message yet...
  
  if(lchan->scans->capacity != self->param_max_buffer_size){
      bot_ptr_circular_resize(lchan->scans, self->param_max_buffer_size);
  }
  
  if (bot_ptr_circular_size(lchan->scans) > 0) {
    laser_projected_scan *last_scan = bot_ptr_circular_index(lchan->scans, 0);

    /* check for a large time difference between scans (typical when
     jumping around an LCM log file) */
    gboolean time_jump = FALSE;
    int64_t dt = msg->utime - last_scan->utime;
    if (dt < -OLD_HISTORY_THRESHOLD || dt > OLD_HISTORY_THRESHOLD)
      time_jump = TRUE;

    /* spacial decimation */
    gboolean stationary = FALSE;
    BotTrans delta;
    if (self->param_spacial_decimate && self->param_scan_memory > 10) {
      bot_trans_invert_and_compose(&lscan->origin, &last_scan->origin, &delta);
      double dist = bot_vector_magnitude_3d(delta.trans_vec);
      double rot;
      double axis[3];
      bot_quat_to_angle_axis(delta.rot_quat, &rot, axis);
      if (dist < SPACIAL_DECIMATION_LIMIT && rot < ANGULAR_DECIMATION_LIMIT) {
        stationary = TRUE;
      }
    }

    if (stationary) {
      laser_scan_destroy(NULL, lscan);
      return;
    }
  }

  bot_ptr_circular_add(lchan->scans, lscan);

  if (self->viewer)
    bot_viewer_request_redraw(self->viewer);

  return;
}
static void on_param_widget_changed(BotGtkParamWidget *pw, const char *name, void *user)
{
  RendererFrames *self = (RendererFrames*) user;
  if (self->updating) {
    return;
  }
  BotViewer *viewer = self->viewer;
  int activeSensorNum = bot_gtk_param_widget_get_enum(pw, PARAM_FRAME_SELECT);
  if (!strcmp(name, PARAM_FRAME_SELECT)) {
    if (activeSensorNum > 0) {
      self->updating = 1;
      bot_viewer_set_status_bar_message(self->viewer, "Modify Calibration relative to %s", bot_frames_get_relative_to(
          self->frames, self->frameNames[activeSensorNum]));
      bot_gtk_param_widget_set_enabled(self->pw, PARAM_X, 1);
      bot_gtk_param_widget_set_enabled(self->pw, PARAM_Y, 1);
      bot_gtk_param_widget_set_enabled(self->pw, PARAM_Z, 1);
      bot_gtk_param_widget_set_enabled(self->pw, PARAM_ROLL, 1);
      bot_gtk_param_widget_set_enabled(self->pw, PARAM_PITCH, 1);
      bot_gtk_param_widget_set_enabled(self->pw, PARAM_YAW, 1);
      bot_gtk_param_widget_set_enabled(self->pw, PARAM_SAVE, 1);
      frames_update_handler(self->frames, self->frameNames[activeSensorNum], bot_frames_get_relative_to(self->frames,
          self->frameNames[activeSensorNum]), bot_timestamp_now(), self);
    }
    else {
      bot_viewer_set_status_bar_message(self->viewer, "");
      bot_gtk_param_widget_set_enabled(self->pw, PARAM_X, 0);
      bot_gtk_param_widget_set_enabled(self->pw, PARAM_Y, 0);
      bot_gtk_param_widget_set_enabled(self->pw, PARAM_Z, 0);
      bot_gtk_param_widget_set_enabled(self->pw, PARAM_ROLL, 0);
      bot_gtk_param_widget_set_enabled(self->pw, PARAM_PITCH, 0);
      bot_gtk_param_widget_set_enabled(self->pw, PARAM_YAW, 0);
      bot_gtk_param_widget_set_enabled(self->pw, PARAM_SAVE, 0);
    }
  }
  else if (!strcmp(name, PARAM_SAVE) && activeSensorNum > 0) {
    char save_fname[1024];
    sprintf(save_fname, "manual_calib_%s.cfg", self->frameNames[activeSensorNum]);
    fprintf(stderr, "saving params to: %s\n", save_fname);
    FILE * f = fopen(save_fname, "w");
    double pos[3];
    pos[0] = bot_gtk_param_widget_get_double(self->pw, PARAM_X);
    pos[1] = bot_gtk_param_widget_get_double(self->pw, PARAM_Y);
    pos[2] = bot_gtk_param_widget_get_double(self->pw, PARAM_Z);
    double rpy[3];
    rpy[0] = bot_gtk_param_widget_get_double(self->pw, PARAM_ROLL);
    rpy[1] = bot_gtk_param_widget_get_double(self->pw, PARAM_PITCH);
    rpy[2] = bot_gtk_param_widget_get_double(self->pw, PARAM_YAW);

    double rpy_rad[3];
    for (int i = 0; i < 3; i++)
      rpy_rad[i] = bot_to_radians(rpy[i]);
    double quat[4];
    bot_roll_pitch_yaw_to_quat(rpy_rad, quat);
    double rod[3];
    bot_quat_to_rodrigues(quat, rod);

    fprintf(f, ""
      "%s {\n"
      "position = [%f, %f, %f];\n"
      "rpy = [%f, %f, %f];\n"
      "relative_to = \"%s\";\n"
      "}", self->frameNames[activeSensorNum], pos[0], pos[1], pos[2], rpy[0], rpy[1], rpy[2],
        bot_frames_get_relative_to(self->frames, self->frameNames[activeSensorNum]));
    fprintf(f, ""
      "\n"
      "%s {\n"
      "position = [%f, %f, %f];\n"
      "rodrigues = [%f, %f, %f];\n"
      "relative_to = \"%s\";\n"
      "}", self->frameNames[activeSensorNum], pos[0], pos[1], pos[2], rod[0], rod[1], rod[2],
        bot_frames_get_relative_to(self->frames, self->frameNames[activeSensorNum]));

    fclose(f);
    bot_viewer_set_status_bar_message(self->viewer, "Calibration saved to %s", save_fname);

  }
  else if (activeSensorNum > 0) {
    self->updating = 1;
    BotTrans curr;
    curr.trans_vec[0] = bot_gtk_param_widget_get_double(self->pw, PARAM_X);
    curr.trans_vec[1] = bot_gtk_param_widget_get_double(self->pw, PARAM_Y);
    curr.trans_vec[2] = bot_gtk_param_widget_get_double(self->pw, PARAM_Z);

    double rpy[3];
    rpy[0] = bot_to_radians(bot_gtk_param_widget_get_double(self->pw, PARAM_ROLL));
    rpy[1] = bot_to_radians(bot_gtk_param_widget_get_double(self->pw, PARAM_PITCH));
    rpy[2] = bot_to_radians(bot_gtk_param_widget_get_double(self->pw, PARAM_YAW));
    bot_roll_pitch_yaw_to_quat(rpy, curr.rot_quat);
    if (fabs(rpy[0]) > M_PI || fabs(rpy[1]) > M_PI || fabs(rpy[2]) > M_PI) {
      bot_gtk_param_widget_set_double(self->pw, PARAM_ROLL, bot_to_degrees(bot_mod2pi(rpy[0])));
      bot_gtk_param_widget_set_double(self->pw, PARAM_PITCH, bot_to_degrees(bot_mod2pi(rpy[1])));
      bot_gtk_param_widget_set_double(self->pw, PARAM_YAW, bot_to_degrees(bot_mod2pi(rpy[2])));
    }

    //and update the link
    const char * frame_name = self->frameNames[activeSensorNum];
    const char * relative_to = bot_frames_get_relative_to(self->frames, frame_name);
    bot_frames_update_frame(self->frames, frame_name, relative_to, &curr, bot_timestamp_now());
  }

  bot_viewer_request_redraw(self->viewer);
}
static void
on_param_widget_changed (BotGtkParamWidget *pw, const char *name, void *user)
{
    GfeRenderer *self = (GfeRenderer*) user;
    bot_viewer_request_redraw(self->viewer);
}