/*
 *  $Id: field.c 30044 2026-06-10 19:12:24Z vojtakl4 $
 *  Copyright (C) 2003-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 <string.h>
#include <stdlib.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/serializable-utils.h"
#include "libgwyddion/field.h"
#include "libgwyddion/interpolation.h"
#include "libgwyddion/stats.h"
#include "libgwyddion/grains.h"

#include "libgwyddion/omp.h"
#include "libgwyddion/internal.h"

#define TYPE_NAME "GwyField"

enum {
    SGNL_DATA_CHANGED,
    NUM_SIGNALS
};

enum {
    ITEM_XRES, ITEM_YRES,
    ITEM_XREAL, ITEM_YREAL,
    ITEM_XOFF, ITEM_YOFF,
    ITEM_UNIT_XY, ITEM_UNIT_Z,
    ITEM_DATA,
    NUM_ITEMS
};

typedef struct {
    gdouble dist;
    gint i;
    gint j;
} MaskedPoint;

static void             finalize                    (GObject *object);
static void             alloc_data                  (GwyField *field,
                                                     gsize xres,
                                                     gsize yres,
                                                     gboolean nullme);
static void             serializable_init           (GwySerializableInterface *iface);
static void             serializable_itemize        (GwySerializable *serializable,
                                                     GwySerializableGroup *group);
static gboolean         serializable_construct      (GwySerializable *serializable,
                                                     GwySerializableGroup *group,
                                                     GwyErrorList **error_list);
static GwySerializable* serializable_copy           (GwySerializable *serializable);
static void             serializable_assign         (GwySerializable *destination,
                                                     GwySerializable *source);
static void             set_cache_for_constant_field(GwyField *field,
                                                     gdouble value);
static gboolean         field_is_constant           (GwyField *dfield,
                                                     gdouble *z);

static guint signals[NUM_SIGNALS];
static GObjectClass *parent_class = NULL;

static GwySerializableItem serializable_items[NUM_ITEMS] = {
    { .name = "xres",    .ctype = GWY_SERIALIZABLE_INT32,        },
    { .name = "yres",    .ctype = GWY_SERIALIZABLE_INT32,        },
    { .name = "xreal",   .ctype = GWY_SERIALIZABLE_DOUBLE,       },
    { .name = "yreal",   .ctype = GWY_SERIALIZABLE_DOUBLE,       },
    { .name = "xoff",    .ctype = GWY_SERIALIZABLE_DOUBLE,       },
    { .name = "yoff",    .ctype = GWY_SERIALIZABLE_DOUBLE,       },
    { .name = "unit_xy", .ctype = GWY_SERIALIZABLE_OBJECT,       },
    { .name = "unit_z",  .ctype = GWY_SERIALIZABLE_OBJECT,       },
    { .name = "data",    .ctype = GWY_SERIALIZABLE_DOUBLE_ARRAY,
                         .flags = GWY_SERIALIZABLE_BIG_DATA      },
};

G_DEFINE_TYPE_WITH_CODE(GwyField, gwy_field, G_TYPE_OBJECT,
                        G_ADD_PRIVATE(GwyField)
                        G_IMPLEMENT_INTERFACE(GWY_TYPE_SERIALIZABLE, serializable_init))

static void
serializable_init(GwySerializableInterface *iface)
{
    iface->itemize   = serializable_itemize;
    iface->construct = serializable_construct;
    iface->copy      = serializable_copy;
    iface->assign    = serializable_assign;

    serializable_items[ITEM_XRES].aux.pspec = g_param_spec_int(serializable_items[ITEM_XRES].name, NULL, NULL,
                                                               1, G_MAXINT, 1,
                                                               G_PARAM_STATIC_STRINGS);
    serializable_items[ITEM_YRES].aux.pspec = g_param_spec_int(serializable_items[ITEM_YRES].name, NULL, NULL,
                                                               1, G_MAXINT, 1,
                                                               G_PARAM_STATIC_STRINGS);
    serializable_items[ITEM_XREAL].aux.pspec = g_param_spec_double(serializable_items[ITEM_XREAL].name, NULL, NULL,
                                                                   G_MINDOUBLE, G_MAXDOUBLE, 1.0,
                                                                   G_PARAM_STATIC_STRINGS);
    serializable_items[ITEM_YREAL].aux.pspec = g_param_spec_double(serializable_items[ITEM_YREAL].name, NULL, NULL,
                                                                   G_MINDOUBLE, G_MAXDOUBLE, 1.0,
                                                                   G_PARAM_STATIC_STRINGS);
    /* No need to add pspecs for offsets because they have unlimited values and default zero. */
    gwy_fill_serializable_defaults_pspec(serializable_items, NUM_ITEMS, FALSE);
    serializable_items[ITEM_UNIT_XY].aux.object_type = GWY_TYPE_UNIT;
    serializable_items[ITEM_UNIT_Z].aux.object_type = GWY_TYPE_UNIT;
}

static void
gwy_field_class_init(GwyFieldClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GType type = G_TYPE_FROM_CLASS(klass);

    parent_class = gwy_field_parent_class;

    gobject_class->finalize = finalize;

    /**
    * GwyField::data-changed:
    * @gwyfield: The #GwyField which received the signal.
    *
     * The ::data-changed signal is never emitted by the object itself.  It is intended as a means to notify object
     * users they should update themselves, usually when a computation modifying the data is finished.
    */
    signals[SGNL_DATA_CHANGED] = g_signal_new("data-changed", type,
                                              G_SIGNAL_RUN_FIRST,
                                              G_STRUCT_OFFSET(GwyFieldClass, data_changed),
                                              NULL, NULL,
                                              g_cclosure_marshal_VOID__VOID,
                                              G_TYPE_NONE, 0);
    g_signal_set_va_marshaller(signals[SGNL_DATA_CHANGED], type, g_cclosure_marshal_VOID__VOIDv);
}

static void
gwy_field_init(GwyField *field)
{
    field->priv = gwy_field_get_instance_private(field);

    alloc_data(field, 1, 1, TRUE);
    field->xreal = field->yreal = 1.0;
}

static void
finalize(GObject *object)
{
    GwyField *field = (GwyField*)object;
    GwyFieldPrivate *priv = field->priv;

    g_clear_object(&priv->unit_xy);
    g_clear_object(&priv->unit_z);
    g_free(priv->data);

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

static void
alloc_data(GwyField *field,
           gsize xres,
           gsize yres,
           gboolean nullme)
{
    gsize n = xres*yres;
    GwyFieldPrivate *priv = field->priv;

    if (field->xres == xres && field->yres == yres) {
        if (nullme)
            gwy_clear(priv->data, n);
        return;
    }

    gsize fieldn = (gsize)field->xres * (gsize)field->yres;
    if (fieldn != n) {
        g_free(priv->data);
        priv->data = (nullme ? g_new0(gdouble, n) : g_new(gdouble, n));
    }
    else if (nullme)
        gwy_clear(priv->data, n);
    field->xres = xres;
    field->yres = yres;
}

static void
copy_info(GwyField *src, GwyField *dest)
{
    dest->xreal = src->xreal;
    dest->yreal = src->yreal;
    dest->xoff = src->xoff;
    dest->yoff = src->yoff;
    gwy_field_copy_units(src, dest);
}

/**
 * gwy_field_new: (constructor)
 * @xres: X-resolution, i.e., the number of columns.
 * @yres: Y-resolution, i.e., the number of rows.
 * @xreal: Real horizontal physical dimension.
 * @yreal: Real vertical physical dimension.
 * @nullme: Whether the data field should be initialized to zeros. If %FALSE, the data will not be initialized.
 *
 * Creates a new data field.
 *
 * Returns: (transfer full):
 *          A newly created data field.
 **/
GwyField*
gwy_field_new(gint xres, gint yres,
              gdouble xreal, gdouble yreal,
              gboolean nullme)
{
    GwyField *field = g_object_new(GWY_TYPE_FIELD, NULL);

    alloc_data(field, xres, yres, nullme);
    field->xreal = xreal;
    field->yreal = yreal;

    return field;
}

/**
 * gwy_field_new_alike:
 * @model: A data field to take resolutions and units from.
 * @nullme: Whether the data field should be initialized to zeros. If %FALSE, the data will not be initialized.
 *
 * Creates a new data field similar to an existing one.
 *
 * Use gwy_field_copy() if you want to copy a data field including data.
 *
 * Returns: (transfer full):
 *          A newly created data field.
 **/
GwyField*
gwy_field_new_alike(GwyField *model,
                    gboolean nullme)
{
    GwyField *field = g_object_new(GWY_TYPE_FIELD, NULL);

    g_return_val_if_fail(GWY_IS_FIELD(model), field);

    alloc_data(field, model->xres, model->yres, nullme);
    copy_info(model, field);

    return field;
}

/**
 * gwy_field_new_resampled:
 * @field: A data field.
 * @xres: Desired X resolution.
 * @yres: Desired Y resolution.
 * @interpolation: Interpolation method to use.
 *
 * Creates a new data field by resampling an existing one.
 *
 * This method is equivalent to gwy_field_copy() followed by gwy_field_resample(), but it is more
 * efficient.
 *
 * Returns: (transfer full):
 *          A newly created data field.
 **/
GwyField*
gwy_field_new_resampled(GwyField *field,
                        gint xres, gint yres,
                        GwyInterpolationType interpolation)
{
    GwyField *result;
    gdouble z;

    g_return_val_if_fail(GWY_IS_FIELD(field), NULL);
    if (field->xres == xres && field->yres == yres)
        return gwy_field_copy(field);

    g_return_val_if_fail(xres > 0 && yres > 0, NULL);

    result = gwy_field_new(xres, yres, field->xreal, field->yreal, FALSE);
    result->xoff = field->xoff;
    result->yoff = field->yoff;
    gwy_field_copy_units(field, result);

    /* Prevent rounding errors from introducing different values in constants field during resampling. */
    if (field_is_constant(field, &z)) {
        gwy_field_fill(result, z);
        return result;
    }

    gwy_interpolation_resample_block_2d(field->xres, field->yres,
                                        field->xres, field->priv->data,
                                        result->xres, result->yres,
                                        result->xres, result->priv->data,
                                        interpolation, TRUE);

    return result;
}

/**
 * gwy_field_data_changed:
 * @field: A data field.
 *
 * Emits signal "data-changed" on a data field.
 **/
void
gwy_field_data_changed(GwyField *field)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    g_signal_emit(field, signals[SGNL_DATA_CHANGED], 0);
}

/**
 * gwy_field_copy_data:
 * @field: Source data field.
 * @target: Destination data field.
 *
 * Copies the contents of a data field to another data field of the same size.
 *
 * Only the data are copied.  To make a data field completely identical to another, including units and change of
 * dimensions, you can use gwy_field_assign().
 *
 * See also gwy_field_copy_data_mask() which copies only some values, decided by a mask.
 **/
void
gwy_field_copy_data(GwyField *field, GwyField *target)
{
    if (!_gwy_field_check_other(field, target, FALSE) || target == field)
        return;

    gint xres = field->xres, yres = field->yres;
    gsize n = (gsize)xres * (gsize)yres;
    GwyFieldPrivate *priv = field->priv, *tpriv = target->priv;
    gwy_assign(tpriv->data, priv->data, n);
    tpriv->cached = priv->cached & ~(GWY_FIELD_CACHE_ARE | GWY_FIELD_CACHE_VAR);
    gwy_assign(tpriv->cache, priv->cache, GWY_FIELD_CACHE_size);
}

/**
 * gwy_field_copy_data_mask:
 * @field: Source data field.
 * @target: Destination data field.
 * @mask: (nullable): Mask specifying which values to copy, or %NULL.
 * @masking: Masking mode to use.
 *
 * Copies some values from a data field to another data field of the same size determined by a mask.
 *
 * Included pixels (according to the masking mode) are copied from @source to @target. Excluded pixels in @target are
 * left untouched.
 *
 * Using @mask=%NULL or @masking=%GWY_MASK_INCLUDE is not particularly useful since the function then just becomes
 * gwy_field_copy_data(). It is, however, allowed for simplicity.
 **/
void
gwy_field_copy_data_mask(GwyField *field,
                         GwyField *target,
                         GwyNield *mask,
                         GwyMaskingType masking)
{
    if (!_gwy_field_check_other(field, target, FALSE)
        || !_gwy_field_check_mask(field, &mask, &masking)
        || target == field)
        return;

    gint xres = field->xres, yres = field->yres;
    gsize n = (gsize)xres * (gsize)yres;
    GwyFieldPrivate *priv = field->priv, *tpriv = target->priv;
    if (!mask) {
        gwy_assign(tpriv->data, priv->data, n);
        tpriv->cached = priv->cached & ~(GWY_FIELD_CACHE_ARE | GWY_FIELD_CACHE_VAR);
        gwy_assign(tpriv->cache, priv->cache, GWY_FIELD_CACHE_size);
        return;
    }

    const gdouble *f = field->priv->data;
    const gint *m = mask->priv->data;
    gdouble *t = target->priv->data;
    for (gsize k = 0; k < n; k++) {
        if (nielded_included(m + k, masking))
            t[k] = f[k];
    }
    gwy_field_invalidate(target);
}

/**
 * gwy_field_area_copy:
 * @src: Source data field.
 * @dest: Destination data field.
 * @col: Area upper-left column coordinate in @src.
 * @row: Area upper-left row coordinate @src.
 * @width: Area width (number of columns). You can also pass -1 for full @src widdth.
 * @height: Area height (number of rows). You can also pass -1 for full @src height.
 * @destcol: Destination column in @dest.
 * @destrow: Destination row in @dest.
 *
 * Copies a rectangular area from one data field to another.
 *
 * The area starts at (@col, @row) in @src and its dimension is @width*@height. It is copied to @dest starting from
 * (@destcol, @destrow).
 *
 * The source area has to be completely contained in @src.  No assumptions are made about destination position,
 * however. Parts of the source area sticking out the destination data field @dest are cut off.
 *
 * If @src is the same data field as @dest, the areas may not overlap.
 **/
void
gwy_field_area_copy(GwyField *src,
                    GwyField *dest,
                    gint col, gint row,
                    gint width, gint height,
                    gint destcol, gint destrow)
{
    g_return_if_fail(GWY_IS_FIELD(src));
    g_return_if_fail(GWY_IS_FIELD(dest));
    if (width == -1)
        width = src->xres;
    if (height == -1)
        height = src->yres;
    g_return_if_fail(col >= 0 && row >= 0 && width >= 0 && height >= 0
                     && col + width <= src->xres && row + height <= src->yres);

    if (destcol + width > dest->xres)
        width = dest->xres - destcol;
    if (destrow + height > dest->yres)
        height = dest->yres - destrow;
    if (destcol < 0) {
        col -= destcol;
        width += destcol;
        destcol = 0;
    }
    if (destrow < 0) {
        row -= destrow;
        height += destrow;
        destrow = 0;
    }
    if (width <= 0 || height <= 0)
        return;

    gwy_field_invalidate(dest);

    const gdouble *sdata = src->priv->data;
    gdouble *ddata = dest->priv->data;
    if (width == src->xres && width == dest->xres) {
        /* make it as fast as gwy_field_copy() whenever possible (and maybe faster, as we don't play with units */
        g_assert(col == 0 && destcol == 0);
        gwy_assign(ddata + width*destrow, sdata + width*row, width*height);
    }
    else {
        for (gint i = 0; i < height; i++)
            gwy_assign(ddata + dest->xres*(destrow + i) + destcol, sdata + src->xres*(row + i) + col, width);
    }
}

