/*
 *  $Id: freq_split.c 30047 2026-06-11 08:30:45Z yeti-dn $
 *  Copyright (C) 2018-2024 David Necas (Yeti).
 *  E-mail: yeti@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 <string.h>
#include <gtk/gtk.h>
#include <gwy.h>
#include "../preview.h"

#define RUN_MODES (GWY_RUN_IMMEDIATE | GWY_RUN_INTERACTIVE)

typedef enum {
    FSPLIT_PREVIEW_ORIGINAL  = 0,
    FSPLIT_PREVIEW_LOW_PASS  = 1,
    FSPLIT_PREVIEW_HIGH_PASS = 2,
} FSplitPreviewType;

typedef enum {
    FSPLIT_BOUNDARY_NONE    = 0,
    FSPLIT_BOUNDARY_LAPLACE = 1,
    FSPLIT_BOUNDARY_SMCONN  = 2,
    FSPLIT_BOUNDARY_MIRROR  = 3,
} FSplitBoundaryType;

typedef enum {
    FSPLIT_OUTPUT_LOW_PASS  = (1 << 0),
    FSPLIT_OUTPUT_HIGH_PASS = (1 << 1),
    FSPLIT_OUTPUT_BOTH      = (FSPLIT_OUTPUT_LOW_PASS | FSPLIT_OUTPUT_HIGH_PASS),
} FSplitOutputType;

enum {
    PARAM_CUTOFF,
    PARAM_WIDTH,
    PARAM_BOUNDARY,
    PARAM_OUTPUT,
    PARAM_PREVIEW,
    PARAM_UPDATE,
    INFO_WAVELENGTH,
};

typedef struct {
    GwyParams *params;
    GwyField *field;
    GwyField *highpass;
    GwyField *lowpass;
    /* Cached expensive intermediate calculation. */
    GwyField *extfftre;
    GwyField *extfftim;
    gint leftext;
    gint topext;
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GtkWidget *dialog;
    GtkWidget *dataview;
    GwyParamTable *table;
    GwyGraphModel *gmodel;
    GwyValueFormat *vf;
} 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 GwyDialogOutcome run_gui              (ModuleArgs *args,
                                              GwyFile *data,
                                              gint id);
static void             param_changed        (ModuleGUI *gui,
                                              gint id);
static void             preview              (gpointer user_data);
static void             update_real_frequency(ModuleGUI *gui);
static void             extend_and_fft       (GwyField *field,
                                              GwyField **extfftre,
                                              GwyField **extfftim,
                                              gint *leftext,
                                              gint *topext,
                                              FSplitBoundaryType boundary);
static void             make_psdf_curve      (ModuleGUI *gui);
static void             update_filter_curve  (ModuleGUI *gui);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Splits image into low and high frequency components."),
    "Yeti <yeti@gwyddion.net>",
    "2.1",
    "David Nečas (Yeti)",
    "2018",
};

GWY_MODULE_QUERY2(module_info, freq_split)

static gboolean
module_register(void)
{
    gwy_image_func_register("freq_split",
                            module_main,
                            N_("/_Level/_Frequency Split..."),
                            GWY_ICON_FREQUENCY_SPLIT,
                            RUN_MODES,
                            GWY_MENU_FLAG_IMAGE,
                            N_("Split into low and high frequencies"));

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    static const GwyEnum boundaries[] = {
        { gwy_NC("boundary-handling", "None"), FSPLIT_BOUNDARY_NONE,    },
        { N_("Laplace"),                FSPLIT_BOUNDARY_LAPLACE, },
        { N_("Smooth connect"),         FSPLIT_BOUNDARY_SMCONN,  },
        { N_("Mirror"),                 FSPLIT_BOUNDARY_MIRROR,  },
    };
    static const GwyEnum previews[] = {
        { N_("Data"),      FSPLIT_PREVIEW_ORIGINAL,  },
        { N_("High-pass"), FSPLIT_PREVIEW_HIGH_PASS, },
        { N_("Low-pass"),  FSPLIT_PREVIEW_LOW_PASS,  },
    };
    static const GwyEnum outputs[] = {
        { N_("Low-pass image"),  FSPLIT_OUTPUT_LOW_PASS  },
        { N_("High-pass image"), FSPLIT_OUTPUT_HIGH_PASS },
    };
    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_double(paramdef, PARAM_CUTOFF, "cutoff", _("C_ut-off"), 0.0, 0.3, 0.3);
    gwy_param_def_add_double(paramdef, PARAM_WIDTH, "width", _("_Edge width"), 0.0, 0.2, 0.03);
    gwy_param_def_add_gwyenum(paramdef, PARAM_BOUNDARY, "boundary", _("_Boundary treatment"),
                              boundaries, G_N_ELEMENTS(boundaries), FSPLIT_BOUNDARY_NONE);
    gwy_param_def_add_gwyflags(paramdef, PARAM_OUTPUT, "output", _("Output type"),
                               outputs, G_N_ELEMENTS(outputs), FSPLIT_OUTPUT_BOTH);
    gwy_param_def_add_gwyenum(paramdef, PARAM_PREVIEW, "preview", C_("verb", "Display"),
                              previews, G_N_ELEMENTS(previews), FSPLIT_PREVIEW_HIGH_PASS);
    gwy_param_def_add_instant_updates(paramdef, PARAM_UPDATE, "update", NULL, TRUE);
    return paramdef;
}

