/*
 *  $Id: stats--sum.c 29741 2026-04-06 08:37:30Z yeti-dn $
 *  Copyright (C) 2003-2018 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 "libgwyddion/macros.h"
#include "libgwyddion/field.h"
#include "libgwyddion/stats.h"
#include "libgwyddion/grains.h"
#include "libgwyddion/linestats.h"

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

enum { GWY_FIELD_VOLUME_NMETHODS = GWY_FIELD_VOLUME_BIQUADRATIC+1 };

/* FIXME: In old Gwyddion 3 we have a similar function with include_borders option. However, it is too simplistic
 * anyway. The correct handling of the border pixels to avoid bias can be very messy. This function always includes
 * all borders (including image, not just area) and returns the number of quarters that are around the image edge. */
static gdouble
_gwy_field_sum_corners(GwyField *field,
                       GwyNield *nield,
                       GwyMaskingType masking,
                       gint col, gint row,
                       gint width, gint height,
                       GwyFieldSumCornersFunc function,
                       GwyFieldSumAllCornersFunc allfunction,
                       const gdouble *param,
                       gsize *nquart, gsize *nqedge)
{
    g_assert((nield && masking != GWY_MASK_IGNORE) || (!nield && masking == GWY_MASK_IGNORE));
    if (nquart)
        *nquart = 0;
    if (nqedge)
        *nqedge = 0;

    if (width*height == 0)
        return 0.0;

    gint xres = field->xres, yres = field->yres;
    const gdouble *data = field->priv->data;
    gdouble sum = 0.0;
    gsize nq = 0, ne = 0;

    gint above = row ? row-1 : 0, below = (row+height == yres ? row+height-1 : row+height);
    gint left = col ? col-1 : 0, right = (col+width == xres ? col+width-1 : col+width);

    if (!nield) {
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(+:sum) \
            shared(data,height,width,col,row,above,below,left,right,xres,function,allfunction,param)
#endif
        /* Include the first and last row in the parallel loop, at the expense of some messy indexing. */
        for (gint i = 0; i <= height; i++) {
            if (i == 0) {
                const gdouble *r1 = data + above*xres, *r2 = data + (i + row)*xres;
                sum += function(r1[left], r1[col], r2[col], r2[left], 0, 0, 1, 0, param);
                for (gint j = col; j < col + width-1; j++)
                    sum += function(r1[j], r1[j+1], r2[j+1], r2[j], 0, 0, 1, 1, param);
                sum += function(r1[col+width-1], r1[right], r2[right], r2[col+width-1], 0, 0, 0, 1, param);
            }
            else if (i == height) {
                const gdouble *r1 = data + (i-1 + row)*xres, *r2 = data + below*xres;
                sum += function(r1[left], r1[col], r2[col], r2[left], 0, 1, 0, 0, param);
                for (gint j = col; j < col + width-1; j++)
                    sum += function(r1[j], r1[j+1], r2[j+1], r2[j], 1, 1, 0, 0, param);
                sum += function(r1[col+width-1], r1[right], r2[right], r2[col+width-1], 1, 0, 0, 0, param);
            }
            else {
                const gdouble *r2 = data + (i + row)*xres, *r1 = r2 - xres;
                sum += function(r1[left], r1[col], r2[col], r2[left], 0, 1, 1, 0, param);
                if (allfunction) {
                    for (gint j = col; j < col + width-1; j++)
                        sum += allfunction(r1[j], r1[j+1], r2[j+1], r2[j], param);
                }
                else {
                    for (gint j = col; j < col + width-1; j++)
                        sum += function(r1[j], r1[j+1], r2[j+1], r2[j], 1, 1, 1, 1, param);
                }
                sum += function(r1[col+width-1], r1[right], r2[right], r2[col+width-1], 1, 0, 0, 1, param);
            }
        }
        nq = 4*width*height;
        ne = 2*width*((row == 0) + (row+height == yres)) + 2*height*((col == 0) + (col+width == xres));
        // Now we have corners counted twice. Correct.
        ne -= (((row == 0) && (col == 0)) + ((row == 0) && (col+width == xres))
               + ((row+height == yres) && (col == 0)) + ((row+height == yres) && (col+width == xres)));
    }
    else {
        const gint *mask = nield->priv->data;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(+:sum,nq,ne) \
            shared(data,mask,masking,height,width,col,row,above,below,left,right,xres,yres,function,allfunction,param)
#endif
        /* Include the first and last row in the parallel loop, at the expense of some messy indexing. */
        for (gint i = 0; i <= height; i++) {
            if (i == 0) {
                const gdouble *r1 = data + above*xres, *r2 = data + (i + row)*xres;
                const gint *m2 = mask + (i + row)*xres;
                gint firstrow = (row == 0);
                if (nielded_included(m2 + col, masking)) {
                    sum += function(r1[left], r1[col], r2[col], r2[left], 0, 0, 1, 0, param);
                    nq++;
                    ne += firstrow;
                }
                for (gint j = col; j < col + width-1; j++) {
                    guint w3 = nielded_included(m2 + j+1, masking);
                    guint w4 = nielded_included(m2 + j, masking);
                    if (w3 + w4) {
                        sum += function(r1[j], r1[j+1], r2[j+1], r2[j], 0, 0, w3, w4, param);
                        nq += w3 + w4;
                        ne += (w3 + w4)*firstrow;
                    }
                }
                if (nielded_included(m2 + col+width-1, masking)) {
                    sum += function(r1[col+width-1], r1[right], r2[right], r2[col+width-1], 0, 0, 0, 1, param);
                    nq++;
                    ne += firstrow;
                }
            }
            else if (i == height) {
                const gdouble *r1 = data + (i-1 + row)*xres, *r2 = data + below*xres;
                const gint *m1 = mask + (i-1 + row)*xres;
                gint lastrow = (row+height == yres);
                if (nielded_included(m1 + col, masking)) {
                    sum += function(r1[left], r1[col], r2[col], r2[left], 0, 1, 0, 0, param);
                    nq++;
                    ne += lastrow;
                }
                for (gint j = col; j < col + width-1; j++) {
                    guint w1 = nielded_included(m1 + j, masking);
                    guint w2 = nielded_included(m1 + j+1, masking);
                    if (w1 + w2) {
                        sum += function(r1[j], r1[j+1], r2[j+1], r2[j], w1, w2, 0, 0, param);
                        nq += w1 + w2;
                        ne += (w1 + w2)*lastrow;
                    }
                }
                if (nielded_included(m1 + col+width-1, masking)) {
                    sum += function(r1[col+width-1], r1[right], r2[right], r2[col+width-1], 1, 0, 0, 0, param);
                    nq++;
                    ne += lastrow;
                }
            }
            else {
                const gdouble *r2 = data + (i + row)*xres, *r1 = r2 - xres;
                const gint *m2 = mask + (i + row)*xres, *m1 = m2 - xres;
                {
                    guint w2 = nielded_included(m1 + col, masking);
                    guint w3 = nielded_included(m2 + col, masking);
                    if (w2 + w3) {
                        sum += function(r1[left], r1[col], r2[col], r2[left], 0, w2, w3, 0, param);
                        nq += w2 + w3;
                        ne += (w2 + w3)*(col == 0);
                    }
                }
                /* These never contribute to ne. */
                for (gint j = col; j < col + width-1; j++) {
                    guint w1 = nielded_included(m1 + j, masking);
                    guint w2 = nielded_included(m1 + j+1, masking);
                    guint w3 = nielded_included(m2 + j+1, masking);
                    guint w4 = nielded_included(m2 + j, masking);
                    guint w = w1 + w2 + w3 + w4;
                    if (w == 4 && allfunction) {
                        sum += allfunction(r1[j], r1[j+1], r2[j+1], r2[j], param);
                        nq += 4;
                    }
                    else if (w) {
                        sum += function(r1[j], r1[j+1], r2[j+1], r2[j], w1, w2, w3, w4, param);
                        nq += w;
                    }
                }
                {
                    guint w1 = nielded_included(m1 + col+width-1, masking);
                    guint w4 = nielded_included(m2 + col+width-1, masking);
                    if (w1 + w4) {
                        sum += function(r1[col+width-1], r1[right], r2[right], r2[col+width-1], w1, 0, 0, w4, param);
                        nq += w1 + w4;
                        ne += (w1 + w4)*(col+width == xres);
                    }
                }
            }
        }
    }

    if (nquart)
        *nquart = nq;
    if (nqedge)
        *nqedge = ne;

    return sum;
}

