/*
 *  $Id: gwyglview.c 29754 2026-04-09 09:35:42Z yeti-dn $
 *  Copyright (C) 2003-2025 David Necas (Yeti), Petr Klapetek.
 *  E-mail: yeti@gwyddion.net, klapetek@gwyddion.net.
 *  Copyright (C) 2004 Martin Siler.
 *  E-mail: silerm@physics.muni.cz.
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 *  warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along with this program; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

/* TODO GTK3 Now we can create GdkGLContext for particular windows using gdk_window_create_gl_context().
 * There does not seem to be any global check, we must see if we succeed for any particular window.
 * See
 *
 * https://stackoverflow.com/questions/69201843/how-do-you-do-3d-rendering-in-gtk-using-opengl
 *
 * for a complete a GTK+3 OpenGL example (including a shader). The gtk3-demo demo program also includes
 * a simple GTK+3 OpenGL demo with shaders and stuff. */

#include "config.h"
#include <string.h>
#include <stdlib.h>

#include <gdk/gdk.h>
#include <gdk/gdkkeysyms.h>
#include <gtk/gtk.h>
#include <glib/gprintf.h>
#include <pango/pangocairo.h>

#ifdef G_OS_WIN32
#define WIN32_LEAN_AND_MEAN 1
#include <windows.h>
#endif

#ifdef HAVE_GTKGLEXT
#ifdef GDK_WINDOWING_QUARTZ
#include <OpenGL/gl.h>
#else
#include <GL/gl.h>
#ifdef HAVE_GL_GLEXT_H
#include <GL/glext.h>
#endif
#endif

#ifndef GL_BGRA
#ifdef GL_BGRA_EXT
#define GL_BGRA GL_BGRA_EXT
#else
#error FIXME: GL_BGRA is not available, should work around it.
#endif
#endif
#else
#define GLfloat gfloat
#endif

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"
#include "libgwyddion/stats.h"
#include "libgwyui/pixbuf-render.h"

#include "libgwyui/gwyui.h"

#define BITS_PER_SAMPLE 8

#define  GWY_GL_ORTHO_CORRECTION  2.0
#define  GWY_GL_Z_DEFORMATION     0.01
#define  GWY_GL_Z_TRANSFORMATION  0.5
#define  GWY_GL_Z_DISPLACEMENT   -0.2
#define  GWY_GL_TICK_LENGTH 10

#define connect_swapped_after(obj, signal, cb, data) \
    g_signal_connect_object(obj, signal, G_CALLBACK(cb), data, G_CONNECT_SWAPPED | G_CONNECT_AFTER)

enum {
    GWY_GL_VIEW_DEFAULT_SIZE_X = 260,
    GWY_GL_VIEW_DEFAULT_SIZE_Y = 260
};

enum {
    GWY_GL_SHAPE = 0,
    GWY_GL_N_LISTS
};

/* Changed components */
enum {
    GWY_GL_GRADIENT   = 1 << 0,
    GWY_GL_CMATERIAL   = 1 << 1,
    GWY_GL_DATA_FIELD = 1 << 2,
    GWY_GL_MASK       = 0x07
};

enum {
    GWY_GL_JUST_LEFT_EDGE = 0,
    GWY_GL_JUST_TOP_EDGE = 0,
    GWY_GL_JUST_CENTER = 1,
    GWY_GL_JUST_RIGHT_EDGE = 2,
    GWY_GL_JUST_BOTTOM_EDGE = 2,
};

enum {
    PROP_0,
    PROP_MOVEMENT,
    PROP_REDUCED_SIZE,
    PROP_DATA_KEY,
    PROP_SETUP_PREFIX,
    PROP_GRADIENT_KEY,
    PROP_MATERIAL_KEY,
    PROP_MASK_KEY,
    PROP_LAST
};

typedef struct {
    GLfloat x, y, z;
} GwyGLVector;

/**
 * GwyGLListPool:
 * @base: The first list id in the pool.
 * @size: List id range length.
 * @pool: Bits correspond to GL lists, 0 means free, 1 assigned.
 *
 * GL list pool.
 **/
typedef struct {
    guint base;
    guint size;
    guint64 pool;
} GwyGLListPool;

struct _GwyGLViewPrivate {
    GwyField *mask_field;
    GQuark mask_key;
    gulong mask_id;
    gulong mask_item_id;
    gboolean need_update_list;
    gboolean button;
    gboolean has_moved;
    gdouble zbot;
    gdouble ztop;

    GwyGLMovement movement;

    GwyDict *data;           /* Container with data */
    guint reduced_size;
    /* Downsampled data for faster rendering.  Unset when the full-size field
     * is sufficiently small, which is now always. */
    GwyField *downsampled;
    GwyField *downsampled2;

    GwyGLSetup *setup;
    GQuark setup_key;
    gulong setup_id;
    gulong setup_item_id;

    GwyField *field;
    GQuark data_key;
    gulong data_id;
    gulong data_item_id;

    /* Unused, intended for future color !~ height visualization */
    //GwyPixmapLayer **ovlays;
    gpointer *ovlays;
    GQuark data2_key;
    gulong novlays;
    gulong data2_item_id;

    GwyGradient *gradient;
    GQuark gradient_key;
    gulong gradient_id;
    gulong gradient_item_id;

    GwyGLMaterial *material;
    GQuark material_key;
    gulong material_id;
    gulong material_item_id;

    GwyGLLabel **labels;          /* labels text, displacement etc */
    gulong *label_ids;
    gulong *label_item_ids;
    GHashTable *variables;        /* Label substitution variables */

    gint changed;                 /* What has changed, XXX: not used */

    gint shape_list_base;         /* Base index of scene display lists */
    guint shape_current;          /* Actually shown shape in the scene
                                     (full or reduced data) */

    gdouble view_scale_max;       /* Maximum zoom of the scene */
    gdouble view_scale_min;       /* Minimum zoom of the scene */

    gdouble mouse_begin_x;        /* Start x-coordinate of mouse */
    gdouble mouse_begin_y;        /* Start y-coordinate of mouse */

    guint timeout_id;             /* Full size redraw timeout id */

    gboolean hide_masked;

    guint     *ovlay_updated_id;
};

#ifdef HAVE_GTKGLEXT
static void         gwy_gl_view_destroy              (GtkWidget *widget);
static void         gwy_gl_view_finalize             (GObject *object);
static void         gwy_gl_view_set_property         (GObject *object,
                                                      guint prop_id,
                                                      const GValue *value,
                                                      GParamSpec *pspec);
static void         gwy_gl_view_get_property         (GObject *object,
                                                      guint prop_id,
                                                      GValue *value,
                                                      GParamSpec *pspec);
static void         gwy_gl_view_realize              (GtkWidget *widget);
static void         gwy_gl_view_unrealize            (GtkWidget *widget);
static void         gwy_gl_view_container_connect    (GwyGLView *glview,
                                                      const gchar *data_key_string,
                                                      gulong *id,
                                                      GCallback callback);
static void         gwy_gl_view_data_item_changed    (GwyGLView *glview);
static void         gwy_gl_view_field_connect        (GwyGLView *glview);
static void         gwy_gl_view_field_disconnect     (GwyGLView *glview);
static void         gwy_gl_view_field_changed        (GwyGLView *glview);
static void         gwy_gl_view_setup_item_changed   (GwyGLView *glview);
static void         gwy_gl_view_ensure_setup         (GwyGLView *glview);
static void         gwy_gl_view_setup_connect        (GwyGLView *glview);
static void         gwy_gl_view_setup_disconnect     (GwyGLView *glview);
static void         gwy_gl_view_setup_changed        (GwyGLView *glview,
                                                      GParamSpec *pspec);
static void         gwy_gl_view_gradient_item_changed(GwyGLView *glview);
static void         gwy_gl_view_gradient_connect     (GwyGLView *glview);
static void         gwy_gl_view_gradient_disconnect  (GwyGLView *glview);
static void         gwy_gl_view_gradient_changed     (GwyGLView *glview);
static void         gwy_gl_view_material_item_changed(GwyGLView *glview);
static void         gwy_gl_view_material_connect     (GwyGLView *glview);
static void         gwy_gl_view_material_disconnect  (GwyGLView *glview);
static void         gwy_gl_view_material_changed     (GwyGLView *glview);
static void         gwy_gl_view_update_lists         (GwyGLView *glview);
static void         gwy_gl_view_realize_gl           (GwyGLView *glview);
static void         gwy_gl_view_size_request         (GtkWidget *widget,
                                                      GtkRequisition *requisition);
static void         gwy_gl_view_size_allocate        (GtkWidget *widget,
                                                      GdkRectangle *allocation);
static gboolean     gwy_gl_view_expose               (GtkWidget *widget,
                                                      GdkEventExpose *event);
static gboolean     gwy_gl_view_configure            (GtkWidget *widget,
                                                      GdkEventConfigure *event);
static void         gwy_gl_view_send_configure       (GwyGLView *glview);
static gboolean     gwy_gl_view_button_press         (GtkWidget *widget,
                                                      GdkEventButton *event);
static gboolean     gwy_gl_view_button_release       (GtkWidget *widget,
                                                      GdkEventButton *event);
static gboolean     gwy_gl_view_scroll               (GtkWidget *widget,
                                                      GdkEventScroll *event);
static void         gwy_gl_calculate_pixel_sizes     (GwyField *dfield,
                                                      GLfloat *dx,
                                                      GLfloat *dy);
static GwyGLVector* gwy_gl_make_normals              (GwyField *dfield,
                                                      GwyGLVector *normals);
static gboolean     gwy_gl_view_motion_notify        (GtkWidget *widget,
                                                      GdkEventMotion *event);
static void         gwy_gl_make_list                 (GwyGLView *glview,
                                                      GwyField *dfield,
                                                      GwyField *mask,
                                                      GwyPixmapLayer **ovlays);
static void         gwy_gl_draw_axes                 (GwyGLView *glview,
                                                      gint width,
                                                      gint height);
static void         gwy_gl_draw_light_position       (GwyGLView *glview);
static void         gwy_gl_set_projection            (GwyGLView *glview,
                                                      GLfloat aspect);
static void         gwy_gl_view_update_labels        (GwyGLView *glview);
static void         gwy_gl_view_label_changed        (GwyGLView *glview);
static void         gwy_gl_view_queue_draw           (GwyGLView *glview);
static void         gwy_gl_texture_text              (GwyGLView *glview,
                                                      GwyGLViewLabel id,
                                                      GLfloat raster_x,
                                                      GLfloat raster_y,
                                                      GLfloat raster_z,
                                                      GLfloat rot,
                                                      guint size,
                                                      gint hjustify,
                                                      gint vjustify);
static gint         gwy_gl_draw_fmscaletex           (GwyGLView *view);
static void         gwy_gl_view_class_make_list_pool (GwyGLListPool *pool);
static void         gwy_gl_view_assign_lists         (GwyGLView *glview);
static void         gwy_gl_view_release_lists        (GwyGLView *glview);
static void         gwy_gl_view_mask_item_changed    (GwyGLView *glview);
static void         gwy_gl_view_mask_field_connect   (GwyGLView *glview);
static void         gwy_gl_view_mask_field_disconnect(GwyGLView *glview);
static void         gwy_gl_view_mask_field_changed   (GwyGLView *glview);

/* Must match GwyGLViewLabel */
static const struct {
    const gchar *key;
    const gchar *default_text;
}
labels[GWY_GL_VIEW_NLABELS] = {
    { "x",   "x: $x", },
    { "y",   "y: $y", },
    { "min", "$min",  },
    { "max", "$max",  },
};

static GwyGLListPool *list_pool = NULL;
#endif /* HAVE_GTKGLEXT */

static gboolean ugly_hack_globally_disable_axis_drawing = FALSE;

G_DEFINE_TYPE_WITH_CODE(GwyGLView, gwy_gl_view, GTK_TYPE_WIDGET,
                        G_ADD_PRIVATE(GwyGLView))

#ifdef HAVE_GTKGLEXT
static void
gwy_gl_view_class_init(GwyGLViewClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);

    gobject_class->finalize = gwy_gl_view_finalize;
    gobject_class->set_property = gwy_gl_view_set_property;
    gobject_class->get_property = gwy_gl_view_get_property;

    widget_class->destroy = gwy_gl_view_destroy;

    widget_class->realize = gwy_gl_view_realize;
    widget_class->unrealize = gwy_gl_view_unrealize;
    widget_class->expose_event = gwy_gl_view_expose;
    widget_class->configure_event = gwy_gl_view_configure;
    widget_class->size_request = gwy_gl_view_size_request;
    widget_class->size_allocate = gwy_gl_view_size_allocate;
    widget_class->button_press_event = gwy_gl_view_button_press;
    widget_class->button_release_event = gwy_gl_view_button_release;
    widget_class->scroll_event = gwy_gl_view_scroll;
    widget_class->motion_notify_event = gwy_gl_view_motion_notify;

    /**
     * GwyGLView:movement-type:
     *
     * The :movement-type property represents type of action on user pointer
     * drag.
     **/
    g_object_class_install_property(gobject_class,
                                    PROP_MOVEMENT,
                                    g_param_spec_enum("movement-type",
                                                      "Movement type",
                                                      "What quantity is changed when uses moves pointer",
                                                      GWY_TYPE_3D_MOVEMENT, GWY_GL_MOVEMENT_NONE,
                                                      G_PARAM_READWRITE));

    /**
     * GwyGLView:reduced-size:
     *
     * The :reduced-size is the size of downsampled data in quick preview.
     *
     * Note since 2.44 the reduced size has no influence because the OpenGL view does not downsample anything.
     **/
    g_object_class_install_property(gobject_class,
                                    PROP_REDUCED_SIZE,
                                    g_param_spec_uint("reduced-size",
                                                      "Reduced size",
                                                      "The size of downsampled data in quick view",
                                                      2, G_MAXINT, 96,
                                                      G_PARAM_READWRITE));

    g_object_class_install_property(gobject_class,
                                    PROP_DATA_KEY,
                                    g_param_spec_string("data-key",
                                                        "Data key",
                                                        "Key identifying data field in container",
                                                        NULL, G_PARAM_READWRITE));

    g_object_class_install_property(gobject_class,
                                    PROP_SETUP_PREFIX,
                                    g_param_spec_string("setup-prefix",
                                                        "Setup prefix",
                                                        "Key prefix identifying view settings and labels "
                                                        "in container",
                                                        NULL, G_PARAM_READWRITE));

    g_object_class_install_property(gobject_class,
                                    PROP_GRADIENT_KEY,
                                    g_param_spec_string("gradient-key",
                                                        "Gradient key",
                                                        "Key identifying color gradient in container",
                                                        NULL, G_PARAM_READWRITE));

    g_object_class_install_property(gobject_class,
                                    PROP_MATERIAL_KEY,
                                    g_param_spec_string("material-key",
                                                        "Material key",
                                                        "Key identifying GL material in container",
                                                        NULL, G_PARAM_READWRITE));

    g_object_class_install_property(gobject_class,
                                    PROP_MASK_KEY,
                                    g_param_spec_string("mask-key",
                                                        "Mask key",
                                                        "Key identifying mask field in container",
                                                        NULL, G_PARAM_READWRITE));
}

static void
gwy_gl_view_init(GwyGLView *glview)
{
    glview->view_scale_max = 8.0;
    glview->view_scale_min = 0.5;
    glview->movement       = GWY_GL_MOVEMENT_NONE;
    glview->reduced_size   = 96;
    glview->ovlays         = NULL;
    glview->priv = G_TYPE_INSTANCE_GET_PRIVATE(glview, GWY_TYPE_3D_VIEW, GwyGLViewPrivate);

    glview->variables = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free);

    glview->labels = g_new0(GwyGLLabel*, GWY_GL_VIEW_NLABELS);
    glview->label_ids = g_new0(gulong, GWY_GL_VIEW_NLABELS);
}