static void
module_main(GwyFile *data, GwyRunModeFlags mode)
{
    GwyDialogOutcome outcome = GWY_DIALOG_PROCEED;
    FSplitOutputType output;
    ModuleArgs args;
    GwyParams *params;
    gint id, newid;

    g_return_if_fail(mode & RUN_MODES);
    gwy_clear1(args);
    gwy_data_browser_get_current(GWY_APP_FIELD, &args.field,
                                 GWY_APP_FIELD_ID, &id,
                                 0);
    g_return_if_fail(args.field);
    args.highpass = gwy_field_new_alike(args.field, TRUE);
    args.lowpass = gwy_field_new_alike(args.field, TRUE);
    args.params = params = gwy_params_new_from_settings(define_module_params());

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

    output = gwy_params_get_flags(params, PARAM_OUTPUT);

    if (output & FSPLIT_OUTPUT_LOW_PASS) {
        newid = gwy_file_add_image(data, args.lowpass);
        gwy_file_set_visible(data, GWY_FILE_IMAGE, newid, TRUE);
        gwy_file_sync_items(data, GWY_FILE_IMAGE, id,
                            data, GWY_FILE_IMAGE, newid,
                            GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_REAL_SQUARE, FALSE);
        gwy_file_set_title(data, GWY_FILE_IMAGE, newid, _("Low-pass"), TRUE);
        gwy_log_add(data, GWY_FILE_IMAGE, id, newid);
    }

    if (output & FSPLIT_OUTPUT_HIGH_PASS) {
        newid = gwy_file_add_image(data, args.highpass);
        gwy_file_set_visible(data, GWY_FILE_IMAGE, newid, TRUE);
        gwy_file_sync_items(data, GWY_FILE_IMAGE, id,
                            data, GWY_FILE_IMAGE, newid,
                            GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_REAL_SQUARE, FALSE);
        gwy_file_set_title(data, GWY_FILE_IMAGE, newid, _("High-pass"), TRUE);
        gwy_log_add(data, GWY_FILE_IMAGE, id, newid);
    }

end:
    g_clear_object(&args.extfftre);
    g_clear_object(&args.extfftim);
    g_object_unref(args.params);
    g_object_unref(args.highpass);
    g_object_unref(args.lowpass);
}

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

    gwy_clear1(gui);
    gui.args = args;
    gui.vf = gwy_field_get_value_format_xy(args->field, GWY_UNIT_FORMAT_VFMARKUP, NULL);

    gui.dialog = gwy_dialog_new(_("Frequency Split"));
    dialog = GWY_DIALOG(gui.dialog);
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_UPDATE, GWY_STOCK_RESET, GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

    dataview = gui.dataview = gwy_create_preview(args->field, NULL, PREVIEW_SIZE);
    gwy_setup_data_view(GWY_DATA_VIEW(dataview), data, GWY_FILE_IMAGE, id,
                        GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_RANGE | GWY_FILE_ITEM_REAL_SQUARE);
    hbox = gwy_create_dialog_preview_hbox(GTK_DIALOG(dialog), GWY_DATA_VIEW(dataview), FALSE);

    vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
    gtk_box_pack_start(GTK_BOX(hbox), vbox, TRUE, TRUE, 0);

    gui.gmodel = gwy_graph_model_new();
    make_psdf_curve(&gui);
    graph = GWY_GRAPH(gwy_graph_new(gui.gmodel));
    gwy_graph_set_axis_visible(graph, GTK_POS_LEFT, FALSE);
    gwy_graph_set_axis_visible(graph, GTK_POS_RIGHT, FALSE);
    gwy_graph_set_axis_visible(graph, GTK_POS_TOP, FALSE);
    gwy_graph_set_axis_visible(graph, GTK_POS_BOTTOM, FALSE);
    gwy_graph_enable_user_input(graph, FALSE);
    GwyGraphArea *area = GWY_GRAPH_AREA(gwy_graph_get_area(graph));
    gwy_graph_area_enable_user_input(area, FALSE);
    gwy_graph_area_set_natural_size(area, 2*PREVIEW_SIZE/3, PREVIEW_SIZE/4);
    gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(graph), TRUE, TRUE, 0);

    table = gui.table = gwy_param_table_new(args->params);
    /* FIXME: Reciprocal value is difficult to add as alternative slider because it goes to ∞.  Keep it as a silly
     * little label on the side. */
    gwy_param_table_append_slider(table, PARAM_CUTOFF);
    gwy_param_table_append_info(table, INFO_WAVELENGTH, _("Wavelength"));
    gwy_param_table_set_unitstr(table, INFO_WAVELENGTH, gui.vf->units);
    gwy_param_table_append_separator(table);
    gwy_param_table_append_slider(table, PARAM_WIDTH);
    gwy_param_table_append_combo(table, PARAM_BOUNDARY);
    gwy_param_table_append_radio(table, PARAM_PREVIEW);
    gwy_param_table_append_separator(table);
    gwy_param_table_append_checkbox(table, PARAM_UPDATE);
    gwy_param_table_append_separator(table);
    gwy_param_table_append_checkboxes(table, PARAM_OUTPUT);

    gtk_box_pack_start(GTK_BOX(vbox), 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);

    outcome = gwy_dialog_run(dialog);

    gwy_value_format_free(gui.vf);
    g_object_unref(gui.gmodel);

    return outcome;
}