/**
 * square_area_corner:
 * @z1: Z-value in first corner.
 * @z2: Z-value in second corner.
 * @z3: Z-value in third corner.
 * @z4: Z-value in fourth corner.
 * @w1: Weight of first corner (0 or 1).
 * @w2: Weight of second corner (0 or 1).
 * @w3: Weight of third corner (0 or 1).
 * @w4: Weight of fourth corner (0 or 1).
 * @param: Calculation parameters.
 *
 * Calculates approximate area of a one square pixel with some corners possibly missing.
 *
 * Array @param has a single item: the area of one pixel dx*dy.
 *
 * The returned area is measured in quarter-pixels (fourth of rectangle projected area). Multiply by q/4 for area in
 * physical units.
 *
 * Returns: The area, in quarter-pixels.
 **/
static gdouble
square_area_corner(gdouble z1, gdouble z2, gdouble z3, gdouble z4,
                   guint w1, guint w2, guint w3, guint w4,
                   const gdouble *param)
{
    gdouble dxdy = param[0];
    gdouble c = (z1 + z2 + z3 + z4)/4.0;
    z1 -= c;
    z2 -= c;
    z3 -= c;
    z4 -= c;

    return ((w1 + w2)*sqrt(1.0 + 2.0*(z1*z1 + z2*z2)/dxdy)
            + (w2 + w3)*sqrt(1.0 + 2.0*(z2*z2 + z3*z3)/dxdy)
            + (w3 + w4)*sqrt(1.0 + 2.0*(z3*z3 + z4*z4)/dxdy)
            + (w4 + w1)*sqrt(1.0 + 2.0*(z4*z4 + z1*z1)/dxdy))/2.0;
}

