/*
 *  $Id: lawn.c 29816 2026-04-15 18:03:04Z yeti-dn $
 *  Copyright (C) 2021-2026 David Necas (Yeti), Petr Klapetek, Radek Slesinger.
 *  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 <glib/gi18n-lib.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/serialize.h"
#include "libgwyddion/serializable-utils.h"
#include "libgwyddion/lawn.h"
#include "libgwyddion/interpolation.h"

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

#define TYPE_NAME "GwyLawn"

enum {
    SGNL_DATA_CHANGED,
    NUM_SIGNALS
};

enum {
    ITEM_XRES, ITEM_YRES,
    ITEM_XREAL, ITEM_YREAL,
    ITEM_XOFF, ITEM_YOFF,
    ITEM_UNIT_XY,
    ITEM_NCURVES, ITEM_CURVELENGTHS, ITEM_DATA,
    ITEM_UNITS_CURVES, ITEM_CURVE_LABELS,
    ITEM_NSEGMENTS, ITEM_SEGMENTS, ITEM_SEGMENT_LABELS,
    NUM_ITEMS,
};

static void             finalize              (GObject *object);
static void             alloc_data            (GwyLawn *lawn,
                                               gint xres,
                                               gint yres,
                                               gint ncurves,
                                               gint nsegments);
static void             serializable_init     (GwySerializableInterface *iface);
static void             serializable_itemize  (GwySerializable *serializable,
                                               GwySerializableGroup *group);
static void             serializable_done     (GwySerializable *serializable);
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 guint signals[NUM_SIGNALS] = { 0 };
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 = "ncurves",        .ctype = GWY_SERIALIZABLE_INT32,        },
    { .name = "curvelengths",   .ctype = GWY_SERIALIZABLE_INT32_ARRAY,
                                .flags = GWY_SERIALIZABLE_BIG_DATA      },
    { .name = "data",           .ctype = GWY_SERIALIZABLE_DOUBLE_ARRAY,
                                .flags = GWY_SERIALIZABLE_BIG_DATA  | GWY_SERIALIZABLE_TRANSFORMED },
    { .name = "units_curves",   .ctype = GWY_SERIALIZABLE_OBJECT_ARRAY, },
    { .name = "curve_labels",   .ctype = GWY_SERIALIZABLE_STRING_ARRAY, },
    { .name = "nsegments",      .ctype = GWY_SERIALIZABLE_INT32,        },
    { .name = "segments",       .ctype = GWY_SERIALIZABLE_INT32_ARRAY,
                                .flags = GWY_SERIALIZABLE_BIG_DATA      },
    { .name = "segment_labels", .ctype = GWY_SERIALIZABLE_STRING_ARRAY, },
};

G_DEFINE_TYPE_WITH_CODE(GwyLawn, gwy_lawn, G_TYPE_OBJECT,
                        G_ADD_PRIVATE(GwyLawn)
                        G_IMPLEMENT_INTERFACE(GWY_TYPE_SERIALIZABLE, serializable_init))

static void
serializable_init(GwySerializableInterface *iface)
{
    iface->itemize   = serializable_itemize;
    iface->done      = serializable_done;
    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_UNITS_CURVES].aux.object_type = GWY_TYPE_UNIT;
}

static void
gwy_lawn_class_init(GwyLawnClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GType type = G_TYPE_FROM_CLASS(klass);

    parent_class = gwy_lawn_parent_class;

    gobject_class->finalize = finalize;

    /**
     * GwyLawn::data-changed:
     * @gwylawn: The #GwyLawn 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(GwyLawnClass, 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_lawn_init(GwyLawn *lawn)
{
    lawn->priv = gwy_lawn_get_instance_private(lawn);
    alloc_data(lawn, 1, 1, 1, 0);
    lawn->xreal = 1.0;
}

static void
finalize(GObject *object)
{
    GwyLawn *lawn = (GwyLawn*)object;

    g_clear_object(&lawn->priv->unit_xy);
    alloc_data(lawn, 0, 0, 0, 0);

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

/* The function does not guarantee to preserve anything, only that all arrays will have the correct sizes. Already
 * existing curve data and other attributes will generally be physically kept instead of freeing everything to a fresh
 * state. Newly added curve data and other attributes will be unset/empty.
 *
 * You can pass zero sizes and numbers to free everything. */
static void
alloc_data(GwyLawn *lawn, gint xres, gint yres, gint ncurves, gboolean nsegments)
{
    GwyLawnPrivate *priv = lawn->priv;
    gint n = xres*yres, nold = lawn->xres*lawn->yres;

    /* Resize curve data. Here we deal with pointers and array lengths so be careful to create a valid state. If the
     * number of curves change we can only create a valid state by freeing all curve data (in the general case). */
    if (priv->ncurves == ncurves) {
        if (nold > n) {
            for (gint i = n; i < nold; i++)
                g_free(priv->curvedata[i]);
        }
        if (nold != n) {
            priv->curvelengths = g_renew(gint, priv->curvelengths, n);
            priv->curvedata = g_renew(gdouble*, priv->curvedata, n);
        }
        if (nold < n) {
            gwy_clear(priv->curvedata + nold, n - nold);
            gwy_clear(priv->curvelengths + nold, n - nold);
        }
    }
    else {
        for (gint i = 0; i < nold; i++)
            g_free(priv->curvedata[i]);
        g_free(priv->curvedata);
        priv->curvedata = g_new0(gdouble*, n);

        if (nold == n)
            gwy_clear(priv->curvelengths, n);
        else {
            g_free(priv->curvelengths);
            priv->curvelengths = g_new0(gint, n);
        }
    }

    /* Resize segmentation. The contents can be bogus. */
    if (nold * priv->nsegments != n*nsegments) {
        GWY_FREE(priv->segments);
        if (n && nsegments)
            priv->segments = g_new(gint, 2*n*nsegments);
    }

    /* Now we can update xres and yres to the new values. */
    lawn->xres = xres;
    lawn->yres = yres;

    /* Resisze curve and segments attributes. They have to be set for all or completely unset (except for units which
     * can be mix of unit objects and NULLs). Either state is valid. So just clear the labels if the sizes do not
     * match. */
    if (ncurves != priv->ncurves) {
        if (priv->ncurves > ncurves) {
            for (gint i = ncurves; i < priv->ncurves; i++)
                g_clear_object(priv->units_curves + i);
        }
        priv->units_curves = g_renew(GwyUnit*, priv->units_curves, ncurves);
        if (priv->ncurves < ncurves)
            gwy_clear(priv->units_curves + priv->ncurves, ncurves - priv->ncurves);

        assign_string_array(&priv->curvelabels, priv->ncurves, NULL, 0);
        priv->ncurves = ncurves;
    }
    if (nsegments != priv->nsegments) {
        assign_string_array(&priv->segmentlabels, priv->nsegments, NULL, 0);
        priv->nsegments = nsegments;
    }
}

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

/**
 * gwy_lawn_new: (constructor)
 * @xres: X resolution, i.e., the number of samples in x direction
 * @yres: Y resolution, i.e., the number of samples in y direction
 * @xreal: Real physical dimension in x direction.
 * @yreal: Real physical dimension in y direction.
 * @ncurves: The number of curves at each sample.
 * @nsegments: The number of curve segments.
 *
 * Creates a new data lawn.
 *
 * Returns: (transfer full):
 *          A newly created data lawn.
 **/
GwyLawn*
gwy_lawn_new(gint xres, gint yres, gdouble xreal, gdouble yreal, gint ncurves, gint nsegments)
{
    GwyLawn *lawn = g_object_new(GWY_TYPE_LAWN, NULL);

    alloc_data(lawn, xres, yres, ncurves, nsegments);
    gwy_clear(lawn->priv->segments, 2*xres*yres*nsegments);
    lawn->xreal = xreal;
    lawn->yreal = yreal;

    return lawn;
}

/**
 * gwy_lawn_new_alike:
 * @model: A data lawn to take resolutions, units, and labels from.
 *
 * Creates a new data lawn similar to an existing one.
 *
 * The new data lawn will have the same dimensions and the same curves, including names and units. It will not contain
 * any data, however, and thus also no segments.
 *
 * Use gwy_lawn_duplicate() if you want to copy a data lawn including data.
 *
 * Returns: (transfer full):
 *          A newly created data lawn.
 **/
GwyLawn*
gwy_lawn_new_alike(GwyLawn *model)
{
    GwyLawn *lawn = g_object_new(GWY_TYPE_LAWN, NULL);

    g_return_val_if_fail(GWY_IS_LAWN(model), lawn);
    GwyLawnPrivate *priv = lawn->priv, *mpriv = model->priv;
    alloc_data(lawn, model->xres, model->yres, mpriv->ncurves, 0);
    copy_info(model, lawn);
    assign_string_array(&priv->curvelabels, priv->ncurves, mpriv->curvelabels, mpriv->ncurves);

    return lawn;
}