static void
param_changed(ModuleGUI *gui, gint id)
{
    ModuleArgs *args = gui->args;
    GwyParams *params = args->params;
    FSplitPreviewType display = gwy_params_get_enum(params, PARAM_PREVIEW);
    gboolean filter_changed = (id == PARAM_CUTOFF || id == PARAM_WIDTH);

    if (id < 0 || id == PARAM_CUTOFF)
        update_real_frequency(gui);

    if (id < 0 || id == PARAM_BOUNDARY || (display == FSPLIT_PREVIEW_ORIGINAL && filter_changed)) {
        /* Recalculate everything when
         * (a) We must because the boundary treatment has changed.
         * (b) We are currently recalculating nothing because original data are shown. */
        g_clear_object(&args->extfftre);
        g_clear_object(&args->extfftim);
    }

    if (id < 0 || id == PARAM_BOUNDARY || filter_changed)
        gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));

    if (id < 0 || id == PARAM_PREVIEW) {
        GwyDataView *dataview = GWY_DATA_VIEW(gui->dataview);
        if (display == FSPLIT_PREVIEW_LOW_PASS)
            gwy_data_view_set_field(dataview, args->lowpass);
        else if (display == FSPLIT_PREVIEW_HIGH_PASS)
            gwy_data_view_set_field(dataview, args->highpass);
        else
            gwy_data_view_set_field(dataview, args->field);
    }

    if (id < 0 || filter_changed)
        update_filter_curve(gui);
}

