/*
 *  $Id: straighten_path.c 30047 2026-06-11 08:30:45Z yeti-dn $
 *  Copyright (C) 2016-2026 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 <stdlib.h>
#include <string.h>
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>
#include <gwy.h>
#include "../preview.h"

#define RUN_MODES (GWY_RUN_INTERACTIVE)

enum {
    COLUMN_I, COLUMN_X, COLUMN_Y, NCOLUMNS
};

enum {
    PARAM_CLOSED,
    PARAM_INTERP,
    PARAM_ORIENTATION,
    PARAM_SLACKNESS,
    PARAM_THICKNESS,
};

typedef struct {
    GwyParams *params;
    GwyField *field;
    GwyField *result;
    GwyNield *result_mask;
    GwySelection *selection;
    /* Cached values for input data field. */
    gboolean realsquare;
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GtkWidget *dialog;
    GwyParamTable *table;
    GtkWidget *coordlist;
    GtkWidget *view;
    GtkWidget *view_result;
    GwySelection *selection;
} 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,
                                                GwySelection *selection);
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             reset_path             (ModuleGUI *gui);
static void             restore_path           (ModuleGUI *gui);
static void             reverse_path           (ModuleGUI *gui);
static void             set_scaled_thickness   (ModuleGUI *gui);
static void             init_selection         (GwySelection *selection,
                                                ModuleArgs *args);
static void             selection_changed      (ModuleGUI *gui,
                                                gint hint);
static void             fill_coord_list        (ModuleGUI *gui);
static GtkWidget*       create_coord_list      (ModuleGUI *gui);
static void             render_coord_cell      (GtkCellLayout *layout,
                                                GtkCellRenderer *renderer,
                                                GtkTreeModel *model,
                                                GtkTreeIter *iter,
                                                gpointer user_data);
static gboolean         delete_selection_object(GtkTreeView *treeview,
                                                GdkEventKey *event,
                                                ModuleGUI *gui);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Extracts a straightened part of image along a curve."),
    "Yeti <yeti@gwyddion.net>",
    "2.2",
    "David Nečas (Yeti)",
    "2016",
};

GWY_MODULE_QUERY2(module_info, straighten_path)

static gboolean
module_register(void)
{
    gwy_image_func_register("straighten_path",
                            module_main,
                            N_("/_Distortion/Straighten _Path..."),
                            GWY_ICON_STRAIGHTEN_PATH,
                            RUN_MODES,
                            GWY_MENU_FLAG_IMAGE,
                            N_("Straighten along a path"));

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    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_boolean(paramdef, PARAM_CLOSED, "closed", _("C_losed curve"), FALSE);
    gwy_param_def_add_enum(paramdef, PARAM_INTERP, "interp", NULL, GWY_TYPE_INTERPOLATION_TYPE,
                           GWY_INTERPOLATION_LINEAR);
    gwy_param_def_add_enum(paramdef, PARAM_ORIENTATION, "orientation", _("Out_put orientation"), GWY_TYPE_ORIENTATION,
                           GWY_ORIENTATION_VERTICAL);
    gwy_param_def_add_double(paramdef, PARAM_SLACKNESS, "slackness", _("_Slackness"), 0.0, G_SQRT2, 1.0/G_SQRT2);
    gwy_param_def_add_int(paramdef, PARAM_THICKNESS, "thickness", _("_Thickness"), 3, 16384, 20);
    return paramdef;
}