/**
 * gwy_lawn_new_part:
 * @lawn: A data lawn to take data from
 * @xpos: x position where to start from
 * @ypos: y position where to start from
 * @xres: x resolution (width) to be extracted
 * @yres: y resolution (height) to be extracted
 * @keep_offsets: keep offsets of data during extraction
 *
 * Creates a new data lawn as a part of existing one.
 *
 * Use gwy_lawn_duplicate() if you want to copy a whole data lawn.
 *
 * Returns: (transfer full):
 *          A newly created data lawn.
 **/
GwyLawn*
gwy_lawn_new_part(GwyLawn *lawn,
                  gint xpos, gint ypos,
                  gint xres, gint yres,
                  gboolean keep_offsets)
{
    gint row, col, lxres, lyres, idx_part, idx_lawn, idx_seg_part, idx_seg_lawn, nsegments;
    gdouble dx, dy;

    g_return_val_if_fail(GWY_IS_LAWN(lawn), NULL);

    g_return_val_if_fail(xpos >= 0 && ypos >= 0 && xres >= 0 && yres >= 0, NULL);
    lxres = lawn->xres;
    lyres = lawn->yres;
    g_return_val_if_fail(xpos + xres <= lxres && ypos + yres <= lyres, NULL);

    dx = gwy_lawn_get_dx(lawn);
    dy = gwy_lawn_get_dy(lawn);

    nsegments = gwy_lawn_get_n_segments(lawn);

    GwyLawnPrivate *lpriv = lawn->priv;

    GwyLawn *part = gwy_lawn_new(xres, yres, xres*dx, yres*dy, lpriv->ncurves, nsegments);
    GwyLawnPrivate *ppriv = part->priv;

    if (keep_offsets) {
        part->xoff = xpos*dx + lawn->xoff;
        part->yoff = ypos*dy + lawn->yoff;
    }

    gwy_lawn_copy_units(lawn, part);
    assign_string_array(&ppriv->curvelabels, ppriv->ncurves, lpriv->curvelabels, lpriv->ncurves);
    assign_string_array(&ppriv->segmentlabels, ppriv->nsegments, lpriv->segmentlabels, lpriv->nsegments);

    ppriv->segments = g_new(gint, xres*yres * 2 * nsegments);

    for (row = 0; row < yres; row++) {
        gwy_assign(ppriv->curvelengths + xres*row, lpriv->curvelengths + lxres*(ypos + row) + xpos, xres);

        for (col = 0; col < xres; col++) {
            idx_part = xres*row + col;
            idx_lawn = lxres*(ypos + row) + (xpos + col);

            ppriv->curvedata[idx_part] = g_new(gdouble, ppriv->curvelengths[idx_part] * ppriv->ncurves);
            gwy_assign(ppriv->curvedata[idx_part],
                       lpriv->curvedata[idx_lawn],
                       ppriv->curvelengths[idx_part] * ppriv->ncurves);

            idx_seg_part = idx_part * 2 * nsegments;
            idx_seg_lawn = idx_lawn * 2 * nsegments;
            gwy_assign(ppriv->segments + idx_seg_part, lpriv->segments + idx_seg_lawn, 2 * nsegments);
        }
    }

    return part;
}

/**
 * gwy_lawn_data_changed:
 * @lawn: A data lawn.
 *
 * Emits signal "data_changed" on a data lawn.
 **/
void
gwy_lawn_data_changed(GwyLawn *lawn)
{
    g_signal_emit(lawn, signals[SGNL_DATA_CHANGED], 0);
}

/**
 * gwy_lawn_copy_data:
 * @lawn: Source lawn.
 * @target: Destination lawn.
 *
 * Copies the contents of an already allocated lawn to a lawn of the same size.
 *
 * The same size means the same pixel dimensions and the same number of curves. Curve lengths do not need to be
 * identical; curves are reallocated in such case.
 *
 * Only the data are copied.  To make a data lawn completely identical to another, including units and change of
 * dimensions, you can use gwy_lawn_assign().
 **/
void
gwy_lawn_copy_data(GwyLawn *lawn, GwyLawn *target)
{
    gint i, npoints;

    g_return_if_fail(GWY_IS_LAWN(lawn));
    g_return_if_fail(GWY_IS_LAWN(target));
    g_return_if_fail(lawn->xres == target->xres && lawn->yres == target->yres);

    if (lawn == target)
        return;

    target->xreal = lawn->xreal;
    target->yreal = lawn->yreal;

    GwyLawnPrivate *spriv = lawn->priv;
    GwyLawnPrivate *dpriv = target->priv;
    g_return_if_fail(dpriv->ncurves != spriv->ncurves);

    npoints = lawn->xres * lawn->yres;

    if (dpriv->curvelengths)
        g_free(dpriv->curvelengths);

    dpriv->curvelengths = g_new(gint, npoints);
    gwy_assign(dpriv->curvelengths, spriv->curvelengths, npoints);

    if (dpriv->curvedata) {
        for (i = 0; i < npoints; i++)
            g_free(dpriv->curvedata[i]);
        g_free(dpriv->curvedata);
    }

    dpriv->curvedata = g_new0(double*, npoints);

    for (i = 0; i < npoints; i++) {
        if (dpriv->curvelengths[i] > 0) {
            dpriv->curvedata[i] = g_new(double, dpriv->curvelengths[i]);
            gwy_assign(dpriv->curvedata[i], spriv->curvedata[i], spriv->curvelengths[i] * spriv->ncurves);
        }
    }
}

/**
 * gwy_lawn_get_xres:
 * @lawn: A data lawn.
 *
 * Gets the x resolution of a data lawn.
 *
 * Returns: Resolution (number of data points).
 **/
gint
gwy_lawn_get_xres(GwyLawn *lawn)
{
    g_return_val_if_fail(GWY_IS_LAWN(lawn), 0);
    return lawn->xres;
}

/**
 * gwy_lawn_get_yres:
 * @lawn: A data lawn.
 *
 * Gets the y resolution of a data lawn.
 *
 * Returns: Resolution (number of data points).
 **/
gint
gwy_lawn_get_yres(GwyLawn *lawn)
{
    g_return_val_if_fail(GWY_IS_LAWN(lawn), 0);
    return lawn->yres;
}

/**
 * gwy_lawn_get_xreal:
 * @lawn: A data lawn.
 *
 * Gets the physical size of a data lawn in the x direction.
 *
 * Returns: Real size of a data lawn the x direction.
 **/
gdouble
gwy_lawn_get_xreal(GwyLawn *lawn)
{
    g_return_val_if_fail(GWY_IS_LAWN(lawn), 0.0);
    return lawn->xreal;
}

/**
 * gwy_lawn_get_yreal:
 * @lawn: A data lawn.
 *
 * Gets the physical size of a data lawn in the y direction.
 *
 * Returns: Real size of a data lawn the y direction.
 **/
gdouble
gwy_lawn_get_yreal(GwyLawn *lawn)
{
    g_return_val_if_fail(GWY_IS_LAWN(lawn), 0.0);
    return lawn->yreal;
}

/**
 * gwy_lawn_get_xoffset:
 * @lawn: A data lawn.
 *
 * Gets the offset of data lawn origin in x direction.
 *
 * Returns: Offset value.
 **/
gdouble
gwy_lawn_get_xoffset(GwyLawn *lawn)
{
    g_return_val_if_fail(GWY_IS_LAWN(lawn), 0.0);
    return lawn->xoff;
}

/**
 * gwy_lawn_get_yoffset:
 * @lawn: A data lawn.
 *
 * Gets the offset of data lawn origin in y direction.
 *
 * Returns: Offset value.
 **/
gdouble
gwy_lawn_get_yoffset(GwyLawn *lawn)
{
    g_return_val_if_fail(GWY_IS_LAWN(lawn), 0.0);
    return lawn->yoff;
}

/**
 * gwy_lawn_get_n_curves:
 * @lawn: A data lawn.
 *
 * Gets the number of curves at each sample.
 *
 * Returns: Number of curves.
 **/
gint
gwy_lawn_get_n_curves(GwyLawn *lawn)
{
    g_return_val_if_fail(GWY_IS_LAWN(lawn), 0.0);
    GwyLawnPrivate *priv = lawn->priv;

    return priv->ncurves;
}

/**
 * gwy_lawn_set_xoffset:
 * @lawn: A data lawn.
 * @xoffset: New offset value.
 *
 * Sets the offset of a data lawn origin in the x direction.
 *
 * Note offsets don't affect any calculation.
 **/
void
gwy_lawn_set_xoffset(GwyLawn *lawn, gdouble xoffset)
{
    g_return_if_fail(GWY_IS_LAWN(lawn));
    lawn->xoff = xoffset;
}