static void
update_real_frequency(ModuleGUI *gui)
{
    GwyParamTable *table = gui->table;
    GwyValueFormat *vf = gui->vf;
    gdouble dx, v = gwy_params_get_double(gui->args->params, PARAM_CUTOFF);
    gchar *s;

    if (!v) {
        gwy_param_table_info_set_valuestr(table, INFO_WAVELENGTH, "∞");
        return;
    }
    dx = gwy_field_get_dx(gui->args->field);
    v = 2.0*dx/v;
    s = g_strdup_printf("%.*f", vf->precision+1, v/vf->magnitude);
    gwy_param_table_info_set_valuestr(table, INFO_WAVELENGTH, s);
    g_free(s);
}

static void
preview(gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;
    ModuleArgs *args = gui->args;

    execute(args);
    gwy_field_data_changed(args->highpass);
    gwy_field_data_changed(args->lowpass);
    gwy_dialog_have_result(GWY_DIALOG(gui->dialog));
}

static void
extend_one_row(const gdouble *row, guint n, gdouble *extrow, guint next)
{
    enum { SMEAR = 6 };
    gint k, i;
    gdouble der0, der1;

    g_return_if_fail(next < 3*n);
    gwy_assign(extrow, row, n);
    /* 0 and 1 in extension row coordinates, not primary row */
    der0 = (2*row[n-1] - row[n-2] - row[n-3])/3;
    der1 = (2*row[0] - row[1] - row[2])/3;
    k = next - n;
    for (i = 0; i < k; i++) {
        gdouble x, y, ww, w;

        y = w = 0.0;
        if (i < SMEAR) {
            ww = 2.0*(SMEAR-1 - i)/SMEAR;
            y += ww*(row[n-1] + der0*(i + 1));
            w += ww;
        }
        if (k-1 - i < SMEAR) {
            ww = 2.0*(i + SMEAR-1 - (k-1))/SMEAR;
            y += ww*(row[0] + der1*(k - i));
            w += ww;
        }
        if (i < n) {
            x = 1.0 - i/(k - 1.0);
            ww = x*x;
            y += ww*row[n-1 - i];
            w += ww;
        }
        if (k-1 - i < n) {
            x = 1.0 - (k-1 - i)/(k - 1.0);
            ww = x*x;
            y += ww*row[k-1 - i];
            w += ww;
        }
        extrow[n + i] = y/w;
    }
}

static GwyField*
extend_field_smconn(GwyField *field)
{
    GwyField *extfield, *flipped;
    gdouble dx = gwy_field_get_dx(field), dy = gwy_field_get_dy(field);
    gint xres, yres, extxres, extyres;
    const gdouble *data;
    gdouble *extdata, *buf;
    gint i, j;

    xres = gwy_field_get_xres(field);
    yres = gwy_field_get_yres(field);
    extxres = gwy_fft_find_nice_size(4*xres/3);
    extyres = gwy_fft_find_nice_size(4*yres/3);
    if (extxres >= 3*xres || extyres >= 3*extyres) {
        /* This is a silly case.  We just do not want to hit the assertion in extend_one_row(). */
        return gwy_field_extend(field, 0, 0, extxres - xres, extyres - yres,
                                GWY_EXTERIOR_FIXED_VALUE, gwy_field_mean(field), FALSE);
    }

    extfield = gwy_field_new(extxres, extyres, extxres*dx, extyres*dy, FALSE);
    flipped = gwy_field_new(extyres, extxres, extyres*dx, extxres*dy, FALSE);
    data = gwy_field_get_data_const(field);

    /* Extend rows horizontally. */
    extdata = gwy_field_get_data(extfield);
    for (i = 0; i < yres; i++)
        extend_one_row(data + i*xres, xres, extdata + i*extxres, extxres);

    /* Extend columns, including the newly created ones. */
    gwy_field_transpose(extfield, flipped, FALSE);
    extdata = gwy_field_get_data(flipped);
    buf = g_new(gdouble, extyres);
    for (i = 0; i < extxres; i++) {
        extend_one_row(extdata + i*extyres, yres, buf, extyres);
        gwy_assign(extdata + i*extyres, buf, extyres);
    }

    /* Copy it back, extend the remaining rows and use the average to fill
     * the area unreachable by a single extension. */
    gwy_field_transpose(flipped, extfield, FALSE);
    g_object_unref(flipped);
    extdata = gwy_field_get_data(extfield);
    buf = g_renew(gdouble, buf, extxres);
    for (i = yres; i < extyres; i++) {
        extend_one_row(extdata + i*extxres, xres, buf, extxres);
        for (j = xres; j < extxres; j++)
            extdata[i*extxres + j] = 0.5*(extdata[i*extxres + j] + buf[j]);
    }

    return extfield;
}

