/*
 *  $Id: slope_dist.c 30047 2026-06-11 08:30:45Z yeti-dn $
 *  Copyright (C) 2004-2026 David Necas (Yeti), Petr Klapetek.
 *  E-mail: yeti@gwyddion.net, klapetek@gwyddion.net.
 *
 *  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.
 */

#include "config.h"
#include <glib/gi18n-lib.h>
#include <gtk/gtk.h>
#include <gwy.h>
#include "../preview.h"

#define RUN_MODES (GWY_RUN_IMMEDIATE | GWY_RUN_INTERACTIVE)

typedef enum {
    SLOPE_DIST_2D_DIST,
    SLOPE_DIST_GRAPH_PHI,
    SLOPE_DIST_GRAPH_THETA,
    SLOPE_DIST_GRAPH_GRADIENT,
    SLOPE_DIST_LAST
} SlopeOutput;

enum {
    PARAM_OUTPUT_TYPE,
    PARAM_SIZE,
    PARAM_LOGSCALE,
    PARAM_FIT_PLANE,
    PARAM_KERNEL_SIZE,
    PARAM_MASKING,
    PARAM_UPDATE,
    PARAM_TARGET_GRAPH,
};

typedef struct {
    GwyParams *params;
    GwyField *field;
    GwyNield *mask;
    GwyField *result;
    GwyGraphModel *gmodel;
    /* Cached input data properties. */
    gboolean same_units;
    GwyField *xder;
    GwyField *yder;
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GtkWidget *dialog;
    GwyParamTable *table;
    GtkWidget *dataview;
    GtkWidget *graph;
} ModuleGUI;

static gboolean         module_register           (void);
static GwyParamDef*     define_module_params      (void);
static void             module_main               (GwyFile *data,
                                                   GwyRunModeFlags mode);
static void             execute                   (ModuleArgs *args);
static void             sanitise_params           (ModuleArgs *args);
static GwyDialogOutcome run_gui                   (ModuleArgs *args,
                                                   GwyFile *data,
                                                   gint id);
static void             param_changed             (ModuleGUI *gui,
                                                   gint id);
static void             preview                   (gpointer user_data);
static GwyField*        slope_do_2d               (ModuleArgs *args);
static void             slope_do_graph_phi        (ModuleArgs *args);
static void             slope_do_graph_theta      (ModuleArgs *args);
static void             slope_do_graph_gradient   (ModuleArgs *args);
static void             set_graph_model_properties(GwyGraphModel *gmodel,
                                                   GwyField *field,
                                                   SlopeOutput output_type);
static void             compute_slopes            (GwyField *field,
                                                   gint kernel_size,
                                                   GwyField *xder,
                                                   GwyField *yder);
static GwyField*        make_field                (GwyField *old,
                                                   gint res,
                                                   gulong *count,
                                                   gdouble real,
                                                   gboolean logscale);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Calculates one- or two-dimensional distribution of slopes or graph of their angular distribution."),
    "Yeti <yeti@gwyddion.net>",
    "3.1",
    "David Nečas (Yeti) & Petr Klapetek",
    "2004",
};

GWY_MODULE_QUERY2(module_info, slope_dist)

static gboolean
module_register(void)
{
    gwy_image_func_register("slope_dist",
                            module_main,
                            N_("/_Statistics/_Slope Distribution..."),
                            GWY_ICON_DISTRIBUTION_SLOPE,
                            RUN_MODES,
                            GWY_MENU_FLAG_IMAGE,
                            N_("Calculate angular slope distribution"));

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    static const GwyEnum output_types[] = {
        { N_("_Two-dimensional distribution"), SLOPE_DIST_2D_DIST,        },
        { N_("Directional (φ) _graph"),        SLOPE_DIST_GRAPH_PHI,      },
        { N_("_Inclination (θ) graph"),        SLOPE_DIST_GRAPH_THETA,    },
        { N_("Inclination (gra_dient) graph"), SLOPE_DIST_GRAPH_GRADIENT, },
    };
    static GwyParamDef *paramdef = NULL;

    if (paramdef)
        return paramdef;

    paramdef = gwy_param_def_new();
    gwy_param_def_set_function_name(paramdef, gwy_image_func_current());
    gwy_param_def_add_gwyenum(paramdef, PARAM_OUTPUT_TYPE, "output_type", _("Output type"),
                              output_types, G_N_ELEMENTS(output_types), SLOPE_DIST_2D_DIST);
    gwy_param_def_add_int(paramdef, PARAM_SIZE, "size", _("Output size"), 1, 1024, 200);
    gwy_param_def_add_boolean(paramdef, PARAM_LOGSCALE, "logscale", _("_Logarithmic value scale"), FALSE);
    gwy_param_def_add_boolean(paramdef, PARAM_FIT_PLANE, "fit_plane", _("Use local plane _fitting"), FALSE);
    gwy_param_def_add_int(paramdef, PARAM_KERNEL_SIZE, "kernel_size", _("Plane size"), 2, 16, 5);
    gwy_param_def_add_enum(paramdef, PARAM_MASKING, "masking", NULL, GWY_TYPE_MASKING_TYPE, GWY_MASK_IGNORE);
    gwy_param_def_add_instant_updates(paramdef, PARAM_UPDATE, "update", NULL, TRUE);
    gwy_param_def_add_target_graph(paramdef, PARAM_TARGET_GRAPH, "target_graph", NULL);
    return paramdef;
}