static void
gwy_gl_view_destroy(GtkWidget *widget)
{
    GwyGLView *glview = GWY_GL_VIEW(widget);
    GwyGLViewPrivate priv = *glview->priv;
    guint i;

    g_clear_signal_handler(&glview->data_item_id, glview->data);
    g_clear_signal_handler(&priv->mask_item_id, glview->data);
    g_clear_signal_handler(&glview->gradient_item_id, glview->data);
    g_clear_signal_handler(&glview->material_item_id, glview->data);

    gwy_gl_view_setup_disconnect(glview);
    gwy_gl_view_field_disconnect(glview);
    gwy_gl_view_mask_field_disconnect(glview);
    gwy_gl_view_gradient_disconnect(glview);
    gwy_gl_view_material_disconnect(glview);

    if (glview->shape_list_base > 0)
        gwy_gl_view_release_lists(glview);

    g_clear_object(&glview->field);
    g_clear_object(&priv->mask_field);
    g_clear_object(&glview->data);

    /* free overlays and disconnect them */
    if (glview->ovlays) {
        for (i = 0; i < glview->novlays; i++) {
            g_clear_signal_handler(glview->ovlay_updated_id + i, glview->ovlays[i]);
            gwy_data_view_layer_unplugged(GWY_DATA_VIEW_LAYER(glview->ovlays[i]));
            g_clear_object(glview->ovlays + i);
        }
        g_free(glview->ovlays);
        glview->ovlays = NULL;
    }

    GTK_WIDGET_CLASS(gwy_gl_view_parent_class)->destroy(widget);
}

static void
gwy_gl_view_finalize(GObject *object)
{
    GwyGLView *glview;

    glview = GWY_GL_VIEW(object);

    g_free(glview->labels);
    g_free(glview->label_ids);

    g_hash_table_destroy(glview->variables);

    G_OBJECT_CLASS(gwy_gl_view_parent_class)->finalize(object);
}