static GwyField*
extend_field_mirror(GwyField *field)
{
    GwyField *extfield;
    gint extxres, extyres, xres, yres, i, j;
    gdouble dx = gwy_field_get_dx(field), dy = gwy_field_get_dy(field);
    const gdouble *data, *srow;
    gdouble *extdata, *trow;

    xres = gwy_field_get_xres(field);
    yres = gwy_field_get_yres(field);
    extxres = 2*xres;
    extyres = 2*yres;
    extfield = gwy_field_new(extxres, extyres, extxres*dx, extyres*dy, FALSE);
    data = gwy_field_get_data_const(field);
    extdata = gwy_field_get_data(extfield);

    for (i = 0; i < yres; i++) {
        srow = data + i*xres;
        trow = extdata + i*extxres;

        for (j = 0; j < xres; j++)
            trow[j] = trow[extxres-1 - j] = srow[j];

        srow = trow;
        trow = extdata + (extyres-1 - i)*extxres;
        gwy_assign(trow, srow, extxres);
    }

    return extfield;
}

static void
extend_and_fft(GwyField *field, GwyField **extfftre, GwyField **extfftim,
               gint *leftext, gint *topext, FSplitBoundaryType boundary)
{
    GwyField *extfield;
    gint xres, yres, xext, yext;

    xres = gwy_field_get_xres(field);
    yres = gwy_field_get_yres(field);

    *leftext = *topext = 0;
    if (boundary == FSPLIT_BOUNDARY_LAPLACE) {
        xext = gwy_fft_find_nice_size(5*xres/3);
        yext = gwy_fft_find_nice_size(5*yres/3);
        extfield = gwy_field_extend(field, xext/2, xext - xext/2, yext/2, yext - yext/2,
                                    GWY_EXTERIOR_LAPLACE, 0.0, FALSE);
        *leftext = xext/2;
        *topext = yext/2;
    }
    else if (boundary == FSPLIT_BOUNDARY_SMCONN) {
        /* The extension is asymmetrical, just to the right and bottom. */
        extfield = extend_field_smconn(field);
    }
    else if (boundary == FSPLIT_BOUNDARY_MIRROR) {
        /* The extension is asymmetrical, just to the right and bottom. */
        extfield = extend_field_mirror(field);
    }
    else {
        extfield = g_object_ref(field);
    }

    *extfftre = gwy_field_new_alike(extfield, FALSE);
    *extfftim = gwy_field_new_alike(extfield, FALSE);
    gwy_field_fft_2d_raw(extfield, NULL, *extfftre, *extfftim, GWY_TRANSFORM_DIRECTION_FORWARD);
    g_object_unref(extfield);
}

static void
filter_frequencies(GwyField *refield, GwyField *imfield,
                   gdouble cutoff, gdouble width)
{
    gint xres = gwy_field_get_xres(refield);
    gint yres = gwy_field_get_yres(refield);
    gdouble *re = gwy_field_get_data(refield);
    gdouble *im = gwy_field_get_data(imfield);

#ifdef _OPENMP
#pragma omp parallel for if (gwy_threads_are_enabled()) default(none) \
            shared(re,im,xres,yres,cutoff,width)
#endif
    for (gint i = 0; i < yres; i++) {
        gdouble fy = 2.0*MIN(i, yres-i)/yres;
        for (gint j = 0; j < xres; j++) {
            gdouble fx = 2.0*MIN(j, xres-j)/xres;
            gdouble q, f = sqrt(fx*fx + fy*fy);

            if (width > 0.0)
                q = 0.5*(erf((f - cutoff)/width) + 1.0);
            else
                q = (f >= cutoff ? 1.0 : 0.0);

            re[i*xres + j] *= q;
            im[i*xres + j] *= q;
        }
    }
}