/**
 * gwy_field_resample:
 * @field: A data field to be resampled.
 * @xres: Desired X resolution.
 * @yres: Desired Y resolution.
 * @interpolation: Interpolation method to use.
 *
 * Resamples a data field using given interpolation method
 *
 * This method may invalidate raw data buffer returned by gwy_field_get_data().
 **/
void
gwy_field_resample(GwyField *field,
                   gint xres, gint yres,
                   GwyInterpolationType interpolation)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(xres > 0 && yres > 0);
    if (field->xres == xres && field->yres == yres)
        return;

    /* Prevent rounding errors from introducing different values in constants field during resampling. */
    gdouble z;
    if (field_is_constant(field, &z)) {
        alloc_data(field, xres, yres, FALSE);
        gwy_field_fill(field, z);
        return;
    }

    gwy_field_invalidate(field);
    gdouble *bdata = g_new(gdouble, xres*yres);
    gwy_interpolation_resample_block_2d(field->xres, field->yres,
                                        field->xres, field->priv->data,
                                        xres, yres, xres, bdata,
                                        interpolation, FALSE);
    g_free(field->priv->data);
    field->priv->data = bdata;
    field->xres = xres;
    field->yres = yres;
}

static gboolean
field_is_constant(GwyField *field, gdouble *z)
{
    const gdouble *d = field->priv->data;
    gint k, n = field->xres * field->yres;

    if (FCTEST(field, MIN) && FCTEST(field, MAX)) {
        gdouble min = FCVAL(field, MIN);
        gdouble max = FCVAL(field, MAX);
        if (min == max) {
            *z = min;
            set_cache_for_constant_field(field, min);
            return TRUE;
        }
    }

    /* This check normally either finishes fast (there are different values) or goes to the end of the field, but then
     * the caller should save a lot of time by knowing the field is constant-valued. */
    for (k = 0; k < n-1; k++) {
        if (d[k] != d[k+1])
            return FALSE;
    }

    *z = d[0];
    set_cache_for_constant_field(field, d[0]);
    return TRUE;
}

/**
 * gwy_field_crop:
 * @field: A data field to be resized
 * @row: Upper-left row coordinate.
 * @col: Upper-left column coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 *
 * Crops a data field to a smaller size.
 *
 * The real dimensions are updated according to the pixel size. Offsets are set to zero.
 *
 * This method may invalidate raw data buffer returned by gwy_field_get_data().
 **/
void
gwy_field_crop(GwyField *field,
               gint col, gint row,
               gint width, gint height)
{
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE))
        return;

    gint xres = field->xres, yres = field->yres;
    field->xoff = field->yoff = 0.0;
    if (col == 0 && row == 0 && width == xres && height == yres)
        return;

    gdouble *rdata = g_new(gdouble, width*height);
    gdouble *data = field->priv->data;
    for (gint i = 0; i < height; i++)
        gwy_assign(rdata + i*width, data + (i + row)*xres + col, width);
    GWY_SWAP(gdouble*, rdata, field->priv->data);
    g_free(rdata);

    field->xreal *= (gdouble)width/field->xres;
    field->yreal *= (gdouble)height/field->yres;
    field->xres = width;
    field->yres = height;

    gwy_field_invalidate(field);
}

/**
 * gwy_field_resize:
 * @field: A data field to be resized
 * @xres: Desired X resolution.
 * @yres: Desired Y resolution.
 *
 * Resizes a data field to a different size, discarding data.
 *
 * The real dimensions and offsets are unchanged. The field contents becomes undefined. The function just makes sure
 * @field has the correct pixel dimensions. See gwy_field_make_alike() for updating all properties to match another
 * field.
 **/
void
gwy_field_resize(GwyField *field,
                 gint xres, gint yres)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(xres > 0);
    g_return_if_fail(yres > 0);
    alloc_data(field, xres, yres, FALSE);
    gwy_field_invalidate(field);
}

/**
 * gwy_field_make_alike:
 * @field: A data field to be resized
 * @model: A data field to take resolutions and units from.
 *
 * Resizes a data field and updates its properties to match another data field.
 *
 * Physical dimensions, offsets and units are all updated to match @model. Since the function can resize @field, its
 * contents must be considered undefined. See gwy_field_resize() for just pixel resize and gwy_field_assign() for
 * making one field completely identical to another.
 **/
void
gwy_field_make_alike(GwyField *field,
                     GwyField *model)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    if (model == field)
        return;
    g_return_if_fail(GWY_IS_FIELD(model));

    alloc_data(field, model->xres, model->yres, FALSE);
    copy_info(model, field);
    gwy_field_invalidate(field);
}

/**
 * gwy_field_area_extract:
 * @field: A data field to be resized
 * @row: Upper-left row coordinate.
 * @col: Upper-left column coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 *
 * Extracts a rectangular part of a data field to a new data field.
 *
 * The real dimensions are computed to match the pixel size. Offsets are set to zero.
 *
 * Returns: (transfer full): The extracted area as a newly created data field.
 **/
GwyField*
gwy_field_area_extract(GwyField *field,
                       gint col, gint row,
                       gint width, gint height)
{
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE))
        return NULL;

    gint xres = field->xres, yres = field->yres;
    if (col == 0 && row == 0 && width == xres && height == yres)
        return gwy_field_copy(field);

    GwyField *result = gwy_field_new(width, height,
                                     field->xreal*width/field->xres,
                                     field->yreal*height/field->yres,
                                     FALSE);

    const gdouble *data = field->priv->data;
    gdouble *rdata = result->priv->data;
    for (gint i = 0; i < height; i++)
        gwy_assign(rdata + i*width, data + (i + row)*xres + col, width);
    gwy_field_copy_units(field, result);

    return result;
}

/**
 * gwy_field_get_dval:
 * @field: A data field
 * @x: Horizontal position in pixel units, in range [0, x-resolution].
 * @y: Vertical postition in pixel units, in range [0, y-resolution].
 * @interpolation: Interpolation method to be used.
 *
 * Gets interpolated value at arbitrary data field point indexed by pixel coordinates.
 *
 * Note pixel values are centered in pixels, so to get the same value as gwy_field_get_val(@field, @j, @i)
 * returns, it's necessary to add 0.5: gwy_field_get_dval(@field, @j+0.5, @i+0.5, @interpolation).
 *
 * See also gwy_field_get_dval_real() that does the same, but takes real coordinates.
 *
 * Returns: Interpolated value at position (@x,@y).
 **/
gdouble
gwy_field_get_dval(GwyField *field,
                   gdouble x, gdouble y,
                   GwyInterpolationType interp)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);

    gint ix, iy, ixp, iyp;
    gint floorx, floory;
    gdouble restx, resty, valxy, valpy, valxp, valpp, va, vb, vc, vd;
    gint xres = field->xres, yres = field->yres;
    gdouble *data = field->priv->data;
    gdouble intline[4];

    if (interp == GWY_INTERPOLATION_ROUND) {
        /* floor() centers pixel value */
        floorx = floor(x);
        floory = floor(y);
        ix = CLAMP(floorx, 0, xres - 1);
        iy = CLAMP(floory, 0, yres - 1);
        return data[ix + xres*iy];
    }
    if (interp == GWY_INTERPOLATION_LINEAR) {
        /* To centered pixel value */
        x -= 0.5;
        y -= 0.5;
        floorx = floor(x);
        floory = floor(y);
        restx = x - floorx;
        resty = y - floory;
        ix = CLAMP(floorx, 0, xres - 1);
        iy = CLAMP(floory, 0, yres - 1);
        ixp = CLAMP(floorx + 1, 0, xres - 1);
        iyp = CLAMP(floory + 1, 0, yres - 1);

        valxy = (1.0 - restx)*(1.0 - resty)*data[ix + xres*iy];
        valxp = (1.0 - restx)*resty*data[ix + xres*iyp];
        valpy = restx*(1.0 - resty)*data[ixp + xres*iy];
        valpp = restx*resty*data[ixp + xres*iyp];
        return valxy + valpy + valxp + valpp;
    }

    /* To centered pixel value */
    x -= 0.5;
    y -= 0.5;
    floorx = floor(x);
    floory = floor(y);
    restx = x - floorx;
    resty = y - floory;

    /* fall back to bilinear for border pixels. */
    if (floorx < 1 || floory < 1 || floorx >= xres-2 || floory >= yres-2) {
        ix = CLAMP(floorx, 0, xres - 1);
        iy = CLAMP(floory, 0, yres - 1);
        ixp = CLAMP(floorx + 1, 0, xres - 1);
        iyp = CLAMP(floory + 1, 0, yres - 1);

        valxy = (1.0 - restx)*(1.0 - resty)*data[ix + xres*iy];
        valxp = (1.0 - restx)*resty*data[ix + xres*iyp];
        valpy = restx*(1.0 - resty)*data[ixp + xres*iy];
        valpp = restx*resty*data[ixp + xres*iyp];
        return valxy + valpy + valxp + valpp;
    }

    /* interpolation in x direction */
    data += floorx-1 + xres*(floory-1);
    gwy_assign(intline, data, 4);
    va = gwy_interpolation_interpolate_equidists(restx, intline, interp);
    gwy_assign(intline, data + xres, 4);
    vb = gwy_interpolation_interpolate_equidists(restx, intline, interp);
    gwy_assign(intline, data + 2*xres, 4);
    vc = gwy_interpolation_interpolate_equidists(restx, intline, interp);
    gwy_assign(intline, data + 3*xres, 4);
    vd = gwy_interpolation_interpolate_equidists(restx, intline, interp);

    /*interpolation in y direction*/
    intline[0] = va;
    intline[1] = vb;
    intline[2] = vc;
    intline[3] = vd;
    return gwy_interpolation_interpolate_equidists(resty, intline, interp);
}

/**
 * gwy_field_get_data: (rename-to gwy_field_get_data_pointer)
 * @field: A data field
 *
 * Gets the raw data buffer of a data field.
 *
 * The returned buffer is not guaranteed to be valid through whole data field life time.  Some function may change it,
 * most notably gwy_field_resize() and gwy_field_resample().
 *
 * This function invalidates any cached information, use gwy_field_get_data_const() if you are not going to
 * change the data.
 *
 * See gwy_field_invalidate() for some discussion.
 *
 * Returns: (type gpointer):
 *          The data field as a pointer to an array of gwy_field_get_xres()*gwy_field_get_yres() #gdouble's,
 *          ordered by lines.  I.e., they are to be accessed as data[row*xres + column].
 **/
gdouble*
gwy_field_get_data(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), NULL);
    gwy_field_invalidate(field);
    return field->priv->data;
}

/**
 * gwy_field_get_data_const:
 * @field: A data field.
 *
 * Gets the raw data buffer of a data field, read-only.
 *
 * The returned buffer is not guaranteed to be valid through whole data field life time.  Some function may change it,
 * most notably gwy_field_resize() and gwy_field_resample().
 *
 * Use gwy_field_get_data() if you want to change the data.
 *
 * See gwy_field_invalidate() for some discussion.
 *
 * Returns: The data field as a pointer to an array of gwy_field_get_xres()*gwy_field_get_yres() #gdouble's,
 *          ordered by lines.  I.e., they are to be accessed as data[row*xres + column].
 **/
const gdouble*
gwy_field_get_data_const(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), NULL);
    return (const gdouble*)field->priv->data;
}

/**
 * gwy_field_get_data_with_size: (rename-to gwy_field_get_data)
 * @field: A data field
 * @out_count: (out): Number of items in the returned buffer.
 *
 * Gets the raw data buffer of a data field. Used for gobject-introspection bindings.
 *
 * The returned buffer is not guaranteed to be valid through whole data field life time.  Some function may change it,
 * most notably gwy_field_resize() and gwy_field_resample().
 *
 * This function invalidates any cached information, use gwy_field_get_data_const() if you are not going to
 * change the data.
 *
 * See gwy_field_invalidate() for some discussion.
 *
 * Returns: (transfer none) (array length=out_count):
 *          The data field as a pointer to an array of gwy_field_get_xres()*gwy_field_get_yres() #gdouble's,
 *          ordered by lines.  I.e., they are to be accessed as data[row*xres + column].
 **/
gdouble*
gwy_field_get_data_with_size(GwyField *field, gsize *out_count)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), NULL);
    gwy_field_invalidate(field);
    *out_count = (gsize)field->xres * (gsize)field->yres;
    return field->priv->data;
}

/**
 * gwy_field_set_data_with_size: (rename-to gwy_field_set_data)
 * @field: A data field
 * @data: (array length=data_count): The data to be set as a pointer to an array of @data_len #gdoubles.
 * @data_count: Number of items in @data. It has to be equal to gwy_field_get_xres()*gwy_field_get_yres(), else the function does nothing.
 *
 * Sets new values to the raw data buffer of a data field. Used for gobject-introspection bindings.
 *
 * This function invalidates any cached information. See gwy_field_invalidate() for some discussion.
 **/
void
gwy_field_set_data_with_size(GwyField *field, gdouble *data, gsize data_count)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    if ((gsize)field->xres * (gsize)field->yres == data_count) {
        gwy_field_invalidate(field);
        memcpy(field->priv->data, data, data_count*sizeof(gdouble));
    }
}

/**
 * gwy_field_get_xres:
 * @field: A data field.
 *
 * Gets X resolution (number of columns) of a data field.
 *
 * Returns: X resolution.
 **/
gint
gwy_field_get_xres(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0);
    return field->xres;
}

/**
 * gwy_field_get_yres:
 * @field: A data field.
 *
 * Gets Y resolution (number of rows) of the field.
 *
 * Returns: Y resolution.
 **/
gint
gwy_field_get_yres(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0);
    return field->yres;
}

/**
 * gwy_field_get_xreal:
 * @field: A data field.
 *
 * Gets the X real (physical) size of a data field.
 *
 * Returns: X real size value.
 **/
gdouble
gwy_field_get_xreal(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    return field->xreal;
}

/**
 * gwy_field_get_yreal:
 * @field: A data field
 *
 * Gets the Y real (physical) size of a data field.
 *
 * Returns: Y real size value.
 **/
gdouble
gwy_field_get_yreal(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    return field->yreal;
}

/**
 * gwy_field_set_xreal:
 * @field: A data field.
 * @xreal: New X real size value.
 *
 * Sets X real (physical) size value of a data field.
 **/