/**
 * rectangular_area_corner:
 * @z1: Z-value in first corner.
 * @z2: Z-value in second corner.
 * @z3: Z-value in third corner.
 * @z4: Z-value in fourth corner.
 * @w1: Weight of first corner (0 or 1).
 * @w2: Weight of second corner (0 or 1).
 * @w3: Weight of third corner (0 or 1).
 * @w4: Weight of fourth corner (0 or 1).
 * @param: Calculation parameters.
 *
 * Calculates approximate area of a one rectangular pixel with some corners possibly missing.
 *
 * Array @param has two items: squared x and y pixel sides dx² and dy².
 *
 * The returned area is measured in quarter-pixels (fourth of rectangle projected area). Multiply by q/4 for area in
 * physical units.
 *
 * Returns: The area, in quarter-pixels.
 **/
static gdouble
rectangular_area_corner(gdouble z1, gdouble z2, gdouble z3, gdouble z4,
                        guint w1, guint w2, guint w3, guint w4,
                        const gdouble *param)
{
    gdouble dx2 = param[0], dy2 = param[1];
    gdouble c = (z1 + z2 + z3 + z4)/2.0;

    return ((w1 + w2)*sqrt(1.0 + (z1 - z2)*(z1 - z2)/dx2 + (z1 + z2 - c)*(z1 + z2 - c)/dy2)
            + (w2 + w3)*sqrt(1.0 + (z2 - z3)*(z2 - z3)/dy2 + (z2 + z3 - c)*(z2 + z3 - c)/dx2)
            + (w3 + w4)*sqrt(1.0 + (z3 - z4)*(z3 - z4)/dx2 + (z3 + z4 - c)*(z3 + z4 - c)/dy2)
            + (w4 + w1)*sqrt(1.0 + (z1 - z4)*(z1 - z4)/dy2 + (z1 + z4 - c)*(z1 + z4 - c)/dx2))/2.0;
}

static gdouble
calculate_surface_area_NIELD(GwyField *field,
                             GwyNield *mask,
                             GwyMaskingType masking,
                             gint col, gint row,
                             gint width, gint height)
{
    gdouble dx = field->xreal/field->xres, dy = field->yreal/field->yres;
    gdouble sarea = 0.0;
    if (fabs(log(dx/dy)) < 1e-7) {
        gdouble param[1] = { dx*dy };
        sarea = _gwy_field_sum_corners(field, mask, masking, col, row, width, height,
                                       square_area_corner, NULL, param, NULL, NULL);
    }
    else {
        gdouble param[2] = { dx*dx, dy*dy };
        sarea = _gwy_field_sum_corners(field, mask, masking, col, row, width, height,
                                       rectangular_area_corner, NULL, param, NULL, NULL);
    }

    return sarea*dx*dy/4.0;
}

/**
 * gwy_field_surface_area:
 * @field: A data field.
 *
 * Computes surface area of a data field.
 *
 * This quantity is cached.
 *
 * Returns: The surface area.
 **/
gdouble
gwy_field_surface_area(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);

    gwy_debug("%s", FCTEST(field, ARE) ? "cache" : "lame");
    if (!FCTEST(field, ARE)) {
        FCVAL(field, ARE) = calculate_surface_area_NIELD(field, NULL, GWY_MASK_IGNORE, 0, 0, field->xres, field->yres);
        field->priv->cached |= FCBIT(ARE);
    }

    return FCVAL(field, ARE);
}

/**
 * gwy_field_area_surface_area:
 * @field: A data field.
 * @mask: Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: 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).
 *
 * Computes surface area of a rectangular part of a data field.
 *
 * This quantity makes sense only if the lateral dimensions and values of @field are the same physical
 * quantities.
 *
 * Returns: The surface area.
 **/
