Skip to content

File gl_widget.hpp

File List > gui > gl_widget.hpp

Go to the documentation of this file

#pragma once

#include <QEvent>
#include <QFutureWatcher>
#include <QMatrix4x4>
#include <QMetaObject>
#include <QMouseEvent>
#include <QOpenGLFunctions>
#include <QOpenGLWidget>
#include <QShowEvent>
#include <QTimer>
#include <QVector3D>
#include <QtConcurrent>
#include <chrono>
#include <functional>
#include <iostream>
#include <optional>
#include <unordered_set>
#include <vector>

#include "gui/camera.hpp"
#include "gui/gl_check.hpp"
#include "gui/layer.hpp"
#include "gui/layer_renderer.hpp"
#include "gui/point_cloud_framebuffer.hpp"
#include "gui/scene_framebuffer.hpp"
#include "io/crs.hpp"

QT_FORWARD_DECLARE_CLASS(QOpenGLShaderProgram)

class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions {
  Q_OBJECT

 public:
  enum class PickAction { None, PanToPoint, LookAtPoint };

  GLWidget(QWidget* parent = nullptr);
  ~GLWidget();

  void add_layer(std::shared_ptr<Layer> layer, bool auto_zoom = true) {
    const std::string layer_projection = layer->projection();
    const std::string layer_native = layer->native_projection();
    if (m_layers.empty()) {
      m_projection = layer_projection;
      m_camera.world_offset() = layer->extent().center();
    } else if (!layer_native.empty() && !m_projection.empty() &&
               !wkt_matches(normalize_crs_wkt(layer_native), normalize_crs_wkt(m_projection))) {
      std::cerr << "Reprojecting layer \"" << layer->name() << "\" from CRS " << layer_native
                << " to " << m_projection << ".\n";
    }
    makeCurrent();
    m_layers.emplace_back(layer);
    auto renderer = LayerRenderer::create(m_layers.back(), m_camera.world_offset());
    if (!renderer) {
      m_layers.pop_back();
      throw std::runtime_error("Unsupported layer type: " + layer->name());
    }
    m_renderers.emplace_back(std::move(renderer));
    m_renderers.back()->set_visible(layer->visible());
    if (auto* las = dynamic_cast<OctreeLASLayerRenderer*>(m_renderers.back().get())) {
      las->set_layer_slot(allocate_layer_slot());
    }
    connect(m_layers.back().get(), &Layer::data_updated, m_renderers.back().get(),
            &LayerRenderer::data_update_required);
    connect(m_layers.back().get(), &Layer::data_updated, this,
            [this, layer = m_layers.back(), auto_zoom] {
              if (layer->kind() != LayerKind::PointCloud) {
                update();
                return;
              }
              const auto* las_layer = dynamic_cast<const LASLayer*>(layer.get());
              if (las_layer == nullptr) {
                update();
                return;
              }
              const Extent3D local_extent = layer->extent() - m_camera.world_offset();
              if (local_extent.max_extent() <= 0) {
                restart_render();
                update();
                return;
              }
              restart_render();
              if (auto_zoom && m_zoomed_layers.count(layer.get()) == 0 &&
                  local_extent.max_extent() > 0) {
                request_zoom_to_extent(local_extent);
                m_zoomed_layers.insert(layer.get());
              }
              update();
            });
    if (auto las_layer = std::dynamic_pointer_cast<LASLayer>(m_layers.back())) {
      connect(las_layer.get(), &LASLayer::point_size_changed, this,
              [this] { refresh_point_cloud_style(); });
      connect(las_layer.get(), &LASLayer::point_colors_changed, this,
              [this] { refresh_point_cloud_style(); });
      connect(las_layer.get(), &LASLayer::point_opacity_changed, this, [this] {
        restart_render();
        update();
      });
      connect(las_layer.get(), &LASLayer::point_stream_budget_changed, this,
              [this] { refresh_point_cloud_style(); });
      connect(las_layer.get(), &LASLayer::lod_settings_changed, this,
              [this] { refresh_point_cloud_style(); });
    }
    connect(m_renderers.back().get(), &LayerRenderer::repaint_required, this,
            [this] { QMetaObject::invokeMethod(this, "update", Qt::QueuedConnection); });
    connect(m_renderers.back().get(), &LayerRenderer::stream_view_reset, this,
            [this] { restart_render(); });
    connect(m_layers.back().get(), &Layer::visibility_changed, this,
            [this, renderer = m_renderers.back().get()](bool visible) {
              renderer->set_visible(visible);
              restart_render();
              update();
            });
    connect(m_layers.back().get(), &Layer::opacity_changed, this, [this] {
      restart_render();
      update();
    });
    connect(m_layers.back().get(), &Layer::vertical_offset_changed, this, [this] {
      restart_render();
      update();
    });
    if (auto_zoom && layer->extent().max_extent() > 0) {
      request_zoom_to_extent(layer->extent() - m_camera.world_offset());
    }
    restart_render();
    update();
  }