static void
gwy_gl_view_set_property(GObject *object,
                         guint prop_id,
                         const GValue *value,
                         GParamSpec *pspec)
{
    GwyGLView *view = GWY_GL_VIEW(object);

    switch (prop_id) {
        case PROP_MOVEMENT:
        gwy_gl_view_set_movement_type(view, g_value_get_enum(value));
        break;

        case PROP_REDUCED_SIZE:
        gwy_gl_view_set_reduced_size(view, g_value_get_uint(value));
        break;

        case PROP_DATA_KEY:
        gwy_gl_view_set_data_key(view, g_value_get_string(value));
        break;

        case PROP_SETUP_PREFIX:
        gwy_gl_view_set_setup_prefix(view, g_value_get_string(value));
        break;

        case PROP_GRADIENT_KEY:
        gwy_gl_view_set_gradient_key(view, g_value_get_string(value));
        break;

        case PROP_MATERIAL_KEY:
        gwy_gl_view_set_material_key(view, g_value_get_string(value));
        break;

        case PROP_MASK_KEY:
        gwy_gl_view_set_mask_key(view, g_value_get_string(value));
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
gwy_gl_view_get_property(GObject *object,
                         guint prop_id,
                         GValue *value,
                         GParamSpec *pspec)
{
    GwyGLView *view = GWY_GL_VIEW(object);

    switch (prop_id) {
        case PROP_MOVEMENT:
        g_value_set_enum(value, view->movement);
        break;

        case PROP_REDUCED_SIZE:
        g_value_set_uint(value, view->reduced_size);
        break;

        case PROP_DATA_KEY:
        g_value_set_static_string(value, g_quark_to_string(view->data_key));
        break;

        case PROP_SETUP_PREFIX:
        g_value_set_static_string(value, g_quark_to_string(view->setup_key));
        break;

        case PROP_GRADIENT_KEY:
        g_value_set_static_string(value, g_quark_to_string(view->gradient_key));
        break;

        case PROP_MATERIAL_KEY:
        g_value_set_static_string(value, g_quark_to_string(view->material_key));
        break;

        case PROP_MASK_KEY:
        g_value_set_static_string(value, g_quark_to_string(GWY_GL_VIEW_GET_PRIVATE(view)->mask_key));
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
gwy_gl_view_unrealize(GtkWidget *widget)
{
    GwyGLView *glview;

    glview = GWY_GL_VIEW(widget);

    gwy_gl_view_release_lists(glview);

    if (GTK_WIDGET_CLASS(gwy_gl_view_parent_class)->unrealize)
        GTK_WIDGET_CLASS(gwy_gl_view_parent_class)->unrealize(widget);
}

/**
 * gwy_gl_view_new:
 * @data: A #GwyDict containing the data to display.
 *
 * Creates a new threedimensional OpenGL display of @data.
 *
 * The widget is initialized from container @data.
 *
 * Returns: The new OpenGL 3D widget as a #GtkWidget.
 **/
GtkWidget*
gwy_gl_view_new(GwyDict *data)
{
    GdkGLConfig *glconfig;
    GtkWidget *widget;
    GwyGLView *glview;

    g_return_val_if_fail(GWY_IS_DICT(data), NULL);
    glconfig = gwy_widgets_get_gl_config();
    g_return_val_if_fail(glconfig, NULL);

    g_object_ref(data);

    glview = (GwyGLView*)g_object_new(GWY_TYPE_3D_VIEW, NULL);
    widget = GTK_WIDGET(glview);

    glview->data = data;

    if (!gtk_widget_set_gl_capability(GTK_WIDGET(glview), glconfig, NULL, TRUE, GDK_GL_RGBA_TYPE))
        g_critical("Cannot set GL capability on widget");

    return widget;
}

static void
get_view_data_ranges(GwyGLView *glview,
                     gdouble *data_min, gdouble *data_max,
                     gdouble *view_min, gdouble *view_max)
{
    GwyGLViewPrivate *priv = GWY_GL_VIEW_GET_PRIVATE(glview);
    GwyField *field = glview->field, *mask = priv->mask_field;
    gboolean masking = glview->setup->hide_masked && mask;
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    gdouble min, max;

    /* Non-masked quantities are cached so do not be afraid getting them repeatedly. */
    gwy_field_min_max(field, &min, &max);
    if (data_min)
        *data_min = min;
    if (data_max)
        *data_max = max;

    if (!masking) {
        if (view_min)
            *view_min = min;
        if (view_max)
            *view_max = max;
        return;
    }

    if (view_min || view_max)
        gwy_field_area_get_min_max_mask(field, mask, GWY_MASK_EXCLUDE, 0, 0, xres, yres, view_min, view_max);
}

static GwyValueFormat*
gwy_gl_view_update_label(GwyGLView *glview,
                         const gchar *key,
                         gdouble value,
                         gdouble maximum,
                         gdouble step,
                         gboolean is_z,
                         GwyValueFormat *vf)
{
    GwyUnit *unit;
    gchar buffer[80], *s;

    if (is_z)
        unit = gwy_field_get_unit_z(glview->field);
    else
        unit = gwy_field_get_unit_xy(glview->field);

    vf = gwy_unit_get_format_with_resolution(unit, GWY_UNIT_FORMAT_VFMARKUP, maximum, step, vf);
    g_snprintf(buffer, sizeof(buffer), "%.*f %s", vf->precision, value/vf->magnitude, vf->units);
    s = g_hash_table_lookup(glview->variables, key);
    if (!s || !gwy_strequal(s, buffer))
        g_hash_table_insert(glview->variables, (gpointer)key, g_strdup(buffer));

    return vf;
}

static void
gwy_gl_view_update_labels(GwyGLView *glview)
{
    GwyValueFormat *format;
    gdouble xreal, yreal, view_min, view_max, range, maximum;

    if (!glview->field)
        return;

    xreal = gwy_field_get_xreal(glview->field);
    yreal = gwy_field_get_yreal(glview->field);
    get_view_data_ranges(glview, NULL, NULL, &view_min, &view_max);
    range = fabs(view_max - view_min);
    maximum = MAX(fabs(view_min), fabs(view_max));

    format = NULL;
    format = gwy_gl_view_update_label(glview, "x", xreal, xreal, xreal/12.0, FALSE, format);
    format = gwy_gl_view_update_label(glview, "y", yreal, yreal, yreal/12.0, FALSE, format);
    format = gwy_gl_view_update_label(glview, "max", view_max, maximum, range/12.0, TRUE, format);
    format = gwy_gl_view_update_label(glview, "min", view_min, maximum, range/12.0, TRUE, format);
    gwy_value_format_free(format);
}

static void
gwy_gl_view_container_connect(GwyGLView *glview,
                              const gchar *data_key_string,
                              gulong *id,
                              GCallback callback)
{
    gchar *detailed_signal;

    if (!data_key_string || !glview->data) {
        gwy_debug("%p zeroing id for <%s>", glview, data_key_string);
        *id = 0;
        return;
    }
    gwy_debug("%p connecting to <%s>", glview, data_key_string);
    detailed_signal = g_newa(gchar, sizeof("item-changed::") + strlen(data_key_string));
    g_stpcpy(g_stpcpy(detailed_signal, "item-changed::"), data_key_string);
    *id = connect_swapped_after(glview->data, detailed_signal, callback, glview);
}

/**
 * gwy_gl_view_set_data_key:
 * @glview: An OpenGL data view widget.
 * @key: Container string key identifying the data field to visualize.
 *
 * Sets the container key identifying the data field to visualize in an OpenGL view.
 **/
void
gwy_gl_view_set_data_key(GwyGLView *glview,
                         const gchar *key)
{
    GQuark quark;

    g_return_if_fail(GWY_IS_GL_VIEW(glview));
    gwy_debug("%p <%s>", glview, key);

    quark = key ? g_quark_from_string(key) : 0;
    if (glview->data_key == quark)
        return;

    g_clear_signal_handler(&glview->data_item_id, glview->data);
    gwy_gl_view_field_disconnect(glview);
    glview->data_key = quark;
    gwy_gl_view_field_connect(glview);
    gwy_gl_view_container_connect(glview, g_quark_to_string(quark), &glview->data_item_id,
                                  G_CALLBACK(gwy_gl_view_data_item_changed));
    g_object_notify_by_pspec(G_OBJECT(glview), properties[PROP_DATA_KEY]);
    gwy_gl_view_field_changed(glview);
}

/**
 * gwy_gl_view_get_data_key:
 * @glview: An OpenGL data view widget.
 *
 * Gets the container key identifying the data field to visualize in an OpenGL view.
 *
 * Returns: The string key, or %NULL if it isn't set.
 **/
const gchar*
gwy_gl_view_get_data_key(GwyGLView *glview)
{
    g_return_val_if_fail(GWY_IS_GL_VIEW(glview), NULL);
    return g_quark_to_string(glview->data_key);
}

static void
gwy_gl_view_data_item_changed(GwyGLView *glview)
{
    gwy_gl_view_field_disconnect(glview);
    gwy_gl_view_field_connect(glview);
    gwy_gl_view_field_changed(glview);
}

static void
gwy_gl_view_field_connect(GwyGLView *glview)
{
    g_return_if_fail(!glview->field);
    if (!glview->data_key)
        return;

    if (!gwy_dict_gis_object(glview->data, glview->data_key, &glview->field))
        return;

    g_object_ref(glview->field);
    glview->data_id = g_signal_connect_swapped(glview->field, "data-changed",
                                               G_CALLBACK(gwy_gl_view_field_changed), glview);
}

static void
gwy_gl_view_field_disconnect(GwyGLView *glview)
{
    g_clear_signal_handler(&glview->data_id, glview->field);
    g_clear_object(&glview->field);
}

static void
gwy_gl_view_field_changed(GwyGLView *glview)
{
    gwy_debug("");
    glview->changed |= GWY_GL_DATA_FIELD;
    gwy_gl_view_update_labels(glview);
    gwy_gl_view_update_lists(glview);
}

/**
 * gwy_gl_view_set_ovlay:
 * @glview: An OpenGL data view widget.
 * @ovlays: List of @novlays pixmap layers usable as overlays.
 * @novlays: Number of items in @ovlays.
 *
 * Sets overlays for an OpenGL view.
 **/
#if 0
void
gwy_gl_view_set_ovlay(GwyGLView *glview,
                      GwyPixmapLayer **ovlays,
                      guint novlays)
{
    gint i = 0;

    g_return_if_fail(GWY_IS_GL_VIEW(glview));
    gwy_debug("");

    if (glview->ovlays) {
        for (i = 0; i < glview->novlays; i++) {
            gwy_data_view_layer_unplugged(GWY_DATA_VIEW_LAYER(glview->ovlays[i]));
            g_clear_object(glview->ovlays + i);
        }
        g_free(glview->ovlays);
    }

    glview->ovlays = g_new(GwyPixmapLayer*, novlays);
    glview->ovlay_updated_id = g_new(guint, novlays);
    glview->novlays = novlays;
    for (i = 0; i < novlays; i++) {
        glview->ovlays[i] = ovlays[i];
        g_object_ref(glview->ovlays[i]);
        g_object_sink(glview->ovlays[i]);
        glview->ovlay_updated_id[i] = g_signal_connect_swapped(glview->ovlays[i], "updated",
                                                               G_CALLBACK(gwy_gl_view_queue_draw), glview);
    }
    gwy_gl_view_update_lists(glview);
}
#endif

/**
 * gwy_gl_view_get_setup_prefix:
 * @glview: An OpenGL data view widget.
 *
 * Gets prefix identifying OpenGL view setup in the container.
 *
 * Returns: The setup key prefix.
 **/
const gchar*
gwy_gl_view_get_setup_prefix(GwyGLView *glview)
{
    g_return_val_if_fail(GWY_IS_GL_VIEW(glview), NULL);
    return g_quark_to_string(glview->setup_key);
}

/**
 * gwy_gl_view_set_setup_prefix:
 * @glview: An OpenGL data view widget.
 * @key: Container string prefix for keys identifying the view setup parameters.  The #GwyGLSetup is stored at key
 *       <literal>"/setup"</literal> under this prefix.  Label objects are also stored under this prefix.
 *
 * Sets the prefix of OpenGL view parameters in the container.
 **/
void
gwy_gl_view_set_setup_prefix(GwyGLView *glview,
                             const gchar *key)
{
    GQuark quark;

    g_return_if_fail(GWY_IS_GL_VIEW(glview));
    gwy_debug("%p <%s>", glview, key);

    quark = key ? g_quark_from_string(key) : 0;
    if (glview->setup_key == quark)
        return;

    g_clear_signal_handler(&glview->setup_item_id, glview->data);
    gwy_gl_view_setup_disconnect(glview);
    glview->setup_key = quark;
    gwy_gl_view_ensure_setup(glview);
    gwy_gl_view_setup_connect(glview);
    gwy_gl_view_container_connect(glview, g_quark_to_string(quark), &glview->setup_item_id,
                                  G_CALLBACK(gwy_gl_view_setup_item_changed));

    g_object_notify_by_pspec(G_OBJECT(glview), properties[PROP_SETUP_PREFIX]);
    /* TODO: must not call with NULL or handle it as `all' */
    gwy_gl_view_setup_changed(glview, NULL);
}

static void
gwy_gl_view_ensure_setup(GwyGLView *glview)
{
    guint i, len;
    GString *key;
    GwyGLSetup *setup;
    GwyGLLabel *label;

    if (!glview->setup_key)
        return;

    key = g_string_new(g_quark_to_string(glview->setup_key));
    g_string_append_c(key, GWY_DICT_PATHSEP);
    len = key->len;

    g_string_append(key, "setup");
    if (!gwy_dict_gis_object_by_name(glview->data, key->str, &setup)) {
        setup = gwy_gl_setup_new();
        gwy_dict_set_object_by_name(glview->data, key->str, setup);
        g_object_unref(setup);
    }

    for (i = 0; i < GWY_GL_VIEW_NLABELS; i++) {
        g_string_truncate(key, len);
        g_string_append(key, labels[i].key);
        if (gwy_dict_gis_object_by_name(glview->data, key->str, &label))
            continue;

        label = gwy_gl_label_new(labels[i].default_text);
        gwy_dict_set_object_by_name(glview->data, key->str, label);
        g_object_unref(label);
    }
    g_string_free(key, TRUE);
}

static void
gwy_gl_view_setup_item_changed(GwyGLView *glview)
{
    gwy_gl_view_setup_disconnect(glview);
    gwy_gl_view_setup_connect(glview);
    gwy_gl_view_setup_changed(glview, NULL);
}

/* Here we assert setup and labels exist as they were instantiated by
 * gwy_gl_view_ensure_setup() (if not present). */
static void
gwy_gl_view_setup_connect(GwyGLView *glview)
{
    guint i, len;
    GString *key;

    g_return_if_fail(!glview->setup);
    if (!glview->setup_key)
        return;

    key = g_string_new(g_quark_to_string(glview->setup_key));
    g_string_append_c(key, GWY_DICT_PATHSEP);
    len = key->len;

    g_string_append(key, "setup");
    gwy_dict_gis_object_by_name(glview->data, key->str, &glview->setup);
    g_assert(glview->setup);
    g_object_ref(glview->setup);
    glview->setup_id = g_signal_connect_swapped(glview->setup, "notify",
                                                G_CALLBACK(gwy_gl_view_setup_changed), glview);

    for (i = 0; i < GWY_GL_VIEW_NLABELS; i++) {
        if (glview->labels[i]) {
            g_critical("Label %u already set!", i);
            continue;
        }
        g_string_truncate(key, len);
        g_string_append(key, labels[i].key);
        gwy_dict_gis_object_by_name(glview->data, key->str, &glview->labels[i]);
        g_assert(glview->labels[i]);
        g_object_ref(glview->labels[i]);
        glview->label_ids[i] = g_signal_connect_swapped(glview->labels[i], "notify",
                                                        G_CALLBACK(gwy_gl_view_label_changed), glview);
    }
    g_string_free(key, TRUE);
}

static void
gwy_gl_view_setup_disconnect(GwyGLView *glview)
{
    guint i;

    g_clear_signal_handler(&glview->setup_id, glview->setup);
    g_clear_object(&glview->setup);

    for (i = 0; i < GWY_GL_VIEW_NLABELS; i++) {
        g_clear_signal_handler(glview->label_ids + i, glview->labels[i]);
        g_clear_object(glview->labels + i);
    }
}

static gboolean
gwy_gl_visualisation_has_light(GwyGLVisualization visualization)
{
    return (visualization == GWY_GL_VISUALIZATION_LIGHTING
            || visualization == GWY_GL_VISUALIZATION_OVERLAY);
}

static gboolean
gwy_gl_visualisation_has_colormap(GwyGLVisualization visualization)
{
    return (visualization == GWY_GL_VISUALIZATION_GRADIENT
            || visualization == GWY_GL_VISUALIZATION_OVERLAY
            || visualization == GWY_GL_VISUALIZATION_OVERLAY_NO_LIGHT);
}

static void
gwy_gl_view_setup_changed(GwyGLView *glview,
                          GParamSpec *pspec)
{
    GwyGLSetup *setup = glview->setup;

    gwy_debug("%p <%s>", glview, pspec ? pspec->name : "NULL");
    /* TODO: must decide what needs redraw, if anything */
    if (pspec) {
        if (!gwy_gl_visualisation_has_light(setup->visualization)
            && gwy_stramong(pspec->name, "light-theta", "light-phi", NULL))
            return;
        if (!setup->axes_visible && gwy_strequal(pspec->name, "labels-visible"))
            return;
        if (gwy_strequal(pspec->name, "visualization")) {
            gwy_gl_view_update_lists(glview);
            return;
        }
        if (gwy_strequal(pspec->name, "hide-masked")) {
            gwy_gl_view_update_labels(glview);
            gwy_gl_view_update_lists(glview);
            return;
        }
        if (!setup->axes_visible && gwy_strequal(pspec->name, "line-width"))
            return;
        if (!setup->fmscale_visible
            && gwy_stramong(pspec->name, "fmscale-size", "fmscale-y-align", "fmscale-reserve-space", NULL))
            return;
    }

    gwy_gl_view_queue_draw(glview);
}

/**
 * gwy_gl_view_get_gradient_key:
 * @glview: An OpenGL data view widget.
 *
 * Gets the container key identifying the colour gradient used to visualize data in an OpenGL view.
 *
 * Returns: The string key, or %NULL if it isn't set.
 **/
const gchar*
gwy_gl_view_get_gradient_key(GwyGLView *glview)
{
    g_return_val_if_fail(GWY_IS_GL_VIEW(glview), NULL);
    return g_quark_to_string(glview->gradient_key);
}

/**
 * gwy_gl_view_set_gradient_key:
 * @glview: An OpenGL data view widget.
 * @key: Container string key identifying the color gradient to use for gradient visualization mode.
 *
 * Sets the container key identifying the color gradient to use to visualize data in an OpenGL view.
 **/
void
gwy_gl_view_set_gradient_key(GwyGLView *glview,
                             const gchar *key)
{
    GQuark quark;

    g_return_if_fail(GWY_IS_GL_VIEW(glview));
    gwy_debug("%p <%s>", glview, key);

    quark = key ? g_quark_from_string(key) : 0;
    if (glview->gradient_key == quark)
        return;

    if (glview->ovlays && glview->ovlays[0])
        gwy_layer_basic_set_gradient_key(GWY_LAYER_BASIC(glview->ovlays[0]), key);

    g_clear_signal_handler(&glview->gradient_item_id, glview->data);
    gwy_gl_view_gradient_disconnect(glview);
    glview->gradient_key = quark;
    gwy_gl_view_gradient_connect(glview);
    gwy_gl_view_container_connect(glview, g_quark_to_string(quark), &glview->gradient_item_id,
                                  G_CALLBACK(gwy_gl_view_gradient_item_changed));
    g_object_notify_by_pspec(G_OBJECT(glview), properties[PROP_GRADIENT_KEY]);
    gwy_gl_view_gradient_changed(glview);
}

static void
gwy_gl_view_gradient_item_changed(GwyGLView *glview)
{
    gwy_gl_view_gradient_disconnect(glview);
    gwy_gl_view_gradient_connect(glview);
    gwy_gl_view_gradient_changed(glview);
}

static void
gwy_gl_view_gradient_connect(GwyGLView *glview)
{
    const guchar *s = NULL;

    g_return_if_fail(!glview->gradient);
    if (glview->gradient_key)
        gwy_dict_gis_string(glview->data, glview->gradient_key, &s);
    glview->gradient = gwy_gradients_get_gradient(s);
    gwy_resource_use(GWY_RESOURCE(glview->gradient));
    glview->gradient_id = g_signal_connect_swapped(glview->gradient, "data-changed",
                                                   G_CALLBACK(gwy_gl_view_gradient_changed), glview);
}

static void
gwy_gl_view_gradient_disconnect(GwyGLView *glview)
{
    if (!glview->gradient)
        return;

    g_clear_signal_handler(&glview->gradient_id, glview->gradient);
    gwy_resource_release(GWY_RESOURCE(glview->gradient));
    glview->gradient = NULL;
}

static void
gwy_gl_view_gradient_changed(GwyGLView *glview)
{
    GwyGLVisualization visualization = glview->setup->visualization;
    gwy_debug("");
    glview->changed |= GWY_GL_GRADIENT;
    if (gwy_gl_visualisation_has_colormap(visualization))
        gwy_gl_view_update_lists(glview);
}

/**
 * gwy_gl_view_get_material_key:
 * @glview: An OpenGL data view widget.
 *
 * Gets the key identifying the GL material used to visualize data in a 3D
 * view.
 *
 * Returns: The string key, or %NULL if it isn't set.
 **/
const gchar*
gwy_gl_view_get_material_key(GwyGLView *glview)
{
    g_return_val_if_fail(GWY_IS_GL_VIEW(glview), NULL);
    return g_quark_to_string(glview->material_key);
}

/**
 * gwy_gl_view_set_material_key:
 * @glview: An OpenGL data view widget.
 * @key: Container string key identifying the color material to use for material visualization mode.
 *
 * Sets the container key of the GL material used to visualize data in an OpenGL view.
 **/
void
gwy_gl_view_set_material_key(GwyGLView *glview,
                             const gchar *key)
{
    GQuark quark;

    g_return_if_fail(GWY_IS_GL_VIEW(glview));
    gwy_debug("%p <%s>", glview, key);

    quark = key ? g_quark_from_string(key) : 0;
    if (glview->material_key == quark)
        return;

    g_clear_signal_handler(&glview->material_item_id, glview->data);
    gwy_gl_view_material_disconnect(glview);
    glview->material_key = quark;
    gwy_gl_view_material_connect(glview);
    gwy_gl_view_container_connect(glview, g_quark_to_string(quark), &glview->material_item_id,
                                  G_CALLBACK(gwy_gl_view_material_item_changed));
    g_object_notify_by_pspec(G_OBJECT(glview), properties[PROP_MATERIAL_KEY]);
    gwy_gl_view_material_changed(glview);
}

static void
gwy_gl_view_material_item_changed(GwyGLView *glview)
{
    gwy_gl_view_material_disconnect(glview);
    gwy_gl_view_material_connect(glview);
    gwy_gl_view_material_changed(glview);
}

static void
gwy_gl_view_material_connect(GwyGLView *glview)
{
    const guchar *s = NULL;

    g_return_if_fail(!glview->material);
    if (glview->material_key)
        gwy_dict_gis_string(glview->data, glview->material_key, &s);
    glview->material = gwy_gl_materials_get_gl_material(s);
    gwy_resource_use(GWY_RESOURCE(glview->material));
    glview->material_id = g_signal_connect_swapped(glview->material, "data-changed",
                                                   G_CALLBACK(gwy_gl_view_material_changed), glview);
}

static void
gwy_gl_view_material_disconnect(GwyGLView *glview)
{
    if (!glview->material)
        return;

    g_clear_signal_handler(&glview->material_id, glview->material);
    gwy_resource_release(GWY_RESOURCE(glview->material));
    glview->material = NULL;
}

static void
gwy_gl_view_material_changed(GwyGLView *glview)
{
    gwy_debug("");
    glview->changed |= GWY_GL_CMATERIAL;
    if (glview->setup->visualization == GWY_GL_VISUALIZATION_LIGHTING)
        gwy_gl_view_update_lists(glview);
}

static void
gwy_gl_view_update_lists(GwyGLView *glview)
{
    GwyGLViewPrivate *priv = GWY_GL_VIEW_GET_PRIVATE(glview);

    if (!gtk_widget_get_realized(GTK_WIDGET(glview)))
        return;

    gwy_debug("");
    priv->need_update_list = TRUE;
    gwy_gl_view_queue_draw(glview);
}

/**
 * gwy_gl_view_get_movement_type:
 * @glview: An OpenGL data view widget.
 *
 * Returns a movement type describing actual type of response on the mouse motion event.
 *
 * Returns: Actual type of response on the mouse motion event
 **/
GwyGLMovement
gwy_gl_view_get_movement_type(GwyGLView *glview)
{
    g_return_val_if_fail(GWY_IS_GL_VIEW(glview), GWY_GL_MOVEMENT_NONE);
    return glview->movement;
}

/**
 * gwy_gl_view_set_movement_type:
 * @glview: An OpenGL data view widget.
 * @movement: A new type of response on the mouse motion event.
 *
 * Sets the type of widget response on the mouse motion event.
 **/
void
gwy_gl_view_set_movement_type(GwyGLView *glview,
                              GwyGLMovement movement)
{
    g_return_if_fail(GWY_IS_GL_VIEW(glview));
    g_return_if_fail(movement <= GWY_GL_MOVEMENT_LIGHT);

    glview->movement = movement;
}

/**
 * gwy_gl_view_set_reduced_size:
 * @glview: An OpenGL data view widget.
 * @reduced_size: New reduced data size.
 *
 * Sets the reduced data size of an OpenGL view.
 *
 * Data larger than reduced size may be shown downsampled during transforms and other changes to speed up the
 * rendering.  Final, full-size rendering is then performed after a timeout.
 *
 * In case of the original data are not square, the @reduced_size is the greater size of the downsampled data, the
 * other dimension is proportional to the original size.
 *
 * Changes in reduced size do not cause immediate redraw when an operation is pending and the view is shown in reduced
 * size.  It only affects future downsampling.
 *
 * Note since 2.44 the reduced size has no influence because the OpenGL view does not downsample anything.
 **/
void
gwy_gl_view_set_reduced_size(GwyGLView *glview,
                             guint reduced_size)
{
    g_return_if_fail(GWY_IS_GL_VIEW(glview));
    g_return_if_fail(reduced_size >= 2);

    if (glview->reduced_size == reduced_size)
        return;

    glview->reduced_size = reduced_size;
    g_object_notify_by_pspec(G_OBJECT(glview), properties[PROP_REDUCED_SIZE]);
}

/**
 * gwy_gl_view_get_reduced_size:
 * @glview: An OpenGL data view widget.
 *
 * Returns the reduced data size of an OpenGL view.
 *
 * See gwy_gl_view_set_reduced_size() for details.
 *
 * Returns: The reduced data size.
 *
 * Note since 2.44 the reduced size has no influence because the OpenGL view does not downsample anything.
 **/
guint
gwy_gl_view_get_reduced_size(GwyGLView *glview)
{
    g_return_val_if_fail(GWY_IS_GL_VIEW(glview), 0);
    return glview->reduced_size;
}

/**
 * gwy_gl_view_get_label:
 * @glview: An OpenGL data view widget.
 * @label: Label type to obtain.
 *
 * Gets requested 3D label object of an OpenGL view.
 *
 * This is a convenience method that can be used instead of fetching the label object from the data container.
 *
 * Returns: The 3D label object representing @label in @glview.
 **/
GwyGLLabel*
gwy_gl_view_get_label(GwyGLView *glview,
                      GwyGLViewLabel label)
{
    g_return_val_if_fail(GWY_IS_GL_VIEW(glview), NULL);
    g_return_val_if_fail(label < GWY_GL_VIEW_NLABELS, NULL);

    return glview->labels[label];
}

/**
 * gwy_gl_view_get_setup:
 * @glview: An OpenGL data view widget.
 *
 * Gets the 3D setup object of an OpenGL view.
 *
 * This is a convenience method that can be used instead of fetching the setup object from the data container.
 *
 * Returns: The corresponding 3D setup.
 **/
GwyGLSetup*
gwy_gl_view_get_setup(GwyGLView *glview)
{
    g_return_val_if_fail(GWY_IS_GL_VIEW(glview), NULL);

    return glview->setup;
}

static void
swap_pixbuf_vertically_in_place(GdkPixbuf *pixbuf)
{
    gint rowstride, height, i, j;
    guchar *pixels, *a, *b;

    rowstride = gdk_pixbuf_get_rowstride(pixbuf);
    height = gdk_pixbuf_get_height(pixbuf);
    pixels = gdk_pixbuf_get_pixels(pixbuf);
    for (i = 0; i < height/2; i++) {
        a = pixels + i*rowstride;
        b = pixels + (height-1 - i)*rowstride;
        for (j = 0; j < rowstride; j++, a++, b++)
            GWY_SWAP(guchar, *a, *b);
    }
}

/**
 * gwy_gl_view_get_pixbuf:
 * @glview: An OpenGL data view widget.
 *
 * Copies the contents of the OpenGL view framebuffer to a GdkPixbuf.
 *
 * The size of the pixbuf is indentical to the size of the widget.
 *
 * Returns: A newly allocated GdkPixbuf a copy of the framebuffer.
 **/
GdkPixbuf*
gwy_gl_view_get_pixbuf(GwyGLView *glview)
{
    gint width, height;
    guchar *pixels;
    GdkPixbuf *pixbuf;

    g_return_val_if_fail(GWY_IS_GL_VIEW(glview), NULL);
    g_return_val_if_fail(gtk_widget_get_realized(GTK_WIDGET(glview)), NULL);

    gwy_gl_view_expose(GTK_WIDGET(glview), NULL);

    width = GTK_WIDGET(glview)->allocation.width;
    height = GTK_WIDGET(glview)->allocation.height;

    pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, width, height);
    pixels = gdk_pixbuf_get_pixels(pixbuf);
    glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, pixels);
    swap_pixbuf_vertically_in_place(pixbuf);

    return pixbuf;
}

/**
 * gwy_gl_view_get_pixbuf_with_alpha:
 * @glview: An OpenGL data view widget.
 *
 * Creates a GdkPixbuf from the contents of the OpenGL view framebuffer, with transparent background.
 *
 * The size of the pixbuf is indentical to the size of the widget.
 *
 * The rendered surface is cut perfectly.  However, if the scene contains also antialiased labels, these are cut in
 * a way which currently only works with a white background.
 *
 * Returns: A newly allocated GdkPixbuf a copy of the framebuffer.
 **/
GdkPixbuf*
gwy_gl_view_get_pixbuf_with_alpha(GwyGLView *glview)
{
    gint width, height, rowstride, i, j;
    guchar *pixels, *p;
    GLfloat *depthdata, *d;
    GdkPixbuf *pixbuf;
    GwyField *mask;
    gdouble *m;

    g_return_val_if_fail(GWY_IS_GL_VIEW(glview), NULL);
    g_return_val_if_fail(gtk_widget_get_realized(GTK_WIDGET(glview)), NULL);

    gwy_gl_view_expose(GTK_WIDGET(glview), NULL);

    width = GTK_WIDGET(glview)->allocation.width;
    height = GTK_WIDGET(glview)->allocation.height;

    pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, TRUE, 8, width, height);
    pixels = gdk_pixbuf_get_pixels(pixbuf);
    rowstride = gdk_pixbuf_get_rowstride(pixbuf);
    glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels);

    /* This reads 1 (infinity) in background values and smaller values where the shape or axes are visible.  But it
     * does not distinguish labels and colour bar because we draw them as pixmaps. */
    depthdata = g_new0(GLfloat, width*height);
    glReadPixels(0, 0, width, height, GL_DEPTH_COMPONENT, GL_FLOAT, depthdata);

    mask = gwy_field_new(width, height, width, height, TRUE);

    for (i = 0; i < height; i++) {
        p = pixels + i*rowstride;
        d = depthdata + i*width;
        m = mask->data + i*width;
        for (j = width; j; j--, m++, d++, p += 4) {
            /* The rendered 3D shape.  This we always keep. */
            if (*d < 1.0)
                continue;
            /* Outside.  This we may want to delete, but only if there is nothing drawn. */
            if (p[0] != 0xff || p[1] != 0xff || p[2] != 0xff)
                *m = 1.0;
        }
    }

    /* XXX: We don't know where exactly OpenGL drew the text after transformations. But all non-white pixels and all
     * pixels enclosed by non-white pixels should be preserved.  This retains insides of letters with holes, but it's
     * better than nothing... */
    gwy_field_fill_voids(mask, TRUE);

    for (i = 0; i < height; i++) {
        p = pixels + i*rowstride;
        d = depthdata + i*width;
        m = mask->data + i*width;
        for (j = width; j; j--, m++, d++, p += 4) {
            if (*d == 1.0 && *m == 0.0)
                p[3] = 0x00;
        }
    }
    g_object_unref(mask);
    g_free(depthdata);

    swap_pixbuf_vertically_in_place(pixbuf);

    return pixbuf;
}