/**
 * gwy_lawn_set_yoffset:
 * @lawn: A data lawn.
 * @yoffset: New offset value.
 *
 * Sets the offset of a data lawn origin in the y direction.
 *
 * Note offsets don't affect any calculation.
 **/
void
gwy_lawn_set_yoffset(GwyLawn *lawn, gdouble yoffset)
{
    g_return_if_fail(GWY_IS_LAWN(lawn));
    lawn->yoff = yoffset;
}

/**
 * gwy_lawn_set_xreal:
 * @lawn: A data lawn.
 * @xreal: New real x dimensions value
 *
 * Sets the real x dimension of a lawn.
 **/
void
gwy_lawn_set_xreal(GwyLawn *lawn, gdouble xreal)
{
    g_return_if_fail(GWY_IS_LAWN(lawn));
    g_return_if_fail(xreal > 0.0);
    lawn->xreal = xreal;
}

/**
 * gwy_lawn_set_yreal:
 * @lawn: A data lawn.
 * @yreal: New real y dimensions value
 *
 * Sets the real y dimension of a lawn.
 **/
void
gwy_lawn_set_yreal(GwyLawn *lawn, gdouble yreal)
{
    g_return_if_fail(GWY_IS_LAWN(lawn));
    g_return_if_fail(yreal > 0.0);
    lawn->yreal = yreal;
}

/**
 * gwy_lawn_get_dx:
 * @lawn: A data lawn.
 *
 * Gets the horizontal (X) pixel size of a lawn in real units.
 *
 * The result is the same as gwy_lawn_get_xreal(lawn)/gwy_lawn_get_xres(lawn).
 *
 * Returns: Horizontal pixel size.
 **/
gdouble
gwy_lawn_get_dx(GwyLawn *lawn)
{
    g_return_val_if_fail(GWY_IS_LAWN(lawn), 0.0);
    return lawn->xreal/lawn->xres;
}

/**
 * gwy_lawn_get_dy:
 * @lawn: A data lawn.
 *
 * Gets the vertical (Y) pixel size of a lawn in real units.
 *
 * The result is the same as gwy_lawn_get_yreal(lawn)/gwy_lawn_get_yres(lawn).
 *
 * Returns: Vertical pixel size.
 **/
gdouble
gwy_lawn_get_dy(GwyLawn *lawn)
{
    g_return_val_if_fail(GWY_IS_LAWN(lawn), 0.0);
    return lawn->yreal/lawn->yres;
}

/**
 * gwy_lawn_get_unit_xy:
 * @lawn: A data lawn.
 *
 * Returns x- and y-direction SI unit of a data lawn.
 *
 * The returned object can be modified to change the lawn lateral units.
 *
 * Returns: (transfer none):
 *          SI unit corresponding to the lateral (X) dimension of the data lawn.  Its reference count is not
 *          incremented.
 **/
GwyUnit*
gwy_lawn_get_unit_xy(GwyLawn *lawn)
{
    g_return_val_if_fail(GWY_IS_LAWN(lawn), NULL);

    GwyLawnPrivate *priv = lawn->priv;
    if (!priv->unit_xy)
        priv->unit_xy = gwy_unit_new(NULL);

    return priv->unit_xy;
}

/**
 * gwy_lawn_get_unit_curve:
 * @lawn: A data lawn.
 * @i: Index of a curve in @lawn.
 *
 * Returns value SI unit of a curve of a data lawn.
 *
 * The returned object can be modified to change the corresponding lawn curve units.
 *
 * Returns: (transfer none):
 *          SI unit corresponding to the "value" of the data lawn.  Its reference count is not incremented.
 **/
GwyUnit*
gwy_lawn_get_unit_curve(GwyLawn *lawn, gint i)
{
    g_return_val_if_fail(GWY_IS_LAWN(lawn), NULL);
    GwyLawnPrivate *priv = lawn->priv;
    g_return_val_if_fail(i >= 0 && i < priv->ncurves, NULL);
    g_return_val_if_fail(priv->units_curves, NULL);

    if (!priv->units_curves[i])
        priv->units_curves[i] = gwy_unit_new(NULL);

    return priv->units_curves[i];
}

/**
 * gwy_lawn_copy_units:
 * @lawn: A data lawn.
 * @target: Target data lawn.
 *
 * Sets lateral and curve units of a data lawn to match another data lawn.
 **/
void
gwy_lawn_copy_units(GwyLawn *lawn, GwyLawn *target)
{
    gint i;

    g_return_if_fail(GWY_IS_LAWN(lawn));
    g_return_if_fail(GWY_IS_LAWN(target));

    GwyLawnPrivate *lpriv = lawn->priv;
    GwyLawnPrivate *tpriv = target->priv;

    g_return_if_fail(lpriv->ncurves == tpriv->ncurves);
    g_return_if_fail(lpriv->units_curves);
    g_return_if_fail(tpriv->units_curves);

    _gwy_copy_unit(lpriv->unit_xy, &tpriv->unit_xy);
    for (i = 0; i < lpriv->ncurves; i++)
        _gwy_copy_unit(lpriv->units_curves[i], &(tpriv->units_curves[i]));
}

/**
 * gwy_lawn_set_curve_label:
 * @lawn: A data lawn.
 * @i: Index of a curve in @lawn.
 * @label: New curve label.
 *
 * Sets the label of a curve in data lawn.
 *
 * All curves can be either unlabelled or labelled. When they are unlabelled and you set the first label, all others
 * automatically get a label (‘Unknown’, ‘Curve 1’ or similar).
 **/
void
gwy_lawn_set_curve_label(GwyLawn *lawn, gint i, const gchar *label)
{
    g_return_if_fail(GWY_IS_LAWN(lawn));
    GwyLawnPrivate *priv = lawn->priv;
    g_return_if_fail(label);
    g_return_if_fail(i >= 0 && i < priv->ncurves);

    if (!priv->curvelabels) {
        priv->curvelabels = g_new0(gchar*, priv->ncurves);
        for (gint j = 0; j < priv->ncurves; j++) {
            if (j != i)
                priv->curvelabels[j] = g_strdup_printf("%s %d", _("Unknown"), j+1);
        }
    }
    gwy_assign_string(priv->curvelabels + i, label);
}

/**
 * gwy_lawn_get_curve_label:
 * @lawn: A data lawn.
 * @i: Index of a curve in @lawn.
 *
 * Gets the label of a curve in data lawn.
 *
 * Returns: (nullable): Curve label, as a string owned by @lawn.  It may be %NULL.
 **/
const gchar*
gwy_lawn_get_curve_label(GwyLawn *lawn, gint i)
{
    g_return_val_if_fail(GWY_IS_LAWN(lawn), NULL);
    GwyLawnPrivate *priv = lawn->priv;
    g_return_val_if_fail(i >= 0 && i < priv->ncurves, NULL);

    if (!priv->curvelabels)
        return NULL;

    return priv->curvelabels[i];
}

/**
 * gwy_lawn_find_curve_by_label:
 * @lawn: A data lawn.
 * @label: (nullable): Curve label to find.
 *
 * Finds the first curve in data lawn matching a label.
 *
 * It is possible to pass %NULL as @label. It can find anything only if all curves are unlabelled, and in such case it
 * will return the first curve index. Hence, it is not particularly useful.
 *
 * Returns: The curve index if found, a negative value (-1) if not found.
 **/
gint
gwy_lawn_find_curve_by_label(GwyLawn *lawn,
                             const gchar *label)
{
    g_return_val_if_fail(GWY_IS_LAWN(lawn), -1);
    GwyLawnPrivate *priv = lawn->priv;
    gint ncurves = priv->ncurves;
    if (!label)
        return priv->curvelabels ? -1 : 0;
    if (!priv->curvelabels)
        return -1;

    for (gint i = 0; i < ncurves; i++) {
        if (gwy_strequal(priv->curvelabels[i], label))
            return i;
    }
    return -1;
}

/**
 * gwy_lawn_clear_curve_labels:
 * @lawn: A data lawn.
 *
 * Completely removes all data labels in a data lawn.
 *
 * The data lawn revert to a clean state when gwy_lawn_get_curve_label() returns %NULL for all curves. See
 * gwy_lawn_set_curve_label() for a discussion.
 **/
void
gwy_lawn_clear_curve_labels(GwyLawn *lawn)
{
    g_return_if_fail(GWY_IS_LAWN(lawn));
    GwyLawnPrivate *priv = lawn->priv;
    if (priv->curvelabels) {
        gint ncurves = priv->ncurves;
        for (gint i = 0; i < ncurves; i++)
            g_free(priv->curvelabels[i]);
        GWY_FREE(priv->curvelabels);
    }
}