gdouble
gwy_field_area_surface_area(GwyField *field,
                            GwyNield *mask,
                            GwyMaskingType masking,
                            gint col, gint row,
                            gint width, gint height)
{
    gdouble area = 0.0;

    if (!_gwy_field_check_area(field, col, row, width, height, FALSE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return area;

    /* The result is the same, but it can be cached. */
    if (!mask && row == 0 && col == 0 && width == field->xres && height == field->yres)
        return gwy_field_surface_area(field);

    return calculate_surface_area_NIELD(field, mask, masking, col, row, width, height);
}

static gdouble
calculate_rms_slope(GwyField *field,
                    GwyNield *mask,
                    GwyMaskingType masking,
                    gint col, gint row,
                    gint width, gint height)
{
    if (!width || !height)
        return 0.0;

    gint xres = field->xres;
    gdouble dx = gwy_field_get_dx(field);
    gdouble dy = gwy_field_get_dy(field);
    const gdouble *datapos = field->priv->data + xres*row + col;
    gdouble sumv = 0.0, sumh = 0.0;
    gsize n = 0;

    /* The gradient has two contributions, from horizontal derivatives and from vertical derivatives.  We could
     * require both to be calculable for masked areas, but we do not.  Each gradient is calculate separately for each
     * pixel where it is defined, regardless whether the orthogonal one can be calculated.  This can give uneven
     * contributions for some odd mask shapes… */
    if (mask && masking != GWY_MASK_IGNORE) {
        const gint *maskpos = mask->priv->data + xres*row + col;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(+:sumh,sumv,n) \
            shared(datapos,maskpos,xres,height,width,masking)
#endif
        for (gint i = 0; i < height-1; i++) {
            const gdouble *r = datapos + xres*i;
            const gint *m = maskpos + xres*i;
            /* Inside. */
            for (gint j = 0; j < width-1; j++) {
                gint w0 = nielded_included(m + j, masking);
                gint wh = nielded_included(m + j+1, masking);
                gint wv = nielded_included(m + xres+j, masking);
                gdouble dzh = r[j+1] - r[j];
                gdouble dzv = r[xres+j] - r[j];
                sumh += w0*wh*dzh*dzh;
                n += w0*wh;
                sumv += w0*wv*dzv*dzv;
                n += w0*wv;
            }

            /* Last-column vertical gradient. */
            gint j = width-1;
            gint w0 = nielded_included(m + j, masking);
            gint wv = nielded_included(m + xres+j, masking);
            gdouble dzv = r[xres+j] - r[j];
            sumv += w0*wv*dzv*dzv;
            n += w0*wv;
        }
        /* Last-row horizontal gradient. */
        gint i = height-1;
        const gdouble *r = datapos + xres*i;
        const gint *m = maskpos + xres*i;
        for (gint j = 0; j < width-1; j++) {
            gint w0 = nielded_included(m + j, masking);
            gint wh = nielded_included(m + j+1, masking);
            gdouble dzh = r[j+1] - r[j];
            sumh += w0*wh*dzh*dzh;
            n += w0*wh;
        }

        if (!n)
            return 0.0;
    }
    else {
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(+:sumh,sumv) \
            shared(datapos,xres,width,height)
#endif
        for (gint i = 0; i < height-1; i++) {
            const gdouble *r = datapos + xres*i;
            /* Inside. */
            for (gint j = 0; j < width-1; j++) {
                gdouble dzh = r[j+1] - r[j];
                sumh += dzh*dzh;
                gdouble dzv = r[xres+j] - r[j];
                sumv += dzv*dzv;
            }
            /* Last-column vertical gradient. */
            gint j = width-1;
            gdouble dzv = r[xres+j] - r[j];
            sumv += dzv*dzv;
        }
        /* Last-row horizontal gradient. */
        gint i = height-1;
        const gdouble *r = datapos + xres*i;
        for (gint j = 0; j < width-1; j++) {
            gdouble dzh = r[j+1] - r[j];
            sumh += dzh*dzh;
        }
        n = (width - 1)*height + width*(height - 1);
    }

    return sqrt(2.0*(sumh/(dx*dx) + sumv/(dy*dy))/n);
}

/**
 * gwy_field_area_rms_slope:
 * @field: A data field.
 * @mask: Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: 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).
 *
 * Computes root mean square surface slope (Sdq) of a rectangular part of a data field.
 *
 * Returns: The root mean square surface slope.
 **/
gdouble
gwy_field_area_rms_slope(GwyField *field,
                         GwyNield *mask,
                         GwyMaskingType masking,
                         gint col, gint row,
                         gint width, gint height)
{
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return 0.0;

    return calculate_rms_slope(field, mask, masking, col, row, width, height);
}

/**
 * gwy_field_rms_slope:
 * @field: A data field.
 *
 * Computes root mean square surface slope (Sdq) of a data field.
 *
 * Returns: The root mean square surface slope.
 **/
gdouble
gwy_field_rms_slope(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);

    return calculate_rms_slope(field, NULL, GWY_MASK_IGNORE, 0, 0, field->xres, field->yres);
}

/**
 * square_variation_corner:
 * @z1: Z-value in first corner.
 * @z2: Z-value in second corner.
 * @z3: Z-value in third corner.
 * @z4: Z-value in fourth corner.
 * @w1: Weight of first corner (0 or 1).
 * @w2: Weight of second corner (0 or 1).
 * @w3: Weight of third corner (0 or 1).
 * @w4: Weight of fourth corner (0 or 1).
 * @param: Calculation parameters.
 *
 * Calculates approximate variation of a one square pixel with some corners possibly missing.
 *
 * Array @param has a single item: the area of one pixel dx*dy.
 *
 * Returns: The variation.
 **/
static gdouble
square_variation_corner(gdouble z1, gdouble z2, gdouble z3, gdouble z4,
                        guint w1, guint w2, guint w3, guint w4,
                        const gdouble *param)
{
    gdouble dxdy = param[0];
    gdouble z12 = z1 - z2, z23 = z2 - z3, z34 = z3 - z4, z41 = z4 - z1;

    return (w1*sqrt((z12*z12 + z41*z41)/dxdy) + w2*sqrt((z23*z23 + z12*z12)/dxdy)
            + w3*sqrt((z34*z34 + z23*z23)/dxdy) + w4*sqrt((z41*z41 + z34*z34)/dxdy));
}

/**
 * rectangular_variation_corner:
 * @z1: Z-value in first corner.
 * @z2: Z-value in second corner.
 * @z3: Z-value in third corner.
 * @z4: Z-value in fourth corner.
 * @w1: Weight of first corner (0 or 1).
 * @w2: Weight of second corner (0 or 1).
 * @w3: Weight of third corner (0 or 1).
 * @w4: Weight of fourth corner (0 or 1).
 * @param: Calculation parameters.
 *
 * Calculates approximate variation of a one general rectangular pixel with some corners possibly missing.
 *
 * Array @param has two items: squared x and y pixel sides dx² and dy².
 *
 * Returns: The variation.
 **/
static gdouble
rectangular_variation_corner(gdouble z1, gdouble z2, gdouble z3, gdouble z4,
                             guint w1, guint w2, guint w3, guint w4,
                             const gdouble *param)
{
    gdouble dx2 = param[0], dy2 = param[1];
    gdouble z12 = z1 - z2, z23 = z2 - z3, z34 = z3 - z4, z41 = z4 - z1;

    return (w1*sqrt(z12*z12/dx2 + z41*z41/dy2) + w2*sqrt(z23*z23/dy2 + z12*z12/dx2)
            + w3*sqrt(z34*z34/dx2 + z23*z23/dy2) + w4*sqrt(z41*z41/dy2 + z34*z34/dx2));
}

static gdouble
calculate_variation_NIELD(GwyField *field,
                          GwyNield *mask,
                          GwyMaskingType masking,
                          gint col, gint row,
                          gint width, gint height)
{
    gdouble dx = field->xreal/field->xres, dy = field->yreal/field->yres;
    gdouble var = 0.0;
    if (fabs(log(dx/dy)) < 1e-7) {
        gdouble param[1] = { dx*dy };
        var = _gwy_field_sum_corners(field, mask, masking, col, row, width, height,
                                     square_variation_corner, NULL, param, NULL, NULL);
    }
    else {
        gdouble param[2] = { dx*dx, dy*dy };
        var = _gwy_field_sum_corners(field, mask, masking, col, row, width, height,
                                     rectangular_variation_corner, NULL, param, NULL, NULL);
    }

    return var*dx*dy/4.0;
}

/**
 * gwy_field_variation:
 * @field: A data field.
 *
 * Computes the total variation of a data field.
 *
 * See gwy_field_area_get_variation() for the definition.
 *
 * This quantity is cached.
 *
 * Returns: The variation.
 **/
gdouble
gwy_field_variation(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);

    gwy_debug("%s", FCTEST(field, VAR) ? "cache" : "lame");
    if (!FCTEST(field, VAR)) {
        FCVAL(field, VAR) = calculate_variation_NIELD(field, NULL, GWY_MASK_IGNORE, 0, 0, field->xres, field->yres);
        field->priv->cached |= FCBIT(VAR);
    }

    return FCVAL(field, VAR);
}