/**
 * gwy_gl_view_get_data:
 * @glview: An OpenGL data view widget.
 *
 * Returns the data container this OpenGL view displays.
 *
 * Returns: The container as a #GwyDict.
 **/
GwyDict*
gwy_gl_view_get_data(GwyGLView *glview)
{
    g_return_val_if_fail(GWY_IS_GL_VIEW(glview), NULL);
    return glview->data;
}

/**
 * gwy_gl_view_get_scale_range:
 * @glview: An OpenGL data view widget.
 * @min_scale: Location to put minimum scale, or %NULL.
 * @max_scale: Location to put maximum scale, or %NULL.
 *
 * Obtains the minimum and maximum zoom of a 3D data view
 **/
void
gwy_gl_view_get_scale_range(GwyGLView *glview,
                            gdouble *min_scale,
                            gdouble *max_scale)
{
    g_return_if_fail(GWY_IS_GL_VIEW(glview));

    if (min_scale)
        *min_scale = glview->view_scale_min;
    if (max_scale)
        *max_scale = glview->view_scale_max;
}

/**
 * gwy_gl_view_set_scale_range:
 * @glview: An OpenGL data view widget.
 * @min_scale: Minimum zoom of the 3D data view, pass 0.0 to keep the current value.
 * @max_scale: Maximum zoom of the 3D data view, pass 0.0 to keep the current value.
 *
 * Sets the minimum and maximum zoom of a 3D data view.
 *
 * Recommended zoom values are 0.5 - 5.0.
 **/
void
gwy_gl_view_set_scale_range(GwyGLView *glview,
                            gdouble min_scale,
                            gdouble max_scale)
{
    g_return_if_fail(GWY_IS_GL_VIEW(glview));
    g_return_if_fail(min_scale >= 0.0 && max_scale >= 0.0);

    if (!min_scale)
        min_scale = glview->view_scale_min;
    if (!max_scale)
        max_scale = glview->view_scale_max;

    g_return_if_fail(min_scale <= max_scale);

    glview->view_scale_min = min_scale;
    glview->view_scale_max = max_scale;
    if (glview->setup) {
        gdouble val;

        val = CLAMP(glview->setup->scale, min_scale, max_scale);
        if (val != glview->setup->scale)
            g_object_set(glview->setup, "scale", val, NULL);
    }
}

/******************************************************************************/
static void
gwy_gl_view_queue_draw(GwyGLView *glview)
{
    gwy_debug("%p", glview);

    if (!GTK_WIDGET_DRAWABLE(glview))
        return;

    gtk_widget_queue_draw(GTK_WIDGET(glview));
}

static void
gwy_gl_view_label_changed(GwyGLView *glview)
{
    gwy_gl_view_queue_draw(glview);
}

static void
gwy_gl_view_realize(GtkWidget *widget)
{
    GwyGLView *glview;
    GdkWindowAttr attributes;
    gint attributes_mask;

    gwy_debug("realizing a GwyGLView (%ux%u)", widget->allocation.width, widget->allocation.height);

    GTK_WIDGET_SET_FLAGS(widget, GTK_REALIZED);
    glview = GWY_GL_VIEW(widget);

    attributes.window_type = GDK_WINDOW_CHILD;
    attributes.x = widget->allocation.x;
    attributes.y = widget->allocation.y;
    attributes.width = widget->allocation.width;
    attributes.height = widget->allocation.height;
    attributes.wclass = GDK_INPUT_OUTPUT;
    attributes.visual = gtk_widget_get_visual(widget);
    attributes.colormap = gtk_widget_get_colormap(widget);
    attributes.event_mask = (gtk_widget_get_events(widget)
                             | GDK_POINTER_MOTION_MASK
                             | GDK_POINTER_MOTION_HINT_MASK
                             | GDK_BUTTON1_MOTION_MASK
                             | GDK_BUTTON_PRESS_MASK
                             | GDK_BUTTON_RELEASE_MASK
                             | GDK_VISIBILITY_NOTIFY_MASK);

    attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL | GDK_WA_COLORMAP;

    widget->window = gdk_window_new(gtk_widget_get_parent_window(widget), &attributes, attributes_mask);
    gdk_window_set_user_data(widget->window, widget);

    /* Force widget to listen to relevant events.  Works around oxygen
     * misdetection of ‘empty’ areas.  See KDE bug #317292. */
    gtk_widget_add_events(widget, attributes.event_mask);

    widget->style = gtk_style_attach(widget->style, widget->window);
    gtk_style_set_background(widget->style, widget->window, GTK_STATE_NORMAL);

    gwy_gl_view_send_configure(glview);
    gwy_gl_view_realize_gl(glview);
}

static gboolean
gwy_gl_view_configure(GtkWidget *widget,
                      GdkEventConfigure *event)
{
    if (GTK_WIDGET_CLASS(gwy_gl_view_parent_class)->configure_event)
        GTK_WIDGET_CLASS(gwy_gl_view_parent_class)->configure_event(widget, event);

    GWY_GL_VIEW_GET_PRIVATE(GWY_GL_VIEW(widget))->need_update_list = TRUE;

    return FALSE;
}

static void
gwy_gl_view_send_configure(GwyGLView *glview)
{
    GtkWidget *widget;
    GdkEvent *event;

    event = gdk_event_new(GDK_CONFIGURE);

    widget = GTK_WIDGET(glview);
    event->configure.window = g_object_ref(widget->window);
    event->configure.send_event = TRUE;
    event->configure.x = widget->allocation.x;
    event->configure.y = widget->allocation.y;
    event->configure.width = widget->allocation.width;
    event->configure.height = widget->allocation.height;

    gtk_widget_event(widget, event);
    gdk_event_free(event);
}

static void
gwy_gl_view_size_request(G_GNUC_UNUSED GtkWidget *widget,
                         GtkRequisition *requisition)
{
    requisition->width = GWY_GL_VIEW_DEFAULT_SIZE_X;
    requisition->height = GWY_GL_VIEW_DEFAULT_SIZE_Y;
}

static void
gwy_gl_view_size_allocate(GtkWidget *widget,
                          GdkRectangle *allocation)
{
    g_return_if_fail(allocation);

    widget->allocation = *allocation;

    if (gtk_widget_get_realized(GTK_WIDGET(widget))) {
        gdk_window_move_resize(widget->window, allocation->x, allocation->y, allocation->width, allocation->height);
        gwy_gl_view_send_configure(GWY_GL_VIEW(widget));
    }
}

/* Convert GwyRGBA to GLfloat[4] array */
static void
gwy_gl_view_rgba_dv(GLenum face,
                    GLenum pname,
                    const GwyRGBA *rgba)
{
    gfloat fparams[4];

    fparams[0] = rgba->r;
    fparams[1] = rgba->g;
    fparams[2] = rgba->b;
    fparams[3] = rgba->a;
    glMaterialfv(face, pname, fparams);
}

static void
check_zrange_update(GwyGLView *view)
{
    GwyGLViewPrivate *priv = GWY_GL_VIEW_GET_PRIVATE(view);
    gdouble min, max;

    if (!view->ovlays || !view->ovlays[0])
        return;

    gwy_layer_basic_get_range(GWY_LAYER_BASIC(view->ovlays[0]), &min, &max);
    if (min != priv->zbot || max != priv->ztop) {
        priv->zbot = min;
        priv->ztop = max;
        priv->need_update_list = TRUE;
    }
}

static gboolean
gwy_gl_view_expose(GtkWidget *widget,
                   G_GNUC_UNUSED GdkEventExpose *event)
{
    GwyGLMaterial *material;
    GdkGLContext  *glcontext;
    GdkGLDrawable *gldrawable;
    GwyGLView *glview;
    GwyGLViewPrivate *priv;
    GLfloat light_position[] = { 0.0, 0.0, 4.0, 1.0 };
    GLfloat w = widget->allocation.width;
    GLfloat h = widget->allocation.height;
    gint sw = 0;

    gwy_debug("width: %f, height: %f", w, h);

    g_return_val_if_fail(GWY_IS_GL_VIEW(widget), FALSE);
    glview = GWY_GL_VIEW(widget);
    check_zrange_update(glview);
    priv = GWY_GL_VIEW_GET_PRIVATE(glview);

    glcontext  = gtk_widget_get_gl_context(widget);
    gldrawable = gtk_widget_get_gl_drawable(widget);
    gwy_debug("GLContext: %p, GLDrawable: %p", glcontext, gldrawable);

    /*** OpenGL BEGIN ***/
    if (!gdk_gl_drawable_gl_begin(gldrawable, glcontext))
        return FALSE;

    if (priv->need_update_list) {
        gwy_gl_make_list(glview, glview->field, priv->mask_field, glview->ovlays);
        priv->need_update_list = FALSE;
    }

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glViewport(0, 0, w, h);
    if (glview->setup->fmscale_visible && gwy_gl_visualisation_has_colormap(glview->setup->visualization)) {
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        glOrtho(0, w, 0, h, -1, 1);

        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();

        glDisable(GL_DEPTH_TEST);
        glEnable(GL_TEXTURE_2D);
        glDisable(GL_LIGHTING);

        sw = gwy_gl_draw_fmscaletex(glview);
        gwy_debug("Scale width: %d", sw);
        if (!glview->setup->fmscale_reserve_space)
            sw = 0;

        glDisable(GL_TEXTURE_2D);
        glEnable(GL_DEPTH_TEST);

        glViewport(0, 0, w-sw, h);
    }
    glLoadIdentity();

    /* View transformation. */
    gwy_gl_set_projection(glview, (w-sw)/h);
    glTranslatef(0.0, 0.0, -10.0);
    glScalef(glview->setup->scale, glview->setup->scale, glview->setup->scale);

    glRotatef(glview->setup->rotation_y*gwy_rad2deg, 1.0, 0.0, 0.0);
    glRotatef(glview->setup->rotation_x*gwy_rad2deg, 0.0,  0.0, 1.0);
    glScalef(1.0f, 1.0f, glview->setup->z_scale);

    /* Render shape */
    if (glview->setup->visualization == GWY_GL_VISUALIZATION_LIGHTING) {
        material = glview->material;

        glDisable(GL_COLOR_MATERIAL);
        glEnable(GL_LIGHTING);
        gwy_gl_view_rgba_dv(GL_FRONT, GL_AMBIENT, gwy_gl_material_get_ambient(material));
        gwy_gl_view_rgba_dv(GL_FRONT, GL_DIFFUSE, gwy_gl_material_get_diffuse(material));
        gwy_gl_view_rgba_dv(GL_FRONT, GL_SPECULAR, gwy_gl_material_get_specular(material));
        glMaterialf(GL_FRONT, GL_SHININESS, (GLfloat)gwy_gl_material_get_shininess(material)*128.0f);
        glPushMatrix();
        /* Scale-out the light source so that it does not come close to the
         * surface for small z_scale. */
        glScalef(1.0f, 1.0f, 1.0/glview->setup->z_scale);
        glRotatef(glview->setup->light_theta * gwy_rad2deg, 0.0f, 0.0f, 1.0f);
        glRotatef(glview->setup->light_phi * gwy_rad2deg, 0.0f, 1.0f, 0.0f);
        glLightfv(GL_LIGHT0, GL_POSITION, light_position);
        glPopMatrix();
    }
    else if (glview->setup->visualization == GWY_GL_VISUALIZATION_OVERLAY) {
        glEnable(GL_COLOR_MATERIAL);
        glEnable(GL_LIGHTING);
        glPushMatrix();
        /* Scale-out the light source so that it does not come close to the
         * surface for small z_scale. */
        glScalef(1.0f, 1.0f, 1.0/glview->setup->z_scale);
        glRotatef(glview->setup->light_theta * gwy_rad2deg, 0.0f, 0.0f, 1.0f);
        glRotatef(glview->setup->light_phi * gwy_rad2deg, 0.0f, 1.0f, 0.0f);
        glLightfv(GL_LIGHT0, GL_POSITION, light_position);
        glPopMatrix();
    }
    else {
        glDisable(GL_LIGHTING);
    }

    glCallList(glview->shape_list_base + glview->shape_current);
    if (glview->setup->axes_visible)
        gwy_gl_draw_axes(glview, w-sw, h);

    if (glview->movement == GWY_GL_MOVEMENT_LIGHT && priv->button)
        gwy_gl_draw_light_position(glview);

    /* Swap buffers */
    if (gdk_gl_drawable_is_double_buffered(gldrawable))
        gdk_gl_drawable_swap_buffers(gldrawable);
    else
        glFlush();

    gdk_gl_drawable_gl_end(gldrawable);
    /*** OpenGL END ***/

    return FALSE;
}