static void
module_main(GwyFile *data, GwyRunModeFlags mode)
{
    GwyDialogOutcome outcome;
    ModuleArgs args;
    GwyParams *params;
    GwyField *field, *tmp;
    gint id, newid, yres;
    GwySelection *selection;
    gboolean closed;
    gdouble slackness;

    g_return_if_fail(mode & RUN_MODES);
    gwy_data_browser_get_current(GWY_APP_FIELD, &field,
                                 GWY_APP_FIELD_ID, &id,
                                 0);
    g_return_if_fail(field);

    gwy_clear1(args);
    args.field = field;
    args.params = params = gwy_params_new_from_settings(define_module_params());
    gwy_dict_gis_boolean(GWY_DICT(data), gwy_file_key_image_real_square(id), &args.realsquare);
    yres = gwy_field_get_yres(field);
    args.result = gwy_field_new(5, yres, 5, yres, TRUE);
    args.result_mask = gwy_field_new_nield_alike(args.result);

    GQuark selection_key = gwy_file_key_image_selection(id, "path");
    if (gwy_dict_gis_object(GWY_DICT(data), selection_key, &selection)
        && gwy_selection_get_data(GWY_SELECTION(selection), NULL) > 1) {
        gwy_debug("init selection from container");
        args.selection = gwy_selection_copy(selection);
        gwy_selection_set_max_objects(args.selection, 1024);
        g_object_get(selection, "slackness", &slackness, "closed", &closed, NULL);
        gwy_params_set_double(params, PARAM_SLACKNESS, slackness);
        gwy_params_set_boolean(params, PARAM_CLOSED, closed);
    }
    else {
        gwy_debug("init selection afresh");
        args.selection = gwy_selection_path_new();
        gwy_selection_set_max_objects(args.selection, 1024);
        init_selection(args.selection, &args);
    }

    outcome = run_gui(&args, data, id);
    gwy_params_save_to_settings(params);
    gwy_dict_set_object(GWY_DICT(data), selection_key, args.selection);

    if (outcome == GWY_DIALOG_CANCEL)
        goto end;
    if (outcome != GWY_DIALOG_HAVE_RESULT)
        execute(&args, args.selection);

    if (gwy_params_get_enum(params, PARAM_ORIENTATION) == GWY_ORIENTATION_HORIZONTAL) {
        tmp = gwy_field_new_rotated_90(args.result, FALSE);
        g_object_unref(args.result);
        args.result = tmp;
    }

    newid = gwy_file_add_image(data, args.result);

    gwy_file_set_visible(data, GWY_FILE_IMAGE, newid, TRUE);
    gwy_file_set_title(data, GWY_FILE_IMAGE, newid, _("Straightened"), TRUE);
    gwy_file_sync_items(data, GWY_FILE_IMAGE, id,
                        data, GWY_FILE_IMAGE, newid,
                        GWY_FILE_ITEM_COLOR_MAPPING | GWY_FILE_ITEM_RANGE
                        | GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_MASK_COLOR, FALSE);
    gint ngrains = gwy_nield_number_contiguous(args.result_mask);
    gwy_file_set_image_mask(data, newid, ngrains ? args.result_mask : NULL);
    gwy_log_add(data, GWY_FILE_IMAGE, id, newid);

end:
    g_object_unref(args.selection);
    g_object_unref(args.result);
    g_object_unref(args.result_mask);
    g_object_unref(params);
}