void
gwy_field_set_xreal(GwyField *field, gdouble xreal)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(xreal > 0.0);
    if (xreal != field->xreal) {
        field->priv->cached &= ~(FCBIT(ARE) | FCBIT(VAR));
        field->xreal = xreal;
    }
}

/**
 * gwy_field_set_yreal:
 * @field: A data field.
 * @yreal: New Y real size value.
 *
 * Sets Y real (physical) size value of a data field.
 **/
void
gwy_field_set_yreal(GwyField *field, gdouble yreal)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(yreal > 0.0);
    if (yreal != field->yreal) {
        field->priv->cached &= ~(FCBIT(ARE) | FCBIT(VAR));
        field->yreal = yreal;
    }
}

/**
 * gwy_field_get_xoffset:
 * @field: A data field.
 *
 * Gets the X offset of data field origin.
 *
 * Returns: X offset value.
 **/
gdouble
gwy_field_get_xoffset(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    return field->xoff;
}

/**
 * gwy_field_get_yoffset:
 * @field: A data field
 *
 * Gets the Y offset of data field origin.
 *
 * Returns: Y offset value.
 **/
gdouble
gwy_field_get_yoffset(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    return field->yoff;
}

/**
 * gwy_field_set_xoffset:
 * @field: A data field.
 * @xoff: New X offset value.
 *
 * Sets the X offset of a data field origin.
 *
 * Note offsets don't affect any calculation, nor functions like gwy_field_rtoj().
 **/
void
gwy_field_set_xoffset(GwyField *field, gdouble xoff)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    field->xoff = xoff;
}

/**
 * gwy_field_set_yoffset:
 * @field: A data field.
 * @yoff: New Y offset value.
 *
 * Sets the Y offset of a data field origin.
 *
 * Note offsets don't affect any calculation, nor functions like gwy_field_rtoi().
 **/
void
gwy_field_set_yoffset(GwyField *field, gdouble yoff)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    field->yoff = yoff;
}

/**
 * gwy_field_get_dx:
 * @field: A data field.
 *
 * Gets the horizontal pixel size of a data field in real units.
 *
 * The result is the same as gwy_field_get_xreal(field)/gwy_field_get_xres(field).
 *
 * Returns: Horizontal pixel size.
 **/
gdouble
gwy_field_get_dx(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    return field->xreal/field->xres;
}

/**
 * gwy_field_get_dy:
 * @field: A data field.
 *
 * Gets the vertical pixel size of a data field in real units.
 *
 * The result is the same as gwy_field_get_yreal(field)/gwy_field_get_yres(field).
 *
 * Returns: Vertical pixel size.
 **/
gdouble
gwy_field_get_dy(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    return field->yreal/field->yres;
}

/**
 * gwy_field_get_unit_xy:
 * @field: A data field.
 *
 * Returns lateral SI unit of a data field.
 *
 * The returned object can be modified to change the field lateral units.
 *
 * Returns: (transfer none):
 *          SI unit corresponding to the lateral (XY) dimensions of the data field.  Its reference count is not
 *          incremented.
 **/
GwyUnit*
gwy_field_get_unit_xy(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), NULL);

    GwyFieldPrivate *priv = field->priv;
    if (!priv->unit_xy)
        priv->unit_xy = gwy_unit_new(NULL);

    return priv->unit_xy;
}

/**
 * gwy_field_get_unit_z:
 * @field: A data field.
 *
 * Returns value SI unit of a data field.
 *
 * The returned object can be modified to change the field value units.
 *
 * Returns: (transfer none):
 *          SI unit corresponding to the "height" (Z) dimension of the data field.  Its reference count is not
 *          incremented.
 **/
GwyUnit*
gwy_field_get_unit_z(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), NULL);

    GwyFieldPrivate *priv = field->priv;
    if (!priv->unit_z)
        priv->unit_z = gwy_unit_new(NULL);

    return priv->unit_z;
}

/**
 * gwy_field_get_value_format_xy:
 * @field: A data field.
 * @style: Unit format style.
 * @format: (nullable): A SI value format to modify, or %NULL to allocate a new one.
 *
 * Finds value format good for displaying coordinates of a data field.
 *
 * Returns: The value format.  If @format is %NULL, a newly allocated format is returned, otherwise (modified) @format
 *          itself is returned.
 **/
GwyValueFormat*
gwy_field_get_value_format_xy(GwyField *field,
                              GwyUnitFormatStyle style,
                              GwyValueFormat *format)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), format);

    gdouble max, unit;

    max = MAX(field->xreal, field->yreal);
    unit = MIN(field->xreal/field->xres,
               field->yreal/field->yres);
    return gwy_unit_get_format_with_resolution(gwy_field_get_unit_xy(field), style, max, unit, format);
}

/**
 * gwy_field_get_value_format_z:
 * @field: A data field.
 * @style: Unit format style.
 * @format: (nullable): A SI value format to modify, or %NULL to allocate a new one.
 *
 * Finds value format good for displaying values of a data field.
 *
 * Returns: The value format.  If @format is %NULL, a newly allocated format is returned, otherwise (modified) @format
 *          itself is returned.
 **/
GwyValueFormat*
gwy_field_get_value_format_z(GwyField *field,
                             GwyUnitFormatStyle style,
                             GwyValueFormat *format)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), format);

    GwyUnit *siunit;
    gdouble max, min;

    gwy_field_min_max(field, &min, &max);
    gwy_field_autorange(field, &min, &max);
    if (max == min) {
        max = ABS(max);
        min = 0.0;
    }
    siunit = gwy_field_get_unit_z(field);

    return gwy_unit_get_format_with_digits(siunit, style, max - min, 3, format);
}

/**
 * gwy_field_itor:
 * @field: A data field.
 * @row: Vertical pixel coordinate.
 *
 * Transforms vertical pixel coordinate to real (physical) Y coordinate.
 *
 * That is it maps range [0..y-resolution] to range [0..real-y-size]. It is not suitable for conversion of matrix
 * indices to physical coordinates, you have to use gwy_field_itor(@field, @row + 0.5) for that.
 *
 * Returns: Real Y coordinate.
 **/
gdouble
gwy_field_itor(GwyField *field, gdouble row)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    return row * field->yreal/field->yres;
}

/**
 * gwy_field_jtor:
 * @field: A data field.
 * @col: Horizontal pixel coordinate.
 *
 * Transforms horizontal pixel coordinate to real (physical) X coordinate.
 *
 * That is it maps range [0..x-resolution] to range [0..real-x-size]. It is not suitable for conversion of matrix
 * indices to physical coordinates, you have to use gwy_field_jtor(@field, @col + 0.5) for that.
 *
 * Returns: Real X coordinate.
 **/
gdouble
gwy_field_jtor(GwyField *field, gdouble col)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    return col * field->xreal/field->xres;
}


/**
 * gwy_field_rtoi:
 * @field: A data field.
 * @realy: Real (physical) Y coordinate.
 *
 * Transforms real (physical) Y coordinate to row.
 *
 * That is it maps range [0..real-y-size] to range [0..y-resolution].
 *
 * Returns: Vertical pixel coodinate.
 **/
gdouble
gwy_field_rtoi(GwyField *field, gdouble realy)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    return realy * field->yres/field->yreal;
}


/**
 * gwy_field_rtoj:
 * @field: A data field.
 * @realx: Real (physical) X coodinate.
 *
 * Transforms real (physical) X coordinate to column.
 *
 * That is it maps range [0..real-x-size] to range [0..x-resolution].
 *
 * Returns: Horizontal pixel coordinate.
 **/
gdouble
gwy_field_rtoj(GwyField *field, gdouble realx)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    return realx * field->xres/field->xreal;
}

static inline gboolean
gwy_field_inside(GwyField *field, gint i, gint j)
{
    if (i >= 0 && j >= 0 && i < field->xres && j < field->yres)
        return TRUE;
    else
        return FALSE;
}

/**
 * gwy_field_get_val:
 * @field: A data field.
 * @col: Column index.
 * @row: Row index.
 *
 * Gets value at given position in a data field.
 *
 * Do not access data with this function inside inner loops, it's slow. Get the raw data buffer with
 * gwy_field_get_data_const() and access it directly instead.
 *
 * Returns: Value at (@col, @row).
 **/
gdouble
gwy_field_get_val(GwyField *field, gint col, gint row)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    g_return_val_if_fail(gwy_field_inside(field, col, row), 0.0);
    return field->priv->data[col + field->xres*row];
}

/**
 * gwy_field_set_val:
 * @field: A data field.
 * @col: Column index.
 * @row: Row index.
 * @value: Value to set.
 *
 * Sets value at given position in a data field.
 *
 * Do not set data with this function inside inner loops, it's slow.  Get the raw data buffer with
 * gwy_field_get_data() and write to it directly instead.
 **/
void
gwy_field_set_val(GwyField *field,
                  gint col, gint row,
                  gdouble value)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(gwy_field_inside(field, col, row));
    gwy_field_invalidate(field);
    field->priv->data[col + field->xres*row] = value;
}

/**
 * gwy_field_get_dval_real:
 * @field: A data field.
 * @x: X postion in real coordinates.
 * @y: Y postition in real coordinates.
 * @interpolation: Interpolation method to use.
 *
 * Gets interpolated value at arbitrary data field point indexed by real coordinates.
 *
 * See also gwy_field_get_dval() that does the same, but takes pixel coordinates.
 *
 * Returns: Value at position (@x,@y).
 **/
gdouble
gwy_field_get_dval_real(GwyField *field,
                        gdouble x, gdouble y,
                        GwyInterpolationType interpolation)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    return gwy_field_get_dval(field,
                              x * field->xres/field->xreal, y * field->xres/field->xreal,
                              interpolation);
}

/**
 * gwy_field_rotate:
 * @field: A data field.
 * @angle: Rotation angle (in radians).
 * @interpolation: Interpolation method to use.
 *
 * Rotates a data field by a given angle.
 *
 * This function is mostly obsolete.  See gwy_field_new_rotated() and gwy_field_new_rotated_90().
 *
 * Values that get outside of data field by the rotation are lost. Undefined values from outside of data field that
 * get inside are set to data field minimum value.
 *
 * The rotation is performed in pixel space, i.e. it can be in fact a more general affine transform in the real
 * coordinates when pixels are not square.
 **/
void
gwy_field_rotate(GwyField *field,
                 gdouble angle,
                 GwyInterpolationType interpolation)
{
    g_return_if_fail(GWY_IS_FIELD(field));

    gdouble icor, jcor, sn, cs, x, y, v;
    gdouble *coeff;
    gint xres, yres, newi, newj, oldi, oldj, i, j, ii, jj, suplen, sf, st;

    suplen = gwy_interpolation_support_size(interpolation);
    if (suplen <= 0)
        return;

    angle = gwy_canonicalize_angle(angle, TRUE, TRUE);
    if (fabs(angle) < 1e-15)
        return;
    if (fabs(angle - G_PI) < 2e-15) {
        gwy_field_flip(field, TRUE, TRUE);
        return;
    }

    if (fabs(angle - G_PI/2) < 1e-15) {
        sn = 1.0;
        cs = 0.0;
    }
    else if (fabs(angle - 3*G_PI/4) < 3e-15) {
        sn = -1.0;
        cs = 0.0;
    }
    else {
        sn = sin(angle);
        cs = cos(angle);
    }

    xres = field->xres;
    yres = field->yres;
    icor = ((yres - 1.0)*(1.0 - cs) - (xres - 1.0)*sn)/2.0;
    jcor = ((xres - 1.0)*(1.0 - cs) + (yres - 1.0)*sn)/2.0;

    coeff = g_new(gdouble, suplen*suplen);
    sf = -((suplen - 1)/2);
    st = suplen/2;

    /* FIXME: Shouldn't we implement this in terms of gwy_field_distort()? */
    gdouble val = gwy_field_min(field);
    GwyField *tmp = gwy_field_copy(field);
    gdouble *tdata = tmp->priv->data, *data = field->priv->data;
    gwy_interpolation_resolve_coeffs_2d(xres, yres, xres, tdata, interpolation);

    for (newi = 0; newi < yres; newi++) {
        for (newj = 0; newj < xres; newj++) {
            y = newi*cs + newj*sn + icor;
            x = -newi*sn + newj*cs + jcor;
            if (y > yres || x > xres || y < 0.0 || x < 0.0)
                v = val;
            else {
                oldi = (gint)floor(y);
                y -= oldi;
                oldj = (gint)floor(x);
                x -= oldj;
                for (i = sf; i <= st; i++) {
                    ii = (oldi + i + 2*st*yres) % (2*yres);
                    if (G_UNLIKELY(ii >= yres))
                        ii = 2*yres-1 - ii;
                    for (j = sf; j <= st; j++) {
                        jj = (oldj + j + 2*st*xres) % (2*xres);
                        if (G_UNLIKELY(jj >= xres))
                            jj = 2*xres-1 - jj;
                        coeff[(i - sf)*suplen + j - sf] = tdata[ii*xres + jj];
                    }
                }
                v = gwy_interpolation_interpolate_2d(x, y, suplen, coeff, interpolation);
            }
            data[newj + xres*newi] = v;
        }
    }

    g_free(coeff);
    g_object_unref(tmp);
    gwy_field_invalidate(field);
}

/**
 * gwy_field_new_rotated_90:
 * @field: A data field.
 * @clockwise: %TRUE to rotate clocwise, %FALSE to rotate anti-clockwise.
 *
 * Creates a new data field by rotating a data field by 90 degrees.
 *
 * Returns: (transfer full): A newly created data field.
 **/
GwyField*
gwy_field_new_rotated_90(GwyField *field,
                         gboolean clockwise)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), NULL);

    GwyField *result = gwy_field_new_alike(field, FALSE);
    gwy_field_transpose(field, result, FALSE);
    /* Clockwise = flip + rowinv; Anti-clockwise = flip + colinv. */
    gwy_field_flip(result, clockwise, !clockwise);
    result->xoff = field->yoff;
    result->yoff = field->xoff;

    return result;
}

static gboolean
rotate_find_out_dimensions(GwyField *dfield,
                           gdouble phi, GwyRotateResizeType resize,
                           gdouble *newxreal, gdouble *newyreal)
{
    gdouble xreal, yreal, sphi, cphi;

    xreal = dfield->xreal;
    yreal = dfield->yreal;
    if (resize == GWY_ROTATE_RESIZE_SAME_SIZE) {
        gdouble q;

        /* FIXME: This should be same area or something like that.  We really do not want to cut the ~π/2-rotated
         * field to the original rectangle for non-square fields! */
        sphi = fabs(sin(phi));
        cphi = fabs(cos(phi));
        q = sqrt(1.0 + (xreal/yreal + yreal/xreal)*cphi*sphi);
        *newxreal = (xreal*cphi + yreal*sphi)/q;
        *newyreal = (yreal*cphi + xreal*sphi)/q;
    }
    else if (resize == GWY_ROTATE_RESIZE_CUT) {
        gdouble c2phi, s2phi;

        /* Make 0 ≤ φ ≤ π. */
        phi = gwy_canonicalize_angle(phi, TRUE, FALSE);
        /* Make 0 ≤ φ ≤ π/2. */
        if (phi > 0.5*G_PI)
            phi = G_PI - phi;

        sphi = sin(phi);
        cphi = cos(phi);
        s2phi = sin(2.0*phi);
        c2phi = cos(2.0*phi);

        if (yreal <= xreal*s2phi) {
            *newxreal = 0.5*yreal/sphi;
            *newyreal = 0.5*yreal/cphi;
        }
        else if (xreal <= yreal*s2phi) {
            *newxreal = 0.5*xreal/cphi;
            *newyreal = 0.5*xreal/sphi;
        }
        else {
            *newxreal = (xreal*cphi - yreal*sphi)/c2phi;
            *newyreal = (yreal*cphi - xreal*sphi)/c2phi;
        }
    }
    else if (resize == GWY_ROTATE_RESIZE_EXPAND) {
        sphi = fabs(sin(phi));
        cphi = fabs(cos(phi));
        *newxreal = xreal*cphi + yreal*sphi;
        *newyreal = yreal*cphi + xreal*sphi;
    }
    else
        return FALSE;

    return TRUE;
}