static gboolean
gwy_gl_view_button_press(GtkWidget *widget,
                         GdkEventButton *event)
{
    GwyGLView *glview = GWY_GL_VIEW(widget);
    GwyGLViewPrivate *priv = GWY_GL_VIEW_GET_PRIVATE(glview);

    glview->mouse_begin_x = event->x;
    glview->mouse_begin_y = event->y;
    priv->button = TRUE;
    priv->has_moved = FALSE;

    return FALSE;
}

static gboolean
gwy_gl_view_button_release(GtkWidget *widget,
                           G_GNUC_UNUSED GdkEventButton *event)
{
    GwyGLView *glview = GWY_GL_VIEW(widget);
    GwyGLViewPrivate *priv = GWY_GL_VIEW_GET_PRIVATE(glview);

    gwy_debug("%d %d", priv->button, priv->has_moved);
    priv->button = FALSE;
    if (priv->has_moved && glview->movement == GWY_GL_MOVEMENT_LIGHT)
        gwy_gl_view_queue_draw(glview);

    return FALSE;
}

static gboolean
gwy_gl_view_scroll(GtkWidget *widget, GdkEventScroll *event)
{
    GwyGLView *glview = GWY_GL_VIEW(widget);
    GdkScrollDirection dir = event->direction;
    gboolean enlarge;
    gdouble val, q;

    enlarge = (dir == GDK_SCROLL_UP || dir == GDK_SCROLL_RIGHT);

    q = exp(2.5*(enlarge ? GWY_GL_Z_DEFORMATION : -GWY_GL_Z_DEFORMATION));
    if (event->state & GDK_SHIFT_MASK) {
        val = glview->setup->z_scale*q;
        g_object_set(glview->setup, "z-scale", val, NULL);
    }
    else {
        val = glview->setup->scale*q;
        val = CLAMP(val, glview->view_scale_min, glview->view_scale_max);
        g_object_set(glview->setup, "scale", val, NULL);
    }

    return TRUE;
}

static gboolean
gwy_gl_view_motion_notify(GtkWidget *widget,
                          G_GNUC_UNUSED GdkEventMotion *event)
{
    GwyGLView *glview = GWY_GL_VIEW(widget);
    GwyGLViewPrivate *priv = GWY_GL_VIEW_GET_PRIVATE(glview);
    GdkModifierType mods;
    gdouble h, dx, dy, val;
    gint ex, ey;

    gdk_window_get_pointer(widget->window, &ex, &ey, &mods);
    h = widget->allocation.height;
    dx = ex - glview->mouse_begin_x;
    dy = ey - glview->mouse_begin_y;
    glview->mouse_begin_x = ex;
    glview->mouse_begin_y = ey;
    priv->has_moved = TRUE;

    gwy_debug("motion event: (%d, %d), shape=%d", ex, ey, glview->shape_current);

    if (mods & GDK_BUTTON1_MASK) {
        gwy_debug("with movement %d", glview->movement);
        switch (glview->movement) {
            case GWY_GL_MOVEMENT_NONE:
            break;

            case GWY_GL_MOVEMENT_ROTATION:
            g_object_set(glview->setup,
                         "rotation-x", glview->setup->rotation_x + dx*gwy_deg2rad,
                         "rotation-y", glview->setup->rotation_y + dy*gwy_deg2rad,
                         NULL);
            break;

            case GWY_GL_MOVEMENT_SCALE:
            val = glview->setup->scale*(1.0 + dy/h);
            val = CLAMP(val, glview->view_scale_min, glview->view_scale_max);
            g_object_set(glview->setup, "scale", val, NULL);
            break;

            case GWY_GL_MOVEMENT_DEFORMATION:
            val = glview->setup->z_scale;
            val *= exp(-GWY_GL_Z_DEFORMATION*dy);
            g_object_set(glview->setup, "z-scale", val, NULL);
            break;

            case GWY_GL_MOVEMENT_LIGHT:
            g_object_set(glview->setup,
                         "light-theta", glview->setup->light_theta + dy*gwy_deg2rad,
                         "light-phi", glview->setup->light_phi + dx*gwy_deg2rad,
                         NULL);
            break;
        }
    }

    return FALSE;
}

/**
 * gwy_gl_calculate_pixel_sizes:
 * @dfield: A data field.
 * @pdx: Location to store x-direction step, or NULL.
 * @pdy: Location to store y-direction step, or NULL.
 *
 * Calculates pixel sizes used for visualization.
 *
 * The smaller step of @pdx and @pdy is always 1.0, which mean the GL dimension will be equal to the resolution.  The
 * larger is calculated to keep the physical aspect ratio.
 **/
static void
gwy_gl_calculate_pixel_sizes(GwyField *dfield,
                             GLfloat *dx,
                             GLfloat *dy)
{
   gint xres, yres;
   gdouble xreal, yreal;

   xres = gwy_field_get_xres(dfield);
   yres = gwy_field_get_yres(dfield);
   xreal = gwy_field_get_xreal(dfield);
   yreal = gwy_field_get_yreal(dfield);
   /* The smaller triangle side is 1.0, the larger is, well, at least that.
    * To get pixel-wise behaviour, just set dx=dy=1. */
   if (xres/xreal <= yres/yreal) {
       *dx = (yres/yreal)/(xres/xreal);
       *dy = 1.0;
   }
   else {
       *dx = 1.0;
       *dy = (xres/xreal)/(yres/yreal);
   }
}

/**
 * gwy_gl_make_normals:
 * @dfield: A data field.
 * @normals: Array of normals to fill.  It must have the same number of elements as @dfield.
 *
 * Calculates surface normals used for visualization.
 *
 * Returns: @normals again, %NULL if when memory could not be allocated.
 **/
static GwyGLVector*
gwy_gl_make_normals(GwyField *dfield,
                    GwyGLVector *normals)
{
   typedef struct {
       GwyGLVector A, B;
   } RectangleNorm;
   const gdouble *data;
   gint i, j, xres, yres;
   GLfloat dx, dy, dx2, dy2;
   RectangleNorm * norms;

   g_return_val_if_fail(normals, NULL);

   xres = gwy_field_get_xres(dfield);
   yres = gwy_field_get_yres(dfield);
   data = gwy_field_get_data_const(dfield);
   gwy_gl_calculate_pixel_sizes(dfield, &dx, &dy);
   dx2 = dx*dx;
   dy2 = dy*dy;

   /* memory for normals of triangles
    * total count of rectangels is (xres-1)*(yres-1), each one has 2n triangles
    * 3 componernts per vector
    */
   norms = g_new(RectangleNorm, (xres - 1) * (yres - 1));
   if (norms == NULL)
       return NULL;

   /* Calculation of normals of triangles */
   for (j = 0; j < yres-1; j++) {
      for (i = 0; i < xres-1; i++) {
         GLfloat a, b, c, ab, ac, bc, n;

         a = data[(yres-1 - j)*xres + i];
         b = data[(yres-2 - j)*xres + i];
         c = data[(yres-1 - j)*xres + i+1];
         ab = a - b;
         ac = a - c;
         n = 1.0/sqrt(dy2*ac*ac + dx2*ab*ab + dx2*dy2);
         norms[j*(xres-1) + i].A.x = dy*ac*n;
         norms[j*(xres-1) + i].A.y = -dx*ab*n;
         norms[j*(xres-1)+ i].A.z = dx*dy*n;

         a = b;
         b = c;
         c = data[(yres-2 - j)*xres + i+1];
         ac = a - c;
         bc = b - c;
         n = 1.0/sqrt(dy2*ac*ac + dx2*bc*bc + dx2*dy2);
         norms[j*(xres-1) + i].B.x = dy*ac*n;
         norms[j*(xres-1) + i].B.y = -bc*dx*n;
         norms[j*(xres-1) + i].B.z = dx*dy*n;
      }
   }

   /* two of corner vertices have only one triangle adjacent
    * (first and last vertex) */
   normals[0] = norms[0].A;
   normals[xres * yres - 1] = norms[(xres-1) * (yres-1) - 1].B;
   /*the other two corner vertecies have two triangles adjacent*/
   normals[xres - 1].x = (norms[xres - 2].A.x + norms[xres - 2].B.x)/2.0f;
   normals[xres - 1].y = (norms[xres - 2].A.y + norms[xres - 2].B.y)/2.0f;
   normals[xres - 1].z = (norms[xres - 2].A.z + norms[xres - 2].B.z)/2.0f;
   normals[xres * (yres - 1)].x = (norms[(xres - 1)*(yres -2)].A.x + norms[(xres - 1)*(yres -2)].B.x)/2.0f;
   normals[xres * (yres - 1)].y = (norms[xres - 2].A.y + norms[(xres - 1)*(yres -2)].B.y)/2.0f;
   normals[xres * (yres - 1)].z = (norms[xres - 2].A.z + norms[(xres - 1)*(yres -2)].B.z)/2.0f;
   /*the vertices on the edge of the matrix have three adjacent triangles*/
   for (i = 1; i < xres - 1; i ++) {
       const GwyGLVector *a = &(norms[i-1].A);
       const GwyGLVector *b = &(norms[i-1].B);
       const GwyGLVector *c = &(norms[i].A);

       normals[i].x = (a->x + b->x + c->x)/3.0;
       normals[i].y = (a->y + b->y + c->y)/3.0;
       normals[i].z = (a->z + b->z + c->z)/3.0;
   }

   for (i = 1; i < xres - 1; i ++) {
       const GwyGLVector *a = &(norms[(xres - 1)*(yres - 2) + i-1].B);
       const GwyGLVector *b = &(norms[(xres - 1)*(yres - 2) + i-1].A);
       const GwyGLVector *c = &(norms[(xres - 1)*(yres - 2) + i].B);

       normals[xres * (yres - 1) + i].x = (a->x + b->x + c->x)/3.0;
       normals[xres * (yres - 1) + i].y = (a->y + b->y + c->y)/3.0;
       normals[xres * (yres - 1) + i].z = (a->z + b->z + c->z)/3.0;
   }

   for (i = 1; i < yres - 1; i ++) {
       const GwyGLVector *a = &(norms[(i-1) * (xres-1)].A);
       const GwyGLVector *b = &(norms[(i-1) * (xres-1)].B);
       const GwyGLVector *c = &(norms[i *     (xres-1)].A);

       normals[i * xres].x = (a->x + b->x + c->x)/3.0;
       normals[i * xres].y = (a->y + b->y + c->y)/3.0;
       normals[i * xres].z = (a->z + b->z + c->z)/3.0;
   }

   for (i = 1; i < yres - 1; i ++) {
       const GwyGLVector *a = &(norms[i     * (xres-1) - 1].B);
       const GwyGLVector *b = &(norms[(i+1) * (xres-1) - 1].A);
       const GwyGLVector *c = &(norms[(i+1) * (xres-1) - 1].B);

       normals[i * xres - 1].x = (a->x + b->x + c->x)/3.0;
       normals[i * xres - 1].y = (a->y + b->y + c->y)/3.0;
       normals[i * xres - 1].z = (a->z + b->z + c->z)/3.0;
   }

   /* The vertices inside of matrix have six adjacent triangles*/
   for (j = 1; j < yres - 1; j++) {
       for (i = 1; i < xres - 1; i++) {
           GwyGLVector *adj_tri[6];
           int k;

           adj_tri[0] = &(norms[(j-1)*(xres-1) + i - 1].B);
           adj_tri[1] = &(norms[(j-1)*(xres-1) + i    ].A);
           adj_tri[2] = &(norms[(j-1)*(xres-1) + i    ].B);
           adj_tri[3] = &(norms[j    *(xres-1) + i    ].A);
           adj_tri[4] = &(norms[j    *(xres-1) + i - 1].A);
           adj_tri[5] = &(norms[j    *(xres-1) + i - 1].B);
           normals[j*xres + i].x = 0.0f;
           normals[j*xres + i].y = 0.0f;
           normals[j*xres + i].z = 0.0f;
           for (k = 0; k < 6; k++) {
               normals[j*xres + i].x += adj_tri[k]->x;
               normals[j*xres + i].y += adj_tri[k]->y;
               normals[j*xres + i].z += adj_tri[k]->z;
           }
           normals[j*xres + i].x /= 6.0f;
           normals[j*xres + i].y /= 6.0f;
           normals[j*xres + i].z /= 6.0f;
       }
   }

   g_free(norms);

   return normals;
}

#define GWY_GL_VERTEX(i,j) \
  do { \
      z = data[(yres-1 - (j))*xres + (i)]; \
      color = colors + (yres-1 - (j))*rowstride + (i)*3; \
      glNormal3d(normals[(j)*xres+(i)].x, normals[(j)*xres+(i)].y, normals[(j)*xres+(i)].z); \
      glColor3d((GLfloat) *(color)/255.0, (GLfloat) *(color+1)/255.0, (GLfloat) *(color+2)/255.0); \
      glVertex3d((i)*dx, (j)*dy, z); \
  } while (0)

static void
gwy_gl_make_list(GwyGLView *glview,
                 GwyField *dfield,
                 GwyField *mask_field,
                 GwyPixmapLayer** ovlays)
{
    gint i, j, xres, yres, rowstride;
    gdouble data_min, data_max, view_min, view_max, recenter, res;
    GLfloat dx, dy;
    GwyGLVector *normals;
    const gdouble *data;
    const gdouble *mask = NULL;
    GdkPixbuf *pixbuf;
    guchar *colors;
    gboolean glon;

    g_return_if_fail(GWY_IS_FIELD(dfield));

    xres = gwy_field_get_xres(dfield);
    yres = gwy_field_get_yres(dfield);
    get_view_data_ranges(glview, &data_min, &data_max, &view_min, &view_max);

    data = gwy_field_get_data_const(dfield);
    if (GWY_IS_FIELD(mask_field))
        mask = gwy_field_get_data_const(mask_field);

    normals = g_new(GwyGLVector, xres * yres);
    if (!gwy_gl_make_normals(dfield, normals)) {
        /*TODO solve not enough memory problem*/
        g_return_if_reached();
    }

    gwy_gl_calculate_pixel_sizes(dfield, &dx, &dy);
    res = MAX(xres*dx, yres*dy);
    if (glview->ovlays) {
        GdkPixbuf *lpb;
        gint l;

        pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, 0, 8, xres, yres);
        gdk_pixbuf_fill(pixbuf, 0x00000000);

        for (l = 0; l < glview->novlays; l++) {
            if (ovlays[l]) {
                lpb = gwy_pixmap_layer_paint(ovlays[l]);
                if (lpb)
                    gdk_pixbuf_composite(lpb, pixbuf, 0, 0, xres, yres, 0, 0,
                                         (gdouble)xres/gdk_pixbuf_get_width(lpb),
                                         (gdouble)yres/gdk_pixbuf_get_height(lpb),
                                         GDK_INTERP_TILES, 0xff);
            }
        }
    }
    else {
        GwyGradient *grad = glview->gradient;

        if (!grad)
            grad = gwy_gradients_get_gradient(NULL);

        gwy_resource_use(GWY_RESOURCE(grad));
        pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, 0, 8, xres, yres);
        gwy_draw_field_pixbuf_full(pixbuf, dfield, grad);
        gwy_resource_release(GWY_RESOURCE(grad));
    }

    colors = gdk_pixbuf_get_pixels(pixbuf);
    rowstride = gdk_pixbuf_get_rowstride(pixbuf);

    glNewList(glview->shape_list_base + GWY_GL_SHAPE, GL_COMPILE);
    glPushMatrix();
    glTranslatef(-xres*dx/res, -yres*dy/res, GWY_GL_Z_DISPLACEMENT);
    /* The scaling should not change by hide masked and must be given by the actual data range. However, since hiding
     * makes the value range smaller we need to recenter the view. */
    glScalef(2.0/res, 2.0/res, GWY_GL_Z_TRANSFORMATION/(data_max - data_min));
    recenter = 0.5*((data_max - data_min) - (view_max - view_min));
    glTranslatef(0.0, 0.0, -view_min + recenter);

    for (j = 0; j < yres-1; j++) {
        glBegin(GL_TRIANGLE_STRIP);
        glon = TRUE;
        for (i = 0; i < xres-1; i++) {
            gdouble z;
            guchar *color;
            if (glview->setup->hide_masked && mask) {
                if (mask[(yres-1 - j)*xres + i] == 0.0 && mask[(yres-2 - j)*xres + i] == 0.0 && !glon) {
                    glBegin(GL_TRIANGLE_STRIP);
                    glon = TRUE;
                    if (mask[(yres-1 - j)*xres + i - 1] > 0.0 && mask[(yres-2 - j)*xres + i - 1] > 0.0)
                        GWY_GL_VERTEX(i, j);
                    else if (mask[(yres-1 - j)*xres + i - 1] > 0.0)
                        GWY_GL_VERTEX(i-1, j+1);
                    else if (mask[(yres-2 - j)*xres + i - 1] > 0.0)
                        GWY_GL_VERTEX(i-1, j);
                }
                if (mask[(yres-1 - j)*xres + i] == 0 && glon)
                    GWY_GL_VERTEX(i, j);
                if (mask[(yres-2 - j)*xres + i] == 0 && glon)
                    GWY_GL_VERTEX(i, j+1);
                if (mask[(yres-1 - j)*xres + i] > 0 || mask[(yres-2 - j)*xres + i] > 0) {
                    glEnd();
                    glon = FALSE;
                }
            }
            else {
                GWY_GL_VERTEX(i, j);
                GWY_GL_VERTEX(i, j+1);
            }
        }
        glEnd();
    }
    g_free(normals);
    glPopMatrix();
    glEndList();

    g_object_unref(pixbuf);
}