static void
module_main(GwyFile *data, GwyRunModeFlags mode)
{
    ModuleArgs args;
    gint oldid, newid;
    GwyAppDataId target_graph_id;
    GwyDialogOutcome outcome = GWY_DIALOG_PROCEED;

    g_return_if_fail(mode & RUN_MODES);
    gwy_clear1(args);
    gwy_data_browser_get_current(GWY_APP_FIELD, &args.field,
                                 GWY_APP_FIELD_ID, &oldid,
                                 GWY_APP_MASK_FIELD, &args.mask,
                                 0);
    g_return_if_fail(args.field);
    args.same_units = gwy_unit_equal(gwy_field_get_unit_xy(args.field), gwy_field_get_unit_z(args.field));
    args.params = gwy_params_new_from_settings(define_module_params());
    sanitise_params(&args);
    args.result = gwy_field_new(PREVIEW_SIZE, PREVIEW_SIZE, 1.0, 1.0, TRUE);
    args.gmodel = gwy_graph_model_new();

    if (mode == GWY_RUN_INTERACTIVE) {
        outcome = run_gui(&args, data, oldid);
        gwy_params_save_to_settings(args.params);
        if (outcome == GWY_DIALOG_CANCEL)
            goto end;
    }
    if (outcome != GWY_DIALOG_HAVE_RESULT)
        execute(&args);

    if (gwy_params_get_enum(args.params, PARAM_OUTPUT_TYPE) == SLOPE_DIST_2D_DIST) {
        newid = gwy_file_add_image(data, args.result);
        gwy_file_set_visible(data, GWY_FILE_IMAGE, newid, TRUE);
        gwy_file_sync_items(data, GWY_FILE_IMAGE, oldid,
                            data, GWY_FILE_IMAGE, newid,
                            GWY_FILE_ITEM_PALETTE, FALSE);
        gwy_file_set_title(data, GWY_FILE_IMAGE, newid, _("Slope distribution"), TRUE);
        gwy_log_add(data, GWY_FILE_IMAGE, oldid, newid);
    }
    else {
        target_graph_id = gwy_params_get_data_id(args.params, PARAM_TARGET_GRAPH);
        gwy_app_add_graph_or_curves(args.gmodel, data, &target_graph_id, 1);
    }

end:
    g_clear_object(&args.xder);
    g_clear_object(&args.yder);
    g_object_unref(args.result);
    g_object_unref(args.gmodel);
    g_object_unref(args.params);
}