/**
 * gwy_lawn_get_value_format_xy:
 * @lawn: A data lawn.
 * @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 lawn.
 *
 * Returns: The value format.  If @format is %NULL, a newly allocated format is returned, otherwise (modified) @format
 *          itself is returned.
 **/
GwyValueFormat*
gwy_lawn_get_value_format_xy(GwyLawn *lawn,
                             GwyUnitFormatStyle style,
                             GwyValueFormat *format)
{
    gdouble max, unit;

    g_return_val_if_fail(GWY_IS_LAWN(lawn), NULL);

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

/**
 * gwy_lawn_get_value_format_curve:
 * @lawn: A data lawn.
 * @i: Index of a curve in @lawn.
 * @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 lawn.
 *
 * Note this functions searches for minimum and maximum value in @lawn, therefore it's relatively slow.
 *
 * Returns: The value format.  If @format is %NULL, a newly allocated format is returned, otherwise (modified) @format
 *          itself is returned.
 **/
GwyValueFormat*
gwy_lawn_get_value_format_curve(GwyLawn *lawn,
                                gint i,
                                GwyUnitFormatStyle style,
                                GwyValueFormat *format)
{
    g_return_val_if_fail(GWY_IS_LAWN(lawn), NULL);
    GwyLawnPrivate *priv = lawn->priv;
    g_return_val_if_fail(i >= 0 && i < priv->ncurves, NULL);

    gdouble max = -G_MAXDOUBLE;
    gdouble min = G_MAXDOUBLE;

    for (gint j = 0; j < lawn->xres * lawn->yres; j++) {
        if (priv->curvedata[j]) {
            for (gint k = 0; k < priv->curvelengths[j]; k++) {
                gsize idx = i * priv->curvelengths[j] + k;

                if (priv->curvedata[j][idx] > max)
                    max = priv->curvedata[j][idx];

                if (priv->curvedata[j][idx] < min)
                    min = priv->curvedata[j][idx];
            }
        }
    }

    if (max == min) {
        max = ABS(max);
        min = 0.0;
    }

    return gwy_unit_get_format(gwy_lawn_get_unit_curve(lawn, i), style, max - min, format);
}

/**
 * gwy_lawn_get_curve_length:
 * @lawn: A data lawn.
 * @col: Position in the lawn (column index).
 * @row: Position in the lawn (row index).
 *
 * Gets the length of the curves at given position in a data lawn.
 *
 * Returns: The curves length at given index.
 **/
gint
gwy_lawn_get_curve_length(GwyLawn *lawn, gint col, gint row)
{
    g_return_val_if_fail(GWY_IS_LAWN(lawn), 0);

    GwyLawnPrivate *priv = lawn->priv;
    g_return_val_if_fail(priv->curvelengths, 0);

    return priv->curvelengths[row * lawn->xres + col];
}

/**
 * gwy_lawn_get_curve_data:
 * @lawn: A data lawn.
 * @col: Position in the lawn (column index).
 * @row: Position in the lawn (row index).
 * @i: Index of a curve in @lawn.
 * @curvelength: (out): Location to store the length of the curve, or %NULL.
 *
 * Gets the data array of a curve at given position in a data lawn.
 *
 * The returned buffer is not guaranteed to be valid through whole data lawn life time.
 *
 * Returns: (array length=curvelength):
 *          The @i-th curve data at given index. When there are no data at the pixel, %NULL is returned.
 **/
gdouble*
gwy_lawn_get_curve_data(GwyLawn *lawn, gint col, gint row, gint i, gint *curvelength)
{
    g_return_val_if_fail(GWY_IS_LAWN(lawn), NULL);
    GwyLawnPrivate *priv = lawn->priv;
    g_return_val_if_fail(col >= 0 && col < lawn->xres && row >= 0 && row < lawn->yres, NULL);
    g_return_val_if_fail(i >= 0 && i < priv->ncurves, NULL);

    gint idx = row * lawn->xres + col;
    gint ndata = priv->curvelengths[idx];

    if (curvelength)
        *curvelength = ndata;

    if (ndata == 0)
        return NULL;
    else
        return priv->curvedata[idx] + i*ndata;
}

/**
 * gwy_lawn_get_curve_data_const:
 * @lawn: A data lawn.
 * @col: Position in the lawn (column index).
 * @row: Position in the lawn (row index).
 * @i: Index of a curve in @lawn.
 * @curvelength: (out): To store the length of the curve (optional).
 *
 * Gets the data array of a curve at given position in a data lawn.
 *
 * Returns: (array length=curvelength):
 *          The @i-th curve data at given index.  The returned data are owned by @lawn and must not be modified nor
 *          freed. When there are no data at the pixel, %NULL is returned.
 **/
const gdouble*
gwy_lawn_get_curve_data_const(GwyLawn *lawn, gint col, gint row, gint i, gint *curvelength)
{
    g_return_val_if_fail(GWY_IS_LAWN(lawn), NULL);
    GwyLawnPrivate *priv = lawn->priv;
    g_return_val_if_fail(col >= 0 && col < lawn->xres && row >= 0 && row < lawn->yres, NULL);
    g_return_val_if_fail(i >= 0 && i < priv->ncurves, NULL);

    gint idx = row * lawn->xres + col;
    gint ndata = priv->curvelengths[idx];

    if (curvelength)
        *curvelength = ndata;

    if (ndata == 0)
        return NULL;
    else
        return (const gdouble*)(priv->curvedata[idx] + i*ndata);
}

/**
 * gwy_lawn_set_curve_data:
 * @lawn: A data lawn.
 * @col: Position in the lawn (column index).
 * @row: Position in the lawn (row index).
 * @i: Index of a curve in @lawn.
 * @curvedata: New data of the @i-th curve.
 *
 * Sets the data of a single curve in a data lawn.
 *
 * The number of points remains the same since all curves at give pixel must have the same number of points.  If you
 * want to change the number of points set the data of all curves together using gwy_lawn_set_curves().
 **/
void
gwy_lawn_set_curve_data(GwyLawn *lawn, gint col, gint row, gint i, const gdouble *curvedata)
{
    g_return_if_fail(GWY_IS_LAWN(lawn));
    GwyLawnPrivate *priv = lawn->priv;
    g_return_if_fail(col >= 0 && col < lawn->xres && row >= 0 && row < lawn->yres);
    g_return_if_fail(i >= 0 && i < priv->ncurves);

    gint idx = row * lawn->xres + col;

    gwy_assign(priv->curvedata[idx] + i * priv->curvelengths[idx], curvedata, priv->curvelengths[idx]);
}

/**
 * gwy_lawn_get_curves_data_const:
 * @lawn: A data lawn.
 * @col: Position in the lawn (column index).
 * @row: Position in the lawn (row index).
 * @curvelength: To store the length of the curves (optional).
 *
 * Gets the data array of all curves at given position in a data lawn.
 *
 * The array contains concatenated data of all curves.
 *
 * Returns: The curves data at given index. When there are no data at the pixel, %NULL is returned.
 **/
const gdouble*
gwy_lawn_get_curves_data_const(GwyLawn *lawn, gint col, gint row, gint *curvelength)
{
    GwyLawnPrivate *priv;
    gint idx, ndata;

    g_return_val_if_fail(GWY_IS_LAWN(lawn), NULL);
    g_return_val_if_fail(col >= 0 && col < lawn->xres && row >= 0 && row < lawn->yres, NULL);

    priv = lawn->priv;
    idx = row * lawn->xres + col;
    ndata = priv->curvelengths[idx];

    if (curvelength)
        *curvelength = ndata;

    if (ndata == 0)
        return NULL;
    else
        return (const gdouble*)priv->curvedata[idx];
}

/**
 * gwy_lawn_set_curves:
 * @lawn: A data lawn.
 * @col: Position in the lawn (column index).
 * @row: Position in the lawn (row index).
 * @curvelength: Length of each of the curves.
 * @curvesdata: (array): Curve data array.
 * @segments: (nullable): Segmentation for the curve.  May be %NULL to leave the current segmentation unchanged.
 *
 * Sets data for all curves at given position in a data lawn.
 *
 * Note that passing %NULL @segments can result in segmentation which is not valid for @curvelength.  In such case you
 * need to correct the segmentation afterwards.
 **/
void
gwy_lawn_set_curves(GwyLawn *lawn,
                    gint col, gint row,
                    gint curvelength, const gdouble* curvesdata,
                    const gint* segments)
{
    GwyLawnPrivate *priv;
    gint idx_lawn, idx_seg, datasize;

    g_return_if_fail(GWY_IS_LAWN(lawn));
    g_return_if_fail(col >= 0 && col < lawn->xres && row >= 0 && row < lawn->yres);

    priv = lawn->priv;

    idx_lawn = row * lawn->xres + col;
    datasize = priv->ncurves * curvelength;

    g_free(priv->curvedata[idx_lawn]);
    priv->curvedata[idx_lawn] = g_new(gdouble, datasize);
    gwy_assign(priv->curvedata[idx_lawn], curvesdata, datasize);
    priv->curvelengths[idx_lawn] = curvelength;

    idx_seg = idx_lawn * 2 * priv->nsegments;

    if (segments && priv->segments)
        gwy_assign(priv->segments + idx_seg, segments, 2 * priv->nsegments);
}

/**
 * gwy_lawn_get_n_segments:
 * @lawn: A data lawn.
 *
 * Gets the number of segments marked in curves in a data lawn.
 *
 * All curves have the same number of segments, even empty curves.  Empty curves simply have the corresponding number
 * of trivial zero-length segments.
 *
 * Returns: The number of segments.  Zero is returned if no segments are marked.
 **/
gint
gwy_lawn_get_n_segments(GwyLawn *lawn)
{
    GwyLawnPrivate *priv;

    g_return_val_if_fail(GWY_IS_LAWN(lawn), 0);

    priv = lawn->priv;

    return priv->nsegments;
}

/**
 * gwy_lawn_get_segments:
 * @lawn: A data lawn.
 * @col: Position in the lawn (column index).
 * @row: Position in the lawn (row index).
 * @nsegments: (out): Location where to store the number of segments, or %NULL.
 *
 * Gets the segmentation of a curve in a data lawn.
 *
 * See gwy_lawn_set_segments() for the array format.
 *
 * The number of segments is filled in @nsegments for convenience. It cannot differ between individual pixels and is
 * always equal to the return value of gwy_lawn_get_n_segments().
 *
 * Returns: (array): The segmentation for the curve, as an array owned by @lawn which must not be modified nor freed.
 **/
const gint*
gwy_lawn_get_segments(GwyLawn *lawn, gint col, gint row, gint *nsegments)
{
    GwyLawnPrivate *priv;

    g_return_val_if_fail(GWY_IS_LAWN(lawn), NULL);

    priv = lawn->priv;

    if (nsegments)
        *nsegments = priv->nsegments;

    return priv->segments + 2*priv->nsegments*(row*lawn->xres + col);
}

/**
 * gwy_lawn_set_segments:
 * @lawn: A data lawn.
 * @nsegments: New number of segments.
 * @segments: (array): The new segmentation for entire @lawn.
 *
 * Sets the segmentation of all curves in a data lawn.
 *
 * This is the only function which allows changing the number of segments.  If the number of segments changes all the
 * labels are reset.
 *
 * See gwy_lawn_curve_set_segments() for the single-curve @segments array format.  When setting the data for an entire
 * @lawn the array must contain this data for all pixels, concatenated.
 **/
void
gwy_lawn_set_segments(GwyLawn *lawn, gint nsegments, const gint *segments)
{
    GwyLawnPrivate *priv;
    gint npoints;

    g_return_if_fail(GWY_IS_LAWN(lawn));

    priv = lawn->priv;
    npoints = lawn->xres * lawn->yres;
    assign_string_array(&priv->segmentlabels, priv->nsegments, NULL, 0);
    g_free(priv->segments);
    priv->segments = g_memdup(segments, npoints * 2*nsegments * sizeof(gint));
    priv->nsegments = nsegments;
}

/**
 * gwy_lawn_get_segment_label:
 * @lawn: A data lawn.
 * @segment: Index of a curve segment in @lawn.
 *
 * Gets the label of a curve segment in data lawn.
 *
 * Returns: (nullable): Segment label, as a string owned by @lawn.  It may be %NULL.
 **/
const gchar*
gwy_lawn_get_segment_label(GwyLawn *lawn, gint segment)
{
    GwyLawnPrivate *priv;

    g_return_val_if_fail(GWY_IS_LAWN(lawn), NULL);
    g_return_val_if_fail(segment >= 0, NULL);

    priv = lawn->priv;
    g_return_val_if_fail(segment < priv->nsegments, NULL);

    if (!priv->segmentlabels)
        return NULL;
    return priv->segmentlabels[segment];
}

/**
 * gwy_lawn_set_segment_label:
 * @lawn: A data lawn.
 * @segment: Index of a curve segment in @lawn.
 * @label: New segment label.
 *
 * Sets the label of a curve segment in data lawn.
 *
 * Since all curves in @lawn are segmented into the same segments the segments share labels.  The first segment label
 * corresponds to the first segment in all the curves.
 *
 * All segments can be either unlabelled or labelled. When they are unlabelled and you set the first label, all others
 * automatically get a label (‘Unknown’, ‘Segment 1’ or similar).
 **/
void
gwy_lawn_set_segment_label(GwyLawn *lawn, gint segment, const gchar *label)
{
    GwyLawnPrivate *priv;
    gint i;

    g_return_if_fail(GWY_IS_LAWN(lawn));
    g_return_if_fail(segment >= 0);
    g_return_if_fail(label);

    priv = lawn->priv;

    g_return_if_fail(segment < priv->nsegments);
    if (!priv->segmentlabels) {
        priv->segmentlabels = g_new0(gchar*, priv->nsegments);
        for (i = 0; i < priv->nsegments; i++) {
            if (i != segment)
                priv->segmentlabels[i] = g_strdup_printf("%s %d", _("Segment"), i+1);
        }
    }
    gwy_assign_string(priv->segmentlabels + segment, label);
}

/**
 * gwy_lawn_find_segment_by_label:
 * @lawn: A data lawn.
 * @label: (nullable): Segment label to find.
 *
 * Finds the first segment in data lawn matching a label.
 *
 * It is possible to pass %NULL as @label. It can find anything only if all segments are unlabelled, and in such case
 * it will return the first segment index. Hence, it is not particularly useful.
 *
 * Returns: The segment index if found, a negative value (-1) if not found.
 **/
gint
gwy_lawn_find_segment_by_label(GwyLawn *lawn,
                               const gchar *label)
{
    g_return_val_if_fail(GWY_IS_LAWN(lawn), -1);
    GwyLawnPrivate *priv = lawn->priv;
    gint nsegments = priv->nsegments;
    if (!label)
        return priv->segmentlabels ? -1 : 0;
    if (!priv->segmentlabels)
        return -1;

    for (gint i = 0; i < nsegments; i++) {
        if (gwy_strequal(priv->segmentlabels[i], label))
            return i;
    }
    return -1;
}

/**
 * gwy_lawn_clear_segment_labels:
 * @lawn: A data lawn.
 *
 * Completely removes all data labels in a data lawn.
 *
 * The data lawn revert to a clean state when gwy_lawn_get_segment_label() returns %NULL for all segments. See
 * gwy_lawn_set_segment_label() for a discussion.
 **/
void
gwy_lawn_clear_segment_labels(GwyLawn *lawn)
{
    g_return_if_fail(GWY_IS_LAWN(lawn));
    GwyLawnPrivate *priv = lawn->priv;
    if (priv->segmentlabels) {
        gint nsegments = priv->nsegments;
        for (gint i = 0; i < nsegments; i++)
            g_free(priv->segmentlabels[i]);
        GWY_FREE(priv->segmentlabels);
    }
}

/**
 * gwy_lawn_curve_set_segments:
 * @lawn: A data lawn.
 * @col: Position in the lawn (column index).
 * @row: Position in the lawn (row index).
 * @segments: (array): The new segmentation for the curve.
 *
 * Sets the segmentation of a curve in data lawn.
 *
 * All curves have the same number of segments, equal to the value returned by gwy_lawn_get_n_segments().
 *
 * The array @segments contains twice as much elements as there are segments.  They are organised [@start₀, @end₀,
 * @start₁, @end₁, etc.].  A segment starts at the postion @start and has length @end-@start.  In other words, @start
 * is inclusive, but @end is exclusive.  If a segment is zero-length even @start can correspond to non-existent data
 * (the typical case is an empty curve).
 **/
void
gwy_lawn_curve_set_segments(GwyLawn *lawn, gint col, gint row, const gint *segments)
{
    GwyLawnPrivate *priv;
    gint idx_seg;

    g_return_if_fail(GWY_IS_LAWN(lawn));

    priv = lawn->priv;

    idx_seg = (row * lawn->xres + col) * 2 * priv->nsegments;
    gwy_assign(priv->segments + idx_seg, segments, 2 * priv->nsegments);
}

/**
 * gwy_lawn_clear:
 * @lawn: A data lawn.
 *
 * Removes curve data in all pixels.
 *
 * The number of curves, their units and labels are preserved as well as the number and labels of segments.
 * Segmentation data should be considered undefined (currently it is preserved).
 **/
void
gwy_lawn_clear(GwyLawn *lawn)
{
    GwyLawnPrivate *priv;
    gint i;

    g_return_if_fail(GWY_IS_LAWN(lawn));

    priv = lawn->priv;
    for (i = 0; i < lawn->xres * lawn->yres; i++) {
        GWY_FREE(priv->curvedata[i]);
        priv->curvelengths[i] = 0;
    }
}

/**
 * gwy_lawn_area_reduce_to_plane:
 * @lawn: A data lawn.
 * @target: Datafield to be filled by summed plane. Its dimensions should be (width, height).
 * @func: (scope call): Function to reduce the curves data array to a single value.
 * @user_data: Data passed to @func.
 * @col: Column where to start (pixel coordinates).
 * @row: Row where to start (pixel coordinates).
 * @width: Pixel width of summed plane.
 * @height: Pixel height of summed plane.
 * @keep_offsets: Keep the physical offsets in extracted field.
 *
 * Reduces curves data at each point of a rectangular region to a value,
 * storing the results in a data field.
 **/
void
gwy_lawn_area_reduce_to_plane(GwyLawn *lawn, GwyField *target,
                              GwyCurveReduceFunction func, gpointer userdata,
                              gint istart, gint jstart,
                              gint width, gint height,
                              gboolean keep_offsets)
{
    gint col, row, idx_lawn, idx_target;
    gdouble *tdata;

    g_return_if_fail(GWY_IS_LAWN(lawn));
    GwyLawnPrivate *priv = lawn->priv;
    g_return_if_fail(istart + width <= lawn->xres && jstart + height <= lawn->yres);
    g_return_if_fail(priv);

    g_return_if_fail(GWY_IS_FIELD(target));
    g_return_if_fail((width == target->xres) && (height == target->yres));

    gwy_field_set_xreal(target, gwy_lawn_get_xreal(lawn));
    gwy_field_set_yreal(target, gwy_lawn_get_yreal(lawn));

    if (keep_offsets) {
        gwy_field_set_xoffset(target, gwy_lawn_get_xoffset(lawn));
        gwy_field_set_yoffset(target, gwy_lawn_get_yoffset(lawn));
    }

    tdata = gwy_field_get_data(target);
    g_return_if_fail(tdata);

    for (row = 0; row < height; row++) {
        for (col = 0; col < width; col++) {
            idx_target = row * target->xres + col;
            idx_lawn = (row + istart) * lawn->xres + (col + jstart);
            tdata[idx_target] = (*func)(priv->ncurves, priv->curvelengths[idx_lawn], priv->curvedata[idx_lawn],
                                        userdata);
        }
    }

    _gwy_copy_unit(priv->unit_xy, &target->priv->unit_xy);
    /* should we care about unit_z? then probably func should tell */

    gwy_field_invalidate(target);
}

/**
 * gwy_lawn_reduce_to_plane:
 * @lawn: A data lawn.
 * @target: Datafield to be filled by the summary data. It should have the same dimensions as lawn.
 * @func: (scope call): Function to reduce the curves data array to a single value.
 * @user_data: Data passed to @func.
 *
 * Sums all z-profiles of a data lawn to a data field.
 *
 * Reduces curves data at each point of the lawn to a value,
 * storing the results in a data field.
 **/
void
gwy_lawn_reduce_to_plane(GwyLawn *lawn, GwyField *target,
                         GwyCurveReduceFunction func, gpointer userdata)
{
    g_return_if_fail(GWY_IS_LAWN(lawn));
    /* check dimensions */

    gwy_lawn_area_reduce_to_plane(lawn, target,
                                  func, userdata,
                                  0, 0,
                                  lawn->xres, lawn->yres,
                                  TRUE);
}

/**
 * gwy_lawn_copy_units_to_field:
 * @lawn: A data lawn.
 * @i: Index of a curve in @lawn.
 * @field: A data field which should have units set.
 *
 * Sets lateral and possibly value units of a data field to match a data lawn.
 *
 * Lateral units will match @lawn lateral units.
 *
 * Value units will match the curve with index @i. Pass negative @i to leave the value units unchanged.
 **/
void
gwy_lawn_copy_units_to_field(GwyLawn *lawn,
                             gint i,
                             GwyField *field)
{
    g_return_if_fail(GWY_IS_LAWN(lawn));
    g_return_if_fail(GWY_IS_FIELD(field));
    GwyLawnPrivate *priv = lawn->priv;
    GwyFieldPrivate *fpriv = field->priv;
    _gwy_copy_unit(priv->unit_xy, &fpriv->unit_xy);
    if (i >= 0) {
        g_return_if_fail(i < priv->ncurves);
        _gwy_copy_unit(priv->units_curves[i], &fpriv->unit_z);
    }
}

/**
 * gwy_lawn_new_field_alike:
 * @lawn: A data lawn.
 * @i: Index of a curve in @lawn.
 * @nullme: Whether the data field should be initialized to zeros. If %FALSE, the data will not be initialized.
 *
 * Creates a new data field matching a data lawn.
 *
 * The pixel dimensions, physical dimensions, offsets and units will match @lawn. Value units will match the curve
 * with index @i. Pass negative @i to leave the value units unset.
 *
 * Returns: (transfer full):
 *          A new data field matching @lawn.
 **/
GwyField*
gwy_lawn_new_field_alike(GwyLawn *lawn,
                         gint i,
                         gboolean nullme)
{
    g_return_val_if_fail(GWY_IS_LAWN(lawn), FALSE);

    GwyField *field = gwy_field_new(lawn->xres, lawn->yres, lawn->xreal, lawn->yreal, nullme);
    field->xoff = lawn->xoff;
    field->yoff = lawn->yoff;
    gwy_lawn_copy_units_to_field(lawn, i, field);

    return field;
}

/**
 * gwy_lawn_new_rotated_90:
 * @lawn: A data lawn.
 * @clockwise: %TRUE to rotate clockwise, %FALSE to rotate anti-clockwise.
 *
 * Creates a new data lawn by rotating a data lawn by 90 degrees.
 *
 * Returns: (transfer full): A newly created data lawn.
 **/
GwyLawn*
gwy_lawn_new_rotated_90(GwyLawn *lawn, gboolean clockwise)
{
    gint row, col, idx_lawn, idx_rot, idx_lseg, idx_rseg, bsize;

    g_return_val_if_fail(GWY_IS_LAWN(lawn), NULL);

    GwyLawnPrivate *lpriv = lawn->priv;

    GwyLawn *rot = gwy_lawn_new(lawn->yres, lawn->xres, lawn->yreal, lawn->xreal, lpriv->ncurves, lpriv->nsegments);
    GwyLawnPrivate *rpriv = rot->priv;

    rot->xoff = lawn->yoff;
    rot->yoff = lawn->xoff;

    assign_string_array(&rpriv->curvelabels, rpriv->ncurves, lpriv->curvelabels, lpriv->ncurves);
    assign_string_array(&rpriv->segmentlabels, rpriv->nsegments, lpriv->segmentlabels, lpriv->nsegments);
    gwy_lawn_copy_units(lawn, rot);

    if (clockwise) {
        for (row = 0; row < lawn->yres; row++) {
            for (col = 0; col < lawn->xres; col++) {
                idx_lawn = row * lawn->xres + col;
                idx_rot = (rot->yres - 1 - col) * rot->xres + row;

                bsize = lpriv->ncurves * lpriv->curvelengths[idx_lawn];
                rpriv->curvedata[idx_rot] = g_new(gdouble, bsize);
                gwy_assign(rpriv->curvedata[idx_rot], lpriv->curvedata[idx_lawn], bsize);

                idx_lseg = idx_lawn * 2 * lpriv->nsegments;
                idx_rseg = idx_rot * 2 * rpriv->nsegments;
                gwy_assign(rpriv->segments + idx_rseg, lpriv->segments + idx_lseg, 2 * lpriv->nsegments);
            }
        }
    }
    else {
        for (row = 0; row < lawn->yres; row++) {
            for (col = 0; col < lawn->xres; col++) {
                idx_lawn = row * lawn->xres + col;
                idx_rot = col * rot->xres + (rot->xres - 1 - row);

                bsize = lpriv->ncurves * lpriv->curvelengths[idx_lawn];
                rpriv->curvedata[idx_rot] = g_new(gdouble, bsize);
                gwy_assign(rpriv->curvedata[idx_rot], lpriv->curvedata[idx_lawn], bsize);

                idx_lseg = idx_lawn * 2 * lpriv->nsegments;
                idx_rseg = idx_rot * 2 * rpriv->nsegments;
                gwy_assign(rpriv->segments + idx_rseg, lpriv->segments + idx_lseg, 2 * lpriv->nsegments);
            }
        }
    }

    return rot;
}


/**
 * gwy_lawn_flip:
 * @lawn: A data lawn.
 * @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.
 *
 * Flips a data lawn in place.
 *
 * Since real sizes cannot go backward, flipping an axis results in the corresponding offset being reset (the real
 * dimension stays positive).
 **/
void
gwy_lawn_flip(GwyLawn *lawn,
              gboolean xflipped, gboolean yflipped)
{
    GwyLawnPrivate *priv;
    gint xres, yres, i, j, k, slen;
    gdouble **curvedata;
    gint *curvelengths, *segments;
    gint idx1, idx2;

    g_return_if_fail(GWY_IS_LAWN(lawn));
    xres = lawn->xres;
    yres = lawn->yres;
    priv = lawn->priv;
    curvedata = priv->curvedata;
    curvelengths = priv->curvelengths;
    segments = priv->segments;
    slen = priv->nsegments * 2;

    if (!xflipped && !yflipped) {
        /* Do nothing. */
    }
    else if (xflipped && !yflipped) {
        for (i = 0; i < yres; i++) {
            for (j = 0; j < xres/2; j++) {
                idx1 = i*xres + j;
                idx2 = i*xres + (xres - 1 - j);
                GWY_SWAP(gint, curvelengths[idx1], curvelengths[idx2]);
                GWY_SWAP(gdouble*, curvedata[idx1], curvedata[idx2]);
                for (k = 0; k < slen; k++)
                    GWY_SWAP(gint, segments[idx1*slen + k], segments[idx2*slen + k]);
            }
        }
    }
    else if (!xflipped && yflipped) {
        for (i = 0; i < yres/2; i++) {
            for (j = 0; j < xres; j++) {
                idx1 = i*xres + j;
                idx2 = (yres - 1 - i)*xres + j;
                GWY_SWAP(gint, curvelengths[idx1], curvelengths[idx2]);
                GWY_SWAP(gdouble*, curvedata[idx1], curvedata[idx2]);
                for (k = 0; k < slen; k++)
                    GWY_SWAP(gint, segments[idx1*slen + k], segments[idx2*slen + k]);
            }
        }
    }
    else if (xflipped && yflipped) {
        for (i = 0; i < yres/2; i++) {
            for (j = 0; j < xres; j++) {
                idx1 = i*xres + j;
                idx2 = (yres - 1 - i)*xres + (xres - 1 - j);
                GWY_SWAP(gint, curvelengths[idx1], curvelengths[idx2]);
                GWY_SWAP(gdouble*, curvedata[idx1], curvedata[idx2]);
                for (k = 0; k < slen; k++)
                    GWY_SWAP(gint, segments[idx1*slen + k], segments[idx2*slen + k]);
            }
        }
    }
    else {
        g_assert_not_reached();
    }

    lawn->xoff = xflipped ? 0.0 : lawn->xoff;
    lawn->yoff = yflipped ? 0.0 : lawn->yoff;
}

/* Fill lawn->priv->ser_curvedata with flattened curve data. */
static gsize
flatten_data(GwyLawn *lawn)
{
    GwyLawnPrivate *priv = lawn->priv;
    const gint *curvelengths = priv->curvelengths;
    const gdouble **curvedata = (const gdouble**)priv->curvedata;
    guint ncurves = priv->ncurves;
    guint n = lawn->xres * lawn->yres;
    gsize len = 0;

    g_assert(!priv->ser_curvedata);

    for (guint i = 0; i < n; i++)
        len += ncurves * curvelengths[i];

    gdouble *flat_data = priv->ser_curvedata = g_new(gdouble, len);
    len = 0;
    for (guint i = 0; i < n; i++) {
        gwy_assign(flat_data + len, curvedata[i], ncurves*curvelengths[i]);
        len += ncurves * curvelengths[i];
    }

    return len;
}

/* Fill curve data with from flattened curve data. A valid allocation is assumed, possibly with completely different
 * curve lengths. */
static void
split_data(GwyLawn *lawn, const guint *new_curvelengths, const gdouble *flat_data)
{
    GwyLawnPrivate *priv = lawn->priv;
    gint *curvelengths = priv->curvelengths;
    gdouble **curvedata = priv->curvedata;
    guint ncurves = priv->ncurves;
    guint n = lawn->xres * lawn->yres;
    gsize pos = 0;

    for (guint i = 0; i < n; i++) {
        gsize len = ncurves*new_curvelengths[i];
        if (curvelengths[i] != new_curvelengths[i]) {
            g_free(curvedata[i]);
            curvedata[i] = new_curvelengths[i] ? g_new(gdouble, len) : NULL;
            curvelengths[i] = new_curvelengths[i];
        }
        if (len)
            gwy_assign(curvedata[i], flat_data + pos, len);
        pos += len;
    }
}

static void
serializable_itemize(GwySerializable *serializable, GwySerializableGroup *group)
{
    GwyLawn *lawn = GWY_LAWN(serializable);
    GwyLawnPrivate *priv = lawn->priv;
    guint npoints = lawn->xres * lawn->yres;
    gsize flat_data_size = flatten_data(lawn);

    gwy_serializable_group_alloc_size(group, NUM_ITEMS);
    gwy_serializable_group_append_int32(group, serializable_items + ITEM_XRES, lawn->xres);
    gwy_serializable_group_append_int32(group, serializable_items + ITEM_YRES, lawn->yres);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_XREAL, lawn->xreal);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_YREAL, lawn->yreal);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_XOFF, lawn->xoff);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_YOFF, lawn->yoff);
    gwy_serializable_group_append_unit(group, serializable_items + ITEM_UNIT_XY, priv->unit_xy);
    gwy_serializable_group_append_int32(group, serializable_items + ITEM_NCURVES, priv->ncurves);
    gwy_serializable_group_append_int32_array(group, serializable_items + ITEM_CURVELENGTHS,
                                              priv->curvelengths, npoints);
    gwy_serializable_group_append_double_array(group, serializable_items + ITEM_DATA,
                                               priv->ser_curvedata, flat_data_size);

    /* The units array can be in a mixed state of unit objects and NULLs. But that we cannot serialise, so make sure
     * all units exist if some exist. */
    guint ncurves = priv->ncurves;
    guint existing_units = 0;
    for (guint i = 0; i < ncurves; i++)
        existing_units += (priv->units_curves[i] && !gwy_unit_equal_string(priv->units_curves[i], NULL));

    if (existing_units) {
        for (guint i = 0; i < ncurves; i++) {
            if (!priv->units_curves[i])
                priv->units_curves[i] = gwy_unit_new(NULL);
        }
        gwy_serializable_group_append_object_array(group, serializable_items + ITEM_UNITS_CURVES,
                                                   (GObject**)priv->units_curves, ncurves);
    }

    gwy_serializable_group_append_string_array(group, serializable_items + ITEM_CURVE_LABELS,
                                               priv->curvelabels, ncurves);

    guint nsegments = priv->nsegments;
    if (nsegments) {
        gwy_serializable_group_append_int32(group, serializable_items + ITEM_NSEGMENTS, priv->nsegments);
        gwy_serializable_group_append_int32_array(group, serializable_items + ITEM_SEGMENTS,
                                                  priv->segments, 2*npoints*nsegments);
        gwy_serializable_group_append_string_array(group, serializable_items + ITEM_SEGMENT_LABELS,
                                                   priv->segmentlabels, nsegments);
    }
    gwy_serializable_group_itemize(group);
}