/**
 * gwy_field_new_rotated:
 * @field: A data field.
 * @exterior_mask: Optional data field where pixels corresponding to exterior will be set to 1.  It will be resized to
 *                 match the returned field.
 * @angle: Rotation angle (in radians).
 * @interp: Interpolation type to use.
 * @resize: Controls how the result size is determined.
 *
 * Creates a new data field by rotating a data field by an atribtrary angle.
 *
 * The returned data field can have pixel corresponding to exterior in @field (unless @resize is
 * %GWY_ROTATE_RESIZE_CUT).  They are filled with a neutral value; pass @exterior_mask and replace them as you wish if
 * you need more control.
 *
 * The rotation is performed in real space, i.e. it is a more general affine transform in the pixel space for data
 * field with non-square pixels. See gwy_field_rotate() which rotates in the pixel space.
 *
 * The returned data field has always square pixels.  If you want to rotate by a multiple of %G_PI/2 while preserving
 * non-square pixels, you must use explicitly a function such as gwy_field_new_rotated_90().
 *
 * Returns: (transfer full): A newly created data field.
 **/
GwyField*
gwy_field_new_rotated(GwyField *field,
                      GwyNield *exterior_mask,
                      gdouble angle,
                      GwyInterpolationType interp,
                      GwyRotateResizeType resize)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), NULL);
    g_return_val_if_fail(!exterior_mask || GWY_IS_NIELD(exterior_mask), NULL);

    angle = gwy_canonicalize_angle(angle, TRUE, TRUE);
    gdouble newxreal, newyreal;
    if (!rotate_find_out_dimensions(field, angle, resize, &newxreal, &newyreal)) {
        g_return_val_if_reached(NULL);
    }

    gint suplen = gwy_interpolation_support_size(interp);
    g_return_val_if_fail(suplen > 0, NULL);

    gint xres = field->xres, yres = field->yres;
    GwyField *coeffield;
    if (gwy_interpolation_has_interpolating_basis(interp))
        coeffield = g_object_ref(field);
    else {
        coeffield = gwy_field_copy(field);
        gwy_interpolation_resolve_coeffs_2d(xres, yres, xres, gwy_field_get_data(coeffield), interp);
    }
    const gdouble *src = coeffield->priv->data;

    gdouble xreal = field->xreal, yreal = field->yreal;
    gdouble dx = xreal/xres, dy = yreal/yres;
    gboolean nonsquare = !(fabs(log(dx/dy)) < 1e-9);
    gdouble h = (nonsquare ? fmin(dx, dy) : sqrt(dx*dy));

    gint newxres = GWY_ROUND(newxreal/h);
    gint newyres = GWY_ROUND(newyreal/h);
    newxres = CLAMP(newxres, 1, 32768);
    newyres = CLAMP(newyres, 1, 32768);
    gdouble q = (newxreal/newxres)/(newyreal/newyres);
    if (resize == GWY_ROTATE_RESIZE_SAME_SIZE) {
        newxreal /= sqrt(q);
        newyreal *= sqrt(q);
    }
    else if (q > 1.0) {
        /* X pixel size is larger.  So reduce xreal when cutting, enlarge yreal
         * when expanding. */
        if (resize == GWY_ROTATE_RESIZE_CUT)
            xreal /= q;
        else if (resize == GWY_ROTATE_RESIZE_EXPAND)
            yreal *= q;
    }
    else if (q < 1.0) {
        /* Y pixel size is larger.  So reduce yreal when cutting, enlarge xreal
         * when expanding. */
        if (resize == GWY_ROTATE_RESIZE_CUT)
            yreal *= q;
        else if (resize == GWY_ROTATE_RESIZE_EXPAND)
            xreal /= q;
    }
    h = sqrt(newxreal/newxres * newyreal/newyres);

    gdouble cphi = cos(angle), sphi = sin(angle);
    gdouble axx = h/dx*cphi, axy = -h/dx*sphi, ayx = h/dy*sphi, ayy = h/dy*cphi;
    gdouble bx = 0.5*xres + 0.5*h/dx*(-(newxres-1)*cphi + (newyres-1)*sphi);
    gdouble by = 0.5*yres - 0.5*h/dy*((newxres-1)*sphi + (newyres-1)*cphi);

    GwyField *result = gwy_field_new(newxres, newyres, newxreal, newyreal, FALSE);
    result->xoff = field->yoff + 0.5*(yreal - newxreal);
    result->yoff = field->xoff + 0.5*(xreal - newyreal);
    gwy_field_copy_units(field, result);

    if (exterior_mask) {
        gwy_nield_resize(exterior_mask, newxres, newyres);
        g_object_ref(exterior_mask);
    }
    else
        exterior_mask = gwy_field_new_nield_alike(result);

    gdouble *dest = result->priv->data;
    gint *m = exterior_mask->priv->data;

    gint sf = -((suplen - 1)/2);
    gint st = suplen/2;

    gdouble avg = 0.0;
    gint n = 0;
#ifdef _OPENMP
#pragma omp parallel if(gwy_threads_are_enabled()) default(none) \
            reduction(+:avg,n) \
            shared(src,dest,m,xres,yres,newxres,newyres,suplen,sf,st,axx,axy,ayx,ayy,bx,by,interp)
#endif
    {
        gdouble *coeff = g_new(gdouble, suplen*suplen);
        gint newifrom = gwy_omp_chunk_start(newyres);
        gint newito = gwy_omp_chunk_end(newyres);

        for (gint newi = newifrom; newi < newito; newi++) {
            for (gint newj = 0; newj < newxres; newj++) {
                gdouble x = axx*newj + axy*newi + bx;
                gdouble y = ayx*newj + ayy*newi + by;
                gdouble v = 0.0;

                if (x >= 0.0 && y >= 0.0 && x < xres && y < yres) {
                    gint oldi = (gint)floor(y);
                    y -= oldi;
                    gint oldj = (gint)floor(x);
                    x -= oldj;
                    for (gint i = sf; i <= st; i++) {
                        gint ii = (oldi + i + 2*st*yres) % (2*yres);
                        if (G_UNLIKELY(ii >= yres))
                            ii = 2*yres-1 - ii;
                        for (gint j = sf; j <= st; j++) {
                            gint jj = (oldj + j + 2*st*xres) % (2*xres);
                            if (G_UNLIKELY(jj >= xres))
                                jj = 2*xres-1 - jj;
                            coeff[(i - sf)*suplen + j - sf] = src[ii*xres + jj];
                        }
                    }
                    v = gwy_interpolation_interpolate_2d(x, y, suplen, coeff, interp);
                    avg += v;
                    n++;
                }
                else
                    m[newxres*newi + newj] = 1;
                dest[newxres*newi + newj] = v;
            }
        }
        g_free(coeff);
    }
    g_object_unref(coeffield);

    if (n != newxres*newyres)
        gwy_field_area_fill(result, exterior_mask, GWY_MASK_INCLUDE, 0, 0, newxres, newyres, avg/n);

    gwy_nield_invalidate(exterior_mask);
    g_object_unref(exterior_mask);

    return result;
}

/**
 * gwy_field_flip:
 * @field: A data field.
 * @xflipped: %TRUE to reflect X, i.e. rows within the XY plane. The image will be left–right mirrored.
 * @yflipped: %TRUE to reflect Y, i.e. columns within the XY plane. The image will be flipped upside down.
 *
 * Reflects a data field in place.
 *
 * Since real sizes cannot go backward, flipping an axis results in the corresponding offset being reset (the real
 * dimension stays positive).
 *
 * Note that in Gwyddion 2 the arguments @xflipped and @yflipped are swapped (to much wailing and gnashing of theeth).
 **/
void
gwy_field_flip(GwyField *field,
               gboolean xflipped,
               gboolean yflipped)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gdouble *data = field->priv->data;
    gint xres = field->xres;
    gint yres = field->yres;
    gint n = xres*yres;

    if (yflipped && xflipped) {
        invert_array_in_place(data, n);
    }
    else if (xflipped) {
        for (gint i = 0; i < yres; i++)
            invert_array_in_place(data + i*xres, xres);
    }
    else if (yflipped) {
        for (gint i = 0; i < yres/2; i++) {
            gdouble *beg = data + i*xres;
            gdouble *end = data + (yres-1 - i)*xres;
            for (gint j = 0; j < xres; j++, end++, beg++)
                GWY_SWAP(gdouble, *beg, *end);
        }
    }
    else
        return;

    /* No cached value changes */
    field->priv->cached &= (FCBIT(MIN) | FCBIT(MAX) | FCBIT(SUM) | FCBIT(RMS) | FCBIT(MED) | FCBIT(ARF)
                            | FCBIT(ART) | FCBIT(ARE) | FCBIT(VAR));
}

/**
 * gwy_field_invert_value:
 * @field: A data field.
 *
 * Inverts values in a data field.
 *
 * The values are inverted about the mean value. Use gwy_field_multiply() to change the signs of all values.
 **/
void
gwy_field_invert_value(GwyField *field)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gdouble *data = field->priv->data;
    gint xres = field->xres;
    gint yres = field->yres;
    gint n = xres*yres;

    gdouble avg = gwy_field_mean(field);
    for (gint i = 0; i < n; i++)
        data[i] = 2.0 * avg - data[i];

    /* We can transform some stats */
    field->priv->cached &= (FCBIT(MIN) | FCBIT(MAX) | FCBIT(SUM) | FCBIT(RMS) | FCBIT(MED)
                            | FCBIT(ARF) | FCBIT(ART) | FCBIT(ARE) | FCBIT(VAR));
    FCVAL(field, MIN) = 2.0 * avg - FCVAL(field, MIN);
    FCVAL(field, MAX) = 2.0 * avg - FCVAL(field, MAX);
    GWY_SWAP(gdouble, FCVAL(field, MIN), FCVAL(field, MAX));
    FCVAL(field, SUM) = 2.0 * n * avg - FCVAL(field, SUM);
    /* RMS doesn't change */
    FCVAL(field, MED) = 2.0 * avg - FCVAL(field, MED);
    FCVAL(field, ARF) = 2.0 * avg - FCVAL(field, ARF);
    FCVAL(field, ART) = 2.0 * avg - FCVAL(field, ART);
    GWY_SWAP(gdouble, FCVAL(field, ARF), FCVAL(field, ART));
    /* Area doesn't change */
}

/* Block sizes are measured in destination, in source, the dims are swapped. */
static inline void
swap_block(const gdouble *sb, gdouble *db,
           guint xblocksize, guint yblocksize,
           guint dxres, guint sxres)
{
    guint i, j;

    for (i = 0; i < yblocksize; i++) {
        const gdouble *s = sb + i;
        gdouble *d = db + i*dxres;
        for (j = xblocksize; j; j--, d++, s += sxres)
            *d = *s;
    }
}

static void
transpose_to(const GwyField *source,
             guint col, guint row,
             guint width, guint height,
             GwyField *dest,
             guint destcol, guint destrow)
{
    enum { BLOCK_SIZE = 64 };

    guint dxres = dest->xres, sxres = source->xres;
    guint jmax = height/BLOCK_SIZE * BLOCK_SIZE;
    guint imax = width/BLOCK_SIZE * BLOCK_SIZE;
    const gdouble *sbase = source->priv->data + sxres*row + col;
    gdouble *dbase = dest->priv->data + dxres*destrow + destcol;
    guint ib, jb;

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(ib, jb) \
            shared(sbase,dbase,sxres,dxres,imax,jmax,height)
#endif
    for (ib = 0; ib < imax; ib += BLOCK_SIZE) {
        for (jb = 0; jb < jmax; jb += BLOCK_SIZE)
            swap_block(sbase + (jb*sxres + ib), dbase + (ib*dxres + jb), BLOCK_SIZE, BLOCK_SIZE, dxres, sxres);
        if (jmax != height)
            swap_block(sbase + (jmax*sxres + ib), dbase + (ib*dxres + jmax), height - jmax, BLOCK_SIZE, dxres, sxres);
    }
    if (imax != width) {
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(jb) \
            shared(sbase,dbase,sxres,dxres,imax,jmax,width)
#endif
        for (jb = 0; jb < jmax; jb += BLOCK_SIZE)
            swap_block(sbase + (jb*sxres + imax), dbase + (imax*dxres + jb), BLOCK_SIZE, width - imax, dxres, sxres);
        if (jmax != height) {
            swap_block(sbase + (jmax*sxres + imax), dbase + (imax*dxres + jmax),
                       height - jmax, width - imax, dxres, sxres);
        }
    }
}

/**
 * gwy_field_transpose:
 * @src: Source data field.
 * @dest: Destination data field.
 * @minor: %TRUE to mirror about the minor diagonal; %FALSE to mirror about major diagonal.
 *
 * Copies data from one data field to another with transposition.
 *
 * The destination data field is resized as necessary, its real dimensions set to transposed @src dimensions and its
 * offsets are reset.  Units are not updated.
 **/
void
gwy_field_transpose(GwyField *src, GwyField *dest,
                    gboolean minor)
{
    g_return_if_fail(GWY_IS_FIELD(src));
    g_return_if_fail(GWY_IS_FIELD(dest));

    gint xres = src->xres;
    gint yres = src->yres;
    alloc_data(dest, yres, xres, FALSE);
    transpose_to(src, 0, 0, xres, yres, dest, 0, 0);
    if (minor)
        invert_array_in_place(dest->priv->data, xres*yres);
    dest->yreal = src->xreal;
    dest->xreal = src->yreal;
    dest->xoff = dest->yoff = 0.0;
}

/**
 * gwy_field_area_transpose:
 * @src: Source data field.
 * @col: Upper-left column coordinate in @src.
 * @row: Upper-left row coordinate in @src.
 * @width: Area width (number of columns) in @src.
 * @height: Area height (number of rows) in @src.
 * @dest: Destination data field.
 * @minor: %TRUE to mirror about the minor diagonal; %FALSE to mirror about
 *         major diagonal.
 *
 * Copies data from a rectangular part of one data field to another with transposition.
 *
 * The destination data field is resized as necessary, its real dimensions set to transposed @src area dimensions and
 * its offsets are reset.  Units are not updated.
 **/