static GwyDialogOutcome
run_gui(ModuleArgs *args, GwyFile *data, gint id)
{
    GtkWidget *hbox, *vbox;
    GwyDialog *dialog;
    GwyParamTable *table;
    ModuleGUI gui;
    SlopeOutput output_type;

    output_type = gwy_params_get_enum(args->params, PARAM_OUTPUT_TYPE);

    gwy_clear1(gui);
    gui.args = args;
    gui.dialog = gwy_dialog_new(_("Slope Distribution"));
    dialog = GWY_DIALOG(gui.dialog);
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_UPDATE, GWY_STOCK_RESET, GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
    gtk_container_set_border_width(GTK_CONTAINER(hbox), 4);
    gwy_dialog_add_content(dialog, hbox, TRUE, TRUE, 0);

    vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_widget_set_size_request(vbox, PREVIEW_SIZE, -1);
    gtk_box_pack_start(GTK_BOX(hbox), vbox, TRUE, TRUE, 0);

    gui.dataview = gwy_create_preview(args->result, NULL, PREVIEW_SIZE);
    gwy_setup_data_view(GWY_DATA_VIEW(gui.dataview), data, GWY_FILE_IMAGE, id, GWY_FILE_ITEM_PALETTE);
    gtk_box_pack_start(GTK_BOX(vbox), gui.dataview, FALSE, FALSE, 0);
    if (output_type != SLOPE_DIST_2D_DIST)
        gtk_widget_set_no_show_all(gui.dataview, TRUE);

    set_graph_model_properties(args->gmodel, args->field, output_type);
    gui.graph = gwy_graph_new(args->gmodel);
    gtk_widget_set_size_request(gui.graph, PREVIEW_SIZE, -1);
    gwy_graph_enable_user_input(GWY_GRAPH(gui.graph), FALSE);
    gtk_box_pack_start(GTK_BOX(vbox), gui.graph, TRUE, TRUE, 0);
    if (output_type == SLOPE_DIST_2D_DIST)
        gtk_widget_set_no_show_all(gui.graph, TRUE);

    table = gui.table = gwy_param_table_new(args->params);
    gwy_param_table_append_radio(table, PARAM_OUTPUT_TYPE);
    gwy_param_table_radio_set_sensitive(table, PARAM_OUTPUT_TYPE, SLOPE_DIST_GRAPH_THETA, args->same_units);
    gwy_param_table_append_target_graph(table, PARAM_TARGET_GRAPH, args->gmodel);
    gwy_param_table_append_header(table, -1, _("Options"));
    gwy_param_table_append_slider(table, PARAM_SIZE);
    gwy_param_table_append_checkbox(table, PARAM_LOGSCALE);
    gwy_param_table_append_checkbox(table, PARAM_FIT_PLANE);
    gwy_param_table_append_slider(table, PARAM_KERNEL_SIZE);
    if (args->mask)
        gwy_param_table_append_combo(table, PARAM_MASKING);
    gwy_param_table_append_checkbox(table, PARAM_UPDATE);
    gtk_box_pack_end(GTK_BOX(hbox), gwy_param_table_widget(table), FALSE, FALSE, 0);

    g_signal_connect_swapped(table, "param-changed", G_CALLBACK(param_changed), &gui);
    gwy_dialog_set_preview_func(dialog, GWY_PREVIEW_IMMEDIATE, preview, &gui, NULL);

    return gwy_dialog_run(dialog);
}

static void
param_changed(ModuleGUI *gui, gint id)
{
    GwyParams *params = gui->args->params;
    GwyParamTable *table = gui->table;

    if (id < 0 || id == PARAM_OUTPUT_TYPE) {
        SlopeOutput output_type = gwy_params_get_enum(params, PARAM_OUTPUT_TYPE);
        gboolean is_2d = (output_type == SLOPE_DIST_2D_DIST);

        gtk_widget_set_no_show_all(gui->dataview, !is_2d);
        gtk_widget_set_no_show_all(gui->graph, is_2d);
        if (is_2d) {
            gtk_widget_hide(gui->graph);
            gtk_widget_show(gui->dataview);
        }
        else {
            gtk_widget_hide(gui->dataview);
            gtk_widget_show(gui->graph);
        }
        gwy_param_table_set_sensitive(table, PARAM_LOGSCALE, is_2d);
        gwy_param_table_set_sensitive(table, PARAM_TARGET_GRAPH, !is_2d);
        /* We normally do this in preview, but it breaks target graph filtering when preview is not instant! */
        if (!is_2d) {
            gwy_graph_model_remove_all_curves(gui->args->gmodel);
            set_graph_model_properties(gui->args->gmodel, gui->args->field, output_type);
            gwy_param_table_data_id_refilter(table, PARAM_TARGET_GRAPH);
        }
    }
    if (id < 0 || id == PARAM_FIT_PLANE)
        gwy_param_table_set_sensitive(gui->table, PARAM_KERNEL_SIZE, gwy_params_get_boolean(params, PARAM_FIT_PLANE));

    if (id != PARAM_TARGET_GRAPH && id != PARAM_UPDATE)
        gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
}

static void
preview(gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;
    ModuleArgs *args = gui->args;
    SlopeOutput output_type = gwy_params_get_enum(args->params, PARAM_OUTPUT_TYPE);

    execute(args);
    if (output_type == SLOPE_DIST_2D_DIST) {
        gwy_field_data_changed(args->result);
        gwy_set_data_preview_size(GWY_DATA_VIEW(gui->dataview), PREVIEW_SIZE);
    }
    gwy_dialog_have_result(GWY_DIALOG(gui->dialog));
}