static void
serializable_done(GwySerializable *serializable)
{
    GwyLawn *lawn = GWY_LAWN(serializable);
    GwyLawnPrivate *priv = lawn->priv;
    GWY_FREE(priv->ser_curvedata);
}

static void
add_size_mismatch_error(GwyErrorList **error_list, const GwySerializableItem *it,
                        const gchar *expected_name, guint expected_n)
{
    /* NB: We do not say the numbers are not equal. Matching may not mean exactly equal. */
    gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_REPLACED,
                       // TRANSLATORS: Error message.
                       // TRANSLATORS: Second %s is a data type name, e.g. GwyField.
                       _("The number of items in ‘%s’ of %s %" G_GSIZE_FORMAT " does not match "
                         "‘%s’ %u."),
                       it->name, TYPE_NAME, it->array_size, expected_name, expected_n);
}

static gboolean
is_too_much(GwyErrorList **error_list, const gchar *name, guint value, guint max_value)
{
    if (G_LIKELY(value <= max_value))
        return FALSE;

    // Avoid translatable message proliferation by using %s format.
    gchar *s = g_strdup_printf("%u", value);
    gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_INVALID,
                       _("Dimension %s of ‘%s’ is invalid."),
                       s, name);
    g_free(s);
    return TRUE;
}

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);

    GwyLawn *lawn = GWY_LAWN(serializable);
    GwyLawnPrivate *priv = lawn->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;
    guint nsegments = its[ITEM_NSEGMENTS].value.v_int32;
    guint ncurves = its[ITEM_NCURVES].value.v_int32;

    /* There is no other explicit check for sane ncurves so do it here. */
    gsize curve_dim[1] = { ncurves };
    if (!gwy_check_data_dimension(error_list, TYPE_NAME ".ncurves", 1, 0, curve_dim))
        goto fail;
    /* Hundred times more than a reasonable large amount is too much. */
    if (is_too_much(error_list,TYPE_NAME ".ncurves", ncurves, 1024))
        goto fail;
    if (is_too_much(error_list,TYPE_NAME ".nsegments", nsegments, 1024))
        goto fail;

    /* Constraint: len(curvelengths) = xres × yres. */
    it = its + ITEM_CURVELENGTHS;
    gsize logical_dim[2] = { xres, yres };
    if (!gwy_check_data_dimension(error_list, TYPE_NAME ".curvelengths", 2, it->array_size, logical_dim))
        goto fail;

    /* Constraint: len(data) = ncurves × ∑ curvelengths. */
    gsize len = 0;
    for (guint i = 0; i < it->array_size; i++)
        len += ncurves * it->value.v_int32_array[i];

    /* XXX: It is borderline valid to have a Lawn with no data inside at all. It may not have a great practical value
     * but we create these in serialisation tests, so… */
    it = its + ITEM_DATA;
    gsize len_dim[1] = { len };
    if ((len || it->array_size) && !gwy_check_data_dimension(error_list, TYPE_NAME, 1, it->array_size, len_dim))
        goto fail;

    if (nsegments) {
        /* Constraint: len(segments) = 2 × xres × yres × nsegments. */
        it = its + ITEM_SEGMENTS;
        gsize segment_dim[4] = { 2, xres, yres, nsegments };
        if (!gwy_check_data_dimension(error_list, TYPE_NAME, 4, it->array_size, segment_dim))
            goto fail;
    }

    /* Do not allocate segments. We already have the array ready if there are segments. */
    alloc_data(lawn, xres, yres, ncurves, 0);
    g_assert(!priv->curvelabels);
    g_assert(!priv->segmentlabels);
    g_assert(priv->units_curves);
    g_assert(!priv->units_curves[0]);
    split_data(lawn, its[ITEM_CURVELENGTHS].value.v_int32_array, its[ITEM_DATA].value.v_double_array);
    if (nsegments) {
        priv->nsegments = nsegments;
        priv->segments = its[ITEM_SEGMENTS].value.v_int32_array;
        its[ITEM_SEGMENTS].value.v_int32_array = NULL;
    }

    /* The rest is already validated by pspec as far as GTypes and atomic types ranges go. */
    lawn->xreal = its[ITEM_XREAL].value.v_double;
    lawn->yreal = its[ITEM_YREAL].value.v_double;
    lawn->xoff = its[ITEM_XOFF].value.v_double;
    lawn->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;

    /* Constraint: len(curvelabels) = ncurves. */
    it = its + ITEM_CURVE_LABELS;
    if (it->array_size) {
        if (it->array_size == ncurves) {
            priv->curvelabels = it->value.v_string_array;
            it->value.v_string_array = NULL;
        }
        else
            add_size_mismatch_error(error_list, it, "ncurves", ncurves);
    }
    else {
        GWY_FREE(priv->curvelabels);
    }

    /* Constraint: len(curveunits) = ncurves. */
    it = its + ITEM_UNITS_CURVES;
    if (it->array_size) {
        if (it->array_size == ncurves) {
            gwy_assign(priv->units_curves, it->value.v_object_array, ncurves);
            GWY_FREE(it->value.v_object_array);
        }
        else
            add_size_mismatch_error(error_list, it, "ncurves", ncurves);
    }
    /* Keeping it filled with NULLs is OK here. */

    /* Constraint: len(segmentlabels) = nsegments. */
    /* alloc_data() created an array of the correct size, but filled with NULLs. */
    it = its + ITEM_SEGMENT_LABELS;
    if (it->array_size) {
        if (it->array_size == nsegments) {
            priv->segmentlabels = it->value.v_string_array;
            it->value.v_string_array = NULL;
        }
        else
            add_size_mismatch_error(error_list, it, "nsegments", nsegments);
    }
    else {
        GWY_FREE(priv->segmentlabels);
    }

    ok = TRUE;