/*
 * gwy_gl_util_mult_matrix:
 * @a, @b: matrices to multiply
 * @mpMatrix: result
 *
 * multiplies the matrices
 */
static void
gwy_gl_util_mult_matrix(const GLdouble *a, const GLdouble *b, GLdouble *mpMatrix)
{
    int i, j;

    for (i = 0; i < 4; i++) {
        for (j = 0; j < 4; j++)
            mpMatrix[i*4+j] = a[i*4+0]*b[0*4+j] + a[i*4+1]*b[1*4+j] + a[i*4+2]*b[2*4+j] + a[i*4+3]*b[3*4+j];
    }
}

/* mpMatrix is the current model-projection-matrix */
static void
gwy_gl_util_get_mpmatrix(GLdouble *mpMatrix)
{
    GLdouble mM[16], pM[16];
    glGetDoublev(GL_MODELVIEW_MATRIX, mM);
    glGetDoublev(GL_PROJECTION_MATRIX, pM);

    gwy_gl_util_mult_matrix(mM, pM, mpMatrix);
}

/* wx,wy,wz denote the pont x,y,z will result in under the model-projection-matrix mpM and the viewport vpMatrix */
static int
gwy_gl_util_project(GLdouble x, GLdouble y, GLdouble z,
                    const GLdouble *mpM, const GLint *vpMatrix,
                    GLdouble *wx, GLdouble *wy, GLdouble *wz)
{
    GLdouble temp[4];
    /* Matrix-Vector-Product */
    temp[0] = mpM[0]*x + mpM[4]*y + mpM[8]*z + mpM[12];
    temp[1] = mpM[1]*x + mpM[5]*y + mpM[9]*z + mpM[13];
    temp[2] = mpM[2]*x + mpM[6]*y + mpM[10]*z + mpM[14];
    temp[3] = mpM[3]*x + mpM[7]*y + mpM[11]*z + mpM[15];

    if (temp[3] == 0.0)
        return GL_FALSE;

    temp[0] /= temp[3];
    temp[1] /= temp[3];
    temp[2] /= temp[3];

    *wx = (0.5*temp[0] + 0.5)*vpMatrix[2] + vpMatrix[0];
    *wy = (0.5*temp[1] + 0.5)*vpMatrix[3] + vpMatrix[1];
    *wz = 0.5*temp[2] + 0.5;

    return GL_TRUE;
}

static inline gint
uppow2(gint x)
{
    x--;
    x |= x >> 1;
    x |= x >> 2;
    x |= x >> 4;
    x |= x >> 8;
    x |= x >> 16;
    x++;

    return x;
}

static void
gwy_gl_draw_axes(GwyGLView *glview, gint width, gint height)
{
    GwyField *field = glview->field;
    GLfloat dx, dy, rx, Ax, Ay, Bx, By, Cx, Cy, ticklen, xt, yt;
    gdouble data_min, data_max, view_min, view_max, recenter;
    gint xres, yres, res;
    gboolean yfirst;
    GwyGLMaterial *mat_none;

    gwy_debug(" ");

    xres = gwy_field_get_xres(field);
    yres = gwy_field_get_yres(field);
    get_view_data_ranges(glview, &data_min, &data_max, &view_min, &view_max);
    gwy_gl_calculate_pixel_sizes(field, &dx, &dy);
    res = MAX(xres*dx, yres*dy);

    Ax = Ay = Bx = By = Cx = Cy = 0.0f;
    yfirst = TRUE;
    rx = fmod(glview->setup->rotation_x*gwy_rad2deg, 360.0);
    if (rx < 0.0)
        rx += 360.0;

    mat_none = gwy_gl_materials_get_gl_material(GWY_GL_MATERIAL_NONE);

    glPushMatrix();
    glTranslatef(-dx*xres/res, -dy*yres/res, GWY_GL_Z_DISPLACEMENT);
    glScalef(2.0/res, 2.0/res, GWY_GL_Z_TRANSFORMATION/(data_max - data_min));
    recenter = 0.5*((data_max - data_min) - (view_max - view_min));  /* See gwy_gl_make_list(). */
    glTranslatef(0.0, 0.0, recenter);
    gwy_gl_view_rgba_dv(GL_FRONT, GL_AMBIENT, gwy_gl_material_get_ambient(mat_none));
    gwy_gl_view_rgba_dv(GL_FRONT, GL_DIFFUSE, gwy_gl_material_get_diffuse(mat_none));
    gwy_gl_view_rgba_dv(GL_FRONT, GL_SPECULAR, gwy_gl_material_get_specular(mat_none));
    glMaterialf(GL_FRONT, GL_SHININESS, (GLfloat)gwy_gl_material_get_shininess(mat_none)*128.0f);

    /* This is optimistic.  Hope something reasonable happens when the selected
     * line width is not actually supported. */
    glLineWidth(glview->setup->line_width);

    if (rx >= 0.0 && rx <= 90.0) {
        Ay = dy*yres;
        Cx = dx*xres;
        yfirst = TRUE;
    }
    else if (rx > 90.0 && rx <= 180.0) {
        Ax = dx*xres;
        Ay = dy*yres;
        By = dy*yres;
        yfirst = FALSE;
    }
    else if (rx > 180.0 && rx <= 270.0) {
        Ax = dx*xres;
        Bx = dx*xres;
        By = dy*yres;
        Cy = dy*yres;
        yfirst = TRUE;
    }
    else if (rx >= 270.0 && rx <= 360.0) {
        Bx = dx*xres;
        Cx = dx*xres;
        Cy = dy*yres;
        yfirst = FALSE;
    }
    ticklen = 0.01f*(dx*xres + dy*yres);

    /* X, Y and Z axes */
    glBegin(GL_LINE_STRIP);
    glColor3f(0.0, 0.0, 0.0);
    glVertex2f(Ax, Ay);
    glVertex2f(Bx, By);
    glVertex2f(Cx, Cy);
    glVertex3f(Cx, Cy, view_max - view_min);
    glEnd();

    glBegin(GL_LINES);
    /* X-axis ticks */
    xt = ticklen*(Bx - Cx)/(ABS(Cx - Bx) + ABS(Cy - By));
    yt = ticklen*(By - Cy)/(ABS(Cx - Bx) + ABS(Cy - By));

    glVertex2f(Ax, Ay);
    glVertex2f(Ax + xt, Ay + yt);

    glVertex2f((Ax+Bx)/2, (Ay+By)/2);
    glVertex2f((Ax+Bx)/2 + xt, (Ay+By)/2 + yt);

    glVertex2f(Bx, By);
    glVertex2f(Bx + xt, By + yt);

    /* Y-axis ticks */
    xt = ticklen*(Bx - Ax)/(ABS(Bx - Ax) + ABS(By - Ay));
    yt = ticklen*(By - Ay)/(ABS(Bx - Ax) + ABS(By - Ay));

    glVertex2f(Bx, By);
    glVertex2f(Bx + xt, By + yt);

    glVertex2f((Cx+Bx)/2, (Cy+By)/2);
    glVertex2f((Cx+Bx)/2 + xt, (Cy+By)/2 + yt);

    glVertex2f(Cx, Cy);
    glVertex2f(Cx + xt, Cy + yt);

    /* Z-axis ticks */
    glVertex3f(Cx, Cy, view_max - view_min);
    glVertex3f(Cx + xt, Cy + yt, view_max - view_min);

    glVertex3f(Cx, Cy, (view_max - view_min)/2);
    glVertex3f(Cx + xt, Cy + yt, (view_max - view_min)/2);
    glEnd();

    /* TODO: create bitmaps with labels in the beginning (possibly in init_gl) into display lists and draw here */
    /* FIXME: I was able to fix the ticks drawing but this is still terribly broken for non-square images.  (Yeti) */
    if (glview->setup->labels_visible && !ugly_hack_globally_disable_axis_drawing) {
        guint view_size;
        gint size;
        GLdouble mM[16], pM[16], mpM[16];
        GLdouble sx, sy, sz, tx, ty, tz, ux, uy, uz, vx, vy, vz, rotA, rotB;
        GLint vpM[4];

        sx = sy = sz = tx = ty = tz = ux = uy = uz = vx = vy = vz = 0;

        view_size = MIN(width, height);
        size = (gint)(sqrt(view_size)*0.8);

        gwy_gl_util_get_mpmatrix(mpM);
        glGetDoublev(GL_MODELVIEW_MATRIX, mM);
        glGetDoublev(GL_PROJECTION_MATRIX, pM);

        glGetIntegerv(GL_VIEWPORT, vpM);

        glColor3f(1.0, 1.0, 1.0);

        gwy_gl_util_project(Ax, Ay, -0.0f, mpM, vpM, &sx, &sy, &sz);
        gwy_gl_util_project(Bx, By, -0.0f, mpM, vpM, &tx, &ty, &tz);
        rotA = atan2(ty-sy, tx-sx);

        gwy_gl_util_project(Cx, Cy, -0.0f, mpM, vpM, &sx, &sy, &sz);
        rotB = G_PI/2.0 - atan2(sx-tx, sy-ty);

        gwy_gl_util_project((Ax+Bx)/2 - (Cx-Bx)*0.1, (Ay+By)/2 - (Cy-By)*0.1, -0.0f, mpM, vpM, &sx, &sy, &sz);
        gwy_gl_util_project((Bx+Cx)/2 - (Ax-Bx)*0.1, (By+Cy)/2 - (Ay-By)*0.1, -0.0f, mpM, vpM, &tx, &ty, &tz);
        gwy_gl_util_project(Cx - (Ax-Bx)*0.1, Cy - (Ay-By)*0.1, 0.0f, mpM, vpM, &ux, &uy, &uz);
        gwy_gl_util_project(Cx - (Ax-Bx)*0.1, Cy - (Ay-By)*0.1, view_max - view_min, mpM, vpM, &vx, &vy, &vz);

        /* setup 2d */
        glMatrixMode(GL_PROJECTION);
        glPushMatrix();
        glLoadIdentity();
        glOrtho(0, width, 0, height, -1, 1);

        glMatrixMode(GL_MODELVIEW);
        glPushMatrix();
        glLoadIdentity();

        glDisable(GL_DEPTH_TEST);
        glEnable(GL_TEXTURE_2D);
        glDisable(GL_LIGHTING);

        glEnable(GL_BLEND);
        glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

        gwy_gl_texture_text(glview, yfirst ? GWY_GL_VIEW_LABEL_Y : GWY_GL_VIEW_LABEL_X, sx, sy, sz, rotA,
                            size, GWY_GL_JUST_CENTER, GWY_GL_JUST_CENTER);

        gwy_gl_texture_text(glview, yfirst ? GWY_GL_VIEW_LABEL_X : GWY_GL_VIEW_LABEL_Y, tx, ty, tz, rotB,
                            size, GWY_GL_JUST_CENTER, GWY_GL_JUST_CENTER);

        gwy_gl_texture_text(glview, GWY_GL_VIEW_LABEL_MAX, vx, vy, vz, 0,
                            size, GWY_GL_JUST_LEFT_EDGE, GWY_GL_JUST_CENTER);

        gwy_gl_texture_text(glview, GWY_GL_VIEW_LABEL_MIN, ux, uy, uz, 0,
                            size, GWY_GL_JUST_LEFT_EDGE, GWY_GL_JUST_CENTER);

        /* unset 2d */
        glDisable(GL_BLEND);

        glDisable(GL_TEXTURE_2D);
        glEnable(GL_DEPTH_TEST);
        glEnable(GL_LIGHTING);

        glMatrixMode(GL_PROJECTION);
        glPopMatrix();

        glMatrixMode(GL_MODELVIEW);
        glPopMatrix();
    }

    glPopMatrix();
}

static void
gwy_gl_draw_light_position(GwyGLView *glview)
{
    GwyGLMaterial *mat_none;
    GLfloat plane_z;
    gdouble data_min, data_max, view_min, view_max;
    int i;

    gwy_debug(" ");

    get_view_data_ranges(glview, &data_min, &data_max, &view_min, &view_max);

    mat_none = gwy_gl_materials_get_gl_material(GWY_GL_MATERIAL_NONE);
    gwy_gl_view_rgba_dv(GL_FRONT, GL_AMBIENT, gwy_gl_material_get_ambient(mat_none));
    gwy_gl_view_rgba_dv(GL_FRONT, GL_DIFFUSE, gwy_gl_material_get_diffuse(mat_none));
    gwy_gl_view_rgba_dv(GL_FRONT, GL_SPECULAR, gwy_gl_material_get_specular(mat_none));
    glMaterialf(GL_FRONT, GL_SHININESS, (GLfloat)gwy_gl_material_get_shininess(mat_none)*128.0f);

    glPushMatrix();
    plane_z = 0.5*GWY_GL_Z_TRANSFORMATION*(view_max - view_min)/(data_max - data_min) + GWY_GL_Z_DISPLACEMENT;

    glTranslatef(0.0f, 0.0f, plane_z);
    glRotatef(-glview->setup->light_theta * gwy_rad2deg, 0.0f, 0.0f, 1.0f);
    glRotatef(glview->setup->light_phi * gwy_rad2deg, 0.0f, 1.0f, 0.0f);
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

    glBegin(GL_QUAD_STRIP);
    for (i = -180; i <= 180; i += 5) {
        GLfloat x = cos(i * gwy_deg2rad) * G_SQRT2;
        GLfloat z = sin(i * gwy_deg2rad) * G_SQRT2;
        glVertex3f(x,  0.05f, z);
        glVertex3f(x, -0.05f, z);
    }
    glEnd();

    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

    glBegin(GL_LINE_STRIP);
        glVertex3f(0.0f,  0.0f,  0.0f);
        glVertex3f(0.0f,  0.05f, G_SQRT2);
        glVertex3f(0.0f, -0.05f, G_SQRT2);
        glVertex3f(0.0f,  0.0f,  0.0f);
    glEnd();

    glPopMatrix();
}