/**
 * gwy_field_area_variation:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: 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).
 *
 * Computes the total variation of a rectangular part of a data field.
 *
 * The total variation is estimated as the integral of the absolute value of local gradient.
 *
 * This quantity has the somewhat odd units of value unit times lateral unit. It can be envisioned as follows.  If the
 * surface has just two height levels (upper and lower planes) then the quantity is the length of the boundary between
 * the upper and lower part, multiplied by the step height.  If the surface is piece-wise constant, then the variation
 * is the step height integrated along the boundaries between the constant parts.  Therefore, for non-fractal surfaces
 * it scales with the linear dimension of the image, not with its area, despite being an area integral.
 *
 * Returns: The variation.
 **/
gdouble
gwy_field_area_variation(GwyField *field,
                         GwyNield *mask,
                         GwyMaskingType masking,
                         gint col, gint row,
                         gint width, gint height)
{
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return 0.0;

    /* The result is the same, but it can be cached. */
    if (!mask && row == 0 && col == 0 && width == field->xres && height == field->yres)
        return gwy_field_variation(field);

    return calculate_variation_NIELD(field, mask, masking, col, row, width, height);
}

/**
 * square_volume:
 * @z1: Z-value in first corner.
 * @z2: Z-value in second corner.
 * @z3: Z-value in third corner.
 * @z4: Z-value in fourth corner.
 *
 * Calculates approximate volume of a one square pixel.
 *
 * Returns: The volume.
 **/