fail:
    g_clear_object(&its[ITEM_UNIT_XY].value.v_object);
    it = its + ITEM_UNITS_CURVES;
    if (it->value.v_object_array) {
        for (gsize i = 0; i < it->array_size; i++)
            g_clear_object(it->value.v_object_array + i);
        g_free(it->value.v_object_array);
    }
    it = its + ITEM_CURVE_LABELS;
    assign_string_array(&it->value.v_string_array, it->array_size, NULL, 0);
    it = its + ITEM_SEGMENT_LABELS;
    assign_string_array(&it->value.v_string_array, it->array_size, NULL, 0);
    g_free(its[ITEM_CURVELENGTHS].value.v_int32_array);
    g_free(its[ITEM_SEGMENTS].value.v_int32_array);
    g_free(its[ITEM_DATA].value.v_double_array);

    return ok;
}

static GwySerializable*
serializable_copy(GwySerializable *serializable)
{
    GwyLawn *lawn = GWY_LAWN(serializable);
    GwyLawn *copy = gwy_lawn_new_alike(lawn);
    /* The new object still lacks data and segments. */
    GwyLawnPrivate *lpriv = lawn->priv;
    GwyLawnPrivate *cpriv = copy->priv;
    guint n = lawn->xres * lawn->yres;
    guint ncurves = lpriv->ncurves;

    for (guint i = 0; i < n; i++) {
        if (lpriv->curvelengths[i])
            cpriv->curvedata[i] = g_memdup2(lpriv->curvedata[i], ncurves*lpriv->curvelengths[i]*sizeof(gdouble));
    }
    gwy_assign(cpriv->curvelengths, lpriv->curvelengths, n);

    if (lpriv->nsegments) {
        cpriv->nsegments = lpriv->nsegments;
        cpriv->segments = g_memdup2(lpriv->segments, 2*n*lpriv->nsegments*sizeof(gint));
        assign_string_array(&cpriv->segmentlabels, 0, lpriv->segmentlabels, lpriv->nsegments);
    }

    return GWY_SERIALIZABLE(copy);
}