void
gwy_field_area_transpose(GwyField *src,
                         gint col, gint row, gint width, gint height,
                         GwyField *dest,
                         gboolean minor)
{
    if (!_gwy_field_check_area(src, col, row, width, height, FALSE))
        return;
    g_return_if_fail(GWY_IS_FIELD(dest));

    alloc_data(dest, height, width, FALSE);
    transpose_to(src, col, row, width, height, dest, 0, 0);
    if (minor)
        invert_array_in_place(dest->priv->data, width*height);

    dest->yreal = dest->yres * gwy_field_get_dx(src);
    dest->xreal = dest->xres * gwy_field_get_dy(src);
    dest->xoff = dest->yoff = 0.0;
}

/**
 * gwy_field_fill:
 * @field: A data field.
 * @value: Value to be entered.
 *
 * Fills a data field with given value.
 **/
void
gwy_field_fill(GwyField *field, gdouble value)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_math_fill(field->priv->data, (gsize)field->xres * (gsize)field->yres, value);
    set_cache_for_constant_field(field, value);
}

static void
set_cache_for_constant_field(GwyField *field, gdouble value)
{
    field->priv->cached = (FCBIT(MIN) | FCBIT(MAX) | FCBIT(SUM) | FCBIT(RMS) | FCBIT(MED)
                           | FCBIT(ARF) | FCBIT(ART) | FCBIT(ARE) | FCBIT(VAR) | FCBIT(MSQ));
    FCVAL(field, MIN) = value;
    FCVAL(field, MAX) = value;
    FCVAL(field, SUM) = field->xres * field->yres * value;
    FCVAL(field, RMS) = 0.0;
    FCVAL(field, MED) = value;
    FCVAL(field, ARF) = value;
    FCVAL(field, ART) = value;
    FCVAL(field, ARE) = field->xreal * field->yreal;
    FCVAL(field, VAR) = 0.0;
    FCVAL(field, MSQ) = value*value;
}

/**
 * gwy_field_area_fill:
 * @field: A data field.
 * @mask: Mask specifying which values to take into account/exclude, or %NULL.
 * @mode: Masking mode to use.  See the introduction for description of masking modes.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @value: Value to set all affected pixels to.
 *
 * Fills a masked rectangular part of a data field with given value.
 *
 * See also gwy_field_area_fill_mask() for filling the entire rectangle, but with different values depending on the
 * mask.
 **/
void
gwy_field_area_fill(GwyField *field,
                    GwyNield *mask, GwyMaskingType mode,
                    gint col, gint row, gint width, gint height,
                    gdouble value)
{
    if (!_gwy_field_check_area(field, col, row, width, height, TRUE)
        || !_gwy_field_check_mask(field, &mask, &mode))
        return;

    for (gint i = 0; i < height; i++) {
        gdouble *drow = field->priv->data + (row + i)*field->xres + col;

        if (mask) {
            const gint *mrow = mask->priv->data + (row + i)*mask->xres + col;
            for (gint j = 0; j < width; j++) {
                if (nielded_included(mrow + j, mode))
                    drow[j] = value;
            }
        }
        else
            gwy_math_fill(drow, width, value);
    }
    gwy_field_invalidate(field);
}

/**
 * gwy_field_area_fill_mask:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to fill with which value.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @unmasked: Value to set all unmasked pixels to.
 * @masked: Value to set all masked pixels to.
 *
 * Fills a rectangular part of a data field with two values depending on a mask.
 *
 * Unlike gwy_field_area_fill(), this function sets all values in the rectangle. But whether to @masked or @unmasked,
 * that depends on @mask. It is equivalent to calling gwy_field_area_fill() twice, once in %GWY_MASK_INCLUDE mode and
 * @masked fill value and once in %GWY_MASK_EXCLUDE mode and @unmasked fill value.
 *
 * The function has no masking mode because it always applies both. It is possible to pass a %NULL mask; the entire
 * rectangle is then filled with @masked.
 **/
void
gwy_field_area_fill_mask(GwyField *field,
                         GwyNield *mask,
                         gint col, gint row, gint width, gint height,
                         gdouble unmasked, gdouble masked)
{
    if (!_gwy_field_check_area(field, col, row, width, height, TRUE)
        || !_gwy_field_check_mask(field, &mask, NULL))
        return;

    if (!mask) {
        gwy_field_area_fill(field, NULL, GWY_MASK_IGNORE, col, row, width, height, masked);
        return;
    }

    for (gint i = 0; i < height; i++) {
        gdouble *drow = field->priv->data + (row + i)*field->xres + col;
        const gint *mrow = mask->priv->data + (row + i)*mask->xres + col;
        for (gint j = 0; j < width; j++)
            drow[j] = (mrow[j] > 0 ? masked : unmasked);
    }
    gwy_field_invalidate(field);
}

/**
 * gwy_field_fill_mask:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to fill with which value.
 * @unmasked: Value to set all unmasked pixels to.
 * @masked: Value to set all masked pixels to.
 *
 * Fills a data field with two values depending on a mask.
 *
 * See gwy_field_area_fill_mask() for description. This function simply sets the values in entire @field.
 **/
void
gwy_field_fill_mask(GwyField *field,
                    GwyNield *mask,
                    gdouble unmasked, gdouble masked)
{
    if (!mask) {
        gwy_field_fill(field, masked);
        return;
    }

    /* This is actually the common case, so simplify it. */
    if (masked == 1.0 && unmasked == 0.0) {
        if (_gwy_field_check_mask(field, &mask, NULL)) {
            _gwy_nield_fill_field_with_01(mask, field);
            gwy_field_invalidate(field);
        }
        return;
    }

    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_field_area_fill_mask(field, mask, 0, 0, field->xres, field->yres, unmasked, masked);
}

/**
 * gwy_field_clear:
 * @field: A data field.
 *
 * Fills a data field with zeros.
 **/
void
gwy_field_clear(GwyField *field)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_clear(field->priv->data, field->xres*field->yres);

    /* We can precompute stats */
    set_cache_for_constant_field(field, 0.0);
}

/**
 * gwy_field_area_clear:
 * @field: A data field.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 *
 * Fills a rectangular part of a data field with zeros.
 *
 * Use gwy_field_area_fill() to clear an area with masking. Clearing is only more efficient without masking.
 **/
void
gwy_field_area_clear(GwyField *field,
                     gint col, gint row, gint width, gint height)
{
    if (!_gwy_field_check_area(field, col, row, width, height, TRUE))
        return;

    gwy_field_invalidate(field);
    if (height == 1 || (col == 0 && width == field->xres)) {
        gwy_clear(field->priv->data + field->xres*row + col, width*height);
        return;
    }

    for (gint i = 0; i < height; i++) {
        gdouble *drow = field->priv->data + (row + i)*field->xres + col;
        gwy_clear(drow, width);
    }
}

/**
 * gwy_field_multiply:
 * @field: A data field.
 * @value: Value to multiply @field with.
 *
 * Multiplies all values in a data field by given value.
 **/
void
gwy_field_multiply(GwyField *field, gdouble value)
{
    gint i;
    gdouble *p;

    g_return_if_fail(GWY_IS_FIELD(field));

    p = field->priv->data;
    for (i = field->xres * field->yres; i; i--, p++)
        *p *= value;

    /* We can transform stats */
    field->priv->cached &= (FCBIT(MIN) | FCBIT(MAX) | FCBIT(SUM) | FCBIT(RMS) | FCBIT(MED)
                            | FCBIT(ARF) | FCBIT(ART) | FCBIT(MSQ));
    FCVAL(field, MIN) *= value;
    FCVAL(field, MAX) *= value;
    FCVAL(field, SUM) *= value;
    FCVAL(field, RMS) *= fabs(value);
    FCVAL(field, MED) *= value;
    FCVAL(field, ARF) *= value;
    FCVAL(field, ART) *= value;
    FCVAL(field, MSQ) *= value*value;
    if (value < 0) {
        GWY_SWAP(gdouble, FCVAL(field, MIN), FCVAL(field, MAX));
        GWY_SWAP(gdouble, FCVAL(field, ARF), FCVAL(field, ART));
    }
}

/**
 * gwy_field_area_multiply:
 * @field: A data field.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @value: Value to multiply area with.
 *
 * Multiplies values in a rectangular part of a data field by given value
 **/
void
gwy_field_area_multiply(GwyField *field,
                        gint col, gint row, gint width, gint height,
                        gdouble value)
{
    gint i, j;
    gdouble *drow;

    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(col >= 0 && row >= 0 && width >= 0 && height >= 0
                     && col + width <= field->xres && row + height <= field->yres);

    for (i = 0; i < height; i++) {
        drow = field->priv->data + (row + i)*field->xres + col;

        for (j = 0; j < width; j++)
            *(drow++) *= value;
    }
    gwy_field_invalidate(field);
}

/**
 * gwy_field_add:
 * @field: A data field.
 * @value: Value to be added to data field values.
 *
 * Adds given value to all values in a data field.
 **/
void
gwy_field_add(GwyField *field, gdouble value)
{
    gint i;
    gdouble *p;

    g_return_if_fail(GWY_IS_FIELD(field));

    p = field->priv->data;
    for (i = field->xres * field->yres; i; i--, p++)
        *p += value;

    /* We can transform stats */
    field->priv->cached &= (FCBIT(MIN) | FCBIT(MAX) | FCBIT(SUM) | FCBIT(RMS) | FCBIT(MED)
                            | FCBIT(ARF) | FCBIT(ART) | FCBIT(ARE) | FCBIT(VAR));
    FCVAL(field, MIN) += value;
    FCVAL(field, MAX) += value;
    FCVAL(field, SUM) += field->xres * field->yres * value;
    /* RMS doesn't change */
    FCVAL(field, MED) += value;
    FCVAL(field, ARF) += value;
    FCVAL(field, ART) += value;
    /* Area doesn't change */
    /* There is transformation formula for MSQ, but it can be prone to ugly cancellation errors. */
}

/**
 * gwy_field_abs:
 * @field: A data field.
 *
 * Takes absolute value of all values in a data field.
 **/
void
gwy_field_abs(GwyField *field)
{
    gint i;
    gdouble *p;

    g_return_if_fail(GWY_IS_FIELD(field));

    p = field->priv->data;
    for (i = field->xres * field->yres; i; i--, p++)
        *p = fabs(*p);

    /* The only cached stat we could transform is the maximum.  That's probably not worth the fuss. */
    gwy_field_invalidate(field);
}

/**
 * gwy_field_area_add:
 * @field: A data field.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @value: Value to be added to area values.
 *
 * Adds given value to all values in a rectangular part of a data field.
 **/
void
gwy_field_area_add(GwyField *field,
                   gint col, gint row, gint width, gint height,
                   gdouble value)
{
    gint i, j;
    gdouble *drow;

    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(col >= 0 && row >= 0 && width >= 0 && height >= 0
                     && col + width <= field->xres && row + height <= field->yres);

    for (i = 0; i < height; i++) {
        drow = field->priv->data + (row + i)*field->xres + col;

        for (j = 0; j < width; j++)
            *(drow++) += value;
    }
    gwy_field_invalidate(field);
}

/**
 * gwy_field_area_abs:
 * @field: A data field.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 *
 * Takes absolute value of values in a rectangular part of a data field.
 **/
void
gwy_field_area_abs(GwyField *field,
                   gint col, gint row, gint width, gint height)
{
    gint i, j;
    gdouble *drow;

    if (!_gwy_field_check_area(field, col, row, width, height, TRUE))
        return;

    for (i = 0; i < height; i++) {
        drow = field->priv->data + (row + i)*field->xres + col;

        for (j = 0; j < width; j++, drow++)
            *drow = fabs(*drow);
    }
    gwy_field_invalidate(field);
}

/**
 * gwy_field_get_row:
 * @field: A data field.
 * @line: A data line.  It will be resized to width ot @field.
 * @row: Row index.
 *
 * Extracts a data field row into a data line.
 **/
void
gwy_field_get_row(GwyField *field,
                  GwyLine* line,
                  gint row)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_LINE(line));
    g_return_if_fail(row >= 0 && row < field->yres);

    gwy_line_resize(line, field->xres);
    line->real = field->xreal;
    gwy_assign(line->priv->data, field->priv->data + row*field->xres, field->xres);
    gwy_field_copy_units_to_line(field, line);
}

/**
 * gwy_field_get_column:
 * @field: A data field
 * @line: A data line.  It will be resized to height of @field.
 * @col: Column index.
 *
 * Extracts a data field column into a data line.
 **/
void
gwy_field_get_column(GwyField *field,
                     GwyLine* line,
                     gint col)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_LINE(line));
    g_return_if_fail(col >= 0 && col < field->xres);

    gwy_line_resize(line, field->yres);
    line->real = field->yreal;
    const gdouble *p = field->priv->data + col;
    gdouble *l = line->priv->data;
    for (gint k = 0; k < field->yres; k++)
        l[k] = p[k*field->xres];
    gwy_field_copy_units_to_line(field, line);
}

/**
 * gwy_field_get_row_part:
 * @field: A data field.
 * @line: A data line.  It will be resized to the row part width.
 * @row: Row index.
 * @from: Start column index.
 * @to: End column index + 1.
 *
 * Extracts part of a data field row into a data line.
 **/
void
gwy_field_get_row_part(GwyField *field,
                       GwyLine *line,
                       gint row,
                       gint from,
                       gint to)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_LINE(line));
    g_return_if_fail(row >= 0 && row < field->yres);
    GWY_ORDER(gint, from, to);

    if (line->res != (to - from))
        gwy_line_resize(line, to - from);

    line->real = field->xreal*(to - from)/field->xres;
    gwy_assign(line->priv->data, field->priv->data + row*field->xres + from, to - from);
    gwy_field_copy_units_to_line(field, line);
}

/**
 * gwy_field_get_column_part:
 * @field: A data field.
 * @line: A data line.  It will be resized to the column part height.
 * @col: Column index.
 * @from: Start row index.
 * @to: End row index + 1.
 *
 * Extracts part of a data field column into a data line.
 **/
void
gwy_field_get_column_part(GwyField *field,
                          GwyLine *line,
                          gint col,
                          gint from,
                          gint to)
{
    gint k;

    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_LINE(line));
    g_return_if_fail(col >= 0 && col < field->xres);
    GWY_ORDER(gint, from, to);

    if (line->res != (to - from))
        gwy_line_resize(line, to-from);

    line->real = field->yreal*(to - from)/field->yres;
    for (k = 0; k < to - from; k++)
        line->priv->data[k] = field->priv->data[(k+from)*field->xres + col];
    gwy_field_copy_units_to_line(field, line);
}