static GwyDialogOutcome
run_gui(ModuleArgs *args, GwyFile *data, gint id)
{
    GtkWidget *hbox, *vbox, *buttonbox, *button;
    GwyDialog *dialog;
    GwyParamTable *table;
    ModuleGUI gui;
    GwyField *field = args->field;
    GwyDialogOutcome outcome;
    gint maxthickness, thickness;

    maxthickness = MAX(gwy_field_get_xres(field), gwy_field_get_yres(field))/2;
    maxthickness = MAX(maxthickness, 3);
    thickness = gwy_params_get_int(args->params, PARAM_THICKNESS);
    if (thickness > maxthickness)
        gwy_params_set_int(args->params, PARAM_THICKNESS, (thickness = maxthickness));

    gwy_clear1(gui);
    gui.args = args;

    gui.dialog = gwy_dialog_new(_("Straighten Path"));
    dialog = GWY_DIALOG(gui.dialog);
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_UPDATE, GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

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

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

    gtk_box_pack_start(GTK_BOX(vbox), create_coord_list(&gui), TRUE, TRUE, 0);
    buttonbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_set_homogeneous(GTK_BOX(buttonbox), TRUE);
    gtk_box_pack_start(GTK_BOX(vbox), buttonbox, FALSE, FALSE, 0);

    button = gtk_button_new_with_mnemonic(_("_Reset"));
    gtk_box_pack_start(GTK_BOX(buttonbox), button, TRUE, TRUE, 0);
    g_signal_connect_swapped(button, "clicked", G_CALLBACK(reset_path), &gui);

    button = gtk_button_new_with_mnemonic(_("Res_tore"));
    gtk_box_pack_start(GTK_BOX(buttonbox), button, TRUE, TRUE, 0);
    g_signal_connect_swapped(button, "clicked", G_CALLBACK(restore_path), &gui);

    button = gtk_button_new_with_mnemonic(_("Re_verse"));
    gtk_box_pack_start(GTK_BOX(buttonbox), button, TRUE, TRUE, 0);
    g_signal_connect_swapped(button, "clicked", G_CALLBACK(reverse_path), &gui);

    table = gui.table = gwy_param_table_new(args->params);
    gwy_param_table_append_combo(table, PARAM_INTERP);
    gwy_param_table_append_combo(table, PARAM_ORIENTATION);
    gwy_param_table_append_slider(table, PARAM_THICKNESS);
    gwy_param_table_slider_restrict_range(table, PARAM_THICKNESS, 3, maxthickness);
    gwy_param_table_set_unitstr(table, PARAM_THICKNESS, _("px"));
    gwy_param_table_append_slider(table, PARAM_SLACKNESS);
    gwy_param_table_slider_set_digits(table, PARAM_SLACKNESS, 3);
    gwy_param_table_slider_set_mapping(table, PARAM_SLACKNESS, GWY_SCALE_MAPPING_LINEAR);
    gwy_param_table_append_checkbox(table, PARAM_CLOSED);

    gtk_box_pack_start(GTK_BOX(vbox), gwy_param_table_widget(table), FALSE, FALSE, 0);

    gui.view = gwy_create_preview(field, NULL, PREVIEW_SIZE);
    gwy_setup_data_view(GWY_DATA_VIEW(gui.view), data, GWY_FILE_IMAGE, id,
                        GWY_FILE_ITEM_COLOR_MAPPING | GWY_FILE_ITEM_RANGE
                        | GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_REAL_SQUARE);
    gtk_widget_set_halign(gui.view, GTK_ALIGN_START);
    gtk_widget_set_valign(gui.view, GTK_ALIGN_START);
    gui.selection = gwy_create_preview_vector_layer(GWY_DATA_VIEW(gui.view), GWY_TYPE_LAYER_PATH, 1024, TRUE);
    g_object_ref(gui.selection);
    gwy_selection_assign(gui.selection, args->selection);
    gtk_box_pack_start(GTK_BOX(hbox), gui.view, FALSE, FALSE, 4);

    gui.view_result = gwy_create_preview(args->result, args->result_mask, PREVIEW_SIZE);
    gwy_setup_data_view(GWY_DATA_VIEW(gui.view_result), data, GWY_FILE_IMAGE, id,
                        GWY_FILE_ITEM_COLOR_MAPPING | GWY_FILE_ITEM_RANGE
                        | GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_MASK_COLOR);
    gtk_widget_set_halign(gui.view_result, GTK_ALIGN_START);
    gtk_widget_set_valign(gui.view_result, GTK_ALIGN_START);
    gtk_box_pack_start(GTK_BOX(hbox), gui.view_result, FALSE, FALSE, 4);

    fill_coord_list(&gui);
    /* We do not get the right value before the data view is shown. */
    g_signal_connect_swapped(gui.view, "map", G_CALLBACK(set_scaled_thickness), &gui);
    g_signal_connect_swapped(table, "param-changed", G_CALLBACK(param_changed), &gui);
    g_signal_connect_swapped(gui.selection, "changed", G_CALLBACK(selection_changed), &gui);
    gwy_dialog_set_preview_func(dialog, GWY_PREVIEW_UPON_REQUEST, preview, &gui, NULL);

    outcome = gwy_dialog_run(dialog);

    gwy_selection_assign(args->selection, gui.selection);
    g_object_unref(gui.selection);

    return outcome;
}

static void
reset_path(ModuleGUI *gui)
{
    init_selection(gui->selection, gui->args);
}

static void
restore_path(ModuleGUI *gui)
{
    gwy_selection_assign(gui->selection, gui->args->selection);
}