static inline gdouble
square_volume(gdouble z1, gdouble z2, gdouble z3, gdouble z4)
{
    gdouble c;

    c = (z1 + z2 + z3 + z4)/4.0;

    return c;
}

/**
 * square_volumew:
 * @z1: Z-value in first corner.
 * @z2: Z-value in second corner.
 * @z3: Z-value in third corner.
 * @z4: Z-value in fourth corner.
 * @w1: Weight of first corner (0 or 1).
 * @w2: Weight of second corner (0 or 1).
 * @w3: Weight of third corner (0 or 1).
 * @w4: Weight of fourth corner (0 or 1).
 *
 * Calculates approximate volume of a one square pixel with some corners possibly missing.
 *
 * Returns: The volume.
 **/
static inline gdouble
square_volumew(gdouble z1, gdouble z2, gdouble z3, gdouble z4,
               gint w1, gint w2, gint w3, gint w4)
{
    gdouble c;

    c = (z1 + z2 + z3 + z4)/4.0;

    return (w1*(3.0*z1 + z2 + z4 + c) + w2*(3.0*z2 + z1 + z3 + c)
            + w3*(3.0*z3 + z2 + z4 + c) + w4*(3.0*z4 + z3 + z1 + c))/24.0;
}

static gdouble
volume_corner(gdouble z1, gdouble z2, gdouble z3, gdouble z4,
              guint w1, guint w2, guint w3, guint w4,
              const gdouble *param)
{
    gdouble wcentre = param[0], wcardinal = param[1];
    guint d1 = w1 + w3, d2 = w2 + w4;
    return (z1*(w1*wcentre + d2*wcardinal + w3) + z2*(w2*wcentre + d1*wcardinal + w4)
            + z3*(w3*wcentre + d2*wcardinal + w1) + z4*(w4*wcentre + d1*wcardinal + w2));
}

static gdouble
volume_corner_all(gdouble z1, gdouble z2, gdouble z3, gdouble z4,
                  const gdouble *param)
{
    return (z1 + z2 + z3 + z4)*param[2];
}

static gdouble
calculate_volume(GwyField *field,
                 GwyNield *mask,
                 GwyMaskingType masking,
                 GwyFieldVolumeMethod method,
                 gint col, gint row,
                 gint width, gint height)
{
    // The centre and cardinal direction weights; the diagonal weight is always 1.
    static const gdouble volume_weights[GWY_FIELD_VOLUME_NMETHODS][2] = {
        { 484.0, 22.0, },  // Default = biqudratic
        {  52.0, 10.0, },  // Gwyddion2
        {  12.0,  2.0, },  // Triangular
        {  28.0,  4.0, },  // Bilinear
        { 484.0, 22.0, },  // Biqudratic
    };
    gdouble wcentre = volume_weights[method][0], wcardinal = volume_weights[method][1];

    gdouble param[3] = { 0.25*wcentre, 0.5*wcardinal, 0.25*wcentre + wcardinal + 1.0 };
    gdouble denom = wcentre + 4.0*wcardinal + 4.0;
    gdouble vol = _gwy_field_sum_corners(field, mask, masking, col, row, width, height,
                                         volume_corner, volume_corner_all, param, NULL, NULL);
    return vol/denom * gwy_field_get_dx(field)*gwy_field_get_dy(field);
}

/**
 * gwy_field_area_volume:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @method: Quadrature method. Pass %GWY_FIELD_VOLUME_DEFAULT (zero).
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 *
 * Computes volume of a rectangular part of a data field.
 *
 * Returns: The volume.
 **/
gdouble
gwy_field_area_volume(GwyField *field,
                      GwyNield *mask,
                      GwyMaskingType masking,
                      GwyFieldVolumeMethod method,
                      gint col, gint row,
                      gint width, gint height)
{
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return 0.0;
    g_return_val_if_fail(method <= GWY_FIELD_VOLUME_BIQUADRATIC, 0.0);

    return calculate_volume(field, mask, masking, method, col, row, width, height);
}

static gdouble
volume_triprism_material(gdouble za, gdouble zb, gdouble zc)
{
    gdouble min1 = fmin(za, zc);
    gdouble min = fmin(min1, zb);
    if (min >= 0.0)
        return za + zb + zc;

    gdouble max1 = fmax(za, zc);
    gdouble max = fmax(max1, zb);
    if (max <= 0.0)
        return 0.0;

    // Zero level crosses the triangle, must calculate the positive part.
    gdouble mid = zb;
    if (min1 != min)
        mid = min1;
    else if (max1 != max)
        mid = max1;

    if (mid <= 0.0)
        return max*max*max/(max - min)/(max - mid);

    gdouble p = mid/(mid - min), q = max/(max - min);
    return p*mid + q*max - p*q*min;
}