/**
 * gwy_field_set_row_part:
 * @field: A data field.
 * @line: A data line.
 * @row: Row index.
 * @from: Start row index.
 * @to: End row index + 1.
 *
 * Puts a data line into a data field row.
 *
 * If data line length differs from @to-@from, it is resampled to this length.
 **/
void
gwy_field_set_row_part(GwyField *field,
                       GwyLine *line,
                       gint row,
                       gint from,
                       gint to)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_LINE(line));
    g_return_if_fail(row >= 0 && row < field->yres);
    GWY_ORDER(gint, from, to);

    if (line->res != (to - from))
        gwy_line_resample(line, to-from, GWY_INTERPOLATION_LINEAR);

    gwy_assign(field->priv->data + row*field->xres + from, line->priv->data, to - from);
    gwy_field_invalidate(field);
}


/**
 * gwy_field_set_column_part:
 * @field: A data field.
 * @line: A data line.
 * @col: Column index.
 * @from: Start row index.
 * @to: End row index + 1.
 *
 * Puts a data line into data field column.
 *
 * If data line length differs from @to-@from, it is resampled to this length.
 **/
void
gwy_field_set_column_part(GwyField *field,
                          GwyLine* line,
                          gint col,
                          gint from,
                          gint to)
{
    gint k;

    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_LINE(line));
    g_return_if_fail(col >= 0 && col < field->xres);
    GWY_ORDER(gint, from, to);

    if (line->res != (to-from))
        gwy_line_resample(line, to-from, GWY_INTERPOLATION_LINEAR);

    for (k = 0; k < to-from; k++)
        field->priv->data[(k+from)*field->xres + col] = line->priv->data[k];
    gwy_field_invalidate(field);
}

/**
 * gwy_field_set_row:
 * @field: A data field.
 * @line: A data line.
 * @row: Row index.
 *
 * Sets a row in the data field to values of a data line.
 *
 * Data line length must be equal to width of data field.
 **/
void
gwy_field_set_row(GwyField *field,
                  GwyLine* line,
                  gint row)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_LINE(line));
    g_return_if_fail(row >= 0 && row < field->yres);
    g_return_if_fail(field->xres == line->res);

    gwy_assign(field->priv->data + row*field->xres, line->priv->data, field->xres);
    gwy_field_invalidate(field);
}

/**
 * gwy_field_set_column:
 * @field: A data field.
 * @line: A data line.
 * @col: Column index.
 *
 * Sets a column in the data field to values of a data line.
 *
 * Data line length must be equal to height of data field.
 **/
void
gwy_field_set_column(GwyField *field,
                     GwyLine* line,
                     gint col)
{
    gint k;
    gdouble *p;

    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_LINE(line));
    g_return_if_fail(col >= 0 && col < field->xres);
    g_return_if_fail(field->yres == line->res);

    p = field->priv->data + col;
    for (k = 0; k < field->yres; k++)
        p[k*field->xres] = line->priv->data[k];
    gwy_field_invalidate(field);
}

/**
 * gwy_field_row_profile:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to fill with which value.
 * @masking: Masking mode to use.
 * @row: Row index.
 * @thickness: Thickness of line to be averaged.
 * @npoints: (out): Location for the number of points in the returned array.
 *
 * Extracts a profile from averaged rows from data field, with masking.
 *
 * If masking is used, the returned array can have fewer points than the field resolution (including no points at
 * all). You need to use @npoints to obtain its actual size.
 *
 * Returns: (transfer full) (array size=npoints): Newly allocated array with the extracted data.
 **/
GwyXY*
gwy_field_row_profile(GwyField *field,
                      GwyNield *mask,
                      GwyMaskingType masking,
                      gint row,
                      gint thickness,
                      gint *npoints)
{
    if (!_gwy_field_check_mask(field, &mask, &masking)) {
        if (npoints)
            *npoints = 0;
        return NULL;
    }

    gint xres = field->xres, yres = field->yres;
    gdouble dx = field->xreal/xres;
    const gdouble *d = field->priv->data;
    GwyXY *xy = g_new0(GwyXY, xres);

    gint ifrom = row - (thickness - 1)/2;
    ifrom = MAX(ifrom, 0);
    gint ito = row + thickness/2 + 1;
    ito = MIN(ito, yres);

    const gint *m = NULL;
    if (mask)
        m = mask->priv->data;
    else {
        for (gint j = 0; j < xres; j++)
            xy[j].x = ito - ifrom;
    }

    for (gint i = ifrom; i < ito; i++) {
        const gdouble *drow = d + i*xres;
        if (m) {
            const gint *mrow = m + i*xres;
            for (gint j = 0; j < xres; j++) {
                if (nielded_included(mrow + j, masking)) {
                    xy[j].y += drow[j];
                    xy[j].x += 1.0;
                }
            }
        }
        else {
            for (gint j = 0; j < xres; j++)
                xy[j].y += drow[j];
        }
    }

    gint n = 0;
    for (gint j = 0; j < xres; j++) {
        if (xy[j].x > 0.0) {
            xy[n].y = xy[j].y/xy[j].x;
            xy[n].x = dx*j;
            n++;
        }
    }

    if (npoints)
        *npoints = n;
    return xy;
}

/**
 * gwy_field_column_profile:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to fill with which value.
 * @masking: Masking mode to use.
 * @col: Column index.
 * @thickness: Thickness of line to be averaged.
 * @npoints: (out): Location for the number of points in the returned array.
 *
 * Extracts a profile from averaged columns from data field, with masking.
 *
 * If masking is used, the returned array can have fewer points than the field resolution (including no points at
 * all). You need to use @npoints to obtain its actual size.
 *
 * Returns: (transfer full) (array size=npoints): Newly allocated array with the extracted data.
 **/
GwyXY*
gwy_field_column_profile(GwyField *field,
                         GwyNield *mask,
                         GwyMaskingType masking,
                         gint col,
                         gint thickness,
                         gint *npoints)
{
    if (!_gwy_field_check_mask(field, &mask, &masking)) {
        if (npoints)
            *npoints = 0;
        return NULL;
    }

    gint xres = field->xres, yres = field->yres;
    gdouble dy = field->yreal/yres;
    const gdouble *d = field->priv->data;
    GwyXY *xy = g_new0(GwyXY, yres);

    gint jfrom = col - (thickness - 1)/2;
    jfrom = MAX(jfrom, 0);
    gint jto = col + thickness/2 + 1;
    jto = MIN(jto, xres);

    const gint *m = NULL;
    if (mask)
        m = mask->priv->data;

    gint n = 0;
    for (gint i = 0; i < yres; i++) {
        const gdouble *drow = d + i*xres + jfrom;
        gdouble z = 0.0;
        guint count = 0;
        if (m) {
            const gint *mrow = m + i*xres + jfrom;
            for (gint j = 0; j < jto - jfrom; j++) {
                if (nielded_included(mrow + j, masking)) {
                    z += drow[j];
                    count++;
                }
            }
        }
        else {
            count = jto - jfrom;
            for (gint j = 0; j < count; j++)
                z += drow[j];
        }

        if (count) {
            xy[n].x = dy*i;
            xy[n].y = z/count;
            n++;
        }
    }

    if (npoints)
        *npoints = 0;
    return xy;
}

/**
 * gwy_field_profile:
 * @field: A data field.
 * @line: (nullable):
 *        A data line.  It will be resized to @res samples.  It is possible to pass %NULL to instantiate and
 *        return a new #GwyLine.
 * @col1: The column the line starts at (inclusive).
 * @row1: The row the line starts at (inclusive).
 * @col2: The column the line ends at (inclusive).
 * @row2: The row the line ends at (inclusive).
 * @res: Requested resolution of data line (the number of samples to take). If nonpositive, data line resolution is
 *       chosen to match @field's.
 * @thickness: Thickness of line to be averaged.
 * @interpolation: Interpolation type to use.
 *
 * Extracts a possibly averaged profile from data field to a data line.
 *
 * Returns: (transfer full): @line itself if it was not %NULL, otherwise a newly created data line.
 **/
GwyLine*
gwy_field_profile(GwyField *field,
                  GwyLine *line,
                  gint col1, gint row1,
                  gint col2, gint row2,
                  gint res,
                  gint thickness,
                  GwyInterpolationType interpolation)
{
    gint k, j;
    gdouble cosa, sina, size, mid, sum;
    gdouble col, row, srcol, srrow;
    gint xres, yres;

    g_return_val_if_fail(GWY_IS_FIELD(field), NULL);
    g_return_val_if_fail(!line || GWY_IS_LINE(line), NULL);
    xres = field->xres;
    yres = field->yres;
    g_return_val_if_fail(col1 >= 0 && row1 >= 0 && col2 >= 0 && row2 >= 0
                         && row1 < yres && col1 < xres && row2 < yres && col2 < xres,
                         NULL);

    size = hypot(abs(col1 - col2) + 1, abs(row1 - row2) + 1);
    size = MAX(size, 1.0);
    if (res <= 0)
        res = GWY_ROUND(size);

    cosa = (col2 - col1)/(res - 1.0);
    sina = (row2 - row1)/(res - 1.0);

    /* Extract regular one-pixel line */
    if (line)
        gwy_line_resize(line, res);
    else
        line = gwy_line_new(res, 1.0, FALSE);

    GwyLinePrivate *lpriv = line->priv;
    for (k = 0; k < res; k++)
        lpriv->data[k] = gwy_field_get_dval(field, col1 + 0.5 + k*cosa, row1 + 0.5 + k*sina, interpolation);
    line->real = hypot(abs(col1 - col2)*field->xreal/xres, abs(row1 - row2)*field->yreal/yres);
    line->real *= res/(res - 1.0);
    gwy_field_copy_units_to_line(field, line);

    if (thickness <= 1)
        return line;

    /*add neighbour values to the line*/
    for (k = 0; k < res; k++) {
        mid = lpriv->data[k];
        sum = 0;
        srcol = col1 + 0.5 + k*cosa;
        srrow = row1 + 0.5 + k*sina;
        for (j = -thickness/2; j < thickness - thickness/2; j++) {
            col = srcol + j*sina;
            row = srrow - j*cosa;
            if (col >= 0 && col < (xres-1) && row >= 0 && row < (yres-1))
                sum += gwy_field_get_dval(field, col, row, interpolation);
            else
                sum += mid;
        }
        lpriv->data[k] = sum/thickness;
    }

    return line;
}

/**
 * gwy_field_profile_mask:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use.
 * @xfrom: The real @x-coordinate where the line starts.
 * @yfrom: The real @y-coordinate where line starts.
 * @xto: The real @x-coordinate where the line ends.
 * @yto: The real @y-coordinate where line ends.
 * @res: Requested resolution, i.e. the number of samples to take. If nonpositive, sampling is chosen to match
 *       @field's.
 * @thickness: Thickness of line to be averaged.
 * @interpolation: Interpolation type to use.
 * @ndata: (out): Location where to store the actual number of extracted points, which may differ from @res. You must
 *                not pass %NULL and remain ignorant of the returned array size.
 *
 * Extracts a possibly averaged profile from data field, with masking.
 *
 * The extracted profile can contain holes due to masking.  It can also contain no points at all if the all data
 * values along the profile were excluded due to masking – in this case %NULL is returned.
 *
 * Unlike gwy_field_profile(), this function takes real coordinates (without offsets), not row and column
 * indices.
 *
 * Returns: (transfer full):
 *          A newly allocated array of #GwyXY coordinare pairs, or %NULL. The caller must free the returned array with
 *          g_free().
 **/
GwyXY*
gwy_field_profile_mask(GwyField *field,
                       GwyNield *mask,
                       GwyMaskingType masking,
                       gdouble xfrom, gdouble yfrom,
                       gdouble xto, gdouble yto,
                       gint res,
                       gint thickness,
                       GwyInterpolationType interpolation,
                       gint *ndata)
{
    if (!_gwy_field_check_mask(field, &mask, &masking))
        return NULL;

    g_return_val_if_fail(ndata, NULL);

    gint xres = field->xres, yres = field->yres;
    gdouble xreal = field->xreal, yreal = field->yreal;
    gdouble dx = xreal/xres, dy = yreal/yres;
    const gint *m = mask ? mask->priv->data : NULL;

    gint size = hypot(fabs(xto - xfrom)/dx + 1, fabs(yto - yfrom)/dy + 1);
    size = MAX(size, 1.0);
    if (res <= 0)
        res = GWY_ROUND(size);

    gwy_debug("size: %g, res: %d", size, res);
    if (xto == xfrom && yto == yfrom) {
        xto += 0.2*dx;
        yto += 0.2*dy;
        xfrom -= 0.2*dx;
        yfrom -= 0.2*dy;
    }
    gdouble xstep = (xto - xfrom)/(res - 1.0);
    gdouble ystep = (yto - yfrom)/(res - 1.0);
    gdouble step = hypot(xstep, ystep);
    gwy_debug("step (%g, %g)", xstep, ystep);

    gint tres = 0;
    gdouble tx = 0.0, ty = 0.0;
    if (thickness > 1) {
        tres = 2*(thickness - 1);
        tx = (yto - yfrom)/dy;
        ty = -(xto - xfrom)/dx;
        gdouble h = hypot(tx, ty);
        tx *= dx/h * 0.5*thickness/tres;
        ty *= dy/h * 0.5*thickness/tres;
    }
    gwy_debug("tres: %d, tstep (%g, %g)", tres, tx, ty);

    GwyXY *xydata = g_new0(GwyXY, res);
    gint n = 0;
    for (gint k = 0; k < res; k++) {
        gdouble xc = xfrom + xstep*k;
        gdouble yc = yfrom + ystep*k;
        gdouble z = 0.0;
        gint w = 0;

        for (gint kk = -tres; kk <= tres; kk++) {
            gdouble x = xc + kk*tx;
            gdouble y = yc + kk*ty;

            x = CLAMP(x, 0.0, 0.999999*xreal);
            y = CLAMP(y, 0.0, 0.999999*yreal);

            if (masking != GWY_MASK_IGNORE) {
                gint i = (gint)floor(y/dy);
                gint j = (gint)floor(x/dx);
                if (!nielded_included(m + i*xres + j, masking))
                    continue;
            }

            z += gwy_field_get_dval_real(field, x, y, interpolation);
            w++;
        }
        gwy_debug("[%d] %d", k, w);
        if (w) {
            xydata[n].x = step*k;
            xydata[n].y = z/w;
            n++;
        }
    }

    *ndata = n;
    if (!n)
        GWY_FREE(xydata);

    return xydata;
}

/**
 * gwy_field_new_binned:
 * @field: A data field.
 * @binw: Bin height (in pixels).
 * @binh: Bin width (in pixels).
 * @xoff: Horizontal offset of bins (in pixels).
 * @yoff: Vertical offset of bins (in pixels).
 * @trimlowest: Number of lowest values to discard.
 * @trimhighest: Number of highest values to discard.
 *
 * Creates a new data field by binning an existing one.
 *
 * The data field is divided into rectangles of dimensions @binw×@binh, offset by (@xoff, @yoff).  The values in each
 * complete rectangle are averaged and the average becomes the pixel value in the newly created, smaller data field.
 *
 * Note that the result is the average – not sum – of the individual values. Multiply the returned data field with
 * @binw×@binh if you want sum.
 *
 * By giving non-zero @trimlowest and @trimhighest you can change the plain average to a trimmed one (even turning it
 * to median in the extreme case). It must always hold that @trimlowest + @trimhighest is smaller than @binw×@binh.
 *
 * Returns: (transfer full): A newly created data field.
 **/