/* See libgwyddion/internal.h */
static inline gint
nielded_included(const gint *m, GwyMaskingType masking)
{
    if (masking == GWY_MASK_INCLUDE)
        return *m > 0;
    if (masking == GWY_MASK_EXCLUDE)
        return *m <= 0;
    return 1;
}

static void
execute(ModuleArgs *args)
{
    GwyParams *params = args->params;
    gboolean fit_plane = gwy_params_get_boolean(params, PARAM_FIT_PLANE);
    gint kernel_size = gwy_params_get_int(params, PARAM_KERNEL_SIZE);
    SlopeOutput output_type = gwy_params_get_enum(params, PARAM_OUTPUT_TYPE);
    GwyGraphCurveModel *gcmodel;
    GwyField *result;

    if (!args->xder)
        args->xder = gwy_field_new_alike(args->field, FALSE);
    if (!args->yder)
        args->yder = gwy_field_new_alike(args->field, FALSE);

    compute_slopes(args->field, fit_plane ? kernel_size : 0, args->xder, args->yder);

    if (output_type == SLOPE_DIST_2D_DIST) {
        result = slope_do_2d(args);
        gwy_field_assign(args->result, result);
        g_object_unref(result);
    }
    else {
        gwy_graph_model_remove_all_curves(args->gmodel);
        gcmodel = gwy_graph_curve_model_new();
        gwy_graph_model_add_curve(args->gmodel, gcmodel);
        g_object_unref(gcmodel);

        if (output_type == SLOPE_DIST_GRAPH_PHI)
            slope_do_graph_phi(args);
        else if (output_type == SLOPE_DIST_GRAPH_THETA)
            slope_do_graph_theta(args);
        else if (output_type == SLOPE_DIST_GRAPH_GRADIENT)
            slope_do_graph_gradient(args);

        set_graph_model_properties(args->gmodel, args->field, output_type);
    }
}

static GwyField*
slope_do_2d(ModuleArgs *args)
{
    GwyParams *params = args->params;
    GwyField *field = args->field;
    GwyNield *mask = args->mask;
    GwyMaskingType masking = gwy_params_get_masking(params, PARAM_MASKING, &mask);
    gint kernel_size = gwy_params_get_int(params, PARAM_KERNEL_SIZE);
    gint size = gwy_params_get_int(params, PARAM_SIZE);
    gboolean logscale = gwy_params_get_boolean(params, PARAM_LOGSCALE);
    gboolean fit_plane = gwy_params_get_boolean(params, PARAM_FIT_PLANE);
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    gint border = fit_plane ? kernel_size : 2;
    gint n = (xres - border)*(yres - border);

    gdouble minxd, maxxd;
    gwy_field_min_max(args->xder, &minxd, &maxxd);
    maxxd = MAX(fabs(minxd), fabs(maxxd));

    gdouble minyd, maxyd;
    gwy_field_min_max(args->yder, &minyd, &maxyd);
    maxyd = MAX(fabs(minyd), fabs(maxyd));

    gdouble max = MAX(maxxd, maxyd);
    if (!max)
        max = 1.0;

    gulong *count = g_new0(gulong, size*size);
    const gdouble *xd = gwy_field_get_data_const(args->xder);
    const gdouble *yd = gwy_field_get_data_const(args->yder);
    const gint *m = mask ? gwy_nield_get_data(mask) : NULL;
    for (gint i = 0; i < n; i++) {
        if (nielded_included(m + i, masking)) {
            gint xider = size*(xd[i]/(2.0*max) + 0.5);
            xider = CLAMP(xider, 0, size-1);
            gint yider = size*(yd[i]/(2.0*max) + 0.5);
            yider = CLAMP(yider, 0, size-1);

            count[yider*size + xider]++;
        }
    }

    return make_field(field, size, count, 2.0*max, logscale);
}