static void
serializable_assign(GwySerializable *destination, GwySerializable *source)
{
    GwyLawn *destlawn = GWY_LAWN(destination), *srclawn = GWY_LAWN(source);
    GwyLawnPrivate *dpriv = destlawn->priv, *spriv = srclawn->priv;
    guint n = srclawn->xres * srclawn->yres;
    guint sncurves = spriv->ncurves, dncurves = dpriv->ncurves;
    guint nsegments = spriv->nsegments;

    alloc_data(destlawn, srclawn->xres, srclawn->yres, sncurves, nsegments);
    copy_info(srclawn, destlawn);

    for (guint i = 0; i < n; i++) {
        guint len = spriv->curvelengths[i];
        /* This can anyway happen only when dncurves == sncurves. */
        if (dpriv->curvelengths[i]*dncurves == len*sncurves)
            gwy_assign(dpriv->curvedata[i], spriv->curvedata[i], sncurves*len);
        else {
            GWY_FREE(dpriv->curvedata[i]);
            if (len)
                dpriv->curvedata[i] = g_memdup2(spriv->curvedata[i], sncurves*len*sizeof(gdouble));
        }
        dpriv->curvelengths[i] = len;
    }
    gwy_assign(dpriv->curvelengths, spriv->curvelengths, n);

    if (nsegments)
        gwy_assign(dpriv->segments, spriv->segments, 2*n*nsegments);

    assign_string_array(&dpriv->curvelabels, sncurves, spriv->curvelabels, sncurves);
    assign_string_array(&dpriv->segmentlabels, nsegments, spriv->segmentlabels, nsegments);
    /* Curve units are already copied by copy_info(). */
}

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

/**
 * gwy_lawn_assign:
 * @destination: Target data lawn.
 * @source: Source data lawn.
 *
 * Makes one data lawn equal to another.
 *
 * This function is a convenience gwy_serializable_assign() wrapper.
 **/
void
gwy_lawn_assign(GwyLawn *destination, GwyLawn *source)
{
    g_return_if_fail(GWY_IS_LAWN(destination));
    g_return_if_fail(GWY_IS_LAWN(source));
    if (destination != source)
        gwy_serializable_assign(GWY_SERIALIZABLE(destination), GWY_SERIALIZABLE(source));
}

/**
 * SECTION:lawn
 * @title: GwyLawn
 * @short_description: Three-dimensional data representation
 *
 * #GwyLawn represents 3D data arrays in Gwyddion. It is typically useful for different volume data obtained from
 * SPMs, like in force volume measurements.
 **/

/**
 * GwyLawn:
 *
 * The #GwyLawn struct contains private data only and should be accessed using the functions below.
 **/

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