GwyField*
gwy_field_new_binned(GwyField *field,
                     gint binw, gint binh,
                     gint xoff, gint yoff,
                     gint trimlowest, gint trimhighest)
{
    GwyField *result;
    gint binsize, xres, yres, newxres, newyres, i, j, k;
    gdouble xreal, yreal, xoffset, yoffset, z;
    gdouble *buf, *d, *r;

    g_return_val_if_fail(GWY_IS_FIELD(field), NULL);
    g_return_val_if_fail(binw > 0, NULL);
    g_return_val_if_fail(binh > 0, NULL);
    binsize = binw*binh;
    g_return_val_if_fail(trimlowest >= 0 && trimlowest < binsize, NULL);
    g_return_val_if_fail(trimhighest >= 0 && trimhighest < binsize - trimlowest, NULL);
    if (binsize == 1)
        return gwy_field_copy(field);

    xoff = ((xoff % binw) + binw) % binw;
    yoff = ((yoff % binh) + binh) % binh;
    xres = field->xres;
    yres = field->yres;
    xreal = field->xreal;
    yreal = field->yreal;
    xoffset = field->xoff;
    yoffset = field->yoff;

    if (xres < binw + xoff || yres < binh + yoff) {
        g_warning("No complete bin can be formed.");
        result = gwy_field_new(1, 1, xreal, yreal, FALSE);
        gwy_field_copy_units(field, result);
        result->xoff = xoffset;
        result->yoff = yoffset;
        result->priv->data[0] = gwy_field_mean(field);
        return result;
    }

    newxres = (xres - xoff)/binw;
    newyres = (yres - yoff)/binh;
    result = gwy_field_new(newxres, newyres, newxres*xreal*binw/xres, newyres*yreal*binh/yres, FALSE);
    result->xoff = xoffset + xoff*xreal/xres;
    result->yoff = yoffset + yoff*yreal/yres;
    gwy_field_copy_units(field, result);

    /* Prevent rounding errors from introducing different values in constants field during resampling. */
    if (field_is_constant(field, &z)) {
        gwy_field_fill(result, z);
        return result;
    }

    d = field->priv->data + yoff*xres + xoff;
    r = result->priv->data;
    buf = g_new(gdouble, binsize);
    for (i = 0; i < newyres; i++) {
        for (j = 0; j < newxres; j++) {
            for (k = 0; k < binh; k++) {
                gwy_assign(buf + k*binw, d + (i*binh + k)*xres + j*binw, binw);
            }
            r[i*newxres + j] = gwy_math_trimmed_mean(buf, binsize, trimlowest, trimhighest);
        }
    }
    g_free(buf);

    return result;
}

/**
 * gwy_field_bin:
 * @field: A data field.
 * @target: Target data field.  It will be resized as necessary.
 * @binw: Bin height (in pixels).
 * @binh: Bin width (in pixels).
 * @xoff: Horizontal offset of bins (in pixels).
 * @yoff: Vertical offset of bins (in pixels).
 * @trimlowest: Number of lowest values to discard.
 * @trimhighest: Number of highest values to discard.
 *
 * Bins a data field into another data field.
 *
 * See gwy_field_new_binned() for a detailed description.
 **/
void
gwy_field_bin(GwyField *field,
              GwyField *target,
              gint binw, gint binh,
              gint xoff, gint yoff,
              gint trimlowest, gint trimhighest)
{
    gint binsize, xres, yres, newxres, newyres, i, j, k;
    gdouble xreal, yreal, xoffset, yoffset, z;
    gdouble *buf, *d, *r;

    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_FIELD(target));
    g_return_if_fail(binw > 0);
    g_return_if_fail(binh > 0);
    binsize = binw*binh;
    g_return_if_fail(trimlowest >= 0 && trimlowest < binsize);
    g_return_if_fail(trimhighest >= 0 && trimhighest < binsize - trimlowest);
    if (binsize == 1) {
        gwy_field_assign(target, field);
        return;
    }

    xoff = ((xoff % binw) + binw) % binw;
    yoff = ((yoff % binh) + binh) % binh;
    xres = field->xres;
    yres = field->yres;
    xreal = field->xreal;
    yreal = field->yreal;
    xoffset = field->xoff;
    yoffset = field->yoff;

    if (xres < binw + xoff || yres < binh + yoff) {
        g_warning("No complete bin can be formed.");
        alloc_data(target, 1, 1, TRUE);
        gwy_field_copy_units(field, target);
        target->xoff = xoffset;
        target->yoff = yoffset;
        target->priv->data[0] = gwy_field_mean(field);
        return;
    }

    newxres = (xres - xoff)/binw;
    newyres = (yres - yoff)/binh;
    alloc_data(target, newxres, newyres, FALSE);
    target->xreal = newxres*xreal*binw/xres;
    target->yreal = newyres*yreal*binh/yres;
    target->xoff = xoffset + xoff*xreal/xres;
    target->yoff = yoffset + yoff*yreal/yres;
    gwy_field_copy_units(field, target);

    /* Prevent rounding errors from introducing different values in constants
     * field during resampling. */
    if (field_is_constant(field, &z)) {
        gwy_field_fill(target, z);
        return;
    }

    d = field->priv->data + yoff*xres + xoff;
    r = target->priv->data;
    buf = g_new(gdouble, binsize);
    for (i = 0; i < newyres; i++) {
        for (j = 0; j < newxres; j++) {
            for (k = 0; k < binh; k++) {
                gwy_assign(buf + k*binw, d + (i*binh + k)*xres + j*binw, binw);
            }
            r[i*newxres + j] = gwy_math_trimmed_mean(buf, binsize, trimlowest, trimhighest);
        }
    }
    g_free(buf);
    gwy_field_invalidate(target);
}

/**
 * gwy_field_xder:
 * @field: A data field.
 * @col: Column index.
 * @row: Row index.
 *
 * Computes central derivative in X direction.
 *
 * On border points, one-side derivative is returned.
 *
 * Returns: Derivative in X direction.
 **/
gdouble
gwy_field_xder(GwyField *field,
               gint col, gint row)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    g_return_val_if_fail(gwy_field_inside(field, col, row), 0.0);
    return _gwy_field_xder(field, col, row);
}

/**
 * gwy_field_yder:
 * @field: A data field.
 * @col: Column index.
 * @row: Row index.
 *
 * Computes central derivative in Y direction.
 *
 * On border points, one-side derivative is returned.
 *
 * Note the derivative is for legacy reasons calulcated for the opposite y direction than is usual elsewhere in
 * Gwyddion, i.e. if values increase with increasing row number, the returned value is negative.
 *
 * Returns: Derivative in Y direction
 **/
gdouble
gwy_field_yder(GwyField *field,
               gint col, gint row)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    g_return_val_if_fail(gwy_field_inside(field, col, row), 0.0);
    return _gwy_field_yder(field, col, row);
}

/**
 * gwy_field_angder:
 * @field: A data field.
 * @col: Column index.
 * @row: Row index.
 * @theta: Angle defining the direction (in radians, counterclockwise).
 *
 * Computes derivative in direction specified by given angle.
 *
 * Returns: Derivative in direction given by angle @theta.
 **/
gdouble
gwy_field_angder(GwyField *field,
                 gint col, gint row,
                 gdouble theta)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    g_return_val_if_fail(gwy_field_inside(field, col, row), 0.0);

    return _gwy_field_xder(field, col, row)*cos(theta)
           + _gwy_field_yder(field, col, row)*sin(theta);
}

/**
 * gwy_field_copy_units:
 * @field: A data field.
 * @target: Target data field.
 *
 * Sets lateral and value units of a data field to match another data field.
 **/
void
gwy_field_copy_units(GwyField *field,
                     GwyField *target)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_FIELD(target));

    GwyFieldPrivate *priv = field->priv, *tpriv = target->priv;
    _gwy_copy_unit(priv->unit_xy, &tpriv->unit_xy);
    _gwy_copy_unit(priv->unit_z, &tpriv->unit_z);
}

/**
 * gwy_field_copy_units_to_line:
 * @field: A data field to get units from.
 * @line: A data line to set units of.
 *
 * Sets lateral and value units of a data line to match a data field.
 **/
void
gwy_field_copy_units_to_line(GwyField *field,
                             GwyLine *line)
{
    g_return_if_fail(GWY_IS_LINE(line));
    g_return_if_fail(GWY_IS_FIELD(field));

    GwyFieldPrivate *fpriv = field->priv;
    GwyLinePrivate *lpriv = line->priv;
    _gwy_copy_unit(fpriv->unit_xy, &lpriv->unit_x);
    _gwy_copy_unit(fpriv->unit_z, &lpriv->unit_y);
}

gboolean
_gwy_field_check_area(GwyField *field,
                      gint col, gint row,
                      gint width, gint height,
                      gboolean empty_is_noop)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), FALSE);
    gint xres = field->xres, yres = field->yres;
    g_return_val_if_fail(col >= 0 && col < xres, FALSE);
    g_return_val_if_fail(row >= 0 && row < yres, FALSE);
    g_return_val_if_fail(width <= xres - col, FALSE);
    g_return_val_if_fail(height <= yres - row, FALSE);
    if (empty_is_noop) {
        g_return_val_if_fail(width >= 0, FALSE);
        g_return_val_if_fail(height >= 0, FALSE);
        /* If empty_is_noop we still return FALSE for empty areas to indicate the caller should avoid any data
         * processing, but we do not print any error. */
        return width > 0 && height > 0;
    }
    g_return_val_if_fail(width > 0, FALSE);
    g_return_val_if_fail(height > 0, FALSE);

    return TRUE;
}

gboolean
_gwy_field_check_mask(GwyField *field,
                      GwyNield **mask,
                      GwyMaskingType *masking)
{
    /* NULL @mask is a direct caller error.  We allow NULL @masking for old functions that do not have mode so masking
     * is implicitly done in INCLUDE mode. */
    g_assert(mask);
    g_return_val_if_fail(GWY_IS_FIELD(field), FALSE);
    if (!*mask) {
        if (masking)
            *masking = GWY_MASK_IGNORE;
        return TRUE;
    }
    if (masking) {
        if (*masking == GWY_MASK_IGNORE) {
            *mask = NULL;
            return TRUE;
        }
        g_return_val_if_fail(*masking == GWY_MASK_INCLUDE || *masking == GWY_MASK_EXCLUDE, FALSE);
    }
    g_return_val_if_fail(GWY_IS_NIELD(*mask), FALSE);
    g_return_val_if_fail((*mask)->xres == field->xres, FALSE);
    g_return_val_if_fail((*mask)->yres == field->yres, FALSE);
    return TRUE;
}

gboolean
_gwy_field_check_other(GwyField *field,
                       GwyField *otherfield,
                       gboolean nullok)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), FALSE);
    if (nullok && !otherfield)
        return TRUE;

    g_return_val_if_fail(GWY_IS_FIELD(otherfield), FALSE);
    g_return_val_if_fail(otherfield->xres == field->xres, FALSE);
    g_return_val_if_fail(otherfield->yres == field->yres, FALSE);
    return TRUE;
}

static void
fill_missing_points(GwyField *field, GwyField *missing)
{
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);

    gdouble *d = gwy_field_get_data(field);
    const gdouble *w = gwy_field_get_data_const(missing);

    gint nmissing = 0;
    for (gint kk = 0; kk < xres*yres; kk++) {
        if (w[kk])
            nmissing++;
    }
    if (!nmissing)
        return;

    if (nmissing == xres*yres) {
        gwy_field_clear(field);
        return;
    }

    /* This physically touches the missing data but does not change their interpretation. */
    GwyNield *mask = gwy_field_new_nield_alike(field);
    gwy_nield_mark_by_threshold(mask, missing, 0.5, TRUE);
    gwy_nield_distance_transform(mask, missing, GWY_DISTANCE_TRANSFORM_EUCLIDEAN, TRUE, FALSE);
    w = gwy_field_get_data(missing);

    MaskedPoint *mpts = g_new(MaskedPoint, nmissing);
    gint k = 0;
    for (gint kk = 0; kk < xres*yres; kk++) {
        if (w[kk]) {
            mpts[k].dist = w[kk];
            mpts[k].i = kk/xres;
            mpts[k].j = kk % xres;
            k++;
        }
    }
    qsort(mpts, nmissing, sizeof(MaskedPoint), gwy_compare_double);

    /* Go from the closest pixels to valid pixels to the the farthest. This way we can be sure we always have
     * neighbours to interpolate from. This is quick and dirty. */
    for (k = 0; k < nmissing; k++) {
        gdouble z = 0.0, dist = mpts[k].dist;
        gint i = mpts[k].i;
        gint j = mpts[k].j;
        gint kk = i*xres + j;
        gint n = 0;

        /* Cardinal. */
        if (i > 0 && w[kk - xres] < dist) {
            z += d[kk - xres];
            n++;
        }
        if (j > 0 && w[kk-1] < dist) {
            z += d[kk-1];
            n++;
        }
        if (j < xres-1 && w[kk+1] < dist) {
            z += d[kk+1];
            n++;
        }
        if (i < yres-1 && w[kk + xres] < dist) {
            z += d[kk + xres];
            n++;
        }
        z *= 2.0;
        n *= 2;

        /* Diagonal, half weight. */
        if (i > 0 && j > 0 && w[kk-1 - xres] < dist) {
            z += d[kk-1 - xres];
            n++;
        }
        if (i > 0 && j < xres-1 && w[kk+1 - xres] < dist) {
            z += d[kk+1 - xres];
            n++;
        }
        if (i < yres-1 && j > 0 && w[kk-1 + xres] < dist) {
            z += d[kk-1 + xres];
            n++;
        }
        if (i < yres-1 && j < xres-1 && w[kk+1 + xres] < dist) {
            z += d[kk+1 + xres];
            n++;
        }

        g_assert(n);
        d[kk] = z/n;
    }

    g_free(mpts);
}

static void
fill_missing_points_all(GwyField *field,
                        const GwyXYZ *xyz,
                        const GwyXY *coords,
                        const gdouble *zvalues,
                        guint npoints)
{
    g_return_if_fail(npoints);

    gdouble xc = field->xoff + 0.5*field->xreal;
    gdouble yc = field->yoff + 0.5*field->yreal;
    gdouble d2min = G_MAXDOUBLE;
    guint kmin = 0;

    for (gint k = 0; k < npoints; k++) {
        gdouble x, y;
        if (xyz) {
            x = xyz[k].x;
            y = xyz[k].y;
        }
        else {
            x = coords[k].x;
            y = coords[k].y;
        }
        x -= xc;
        y -= yc;
        gdouble d2 = x*x + y*y;
        if (d2 < d2min) {
            d2min = d2;
            kmin = k;
        }
    }

    if (xyz)
        gwy_field_fill(field, xyz[kmin].z);
    else
        gwy_field_fill(field, zvalues[kmin]);
}