static void
slope_do_graph_phi(ModuleArgs *args)
{
    GwyParams *params = args->params;
    GwyField *field = args->field;
    GwyNield *mask = args->mask;
    GwyMaskingType masking = gwy_params_get_masking(params, PARAM_MASKING, &mask);
    gint kernel_size = gwy_params_get_int(params, PARAM_KERNEL_SIZE);
    gint size = gwy_params_get_int(params, PARAM_SIZE);
    gboolean fit_plane = gwy_params_get_boolean(params, PARAM_FIT_PLANE);
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    gint border = fit_plane ? kernel_size : 2;
    gint n = (xres - border)*(yres - border);

    GwyLine *line = gwy_line_new(size, 360, TRUE);
    gdouble *data = gwy_line_get_data(line);
    const gdouble *xd = gwy_field_get_data_const(args->xder);
    const gdouble *yd = gwy_field_get_data_const(args->yder);
    const gint *m = mask ? gwy_nield_get_data(mask) : NULL;
    for (gint i = 0; i < n; i++) {
        if (nielded_included(m + i, masking)) {
            gdouble phi = gwy_canonicalize_angle(atan2(yd[i], -xd[i]), TRUE, TRUE);
            gdouble d = (xd[i]*xd[i] + yd[i]*yd[i]);
            gint iphi = floor(size*phi/(2.0*G_PI));

            iphi = CLAMP(iphi, 0, size-1);
            data[iphi] += d;
        }
    }

    gwy_graph_curve_model_set_data_from_line(gwy_graph_model_get_curve(args->gmodel, 0), line, 0, 0);
    g_object_unref(line);
}

static void
slope_do_graph_theta(ModuleArgs *args)
{
    GwyParams *params = args->params;
    GwyField *field = args->field;
    GwyNield *mask = args->mask;
    GwyMaskingType masking = gwy_params_get_masking(params, PARAM_MASKING, &mask);
    gint kernel_size = gwy_params_get_int(params, PARAM_KERNEL_SIZE);
    gint size = gwy_params_get_int(params, PARAM_SIZE);
    gboolean fit_plane = gwy_params_get_boolean(params, PARAM_FIT_PLANE);
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    gint border = fit_plane ? kernel_size : 2;
    gint n = (xres - border)*(yres - border);

    GwyLine *line = gwy_line_new(size, 90, TRUE);
    gdouble *data = gwy_line_get_data(line);
    gdouble *xd = gwy_field_get_data(args->xder);
    const gdouble *yd = gwy_field_get_data_const(args->yder);
    for (gint i = 0; i < n; i++)
        xd[i] = gwy_rad2deg(atan(hypot(xd[i], yd[i])));
    gdouble max = gwy_field_area_max(args->xder, mask, masking, 0, 0, xres, yres);
    gwy_line_set_real(line, max);
    gint nc = 0;
    const gint *m = mask ? gwy_nield_get_data(mask) : NULL;
    for (gint i = 0; i < n; i++) {
        if (nielded_included(m + i, masking)) {
            gint itheta = floor(size*xd[i]/max);

            itheta = CLAMP(itheta, 0, size-1);
            data[itheta]++;
            nc++;
        }
    }

    if (nc && max)
        gwy_line_multiply(line, size/(nc*max));

    gwy_graph_curve_model_set_data_from_line(gwy_graph_model_get_curve(args->gmodel, 0), line, 0, 0);
    g_object_unref(line);
}

static void
slope_do_graph_gradient(ModuleArgs *args)
{
    GwyParams *params = args->params;
    GwyField *field = args->field;
    GwyNield *mask = args->mask;
    GwyMaskingType masking = gwy_params_get_masking(params, PARAM_MASKING, &mask);
    gint kernel_size = gwy_params_get_int(params, PARAM_KERNEL_SIZE);
    gint size = gwy_params_get_int(params, PARAM_SIZE);
    gboolean fit_plane = gwy_params_get_boolean(params, PARAM_FIT_PLANE);
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    gint border = fit_plane ? kernel_size : 2;
    gint n = (xres - border)*(yres - border);

    gdouble *xd = gwy_field_get_data(args->xder);
    const gdouble *yd = gwy_field_get_data_const(args->yder);
    for (gint i = 0; i < n; i++)
        xd[i] = hypot(xd[i], yd[i]);
    gdouble max = gwy_field_area_max(args->xder, mask, masking, 0, 0, xres, yres);

    GwyLine *line = gwy_line_new(size, max, TRUE);
    gdouble *data = gwy_line_get_data(line);
    gint nc = 0;
    const gint *m = mask ? gwy_nield_get_data(mask) : NULL;
    for (gint i = 0; i < n; i++) {
        if (nielded_included(m + i, masking)) {
            gint ider = floor(size*xd[i]/max);

            ider = CLAMP(ider, 0, size-1);
            data[ider]++;
            nc++;
        }
    }

    if (nc && max)
        gwy_line_multiply(line, size/(nc*max));

    gwy_graph_curve_model_set_data_from_line(gwy_graph_model_get_curve(args->gmodel, 0), line, 0, 0);
    g_object_unref(line);
}