static void
reverse_path(ModuleGUI *gui)
{
    guint i, n = gwy_selection_get_data(gui->selection, NULL);
    gdouble *xy = g_new(gdouble, 2*n);

    gwy_selection_get_data(gui->selection, xy);
    for (i = 0; i < n/2; i++) {
        GWY_SWAP(gdouble, xy[2*i], xy[2*(n-1 - i)]);
        GWY_SWAP(gdouble, xy[2*i + 1], xy[2*(n-1 - i) + 1]);
    }
    gwy_selection_set_data(gui->selection, n, xy);
    g_free(xy);
}

static void
set_scaled_thickness(ModuleGUI *gui)
{
    gint thickness = gwy_params_get_int(gui->args->params, PARAM_THICKNESS);
    gdouble zoom = gwy_data_view_get_real_zoom(GWY_DATA_VIEW(gui->view));
    GwyVectorLayer *vlayer = gwy_data_view_get_interactive_layer(GWY_DATA_VIEW(gui->view));
    gwy_layer_path_set_thickness(GWY_LAYER_PATH(vlayer), zoom*thickness);
}

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

    execute(args, gui->selection);
    gwy_field_data_changed(args->result);
    gwy_nield_data_changed(args->result_mask);
    gwy_set_data_preview_size(GWY_DATA_VIEW(gui->view_result), PREVIEW_SIZE);
    gwy_dialog_have_result(GWY_DIALOG(gui->dialog));
}

static GtkWidget*
create_coord_list(ModuleGUI *gui)
{
    static const gchar *column_labels[] = { "n", "x", "y" };

    GwyNullStore *store;
    GtkTreeView *treeview;
    GtkTreeViewColumn *column;
    GtkCellRenderer *renderer;
    GtkWidget *label, *scwin;
    guint i;

    store = gwy_null_store_new(0);
    gui->coordlist = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
    treeview = GTK_TREE_VIEW(gui->coordlist);
    g_signal_connect(treeview, "key-press-event", G_CALLBACK(delete_selection_object), gui);

    for (i = 0; i < NCOLUMNS; i++) {
        column = gtk_tree_view_column_new();
        gtk_tree_view_column_set_expand(column, TRUE);
        gtk_tree_view_column_set_alignment(column, 0.5);
        g_object_set_data(G_OBJECT(column), "id", GUINT_TO_POINTER(i));
        renderer = gtk_cell_renderer_text_new();
        g_object_set(renderer, "xalign", 1.0, NULL);
        gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(column), renderer, TRUE);
        gtk_cell_layout_set_cell_data_func(GTK_CELL_LAYOUT(column), renderer, render_coord_cell, gui, NULL);
        label = gtk_label_new(column_labels[i]);
        gtk_tree_view_column_set_widget(column, label);
        gtk_widget_show(label);
        gtk_tree_view_append_column(treeview, column);
    }

    scwin = gtk_scrolled_window_new(NULL, NULL);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scwin), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
    gtk_container_add(GTK_CONTAINER(scwin), gui->coordlist);

    return scwin;
}

static void
render_coord_cell(GtkCellLayout *layout,
                  GtkCellRenderer *renderer,
                  GtkTreeModel *model,
                  GtkTreeIter *iter,
                  gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;
    GwyField *field = gui->args->field;
    gchar buf[32];
    guint idx, id;
    gint ival;

    id = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(layout), "id"));
    gtk_tree_model_get(model, iter, 0, &idx, -1);
    if (id == COLUMN_I)
        ival = idx+1;
    else {
        gdouble xy[2];

        gwy_selection_get_object(gui->selection, idx, xy);
        if (id == COLUMN_X)
            ival = gwy_field_rtoj(field, xy[0]);
        else
            ival = gwy_field_rtoi(field, xy[1]);
    }

    g_snprintf(buf, sizeof(buf), "%d", ival);
    g_object_set(renderer, "text", buf, NULL);
}