static void
find_ij_range(const GwyXYZ *xyz, const GwyXY *coords, gsize npoints,
              gdouble dx, gdouble xoff,
              gdouble dy, gdouble yoff,
              gint *pjmin, gint *pjmax,
              gint *pimin, gint *pimax)
{
    gint imin = G_MAXINT, imax = G_MININT, jmin = G_MAXINT, jmax = G_MININT;

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(min:imin,jmin) reduction(max:imax,jmax) \
            shared(xyz,coords,npoints,dx,dy,xoff,yoff)
#endif
    for (gint k = 0; k < npoints; k++) {
        gdouble x, y;
        if (xyz) {
            x = xyz[k].x;
            y = xyz[k].y;
        }
        else {
            x = coords[k].x;
            y = coords[k].y;
        }
        x = (x - xoff)/dx;
        y = (y - yoff)/dy;

        gint j = (gint)floor(x);
        gint i = (gint)floor(y);

        if (j < jmin)
            jmin = j;
        if (j > jmax)
            jmax = j;

        if (i < imin)
            imin = i;
        if (i > imax)
            imax = i;
    }

    *pjmin = jmin;
    *pjmax = jmax;
    *pimin = imin;
    *pimax = imax;
}

static void
average_xyz(GwyField *field,
            GwyField *densitymap,
            const GwyXYZ *points,
            const GwyXY *coords,
            const gdouble *zvalues,
            gsize npoints)
{
    if (!npoints) {
        gwy_field_clear(field);
        if (densitymap)
            gwy_field_clear(densitymap);
        return;
    }

    gint xres = field->xres, yres = field->yres;
    gdouble xoff = field->xoff, yoff = field->yoff;
    gdouble dx = field->xreal/xres, dy = field->yreal/yres;
    g_return_if_fail(dx > 0.0);
    g_return_if_fail(dy > 0.0);
    gwy_debug("field %dx%d", xres, yres);

    gint imin = G_MAXINT, imax = G_MININT, jmin = G_MAXINT, jmax = G_MININT;
    find_ij_range(points, coords, npoints, dx, xoff, dy, yoff, &jmin, &jmax, &imin, &imax);

    /* Honour exterior if it is not too far away.  We do not want to construct useless huge data fields for zoom-in
     * scenarios. */
    gwy_debug("true extrange [%d,%d)x[%d,%d)", jmin, jmax, imin, imax);
    imin = CLAMP(imin, -(yres/4 + 16), 0);
    imax = CLAMP(imax, yres-1, yres + yres/4 + 15);
    jmin = CLAMP(jmin, -(xres/4 + 16), 0);
    jmax = CLAMP(jmax, xres-1, xres + xres/4 + 15);
    gwy_debug("extrange [%d,%d)x[%d,%d)", jmin, jmax, imin, imax);

    gint extxres = jmax+1 - jmin;
    gint extyres = imax+1 - imin;
    gwy_debug("extfield %dx%d", extxres, extyres);
    GwyField *extfield = gwy_field_new(extxres, extyres, dx*extxres, dy*extyres, TRUE);
    GwyField *extweights = gwy_field_new(extxres, extyres, dx*extxres, dy*extyres, TRUE);
    gdouble *d = extfield->priv->data;
    gdouble *w = extweights->priv->data;

    for (gint k = 0; k < npoints; k++) {
        gdouble x, y, z;
        if (points) {
            x = points[k].x;
            y = points[k].y;
            z = points[k].z;
        }
        else {
            x = coords[k].x;
            y = coords[k].y;
            z = zvalues ? zvalues[k] : 1.0;
        }
        x = (x - xoff)/dx - jmin;
        y = (y - yoff)/dy - imin;

        gint j = (gint)floor(x);
        gint i = (gint)floor(y);
        gdouble xx = x - j;
        gdouble yy = y - i;

        /* Ensure we are always working in (j,j+1) x (i,i+1) rectangle. */
        if (xx < 0.5) {
            xx += 1.0;
            j--;
        }
        xx -= 0.5;
        if (yy < 0.5) {
            yy += 1.0;
            i--;
        }
        yy -= 0.5;

        gint kk = i*extxres + j;
        gdouble ww;
        if (j >= 0 && j < extxres && i >= 0 && i < extyres) {
            ww = (1.0 - xx)*(1.0 - yy);
            d[kk] += ww*z;
            w[kk] += ww;
        }
        if (j+1 >= 0 && j+1 < extxres && i >= 0 && i < extyres) {
            ww = xx*(1.0 - yy);
            d[kk+1] += ww*z;
            w[kk+1] += ww;
        }
        if (j >= 0 && j < extxres && i+1 >= 0 && i+1 < extyres) {
            ww = (1.0 - xx)*yy;
            d[kk + extxres] += ww*z;
            w[kk + extxres] += ww;
        }
        if (j+1 >= 0 && j+1 < extxres && i+1 >= 0 && i+1 < extyres) {
            ww = xx*yy;
            d[kk + extxres+1] += ww*z;
            w[kk + extxres+1] += ww;
        }
    }

    if (densitymap)
        gwy_field_area_copy(extweights, densitymap, -jmin, -imin, xres, yres, 0, 0);

    if (points || zvalues) {
        gint ninside = 0;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
        reduction(+:ninside) \
        shared(d,w,extxres,extyres)
#endif
        for (gint k = 0; k < extyres; k++) {
            for (gint j = 0; j < extxres; j++) {
                gint kk = k*extxres + j;
                if (w[kk]) {
                    d[kk] = d[kk]/w[kk];
                    w[kk] = 0.0;
                    ninside++;
                }
                else
                    w[kk] = 1.0;
            }
        }

        gwy_debug("nfilled: %d, nmissing: %d", ninside, extxres*extyres - ninside);
        if (ninside) {
            fill_missing_points(extfield, extweights);
            gwy_field_area_copy(extfield, field, -jmin, -imin, xres, yres, 0, 0);
        }
        else {
            fill_missing_points_all(field, points, coords, zvalues, npoints);
        }
    }
    else
        gwy_field_area_copy(extweights, field, -jmin, -imin, xres, yres, 0, 0);

    g_object_unref(extfield);
    g_object_unref(extweights);
}

/**
 * gwy_field_average_xyz:
 * @field: A data field to fill with regularised XYZ data.
 * @density_map: (nullable): Optional data field to fill with XYZ point density map.  It can be %NULL.
 * @points: Array of XYZ points.  Coordinates X and Y represent positions in the plane; the Z-coordinate represents
 *          values.
 * @npoints: Number of points.
 *
 * Fills a data field with regularised XYZ data using a simple method.
 *
 * See gwy_field_average_xy_z() for discussion.
 **/
void
gwy_field_average_xyz(GwyField *field,
                      GwyField *densitymap,
                      const GwyXYZ *points,
                      gsize npoints)
{
    if (!_gwy_field_check_other(field, densitymap, TRUE))
        return;

    g_return_if_fail(!npoints || points);
    average_xyz(field, densitymap, points, NULL, NULL, npoints);
}

/**
 * gwy_field_average_xy_z:
 * @field: A data field to fill with regularised XYZ data.
 * @density_map: (nullable): Optional data field to fill with XYZ point density map.  It can be %NULL.
 * @coords: Array of @npoints XY coordinates which represent positions in the plane.
 * @values: (nullable): Array of @npoints values in each point. Pass %NULL to give each point the constant value of 1,
 *          useful when you are only interested in the density map.
 * @npoints: Number of points.
 *
 * Fills a data field with regularised XY+Z data using a simple method.
 *
 * The real dimensions and offsets of @field determine the rectangle in the XY plane that will be regularised.  The
 * regularisation method is fast but simple and there are no absolute guarantees of quality, even though the result
 * will be usually quite acceptable.
 *
 * This especially applies to reasonable views of the XYZ data.  Unreasonable views can be rendered unreasonably.  In
 * particular if the rectangle does not contain any point from @points (either due to high zoom to an empty region or
 * by just being completely off) @field will be filled entirely with the value of the closest point or something
 * similar.
 **/
void
gwy_field_average_xy_z(GwyField *field,
                       GwyField *densitymap,
                       const GwyXY *coords,
                       const gdouble *values,
                       gsize npoints)
{
    if (!_gwy_field_check_other(field, densitymap, TRUE))
        return;

    // NULL values is OK to create only a density map (instead of the normal main output).
    g_return_if_fail(!npoints || coords);
    average_xyz(field, densitymap, NULL, coords, values, npoints);
}

static void
serializable_itemize(GwySerializable *serializable, GwySerializableGroup *group)
{
    GwyField *field = GWY_FIELD(serializable);
    GwyFieldPrivate *priv = field->priv;
    gsize ndata = (gsize)field->xres * (gsize)field->yres;

    gwy_serializable_group_alloc_size(group, NUM_ITEMS);
    gwy_serializable_group_append_int32(group, serializable_items + ITEM_XRES, field->xres);
    gwy_serializable_group_append_int32(group, serializable_items + ITEM_YRES, field->yres);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_XREAL, field->xreal);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_YREAL, field->yreal);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_XOFF, field->xoff);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_YOFF, field->yoff);
    gwy_serializable_group_append_unit(group, serializable_items + ITEM_UNIT_XY, priv->unit_xy);
    gwy_serializable_group_append_unit(group, serializable_items + ITEM_UNIT_Z, priv->unit_z);
    gwy_serializable_group_append_double_array(group, serializable_items + ITEM_DATA, priv->data, ndata);
    gwy_serializable_group_itemize(group);
}

static gboolean
serializable_construct(GwySerializable *serializable, GwySerializableGroup *group, GwyErrorList **error_list)
{
    GwySerializableItem its[NUM_ITEMS], *it;
    gboolean ok = FALSE;
    gwy_assign(its, serializable_items, NUM_ITEMS);
    gwy_deserialize_filter_items(its, NUM_ITEMS, group, TYPE_NAME, error_list);

    GwyField *field = GWY_FIELD(serializable);
    GwyFieldPrivate *priv = field->priv;

    /* Botched up data dimensions is a hard fail. */
    guint xres = its[ITEM_XRES].value.v_int32;
    guint yres = its[ITEM_YRES].value.v_int32;

    it = its + ITEM_DATA;
    gsize logical_dims[2] = { xres, yres };
    if (!gwy_check_data_dimension(error_list, TYPE_NAME, 2, it->array_size, logical_dims))
        goto fail;

    field->xres = xres;
    field->yres = yres;
    g_free(priv->data);
    priv->data = it->value.v_double_array;
    it->value.v_double_array = NULL;

    /* The rest is already validated by pspec. */
    field->xreal = its[ITEM_XREAL].value.v_double;
    field->yreal = its[ITEM_YREAL].value.v_double;
    field->xoff = its[ITEM_XOFF].value.v_double;
    field->yoff = its[ITEM_YOFF].value.v_double;

    priv->unit_xy = (GwyUnit*)its[ITEM_UNIT_XY].value.v_object;
    its[ITEM_UNIT_XY].value.v_object = NULL;
    priv->unit_z = (GwyUnit*)its[ITEM_UNIT_Z].value.v_object;
    its[ITEM_UNIT_Z].value.v_object = NULL;

    ok = TRUE;

fail:
    g_free(its[ITEM_DATA].value.v_double_array);
    g_clear_object(&its[ITEM_UNIT_XY].value.v_object);
    g_clear_object(&its[ITEM_UNIT_Z].value.v_object);
    return ok;
}

static GwySerializable*
serializable_copy(GwySerializable *serializable)
{
    GwyField *field = GWY_FIELD(serializable);
    GwyField *copy = gwy_field_new_alike(field, FALSE);
    GwyFieldPrivate *priv = field->priv, *cpriv = copy->priv;
    gwy_assign(cpriv->data, priv->data, field->xres*field->yres);
    cpriv->cached = priv->cached;
    gwy_assign(cpriv->cache, priv->cache, GWY_FIELD_CACHE_size);
    return GWY_SERIALIZABLE(copy);
}

static void
serializable_assign(GwySerializable *destination, GwySerializable *source)
{
    GwyField *destfield = GWY_FIELD(destination), *srcfield = GWY_FIELD(source);
    GwyFieldPrivate *dpriv = destfield->priv, *spriv = srcfield->priv;

    alloc_data(destfield, srcfield->xres, srcfield->yres, FALSE);
    gwy_field_copy_data(srcfield, destfield);
    copy_info(srcfield, destfield);
    dpriv->cached = spriv->cached;
    gwy_assign(dpriv->cache, spriv->cache, GWY_FIELD_CACHE_size);
}

/**
 * gwy_field_copy:
 * @field: A data field to duplicate.
 *
 * Create a new data field as a copy of an existing one.
 *
 * This function is a convenience gwy_serializable_copy() wrapper.
 *
 * Returns: (transfer full):
 *          A copy of the data field.
 **/
GwyField*
gwy_field_copy(GwyField *field)
{
    /* Try to return a valid object even on utter failure. Returning NULL probably would crash something soon. */
    if (!GWY_IS_FIELD(field)) {
        g_assert(GWY_IS_FIELD(field));
        return g_object_new(GWY_TYPE_FIELD, NULL);
    }
    return GWY_FIELD(gwy_serializable_copy(GWY_SERIALIZABLE(field)));
}

/**
 * gwy_field_assign:
 * @destination: Target data field.
 * @source: Source data field.
 *
 * Makes one data field equal to another.
 *
 * This function is a convenience gwy_serializable_assign() wrapper. See gwy_field_make_alike() for updating
 * properties to match another field without copying data.
 **/
void
gwy_field_assign(GwyField *destination, GwyField *source)
{
    g_return_if_fail(GWY_IS_FIELD(destination));
    g_return_if_fail(GWY_IS_FIELD(source));
    if (destination != source)
        gwy_serializable_assign(GWY_SERIALIZABLE(destination), GWY_SERIALIZABLE(source));
}

/**
 * gwy_field_invalidate:
 * @field: A data field to invalidate.
 *
 * Invalidates cached data field stats.
 *
 * User code should rarely need this macro, as all #GwyField methods do proper invalidation when they change data,
 * as well as gwy_field_get_data() does.
 *
 * However, if you get raw data with gwy_field_get_data() and then mix direct changes to it with calls to methods
 * like gwy_field_max(), you may need to explicitely invalidate cached values to let gwy_field_get_max()
 * know it has to recompute the maximum.
 **/
void
gwy_field_invalidate(GwyField *field)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    field->priv->cached = 0;
}

/**
 * SECTION: field
 * @title: GwyField
 * @short_description: Two-dimensional floating point data
 *
 * #GwyField is a regular two-dimensional floating point data array (matrices, height field, image, …). Most of the
 * basic data handling and processing functions in Gwyddion are connected with #GwyField.
 **/

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