static gdouble
material_volume_quadrature(gdouble z1, gdouble z2, gdouble z3, gdouble z4,
                           guint w1, guint w2, guint w3, guint w4)
{
    gdouble zc = 0.25*(z1 + z2 + z3 + z4);
    gdouble v = 0.0;
    if (w1) {
        v += (volume_triprism_material(0.5*(z1 + z2), z1, zc)
              + volume_triprism_material(0.5*(z4 + z1), zc, z1));
    }
    if (w2) {
        v += (volume_triprism_material(0.5*(z1 + z2), z2, zc)
              + volume_triprism_material(0.5*(z2 + z3), zc, z2));
    }
    if (w3) {
        v += (volume_triprism_material(0.5*(z2 + z3), zc, z3)
              + volume_triprism_material(0.5*(z3 + z4), z3, zc));
    }
    if (w4) {
        v += (volume_triprism_material(0.5*(z4 + z1), zc, z4)
              + volume_triprism_material(0.5*(z3 + z4), z4, zc));
    }
    return v;
}

static gdouble
material_quadrature_all(gdouble z1, gdouble z2, gdouble z3, gdouble z4)
{
    gdouble zc = 0.25*(z1 + z2 + z3 + z4);
    return 2.0*(volume_triprism_material(zc, z1, z2)
                + volume_triprism_material(zc, z2, z3)
                + volume_triprism_material(zc, z3, z4)
                + volume_triprism_material(zc, z4, z1));
}

static gdouble
material_volume_corner(gdouble z1, gdouble z2, gdouble z3, gdouble z4,
                       guint w1, guint w2, guint w3, guint w4,
                       const gdouble *param)
{
    gdouble base = param[0];
    return material_volume_quadrature(z1 - base, z2 - base, z3 - base, z4 - base, w1, w2, w3, w4);
}

static gdouble
material_volume_corner_all(gdouble z1, gdouble z2, gdouble z3, gdouble z4,
                           const gdouble *param)
{
    gdouble base = param[0];
    return material_quadrature_all(z1 - base, z2 - base, z3 - base, z4 - base);
}

static gdouble
voids_volume_corner(gdouble z1, gdouble z2, gdouble z3, gdouble z4,
                    guint w1, guint w2, guint w3, guint w4,
                    const gdouble *param)
{
    gdouble base = param[0];
    return material_volume_quadrature(base - z1, base - z2, base - z3, base - z4, w1, w2, w3, w4);
}

static gdouble
voids_volume_corner_all(gdouble z1, gdouble z2, gdouble z3, gdouble z4,
                        const gdouble *param)
{
    gdouble base = param[0];
    return material_quadrature_all(base - z1, base - z2, base - z3, base - z4);
}

/**
 * gwy_field_area_material_volume:
 * @field: A two-dimensional data field.
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use (has any effect only with non-%NULL @mask).
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @material: %TRUE to calculate the material volume above @base, %FALSE to calculate the volume of voids below @base.
 * @base: Base level above or below which the volume is to be calculated.
 *
 * Calculates the volume of material or voids between a field surface and given base level.
 *
 * This functions differs from gwy_field_volume() substantially. If material volume above a certain base is
 * calculated, then only the parts of the surface that are above the base contribute to the result. The parts below
 * the base do not contribute at all, whereas in gwy_field_volume() they contribute a negative volume.
 *
 * The volume quadrature method is fixed and corresponds to %GWY_FIELD_VOLUME_TRIANGULAR. Unlike methods integrating
 * exactly certain polynomials, it explicitly specifies the surface shape. Therefore, surface intersections with the
 * base level are well-defined.
 *
 * Returns: The volume of material above @base and below the field surface (or the opposite when @material is %FALSE).
 **/
gdouble
gwy_field_area_material_volume(GwyField *field,
                               GwyNield *mask,
                               GwyMaskingType masking,
                               gint col, gint row,
                               gint width, gint height,
                               gboolean material,
                               gdouble base)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);

    gdouble vol;
    if (material) {
        vol = _gwy_field_sum_corners(field, mask, masking, col, row, width, height,
                                     material_volume_corner, material_volume_corner_all, &base, NULL, NULL);
    }
    else {
        vol = _gwy_field_sum_corners(field, mask, masking, col, row, width, height,
                                     voids_volume_corner, voids_volume_corner_all, &base, NULL, NULL);
    }

    return vol * gwy_field_get_dx(field)*gwy_field_get_dy(field)/24.0;
}