static void
set_graph_model_properties(GwyGraphModel *gmodel,
                           GwyField *field,
                           SlopeOutput output_type)
{
    GwyUnit *unitx, *unity;
    GwyGraphCurveModel *gcmodel;
    const gchar *xlabel, *ylabel, *title, *desc;

    if (output_type == SLOPE_DIST_GRAPH_PHI) {
        unitx = gwy_unit_new("deg");
        unity = gwy_unit_divide(gwy_unit_new(NULL), gwy_field_get_unit_z(field), gwy_field_get_unit_xy(field));
        gwy_unit_power(unity, unity, 2);
        xlabel = "φ";
        ylabel = "w";
        title = _("Angular Slope Distribution");
        desc = _("Slopes");
    }
    else if (output_type == SLOPE_DIST_GRAPH_THETA) {
        unitx = gwy_unit_new("deg");
        unity = gwy_unit_power(gwy_unit_new(NULL), unitx, -1);
        xlabel = "θ";
        ylabel = "ρ";
        title = _("Inclination Distribution");
        desc = _("Inclinations");
    }
    else if (output_type == SLOPE_DIST_GRAPH_GRADIENT) {
        unitx = gwy_unit_divide(gwy_unit_new(NULL), gwy_field_get_unit_z(field), gwy_field_get_unit_xy(field));
        unity = gwy_unit_power(gwy_unit_new(NULL), unitx, -1);
        xlabel = "η";
        ylabel = "ρ";
        title = _("Inclination Distribution");
        desc = _("Inclinations");
    }
    else {
        g_assert(output_type == SLOPE_DIST_2D_DIST);
        return;
    }

    g_object_set(gmodel,
                 "unit-x", unitx,
                 "unit-y", unity,
                 "axis-label-bottom", xlabel,
                 "axis-label-left", ylabel,
                 "title", title,
                 NULL);
    g_object_unref(unity);
    g_object_unref(unitx);

    if (gwy_graph_model_get_n_curves(gmodel)) {
        gcmodel = gwy_graph_model_get_curve(gmodel, 0);
        g_object_set(gcmodel,
                     "mode", GWY_GRAPH_CURVE_LINE,
                     "description", desc,
                     NULL);
    }
}

static void
compute_slopes(GwyField *field,
               gint kernel_size,
               GwyField *xder,
               GwyField *yder)
{
    if (!kernel_size) {
        gwy_field_derivative_filter(field, xder, GWY_DERIVATIVE_FILTER_XDER, TRUE);
        gwy_field_derivative_filter(field, yder, GWY_DERIVATIVE_FILTER_YDER, TRUE);
        return;
    }

    GwyPlaneFitQuantity quantites[2] = { GWY_PLANE_FIT_BX, GWY_PLANE_FIT_BY };
    GwyField *fields[2] = { xder, yder };

    gwy_field_fit_local_planes(field, kernel_size, 2, quantites, fields);
    gwy_field_multiply(xder, 1.0/gwy_field_get_dx(field));
    gwy_field_multiply(yder, 1.0/gwy_field_get_dy(field));
}

static GwyField*
make_field(GwyField *old,
           gint res, gulong *count,
           gdouble real, gboolean logscale)
{
    GwyField *field;
    gdouble *d;
    gint i;

    field = gwy_field_new(res, res, real, real, FALSE);
    gwy_field_set_xoffset(field, -real/2);
    gwy_field_set_yoffset(field, -real/2);

    gwy_unit_divide(gwy_field_get_unit_xy(field), gwy_field_get_unit_z(old), gwy_field_get_unit_xy(old));

    d = gwy_field_get_data(field);
    if (logscale) {
        for (i = 0; i < res*res; i++)
            d[i] = count[i] ? log((gdouble)count[i]) + 1.0 : 0.0;
    }
    else {
        for (i = 0; i < res*res; i++)
            d[i] = count[i];
    }
    g_free(count);

    return field;
}

static void
sanitise_params(ModuleArgs *args)
{
    GwyParams *params = args->params;
    SlopeOutput output_type = gwy_params_get_enum(params, PARAM_OUTPUT_TYPE);

    if (!args->same_units && output_type == SLOPE_DIST_GRAPH_THETA)
        gwy_params_set_enum(params, PARAM_OUTPUT_TYPE, SLOPE_DIST_GRAPH_GRADIENT);
}

/* 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 : */