static gboolean
delete_selection_object(GtkTreeView *treeview,
                        GdkEventKey *event,
                        ModuleGUI *gui)
{
    GtkTreeSelection *selection;
    GtkTreeModel *model;
    GtkTreePath *path;
    GtkTreeIter iter;
    const gint *indices;

    if (event->keyval != GDK_KEY_Delete)
        return FALSE;

    selection = gtk_tree_view_get_selection(treeview);
    if (!gtk_tree_selection_get_selected(selection, &model, &iter))
        return FALSE;

    /* Do not permit reduction to a single point. */
    if (gwy_selection_get_data(gui->selection, NULL) < 3)
        return FALSE;

    path = gtk_tree_model_get_path(model, &iter);
    indices = gtk_tree_path_get_indices(path);
    gwy_selection_delete_object(gui->selection, indices[0]);
    gtk_tree_path_free(path);

    return TRUE;
}

static void
init_selection(GwySelection *selection, ModuleArgs *args)
{
    gdouble xreal = gwy_field_get_xreal(args->field);
    gdouble yreal = gwy_field_get_yreal(args->field);
    gboolean closed = gwy_params_get_boolean(args->params, PARAM_CLOSED);
    gdouble xy[8];

    if (closed) {
        xy[0] = 0.75*xreal;
        xy[1] = xy[5] = 0.5*yreal;
        xy[2] = xy[6] = 0.5*xreal;
        xy[3] = 0.25*yreal;
        xy[4] = 0.25*xreal;
        xy[7] = 0.75*yreal;
        gwy_selection_set_data(selection, 4, xy);
    }
    else {
        xy[0] = xy[2] = xy[4] = 0.5*xreal;
        xy[1] = 0.2*yreal;
        xy[3] = 0.5*yreal;
        xy[5] = 0.8*yreal;
        gwy_selection_set_data(selection, 3, xy);
    }

    GwySelectionPath *selpath = GWY_SELECTION_PATH(selection);
    gwy_selection_path_set_slackness(selpath, gwy_params_get_double(args->params, PARAM_SLACKNESS));
    gwy_selection_path_set_closed(selpath, closed);
}

static void
selection_changed(ModuleGUI *gui, gint hint)
{
    GtkTreeView *treeview = GTK_TREE_VIEW(gui->coordlist);
    GtkTreeModel *model = gtk_tree_view_get_model(treeview);
    GwyNullStore *store = GWY_NULL_STORE(model);

    if (hint < 0)
        fill_coord_list(gui);
    else {
        gint n = gwy_null_store_get_n_rows(store);
        GtkTreeSelection *selection;
        GtkTreePath *path;
        GtkTreeIter iter;

        g_return_if_fail(hint <= n);
        if (hint < n)
            gwy_null_store_row_changed(store, hint);
        else
            gwy_null_store_set_n_rows(store, n+1);

        gtk_tree_model_iter_nth_child(model, &iter, NULL, hint);
        path = gtk_tree_model_get_path(model, &iter);
        selection = gtk_tree_view_get_selection(treeview);
        gtk_tree_selection_select_iter(selection, &iter);
        gtk_tree_view_scroll_to_cell(treeview, path, NULL, FALSE, 0.0, 0.0);
        gtk_tree_path_free(path);
    }
    gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
}

static void
fill_coord_list(ModuleGUI *gui)
{
    GtkTreeView *treeview = GTK_TREE_VIEW(gui->coordlist);
    GtkTreeModel *model = gtk_tree_view_get_model(treeview);
    GwyNullStore *store = GWY_NULL_STORE(model);
    gint n;

    g_object_ref(model);
    gtk_tree_view_set_model(treeview, NULL);
    n = gwy_selection_get_data(gui->selection, NULL);
    gwy_null_store_set_n_rows(store, n);
    gtk_tree_view_set_model(treeview, model);
    g_object_unref(model);
}

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

    if (id < 0 || id == PARAM_CLOSED)
        gwy_selection_path_set_closed(GWY_SELECTION_PATH(gui->selection), gwy_params_get_boolean(params, PARAM_CLOSED));
    if (id < 0 || id == PARAM_SLACKNESS) {
        gwy_selection_path_set_slackness(GWY_SELECTION_PATH(gui->selection),
                                         gwy_params_get_double(params, PARAM_SLACKNESS));
    }
    if (id < 0 || id == PARAM_THICKNESS)
        set_scaled_thickness(gui);

    gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
}