  enum class AnimType { None, Orbit, Wobble };
  void start_bench_orbit(double duration_seconds = 10.0);
  void start_animation(AnimType type);
  void stop_animation();
  void set_anim_type(int t);
  int anim_type() const { return static_cast<int>(m_anim_type); }
  void set_orbit_period(double secs) { m_orbit_period_secs = secs; }
  double orbit_period() const { return m_orbit_period_secs; }
  void set_wobble_period(double secs) { m_wobble_period_secs = secs; }
  double wobble_period() const { return m_wobble_period_secs; }
  void set_wobble_amplitude(double deg) { m_wobble_amplitude_deg = deg; }
  double wobble_amplitude() const { return m_wobble_amplitude_deg; }
  bool is_anim_active() const { return m_orbit_timer && m_orbit_timer->isActive(); }

  void set_layer_visible(Layer* layer, bool visible) {
    for (size_t i = 0; i < m_layers.size(); ++i) {
      if (m_layers[i].get() == layer) {
        m_renderers[i]->set_visible(visible);
        restart_render();
        update();
        return;
      }
    }
  }

  void remove_layer(Layer* layer);

  std::vector<std::shared_ptr<Layer>> layers() const { return m_layers; }

  const std::string& reference_crs_wkt() const { return m_projection; }

  void set_load_only_mode(bool load_only) { m_load_only = load_only; }

  void zoom_to_layer(const Layer* layer) {
    if (!layer || layer->extent().max_extent() <= 0) {
      return;
    }
    request_zoom_to_extent(layer->extent() - m_camera.world_offset());
    restart_render();
    update();
  }

  void zoom_to_all_layers() {
    Extent3D combined_extent;
    for (const auto& layer : m_layers) {
      combined_extent.grow(layer->extent() - m_camera.world_offset());
    }
    if (combined_extent.max_extent() > 0) {
      request_zoom_to_extent(combined_extent);
      restart_render();
      update();
    }
  }

  void restart_render();
  void refresh_point_cloud_style();
  void set_camera_light_angles(float azimuth_deg, float elevation_deg);
  float camera_light_azimuth_deg() const { return m_light_azimuth_deg; }
  float camera_light_elevation_deg() const { return m_light_elevation_deg; }
  void set_lighting_strength(float ambient_light, float diffuse_light);
  float ambient_light() const { return m_ambient_light; }
  float diffuse_light() const { return m_diffuse_light; }
  void set_point_ambient_light(float point_ambient_light);
  float point_ambient_light() const { return m_point_ambient_light; }

  using PointPickCallback = std::function<void(const std::optional<PointPickResult>&)>;
  void set_point_pick_callback(PointPickCallback callback) {
    m_point_pick_callback = std::move(callback);
  }

  void set_selected_point(const std::optional<PointPickResult>& pick, bool repaint = true);

 protected:
  QSize sizeHint() const override;
  QSize minimumSizeHint() const override;
  void initializeGL() override;
  void resizeGL(int w, int h) override;
  void showEvent(QShowEvent* event) override;
  void paintGL() override;
  void mousePressEvent(QMouseEvent* event) override;
  void mouseMoveEvent(QMouseEvent* event) override;
  void mouseReleaseEvent(QMouseEvent* event) override;
  void leaveEvent(QEvent* event) override;
  void wheelEvent(QWheelEvent* event) override;
  void keyPressEvent(QKeyEvent* event) override;