static void
gwy_gl_view_realize_gl(GwyGLView *glview)
{
    GdkGLContext *glcontext;
    GdkGLDrawable *gldrawable;

    GLfloat ambient[] = { 0.1, 0.1, 0.1, 1.0 };
    GLfloat diffuse[] = { 1.0, 1.0, 1.0, 1.0 };
    GLfloat position[] = { 0.0, 3.0, 3.0, 1.0 };

    GLfloat lmodel_ambient[] = { 0.2, 0.2, 0.2, 1.0 };
    GLfloat local_view[] = { 0.0 } ;

    gwy_debug("GL capable %d", gtk_widget_is_gl_capable(GTK_WIDGET(glview)));

    glcontext = gtk_widget_get_gl_context(GTK_WIDGET(glview));
    gldrawable = gtk_widget_get_gl_drawable(GTK_WIDGET(glview));
    /*** OpenGL BEGIN ***/
    gwy_debug("drawable %p, context %p", gldrawable, glcontext);

    if (!gdk_gl_drawable_gl_begin(gldrawable, glcontext))
        return;

    glClearColor(1.0, 1.0, 1.0, 1.0);
    glClearDepth(1.0);

    glLightfv(GL_LIGHT0, GL_AMBIENT, ambient);
    glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse);
    glLightfv(GL_LIGHT0, GL_POSITION, position);
    glLightModelfv(GL_LIGHT_MODEL_AMBIENT, lmodel_ambient);
    glLightModelfv(GL_LIGHT_MODEL_LOCAL_VIEWER, local_view);

    glFrontFace(GL_CW);
    glEnable(GL_LIGHTING);
    glEnable(GL_LIGHT0);
    glEnable(GL_AUTO_NORMAL);
    glEnable(GL_NORMALIZE);
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LESS);

    /* Shape display lists */
    gwy_gl_view_assign_lists(glview);
    //gwy_gl_make_list(glview, glview->field, mask, glview->ovlays);
    GWY_GL_VIEW_GET_PRIVATE(glview)->need_update_list = TRUE;

    gdk_gl_drawable_gl_end(gldrawable);
    /*** OpenGL END ***/

    return;
}

static void
gwy_gl_set_projection(GwyGLView *glview, GLfloat aspect)
{
    gwy_debug(" ");

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    if (aspect > 1) {
        switch (glview->setup->projection) {
            case GWY_GL_PROJECTION_ORTHOGRAPHIC:
            glOrtho(-aspect * GWY_GL_ORTHO_CORRECTION,
                    aspect * GWY_GL_ORTHO_CORRECTION,
                    -GWY_GL_ORTHO_CORRECTION, /* * deformation_z*/
                    GWY_GL_ORTHO_CORRECTION, /* * deformation_z*/
                    5.0,
                    60.0);
            break;

            case GWY_GL_PROJECTION_PERSPECTIVE:
            glFrustum(-aspect, aspect, -1.0, 1.0, 5.0, 60.0);
            break;
        }
    }
    else {
        aspect = 1.0/aspect;
        switch (glview->setup->projection) {
            case GWY_GL_PROJECTION_ORTHOGRAPHIC:
            glOrtho(-GWY_GL_ORTHO_CORRECTION,
                    GWY_GL_ORTHO_CORRECTION,
                    -aspect * GWY_GL_ORTHO_CORRECTION, /* * deformation_z*/
                    aspect * GWY_GL_ORTHO_CORRECTION, /* * deformation_z*/
                    5.0,
                    60.0);
            break;

            case GWY_GL_PROJECTION_PERSPECTIVE:
            glFrustum(-1.0, 1.0, -aspect, aspect, 5.0, 60.0);
            break;
        }
    }
    glMatrixMode(GL_MODELVIEW);
}

static PangoLayout*
gwy_gl_prepare_layout(cairo_t *cr, gdouble zoom)
{
    PangoFontDescription *fontdesc;
    PangoLayout *layout;

    layout = pango_cairo_create_layout(cr);
    fontdesc = pango_font_description_from_string("Arial 12");
    pango_font_description_set_size(fontdesc, GWY_ROUND(0.8*PANGO_SCALE*zoom));
    pango_layout_set_font_description(layout, fontdesc);
    pango_font_description_free(fontdesc);

    return layout;
}

static guchar*
gwy_gl_view_render_string(gdouble size,
                          const gchar *text,
                          gint *width,
                          gint *height,
                          gint *stride)
{
    PangoLayout *clayout;
    gint wstride;
    gint px, py;
    cairo_t *cr;
    cairo_surface_t *surface;
    guchar *alpha;

    wstride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, 1);
    alpha = g_new(guchar, wstride);
    surface = cairo_image_surface_create_for_data(alpha, CAIRO_FORMAT_ARGB32, 1, 1, wstride);
    cr = cairo_create(surface);
    clayout = gwy_gl_prepare_layout(cr, size);

    cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
    pango_layout_set_markup(clayout, text, -1);
    pango_layout_get_pixel_size(clayout, &px, &py);
    *width = px;
    *height = py;
    px = uppow2(px);
    py = uppow2(py);

    g_free(alpha);
    cairo_destroy(cr);

    wstride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, px);
    alpha = g_new0(guchar, wstride*py);
    surface = cairo_image_surface_create_for_data(alpha, CAIRO_FORMAT_ARGB32, px, py, wstride);
    cr = cairo_create(surface);
    cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
    pango_cairo_update_layout(cr, clayout);
    pango_layout_set_markup(clayout, text, -1);
    pango_cairo_show_layout(cr, clayout);

    gwy_debug("c-layout size: %d %d stride %d", *width, *height, wstride);
    *stride = wstride;

    g_object_unref(clayout);
    cairo_destroy(cr);
    cairo_surface_destroy(surface);

    return alpha;
}

/* image to opengl-texture */
static void
gwy_gl_cairo_to_tex(guchar *image,
                    gint width, gint height, gint stride)
{
    GLint w2 = uppow2(width), h2 = uppow2(height);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glPixelStorei(GL_UNPACK_ROW_LENGTH, stride/4);
    glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w2, h2, 0, GL_BGRA, GL_UNSIGNED_BYTE, image);
}

/* puts textured rectangle t into view */
static void
gwy_gl_draw_ctex(GLuint t,
                 gint hjust, gint vjust,
                 gint width, gint height)
{
    GLfloat w2 = uppow2(width), h2 = uppow2(height);
    GLfloat wt = width/w2, ht = height/h2;
    GLfloat ho = -(hjust)*width/2;
    GLfloat vo = (int)(vjust-2)*height/2;

    glBindTexture(GL_TEXTURE_2D, t);

    glBegin(GL_QUADS);
    glTexCoord2f(0, ht);
    glVertex2f(ho, vo);

    glTexCoord2f(0, 0);
    glVertex2f(ho, vo+height);

    glTexCoord2f(wt, 0);
    glVertex2f(ho+width, vo+height);

    glTexCoord2f(wt, ht);
    glVertex2f(ho+width, vo);
    glEnd();

    glBindTexture(GL_TEXTURE_2D, 0);
}

/* draws text using a texture */
static void
gwy_gl_texture_text(GwyGLView     *glview,
                    GwyGLViewLabel id,
                    GLfloat        raster_x,
                    GLfloat        raster_y,
                    GLfloat        raster_z,
                    GLfloat        rot,
                    guint          size,
                    gint           hjustify,
                    gint           vjustify)
{
    GLuint tex = 0;
    GwyGLLabel *label;
    gint width, height, stride, displacement_x, displacement_y;
    guchar *img;
    gchar *text;

    /* Render the label into an off-screen buffer */
    label = glview->labels[id];
    displacement_x = GWY_ROUND(label->delta_x);
    displacement_y = GWY_ROUND(label->delta_y);
    text = gwy_gl_label_expand_text(label, glview->variables);
    size = gwy_gl_label_user_size(label, size);

    img = gwy_gl_view_render_string(size, text, &width, &height, &stride);
    g_free(text);

    gwy_gl_cairo_to_tex(img, width, height, stride);
    glPushMatrix();
    glTranslatef(raster_x+displacement_x, raster_y+displacement_y, raster_z);
    glRotatef(gwy_rad2deg(rot), 0, 0, 1);
    gwy_gl_draw_ctex(tex, hjustify, vjustify, width, height);
    glPopMatrix();

    g_free(img);
}

/* converts GwyGradient in gradient to cairo_pattern_t in pattern */
static void
gwy_gradient_to_cairo_pattern(cairo_pattern_t *pattern, GwyGradient *gradient)
{
    GwyGradientPoint gp;
    gint i = 0, npoints = gwy_gradient_get_n_points(gradient);
    for (i = 0; i < npoints; i++) {
        gp = gwy_gradient_get_point(gradient, i);
        cairo_pattern_add_color_stop_rgba(pattern, gp.x, gp.color.r, gp.color.g, gp.color.b, gp.color.a);
    }
}

/* puts string into PangoLayout, printf style */
static void
gwy_gl_format_layout(PangoLayout *layout,
                     GString *string,
                     const gchar *format,
                     ...)
{
    gchar *buffer;
    gint length;
    guint i;
    va_list args;

    g_string_truncate(string, 0);
    va_start(args, format);
    length = g_vasprintf(&buffer, format, args);
    va_end(args);
    g_string_append_len(string, buffer, length);
    g_free(buffer);

    /* Replace ASCII with proper minus */
    if (string->str[0] == '-') {
        /* Fix -0.0 */
        i = 1;
        while (string->str[i] == '0')
            i++;
        if (string->str[i] == '.' || string->str[i] == ',')
            i++;
        while (string->str[i] == '0')
            i++;

        if (i > 1 && (!string->str[i] || g_ascii_isspace(string->str[i]))) {
            g_string_erase(string, 0, 1);
        }
        else {
            g_string_erase(string, 0, 1);
            g_string_prepend_unichar(string, 0x2212);
        }
    }

    pango_layout_set_markup(layout, string->str, string->len);
}

/* auxilliary function to compute decimal points from tickdist */
static gint
gwy_gl_step_to_prec(gdouble d)
{
    gdouble resd = log10(7.5)-log10(d);
    if (resd != resd)
        return 1;
    if (resd > 1e20)
        return 1;
    if (resd < 1.0)
        resd = 1.0;
    return (gint) floor(resd);
}

/* prints layout onto cairo context, vertically centered */
static void
gwy_gl_pango_cairo_show_layout_vcentered(cairo_t *cr, PangoLayout *layout)
{
    int lw, lh;
    gdouble ch;
    pango_layout_get_size(layout, &lw, &lh);
    ch = (double)(lh)/PANGO_SCALE;
    cairo_rel_move_to(cr, 0, -ch/2.0);
    pango_cairo_show_layout(cr, layout);
}

/* init cario context */
static cairo_t*
gwy_cairo_create_cairo_context(gint width,
                               gint height,
                               cairo_surface_t **surf)
{
    cairo_t *cr;

    *surf = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
    cr = cairo_create(*surf);
    cairo_set_source_rgb(cr, 1, 1, 1);
    cairo_paint(cr);
    cairo_set_source_rgb(cr, 0, 0, 0);
    return cr;
}

/* renders false color bar into returned surface */
static cairo_surface_t*
gwy_gl_fmscaletex(gint height, gdouble bot, gdouble top,
                  GwyUnit *siunit,
                  gint fontsize, gdouble line_width,
                  GwyGradient *gradient,
                  gboolean noticks,
                  gint *width)
{
    PangoLayout *layout;
    cairo_t *cr;
    cairo_surface_t *surf;
    gdouble scale, x, m, tickdist, max;
    GwyValueFormat *format;
    GString *s;
    gint tick, layw1, layw2, layh;
    gint units_width, label_height, mintickdist, prec = 1;
    gint size, fmw, l;
    cairo_pattern_t *pattern;
    gboolean inverted = FALSE;
    gdouble zoom;

    /* The z-axis always goes up.  So bot > top means inverted gradient. */
    if (bot > top) {
        GWY_SWAP(gdouble, bot, top);
        inverted = TRUE;
    }

    max = MAX(fabs(bot), fabs(top));
    gwy_debug("bot %g, top %g, max %g", bot, top, max);
    /* Don't attempt to draw anything if rounding errors are too large or scale calculation can overflow */
    x = top - bot;
    if (x < 1e-15*max || x <= 1e4*G_MINDOUBLE || max >= 1e-4*G_MAXDOUBLE)
        return NULL;

    s = g_string_new(NULL);
    cr = gwy_cairo_create_cairo_context(1, 1, &surf);
    layout = gwy_gl_prepare_layout(cr, fontsize);
    zoom = 0.8/12.0*(float)fontsize;

    format = gwy_unit_get_format(siunit, GWY_UNIT_FORMAT_VFMARKUP, max, NULL);
    gwy_gl_format_layout(layout, s, " %s", format->units);
    pango_layout_get_pixel_size(layout, &units_width, &label_height);
    size = height - label_height*2;
    mintickdist = label_height*1.5; /* mintickdist is in pixels; tickdist is in meters or whichever basic unit */
    /* prec computation starts here */
    scale = size/(top - bot);
    x = mintickdist/scale;
    m = gwy_exp10(floor(log10(x)));
    x /= m;
    if (x == 1.0)
        x = 1.0;
    else if (x <= 2.0)
        x = 2.0;
    else if (x <= 5.0)
        x = 5.0;
    else
        x = 10.0;
    tickdist = x*m;
    x = ceil(bot/tickdist)*tickdist;
    max = top - 0.999999*label_height/scale;
    prec = gwy_gl_step_to_prec(tickdist/format->magnitude);
    /* prec computation ends here */

    gwy_gl_format_layout(layout, s, "%.*f %s", prec, top/format->magnitude, format->units);
    pango_layout_get_pixel_size(layout, &layw1, &layh);
    gwy_gl_format_layout(layout, s, "%.*f %s", prec, bot/format->magnitude, format->units);
    pango_layout_get_pixel_size(layout, &layw2, &layh);

    l = MAX(layw1, layw2);
    tick = zoom*GWY_GL_TICK_LENGTH; /* physical tick length */
    fmw = 18*zoom;
    *width = fmw + 2*line_width + l + 2*zoom + tick + 2;

    cairo_surface_destroy(surf);
    cairo_destroy(cr);

    cr = gwy_cairo_create_cairo_context(uppow2(*width), uppow2(height), &surf);
    pango_cairo_update_layout(cr, layout);
    cairo_set_line_width(cr, line_width);

    gwy_gl_format_layout(layout, s, "%.*f", prec, bot/format->magnitude);

    cairo_translate(cr, fmw+2, height - label_height+0.5);
    cairo_move_to(cr, 0, 0);
    cairo_rel_line_to(cr, tick, 0);
    cairo_stroke(cr);
    cairo_move_to(cr, tick+2*zoom, 0);
    gwy_gl_pango_cairo_show_layout_vcentered(cr, layout);
    if (!noticks) {
        if ((x - bot)*scale < label_height) {
            x += tickdist;
            /* cairo_translate(cr, 0, -tickdist*scale); */
        }

        cairo_translate(cr, 0, -(x-bot)*scale);

        while (x <= max) {
            cairo_move_to(cr, 0, 0);
            cairo_rel_line_to(cr, tick, 0);
            cairo_stroke(cr);
            cairo_move_to(cr, tick+2*zoom, 0);
            gwy_gl_format_layout(layout, s, "%.*f", prec, x/format->magnitude);
            gwy_gl_pango_cairo_show_layout_vcentered(cr, layout);
            cairo_translate(cr, 0, -tickdist*scale);
            x += tickdist;
        }
    }

    cairo_identity_matrix(cr);
    cairo_translate(cr, fmw+2, label_height+0.5);
    cairo_move_to(cr, 0, 0);
    cairo_rel_line_to(cr, tick, 0);
    cairo_stroke(cr);
    cairo_move_to(cr, tick+2*zoom, 0);
    gwy_gl_format_layout(layout, s, "%.*f %s", prec, top/format->magnitude, format->units);
    gwy_gl_pango_cairo_show_layout_vcentered(cr, layout);

    cairo_identity_matrix(cr);

    cairo_translate(cr, 1.5, label_height+0.5);
    if (inverted)
      pattern = cairo_pattern_create_linear(0, 0, 0, size);
    else
      pattern = cairo_pattern_create_linear(0, size, 0, 0);
    gwy_gradient_to_cairo_pattern(pattern, gradient);
    cairo_set_source(cr, pattern);
    cairo_rectangle(cr, 0, 0, fmw, size);
    cairo_fill(cr);
    cairo_pattern_destroy(pattern);

    cairo_set_source_rgb(cr, 0, 0, 0);
    cairo_rectangle(cr, 0, 0, fmw, size);
    cairo_stroke(cr);

    cairo_surface_flush(surf);

    gwy_value_format_free(format);
    g_object_unref(layout);
    cairo_destroy(cr);
    g_string_free(s, TRUE);

    return surf;
}