/**
 * gwy_field_area_dispersion:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use (has any effect only with non-%NULL @mask).
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @xcenter: (out) (optional):
 *           Location where to store the horizontal position of centre of mass (in pixel coordinates), or %NULL.
 * @ycenter: (out) (optional):
 *           Location where to store the vertical position of centre of mass (in pixel coordinates), or %NULL.
 *
 * Calculates the dispersion of a data field area, taking it as a distribution.
 *
 * The function takes @field as a distribution, finds the centre of mass in the area and then calculates the mean
 * squared distance from this centre, weighted by @field values.  Normally @field should contain only
 * non-negative data.
 *
 * The dispersion is measured in real coordinates, so horizontal and vertical pixel sizes play a role and the units
 * are squared lateral units of @field.  Note, however, that @xcenter and @ycenter is returned in pixel
 * coordinates since it is usually more convenient.
 *
 * Returns: Dispersion, i.e. estimated average squared distance from the centre of mass.
 **/
gdouble
gwy_field_area_dispersion(GwyField *field,
                          GwyNield *mask,
                          GwyMaskingType masking,
                          gint col,
                          gint row,
                          gint width,
                          gint height,
                          gdouble *xcenter,
                          gdouble *ycenter)
{
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return 0.0;

    gint xres = field->xres, yres = field->yres;
    const gdouble *datapos = field->priv->data + xres*row + col;
    const gint *maskpos = (mask ? mask->priv->data + xres*row + col : NULL);

    gdouble sx = 0.0, sy = 0.0, sw = 0.0;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(+:sx,sy,sw) \
            shared(datapos,maskpos,xres,width,height,row,col,masking)
#endif
    for (gint i = 0; i < height; i++) {
        const gdouble *d = datapos + i*xres;

        if (maskpos) {
            const gint *m = maskpos + i*xres;
            for (gint j = 0; j < width; j++) {
                gdouble w = d[j]*nielded_included(m + j, masking);
                sw += w;
                sx += j*w;
                sy += i*w;
            }
        }
        else {
            for (gint j = 0; j < width; j++) {
                gdouble w = d[j];
                sw += w;
                sx += j*w;
                sy += i*w;
            }
        }
    }

    /* Negative values are silly but do not prevent us from continuing. */
    if (sw == 0.0) {
        if (xcenter)
            *xcenter = col + 0.5*width;
        if (ycenter)
            *ycenter = row + 0.5*height;
        return 0.0;
    }

    sx /= sw;
    sy /= sw;
    if (xcenter)
        *xcenter = sx;
    if (ycenter)
        *ycenter = sy;

    gdouble dx = field->xreal/xres, dy = field->yreal/yres;
    sx *= dx;
    sy *= dy;

    gdouble s2 = 0.0;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(+:s2) \
            shared(datapos,maskpos,xres,width,height,row,col,dx,dy,sx,sy,masking)
#endif
    for (gint i = 0; i < height; i++) {
        const gdouble *d = datapos + i*xres;
        gdouble y = i*dy - sy;

        if (maskpos) {
            const gint *m = maskpos + xres*i;
            for (gint j = 0; j < width; j++) {
                gdouble x = j*dx - sx;
                s2 += (x*x + y*y)*d[j]*nielded_included(m + j, masking);
            }
        }
        else {
            for (gint j = 0; j < width; j++) {
                gdouble x = j*dx - sx;
                s2 += (x*x + y*y)*d[j];
            }
        }
    }

    return s2/sw;
}

/**
 * gwy_field_dispersion:
 * @field: A data field.
 * @xcenter: (out) (optional):
 *           Location where to store the horizontal position of centre of mass (in pixel coordinates), or %NULL.
 * @ycenter: (out) (optional):
 *           Location where to store the vertical position of centre of mass (in pixel coordinates), or %NULL.
 *
 * Calculates the dispersion of a data field, taking it as a distribution.
 *
 * See gwy_field_area_get_dispersion() for discussion.
 *
 * Returns: Dispersion, i.e. estimated average squared distance from centre of mass.
 **/
gdouble
gwy_field_dispersion(GwyField *field,
                     gdouble *xcenter, gdouble *ycenter)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    return gwy_field_area_dispersion(field, NULL, GWY_MASK_IGNORE, 0, 0,
                                     field->xres, field->yres, xcenter, ycenter);
}

/**
 * GwyFieldVolumeMethod:
 * @GWY_FIELD_VOLUME_DEFAULT: The default method, whatever it is (currently %GWY_FIELD_VOLUME_BIQUADRATIC).  The only
 *                            method you need.
 * @GWY_FIELD_VOLUME_GWYDDION2: Reproduce how Gwyddion 2.x calculated the volume by using the same quadrature weights,
 *                              although they are of unclear origin.
 * @GWY_FIELD_VOLUME_TRIANGULAR: Calculate the volume using the pixel centre point surface triangulation.
 * @GWY_FIELD_VOLUME_BILINEAR: Exactly integrate bilinear interpolation in each quarter.
 * @GWY_FIELD_VOLUME_BIQUADRATIC: Exactly integrate biquadratic interpolation.
 *
 * Field volume calculation methods.
 **/

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