  void begin_camera_interaction();
  void note_camera_motion();
  void schedule_camera_idle();

 private slots:
  void on_camera_idle();
  void on_stream_tick();
  void on_orbit_tick();

 private:
  void request_zoom_to_extent(const Extent3D& extent) {
    m_pending_zoom_extent = extent;
    m_needs_zoom = true;
    apply_pending_zoom();
  }

  void apply_pending_zoom() {
    if (!m_needs_zoom || !m_pending_zoom_extent.has_value()) {
      return;
    }
    const int w = width();
    const int h = height();
    if (w <= 0 || h <= 0) {
      return;
    }
    m_camera.set_screen_size(w, h);
    m_camera.zoom_to_fit(*m_pending_zoom_extent);
    m_needs_zoom = false;
    m_pending_zoom_extent.reset();
    restart_render();
  }

  void try_pick_point(const QPointF& pixel, PickAction action = PickAction::None);
  void update_hover_at(const QPointF& pixel, int max_radius, bool repaint = true);
  void do_pick_at(const QPointF& pixel, PickAction action, bool repaint = true);
  std::optional<PointPickResult> fb_pick_point(const QPointF& pixel, int max_radius);
  bool fbo_pick_ready() const;
  float point_layer_alpha() const;
  bool pick_interaction_enabled() const;
  void apply_hover_result(const std::optional<PointPickResult>& result, bool repaint = true);
  std::optional<QPointF> project_pick_to_screen(const PointPickResult& pick) const;
  int allocate_layer_slot();
  void release_layer_slot(int slot);
  void pan_to_selected_point();
  void look_at_selected_point();
  void clear_picks_for_layer(const std::string& layer_name);
  void draw_stats_overlay();
  void update_scene_bounds();

  SceneFramebuffer m_scene_fbo;
  PointCloudFramebuffer m_points_fbo;
  PointCloudCompositor m_point_compositor;
  bool m_incremental_draw = false;
  bool m_restarted_during_paint = false;
  bool m_camera_interacting = false;
  QTimer* m_idle_timer = nullptr;
  QTimer* m_stream_timer = nullptr;
  QTimer* m_orbit_timer = nullptr;
  std::chrono::steady_clock::time_point m_last_orbit_tick;
  double m_anim_phase = 0.0;
  AnimType m_anim_type = AnimType::None;
  AnimType m_last_anim_type = AnimType::Orbit;  // remembered for Space toggle
  double m_orbit_period_secs = 15.0;
  double m_wobble_period_secs = 4.0;
  double m_wobble_amplitude_deg = 0.5;
  QPointF m_last_mouse_pos;
  QPointF m_press_mouse_pos;
  bool m_left_button_pressed = false;
  std::optional<QPointF> m_pending_pick_pixel;
  PickAction m_pending_pick_action = PickAction::None;

  PointPickCallback m_point_pick_callback;
  std::optional<PointPickResult> m_selected_point;
  std::optional<PointPickResult> m_hovered_point;
  bool m_fbo_was_pick_ready = false;
  std::optional<QPointF> m_last_hover_probe_pos;
  bool m_hover_probe_needed = false;
  int m_next_layer_slot = 1;
  std::vector<int> m_free_layer_slots;

  Camera m_camera;

  std::vector<std::shared_ptr<Layer>> m_layers;
  std::vector<std::unique_ptr<LayerRenderer>> m_renderers;

  std::string m_projection;
  bool m_painting = false;
  bool m_load_only = false;
  bool m_needs_zoom = false;
  std::optional<Extent3D> m_pending_zoom_extent;
  std::unordered_set<Layer*> m_zoomed_layers;

  std::optional<std::chrono::steady_clock::time_point> m_prev_paint_time;
  double m_present_frame_ms_ema = 16.0;
  double m_last_present_frame_ms = 0.0;
  double m_last_paint_ms = 0.0;
  double m_last_point_draw_ms = 0.0;
  double m_last_point_gpu_ms = 0.0;
  size_t m_last_point_vertices = 0;
  float m_light_azimuth_deg = -50.0f;
  float m_light_elevation_deg = 35.0f;
  float m_ambient_light = 0.30f;
  float m_diffuse_light = 1.00f;
  float m_point_ambient_light = 0.55f;
};