/* draws a false color bar */
static gint
gwy_gl_draw_fmscaletex(GwyGLView *view)
{
    gint height = GTK_WIDGET(view)->allocation.height;
    gint width = GTK_WIDGET(view)->allocation.width;
    gint fontsize = (gint)(sqrt(MIN(width, height))*0.8);
    gint reduced_height, actual_width, he;
    cairo_surface_t *surf;
    GwyGLSetup *setup = view->setup;
    gdouble min, max;
    gboolean noticks;
    GwyUnit *zunit;
    GwyLayerBasic *blayer;
    GwyField *dfield;

    /* XXX: This function requires view->ovlays[0] to be present.  Is is always satisfied when we can get here?  */
    g_return_val_if_fail(view->ovlays[0], 0);

    reduced_height = GWY_ROUND(height * setup->fmscale_size);
    if (reduced_height < 12)
        return 0;

    glTranslatef(width, 0, 0);
    glColor3f(1.0, 1.0, 1.0);

    blayer = GWY_LAYER_BASIC(view->ovlays[0]);
    gwy_layer_basic_get_range(blayer, &min, &max);
    noticks = (gwy_layer_basic_get_range_type(blayer) == GWY_COLOR_MAPPING_ADAPT);

    dfield = GWY_FIELD(view->ovlays[0]->field);
    zunit = gwy_field_get_unit_z(dfield);
    surf = gwy_gl_fmscaletex(reduced_height, min, max, zunit,
                             fontsize, sqrt(setup->line_width), view->gradient, noticks,
                             &actual_width);

    if (!surf)
        return 0;

    gwy_gl_cairo_to_tex(cairo_image_surface_get_data(surf),
                        actual_width, reduced_height, cairo_image_surface_get_stride(surf));

    he = GWY_ROUND((1.0 - setup->fmscale_yalign) * (height - reduced_height));
    glTranslatef(0, he, 0);

    gwy_gl_draw_ctex(0, GWY_GL_JUST_RIGHT_EDGE, GWY_GL_JUST_BOTTOM_EDGE, actual_width, reduced_height);
    cairo_surface_destroy(surf);

    return actual_width;
}

/**
 * gwy_gl_view_class_make_list_pool:
 * @klass: OpenGL view class.
 *
 * Allocates a pool of OpenGL lists to assign list numbers from.
 *
 * We are only able to get GLContext-unique list id's from glGenLists(), but lists with the same id's seem to
 * interfere across GLContexts.  See bug #53 for more.
 *
 * We store the pool in bits of one 64bit integer.
 **/
static void
gwy_gl_view_class_make_list_pool(GwyGLListPool *pool)
{
    guint try_size = 64;

    glGetError();
    while (try_size >= 1) {
        list_pool->base = glGenLists(GWY_GL_N_LISTS*try_size);
        if (!glGetError()) {
            gwy_debug("Allocated a pool with %u items (%u lists)", try_size, GWY_GL_N_LISTS*try_size);
            list_pool->size = try_size;
            return;
        }
        try_size = (try_size*2)/3;
    }
    g_warning("Cannot get any OpenGL lists");
    list_pool->base = 0;
}

/**
 * gwy_gl_view_allocate_lists:
 * @glview: An OpenGL data view widget.
 *
 * Allocates a block of free OpenGL lists from pool for this OpenGL view to use.
 **/
static void
gwy_gl_view_assign_lists(GwyGLView *glview)
{
    guint64 b;
    guint i;

    if (!list_pool->size) {
        g_return_if_fail(gtk_widget_get_realized(GTK_WIDGET(glview)));
        gwy_gl_view_class_make_list_pool(list_pool);
    }
    g_return_if_fail(list_pool->size > 0);

    b = 1;
    for (i = 0; i < list_pool->size && (list_pool->pool & b); i++)
        b <<= 1;
    if (i == list_pool->size) {
        g_critical("No more free OpenGL lists");
        return;
    }

    gwy_debug("Assigned list #%u", i);
    list_pool->pool |= b;
    glview->shape_list_base = list_pool->base + i*GWY_GL_N_LISTS;
}

/**
 * gwy_gl_view_release_lists:
 * @glview: An OpenGL data view widget.
 *
 * Release OpenGL lists in use by this OpenGL view back to the pool.
 **/
static void
gwy_gl_view_release_lists(GwyGLView *glview)
{
    guint i;
    guint64 b = 1;

    g_return_if_fail(glview->shape_list_base >= list_pool->base);

    i = (glview->shape_list_base - list_pool->base)/GWY_GL_N_LISTS;
    gwy_debug("Released list #%u", i);
    b <<= i;
    list_pool->pool &= ~b;
    glview->shape_list_base = 0;
}


/**
 * gwy_gl_view_set_mask_key:
 * @glview: An OpenGL data view widget.
 * @key: Container string key identifying the mask field for visualisation
 *
 * Sets the container key identifying the mask field for visualisation in an OpenGL view.
 **/
void
gwy_gl_view_set_mask_key(GwyGLView *glview,
                         const gchar *key)
{
    GQuark quark;
    GwyGLViewPrivate *priv = GWY_GL_VIEW_GET_PRIVATE(glview);

    g_return_if_fail(GWY_IS_GL_VIEW(glview));
    gwy_debug("%p <%s>", glview, key);

    quark = key ? g_quark_from_string(key) : 0;
    if (priv->mask_key == quark)
        return;

    g_clear_signal_handler(&priv->mask_item_id, glview->data);
    gwy_gl_view_mask_field_disconnect(glview);
    priv->mask_key = quark;
    gwy_gl_view_mask_field_connect(glview);
    gwy_gl_view_container_connect(glview, g_quark_to_string(quark), &priv->mask_item_id,
                                  G_CALLBACK(gwy_gl_view_mask_item_changed));
    g_object_notify_by_pspec(G_OBJECT(glview), properties[PROP_MASK_KEY]);
    gwy_gl_view_mask_field_changed(glview);
}

/**
 * gwy_gl_view_get_mask_key:
 * @glview: An OpenGL data view widget.
 *
 * Gets the container key identifying the mask field for visualisation in an OpenGL view.
 *
 * Returns: The string key, or %NULL if it isn't set.
 **/
const gchar*
gwy_gl_view_get_mask_key(GwyGLView *glview)
{
    g_return_val_if_fail(GWY_IS_GL_VIEW(glview), NULL);
    return g_quark_to_string(GWY_GL_VIEW_GET_PRIVATE(glview)->mask_key);
}

static void
gwy_gl_view_mask_item_changed(GwyGLView *glview)
{
    gwy_gl_view_mask_field_disconnect(glview);
    gwy_gl_view_mask_field_connect(glview);
    gwy_gl_view_mask_field_changed(glview);
}

static void
gwy_gl_view_mask_field_connect(GwyGLView *glview)
{
    GwyGLViewPrivate *priv = GWY_GL_VIEW_GET_PRIVATE(glview);
    g_return_if_fail(!priv->mask_field);
    if (!priv->mask_key)
        return;

    if (!gwy_dict_gis_object(glview->data, priv->mask_key, &priv->mask_field))
        return;

    g_object_ref(priv->mask_field);
    priv->mask_id = g_signal_connect_swapped(priv->mask_field, "data-changed",
                                             G_CALLBACK(gwy_gl_view_mask_field_changed), glview);
}

static void
gwy_gl_view_mask_field_disconnect(GwyGLView *glview)
{
    GwyGLViewPrivate *priv = GWY_GL_VIEW_GET_PRIVATE(glview);

    g_clear_signal_handler(&priv->mask_id, priv->mask_field);
    g_clear_object(&priv->mask_field);
}

static void
gwy_gl_view_mask_field_changed(GwyGLView *glview)
{
    gwy_debug("");
    glview->changed |= GWY_GL_DATA_FIELD;
    gwy_gl_view_update_labels(glview);
    gwy_gl_view_update_lists(glview);
}

#else /* HAVE_GTKGLEXT */
/* Export the same set of symbols if we don't have OpenGL support for correct linking.  But let them all fail
 * run-time. */
static void
gwy_gl_view_init(G_GNUC_UNUSED GwyGLView *glview)
{
}

static void
gwy_gl_view_class_init(G_GNUC_UNUSED GwyGLViewClass *klass)
{
}

GtkWidget*
gwy_gl_view_new(G_GNUC_UNUSED GwyDict *data)
{
    g_critical("OpenGL support was not compiled in.");
    return NULL;
}

const gchar*
gwy_gl_view_get_setup_prefix(G_GNUC_UNUSED GwyGLView *glview)
{
    g_critical("OpenGL support was not compiled in.");
    return NULL;
}

void
gwy_gl_view_set_setup_prefix(G_GNUC_UNUSED GwyGLView *glview,
                             G_GNUC_UNUSED const gchar *key)
{
    g_critical("OpenGL support was not compiled in.");
}

const gchar*
gwy_gl_view_get_data_key(G_GNUC_UNUSED GwyGLView *glview)
{
    g_critical("OpenGL support was not compiled in.");
    return NULL;
}

void
gwy_gl_view_set_data_key(G_GNUC_UNUSED GwyGLView *glview,
                         G_GNUC_UNUSED const gchar *key)
{
    g_critical("OpenGL support was not compiled in.");
}

const gchar*
gwy_gl_view_get_mask_key(G_GNUC_UNUSED GwyGLView *glview)
{
    g_critical("OpenGL support was not compiled in.");
    return NULL;
}

void
gwy_gl_view_set_mask_key(G_GNUC_UNUSED GwyGLView *glview,
                         G_GNUC_UNUSED const gchar *key)
{
    g_critical("OpenGL support was not compiled in.");
}

const gchar*
gwy_gl_view_get_gradient_key(G_GNUC_UNUSED GwyGLView *glview)
{
    g_critical("OpenGL support was not compiled in.");
    return NULL;
}

void
gwy_gl_view_set_gradient_key(G_GNUC_UNUSED GwyGLView *glview,
                             G_GNUC_UNUSED const gchar *key)
{
    g_critical("OpenGL support was not compiled in.");
}

const gchar*
gwy_gl_view_get_material_key(G_GNUC_UNUSED GwyGLView *glview)
{
    g_critical("OpenGL support was not compiled in.");
    return NULL;
}

void
gwy_gl_view_set_material_key(G_GNUC_UNUSED GwyGLView *glview,
                             G_GNUC_UNUSED const gchar *key)
{
    g_critical("OpenGL support was not compiled in.");
}

guint
gwy_gl_view_get_reduced_size(G_GNUC_UNUSED GwyGLView *glview)
{
    g_critical("OpenGL support was not compiled in.");
    return 0;
}

void
gwy_gl_view_set_reduced_size(G_GNUC_UNUSED GwyGLView *glview,
                             G_GNUC_UNUSED guint reduced_size)
{
    g_critical("OpenGL support was not compiled in.");
}

GwyGLMovement
gwy_gl_view_get_movement_type(G_GNUC_UNUSED GwyGLView *glview)
{
    g_critical("OpenGL support was not compiled in.");
    return 0;
}

void
gwy_gl_view_set_movement_type(G_GNUC_UNUSED GwyGLView *glview,
                              G_GNUC_UNUSED GwyGLMovement movement)
{
    g_critical("OpenGL support was not compiled in.");
}

GdkPixbuf*
gwy_gl_view_get_pixbuf(G_GNUC_UNUSED GwyGLView *glview)
{
    g_critical("OpenGL support was not compiled in.");
    return NULL;
}

GdkPixbuf*
gwy_gl_view_get_pixbuf_with_alpha(G_GNUC_UNUSED GwyGLView *glview)
{
    g_critical("OpenGL support was not compiled in.");
    return NULL;
}

GwyGLLabel*
gwy_gl_view_get_label(G_GNUC_UNUSED GwyGLView *glview,
                      G_GNUC_UNUSED GwyGLViewLabel label)
{
    g_critical("OpenGL support was not compiled in.");
    return NULL;
}

GwyGLSetup*
gwy_gl_view_get_setup(G_GNUC_UNUSED GwyGLView *glview)
{
    g_critical("OpenGL support was not compiled in.");
    return NULL;
}

GwyDict*
gwy_gl_view_get_data(G_GNUC_UNUSED GwyGLView *glview)
{
    g_critical("OpenGL support was not compiled in.");
    return NULL;
}

void
gwy_gl_view_get_scale_range(G_GNUC_UNUSED GwyGLView *glview,
                            G_GNUC_UNUSED gdouble *min_scale,
                            G_GNUC_UNUSED gdouble *max_scale)
{
    g_critical("OpenGL support was not compiled in.");
}

void
gwy_gl_view_set_scale_range(G_GNUC_UNUSED GwyGLView *glview,
                            G_GNUC_UNUSED gdouble min_scale,
                            G_GNUC_UNUSED gdouble max_scale)
{
    g_critical("OpenGL support was not compiled in.");
}
#endif /* HAVE_GTKGLEXT */

/**
 * gwy_gl_view_class_disable_axis_drawing:
 * @disable: %TRUE to disable OpenGL view axes globally, %FALSE to enable them.
 *
 * Globally disables drawing of OpenGL view axes.
 *
 * If axis drawing is disabled, axes are never drawn.  If it is not disabled, their rendering depends on the OpenGL view
 * setup.
 *
 * This function is a hack and exists to work around various GL implementations that crash on pixmap drawing
 * operations.
 **/
void
gwy_gl_view_class_disable_axis_drawing(gboolean disable)
{
    ugly_hack_globally_disable_axis_drawing = disable;
}

/************************** Documentation ****************************/

/**
 * SECTION:gwyglview
 * @title: GwyGLView
 * @short_description: OpenGL 3D data display
 * @see_also: #GwyGLWindow -- window combining OpenGL view with controls,
 *            #GwyGLMaterial -- OpenGL materials,
 *            #GwyGLLabel -- Labels on OpenGL view,
 *            #GwyGLSetup -- 3D scene setup
 *
 * #GwyGLView displays a data field as a threedimensional heightfield using OpenGL. You can create a new OpenGL view for
 * a data container with gwy_gl_view_new().  By default, it inherits properties like palette from <link
 * linkend="GwyDataView">data view</link> settings, but supports separate settings -- see
 * gwy_gl_view_set_gradient_key() et al.
 *
 * #GwyGLView allows the user to interactively rotate, scale, z-scale the data or move lights, depending on its <link
 * linkend="GwyGLMovement">movement state</link>. There are no controls provided for mode change, you have to provide
 * some yourself and set the movement mode with gwy_gl_view_set_movement_type().
 **/

/* vim: set cin columns=120 tw=118 et ts=4 sw=4 cino=>1s,e0,n0,f0,{0,}0,^0,\:1s,=0,g1s,h0,t0,+1s,c3,(0,u0 : */