static void
execute(ModuleArgs *args)
{
    GwyParams *params = args->params;
    GwyField *field = args->field, *lowpass = args->lowpass, *highpass = args->highpass;
    GwyField *tmpre, *tmpim, *fre, *fim;
    FSplitBoundaryType boundary = gwy_params_get_enum(params, PARAM_BOUNDARY);
    gdouble cutoff = gwy_params_get_double(params, PARAM_CUTOFF);
    gdouble width = gwy_params_get_double(params, PARAM_WIDTH);
    gint xres, yres;

    if (!args->extfftre)
        extend_and_fft(args->field, &args->extfftre, &args->extfftim, &args->leftext, &args->topext, boundary);
    xres = gwy_field_get_xres(field);
    yres = gwy_field_get_yres(field);
    tmpre = gwy_field_new_alike(args->extfftre, FALSE);
    tmpim = gwy_field_new_alike(args->extfftre, FALSE);
    fre = gwy_field_copy(args->extfftre);
    fim = gwy_field_copy(args->extfftim);
    filter_frequencies(fre, fim, cutoff, width);
    gwy_field_fft_2d_raw(fre, fim, tmpre, tmpim, GWY_TRANSFORM_DIRECTION_BACKWARD);
    g_object_unref(tmpim);
    g_object_unref(fre);
    g_object_unref(fim);
    gwy_field_area_copy(tmpre, highpass, args->leftext, args->topext, xres, yres, 0, 0);
    g_object_unref(tmpre);
    gwy_field_subtract_fields(lowpass, field, highpass);
}

static void
make_psdf_curve(ModuleGUI *gui)
{
    GwyField *field = gui->args->field;
    GwyLine *rpsdf = gwy_line_new(1, 1, FALSE);
    GwyGraphCurveModel *gcmodel;
    gdouble q;

    gwy_field_rpsdf(field, rpsdf, GWY_WINDOWING_BLACKMANN, -1);
    /* Transform to units of Nyquist frequency. */
    q = 1.0/(G_PI/gwy_field_get_dx(field));
    gwy_line_set_real(rpsdf, gwy_line_get_real(rpsdf)*q);
    gwy_line_set_offset(rpsdf, gwy_line_get_offset(rpsdf)*q);
    gwy_line_multiply(rpsdf, 1.0/fmax(gwy_line_max(rpsdf), G_MINDOUBLE));

    gcmodel = gwy_graph_curve_model_new();
    gwy_graph_curve_model_set_data_from_line(gcmodel, rpsdf, 0, 0);
    g_object_set(gcmodel,
                 "mode", GWY_GRAPH_CURVE_LINE,
                 "color", gwy_graph_get_preset_color(0),
                 "description", _("PSDF"),
                 NULL);
    gwy_graph_model_add_curve(gui->gmodel, gcmodel);

    g_object_unref(rpsdf);

    gcmodel = gwy_graph_curve_model_new();
    g_object_set(gcmodel,
                 "mode", GWY_GRAPH_CURVE_LINE,
                 "color", gwy_graph_get_preset_color(1),
                 "description", _("Filter"),
                 NULL);
    gwy_graph_model_add_curve(gui->gmodel, gcmodel);

    g_object_set(gui->gmodel, "x-max", 1.0, "x-max-set", TRUE, NULL);
}

static void
update_filter_curve(ModuleGUI *gui)
{
    GwyParams *params = gui->args->params;
    gdouble cutoff = gwy_params_get_double(params, PARAM_CUTOFF);
    gdouble width = gwy_params_get_double(params, PARAM_WIDTH);
    GwyGraphCurveModel *gcmodel;
    const gdouble *xdata;
    gdouble *ydata;
    gint n, i;

    gcmodel = gwy_graph_model_get_curve(gui->gmodel, 0);
    n = gwy_graph_curve_model_get_n_data(gcmodel);
    xdata = gwy_graph_curve_model_get_xdata(gcmodel);
    ydata = g_new(gdouble, n);

    for (i = 0; i < n; i++) {
        gdouble q, f = xdata[i];
        if (width > 0.0)
            q = 0.5*(erf((f - cutoff)/width) + 1.0);
        else
            q = (f >= cutoff ? 1.0 : 0.0);
        ydata[i] = q;
    }

    gcmodel = gwy_graph_model_get_curve(gui->gmodel, 1);
    gwy_graph_curve_model_set_data(gcmodel, xdata, ydata, n);

    g_free(ydata);
}

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