/* XXX: This replicates extract_path.c */
static GwyXY*
rescale_points(GwySelection *selection, GwyField *field,
               gboolean realsquare,
               gdouble *pdx, gdouble *pdy, gdouble *pqx, gdouble *pqy)
{
    gdouble dx, dy, qx, qy, h;
    GwyXY *points;
    guint n, i;

    dx = gwy_field_get_dx(field);
    dy = gwy_field_get_dy(field);
    h = MIN(dx, dy);
    if (realsquare) {
        qx = h/dx;
        qy = h/dy;
        dx = dy = h;
    }
    else
        qx = qy = 1.0;

    n = gwy_selection_get_data(selection, NULL);
    points = g_new(GwyXY, n);
    for (i = 0; i < n; i++) {
        gdouble xy[2];

        gwy_selection_get_object(selection, i, xy);
        points[i].x = xy[0]/dx;
        points[i].y = xy[1]/dy;
    }

    *pdx = dx;
    *pdy = dy;
    *pqx = qx;
    *pqy = qy;
    return points;
}

static void
execute(ModuleArgs *args, GwySelection *selection)
{
    GwyField *field = args->field, *result = args->result;
    GwyNield *mask = args->result_mask;
    GwyInterpolationType interp = gwy_params_get_enum(args->params, PARAM_INTERP);
    guint thickness = gwy_params_get_int(args->params, PARAM_THICKNESS);
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    gdouble dx, dy, qx, qy, length;
    guint i, j, k;
    gboolean have_exterior = FALSE;

    guint n = gwy_selection_get_data(selection, NULL);
    GwyXY *rpoints = rescale_points(selection, field, args->realsquare, &dx, &dy, &qx, &qy);
    gdouble h = MIN(dx, dy);
    GwySpline *spline = gwy_spline_new_from_points(rpoints, n);
    gwy_spline_set_closed(spline, gwy_params_get_boolean(args->params, PARAM_CLOSED));
    gwy_spline_set_slackness(spline, gwy_params_get_double(args->params, PARAM_SLACKNESS));
    g_free(rpoints);

    length = gwy_spline_length(spline);

    /* This would give natural sampling for a straight line along some axis. */
    n = GWY_ROUND(length + 1.0);
    gwy_field_resize(result, thickness, n);
    gwy_field_set_xreal(result, h*thickness);
    gwy_field_set_yreal(result, h*n);
    gwy_field_set_xoffset(result, 0.0);
    gwy_field_set_yoffset(result, 0.0);
    gwy_field_copy_units(field, result);
    gwy_nield_resize(mask, thickness, n);
    gwy_nield_clear(mask);
    if (n < 2) {
        gwy_field_clear(result);
        return;
    }

    GwyXY *points = g_new(GwyXY, n);
    GwyXY *tangents = g_new(GwyXY, n);
    GwyXY *coords = g_new(GwyXY, n*thickness);
    gwy_spline_sample_uniformly(spline, points, tangents, n);
    gint *m = gwy_nield_get_data(mask);
    for (i = k = 0; i < n; i++) {
        gdouble xc = qx*points[i].x, yc = qy*points[i].y;
        gdouble vx = qx*tangents[i].y, vy = -qy*tangents[i].x;

        /* If the derivative is zero we just fill the entire row with the same value.  I declare it acceptable. */
        for (j = 0; j < thickness; j++, k++) {
            gdouble x = xc + (j + 0.5 - 0.5*thickness)*vx;
            gdouble y = yc + (j + 0.5 - 0.5*thickness)*vy;

            coords[k].x = x;
            coords[k].y = y;
            if (y > yres || x > xres || y < 0.0 || x < 0.0) {
                m[i*thickness + j] = 1;
                have_exterior = TRUE;
            }
        }
    }
    /* Pass mirror because we handle exterior ourselves here and mirror is the least code which simultaneously does
     * not produce undefined pixels where we disagree with the function on which pixels are numerically outside. */
    gwy_field_sample_distorted(field, result, coords, interp, GWY_EXTERIOR_MIRROR, 0.0);

    g_free(coords);
    g_free(points);
    g_free(tangents);

    if (have_exterior)
        gwy_field_correct_average(result, mask);
}

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