/*
 *  $Id: layer-line.c 30033 2026-06-10 09:11:02Z yeti-dn $
 *  Copyright (C) 2024-2026 David Necas (Yeti), Petr Klapetek.
 *  E-mail: yeti@gwyddion.net, klapetek@gwyddion.net.
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 *  warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along with this program; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include <gdk/gdkkeysyms.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"
#include "libgwyddion/selection-line.h"

#include "libgwyui/data-view.h"
#include "libgwyui/layer-line.h"
#include "libgwyui/cairo-utils.h"
#include "libgwyui/layer-utils.h"

enum {
    OBJECT_SIZE = 4
};

enum {
    PROP_0,
    PROP_NUMBERED,
    PROP_CENTER_TICK,
    PROP_THICKNESS,
    NUM_PROPERTIES,
};

struct _GwyLayerLinePrivate {
    /* Properties */
    gboolean numbered;
    gboolean center_tick;
    gdouble thickness;

    /* Dynamic state */
    gboolean constrain_to_15deg;
    gboolean moving_whole_line;
    gdouble linemove_x0;
    gdouble linemove_y0;
    gdouble linemove_before[OBJECT_SIZE];
    gint moving_endpoint;
};

static void set_property       (GObject *object,
                                guint prop_id,
                                const GValue *value,
                                GParamSpec *pspec);
static void get_property       (GObject *object,
                                guint prop_id,
                                GValue *value,
                                GParamSpec *pspec);
static void draw               (GwyVectorLayer *layer,
                                cairo_t *cr);
static void draw_or_invalidate (GwyVectorLayer *layer,
                                cairo_t *cr,
                                GwyDataView *dataview,
                                cairo_region_t *region,
                                const gdouble *xy,
                                guint i);
static void invalidate_object  (GwyVectorLayer *layer,
                                cairo_region_t *region,
                                const gdouble *xy);
static void pointer_moved      (GwyVectorLayer *layer,
                                gdouble x,
                                gdouble y);
static void button_pressed     (GwyVectorLayer *layer,
                                gdouble x,
                                gdouble y);
static void button_released    (GwyVectorLayer *layer,
                                gdouble x,
                                gdouble y);
static gint near_endpoint      (GwyVectorLayer *layer,
                                gdouble xreal,
                                gdouble yreal);
static gint near_line          (GwyVectorLayer *layer,
                                gdouble xreal,
                                gdouble yreal);
static void enforce_constraints(GwyDataView *dataview,
                                gboolean multiple_of_15deg,
                                gint endpoint,
                                gdouble *xy);
//static gboolean key_pressed        (GwyVectorLayer *layer,
//                                    GdkEventKey *event);

static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
static GwyVectorLayerClass *parent_class = NULL;

G_DEFINE_TYPE_WITH_CODE(GwyLayerLine, gwy_layer_line, GWY_TYPE_VECTOR_LAYER,
                        G_ADD_PRIVATE(GwyLayerLine))

static void
gwy_layer_line_class_init(GwyLayerLineClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GwyVectorLayerClass *vector_class = GWY_VECTOR_LAYER_CLASS(klass);

    parent_class = gwy_layer_line_parent_class;

    gobject_class->set_property = set_property;
    gobject_class->get_property = get_property;

    vector_class->selection_type = GWY_TYPE_SELECTION_LINE;
    vector_class->draw = draw;
    vector_class->invalidate_object = invalidate_object;
    vector_class->motion = pointer_moved;
    vector_class->button_press = button_pressed;
    vector_class->button_release = button_released;
    //vector_class->key_press = key_pressed;

    properties[PROP_NUMBERED] = g_param_spec_boolean("numbered", NULL,
                                                     "Whether to show line numbers.",
                                                     FALSE,
                                                     GWY_GPARAM_RWE);
    properties[PROP_CENTER_TICK] = g_param_spec_boolean("center-tick", NULL,
                                                        "Whether to draw a tick in the line center",
                                                        FALSE,
                                                        GWY_GPARAM_RWE);
    properties[PROP_THICKNESS] = g_param_spec_double("thickness", NULL,
                                                     "Size of markers denoting line thickness",
                                                     0.0, 1024.0, 0.0,
                                                     GWY_GPARAM_RWE);

    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);
}

static void
gwy_layer_line_init(GwyLayerLine *layer)
{
    layer->priv = gwy_layer_line_get_instance_private(layer);
}

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwyLayerLine *layer = GWY_LAYER_LINE(object);

    switch (prop_id) {
        case PROP_NUMBERED:
        gwy_layer_line_set_numbered(layer, g_value_get_boolean(value));
        break;

        case PROP_CENTER_TICK:
        gwy_layer_line_set_center_tick(layer, g_value_get_boolean(value));
        break;

        case PROP_THICKNESS:
        gwy_layer_line_set_thickness(layer, g_value_get_double(value));
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
get_property(GObject *object,
             guint prop_id,
             GValue *value,
             GParamSpec *pspec)
{
    GwyLayerLinePrivate *priv = GWY_LAYER_LINE(object)->priv;

    switch (prop_id) {
        case PROP_NUMBERED:
        g_value_set_boolean(value, priv->numbered);
        break;

        case PROP_CENTER_TICK:
        g_value_set_boolean(value, priv->center_tick);
        break;

        case PROP_THICKNESS:
        g_value_set_double(value, priv->thickness);
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
draw(GwyVectorLayer *layer, cairo_t *cr)
{
    GwySelection *selection = gwy_vector_layer_get_selection(layer);
    GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
    if (!parent) {
        g_warning("Standalone drawing is not implemented yet.");
        return;
    }
    g_return_if_fail(GWY_IS_DATA_VIEW(parent));

    cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
    cairo_set_line_width(cr, 1.0);
    cairo_set_operator(cr, CAIRO_OPERATOR_DIFFERENCE);

    gint n = gwy_selection_get_n_objects(selection);
    for (gint i = 0; i < n; i++) {
        gdouble xy[OBJECT_SIZE];
        gwy_selection_get_object(selection, i, xy);
        draw_or_invalidate(layer, cr, GWY_DATA_VIEW(parent), NULL, xy, i);
    }

    cairo_stroke(cr);
}

static void
draw_or_invalidate(GwyVectorLayer *layer,
                   cairo_t *cr, GwyDataView *dataview,
                   cairo_region_t *region,
                   const gdouble *xy, guint i)
{
    GwyLayerLinePrivate *priv = GWY_LAYER_LINE(layer)->priv;
    gdouble x0, y0, x1, y1;

    _gwy_transform_line_to_target(dataview, xy[0], xy[1], xy[2], xy[3], &x0, &y0, &x1, &y1);
    gwy_cairo_draw_or_add_thin_line(cr, region, x0, y0, x1, y1);

    gdouble lscr = sqrt((x1 - x0)*(x1 - x0) + (y1 - y0)*(y1 - y0)) + 1e-9;
    gdouble thickness = priv->thickness;
    if (thickness > 0.0 && lscr > 0.0) {
        gdouble cosphi = (y1 - y0)/lscr, sinphi = (x0 - x1)/lscr;
        gdouble tx = 0.5*thickness*cosphi * gwy_data_view_get_xzoom(dataview);
        gdouble ty = 0.5*thickness*sinphi * gwy_data_view_get_yzoom(dataview);

        gwy_cairo_draw_or_add_thin_line(cr, region, x0 - tx, y0 - ty, x0 + tx, y0 + ty);
        gwy_cairo_draw_or_add_thin_line(cr, region, x1 - tx, y1 - ty, x1 + tx, y1 + ty);
    }

    if (priv->center_tick && lscr > 0.0) {
        gdouble cosphi = (y1 - y0)/lscr, sinphi = (x0 - x1)/lscr;
        gdouble xc = 0.5*(x0 + x1), yc = 0.5*(y0 + y1);
        gdouble tx = 0.5*CROSS_SIZE*cosphi, ty = 0.5*CROSS_SIZE*sinphi;

        gwy_cairo_draw_or_add_thin_line(cr, region, xc - tx, yc - ty, xc + tx, yc + ty);
    }

    if (priv->numbered) {
        PangoLayout *layout = gwy_vector_layer_ensure_layout(layer);
        g_return_if_fail(layout);

        gchar buf[16];
        g_snprintf(buf, sizeof(buf), "%u", i+1);
        pango_layout_set_text(layout, buf, -1);
        gdouble xt = 0.5*(x0 + x1), yt = 0.5*(y0 + y1);
        gdouble xfrom = x0, xto = x1, yfrom = y0, yto = y1;
        if (xfrom > xto) {
            GWY_SWAP(gdouble, xfrom, xto);
            GWY_SWAP(gdouble, yfrom, yto);
        }
        gdouble cosphi = (xto - xfrom)/lscr, sinphi = (yto - yfrom)/lscr;

        PangoRectangle rect;
        pango_layout_get_pixel_extents(layout, &rect, NULL);
        gdouble lrect = hypot(rect.width, rect.height);

        xt -= 0.5*(rect.width + lrect*sinphi);
        yt -= 0.5*(rect.height - lrect*cosphi);
        gwy_cairo_draw_or_add_layout(cr, region, layout, xt, yt);
    }
}

static void
invalidate_object(GwyVectorLayer *layer,
                  cairo_region_t *region,
                  const gdouble *xy)
{
    GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
    g_return_if_fail(GWY_IS_DATA_VIEW(parent));
    draw_or_invalidate(layer, NULL, GWY_DATA_VIEW(parent), region, xy, 9999);
}

static void
set_cursor_according_to_proximity(GwyVectorLayer *layer, gdouble xreal, gdouble yreal)
{
    const gchar *name = NULL;

    if (!name && near_endpoint(layer, xreal, yreal) >= 0)
        name = "move";
    if (!name && near_line(layer, xreal, yreal) >= 0)
        name = "grab";
    gwy_data_view_set_named_cursor(GWY_DATA_VIEW(gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer))), name);
}

static void
update_line_coordinates(GwyVectorLayer *layer, gint iobj,
                        gdouble x, gdouble y, gdouble *xy)
{
    GwySelection *selection = gwy_vector_layer_get_selection(layer);
    GwyLayerLinePrivate *priv = GWY_LAYER_LINE(layer)->priv;
    GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
    g_return_if_fail(GWY_IS_DATA_VIEW(parent));
    GwyDataView *dataview = GWY_DATA_VIEW(parent);
    gdouble rect[4] = { 0.0, 0.0, 0.0, 0.0 };
    gwy_data_view_get_real_data_sizes(dataview, rect+2, rect+3);

    gwy_selection_get_object(selection, iobj, xy);
    if (priv->moving_whole_line) {
        /* The user may be trying to move the line outside the image. The natural-feeling restriction is to consider
         * all possible cursor positions which would not still result in line being inside the rectangle. This is
         * rect[] with some margins subtracted on all sides. And now we choose the closest point to the cursor from
         * this rectangle of valid positions – which is done by simple clamping. */
        gwy_assign(xy, priv->linemove_before, OBJECT_SIZE);
        gdouble ml = priv->linemove_x0 - fmin(xy[0], xy[2]), mr = fmax(xy[0], xy[2]) - priv->linemove_x0;
        gdouble mt = priv->linemove_y0 - fmin(xy[1], xy[3]), mb = fmax(xy[1], xy[3]) - priv->linemove_y0;
        x = fmin(fmax(x, rect[0] + ml), rect[2] - mr);
        y = fmin(fmax(y, rect[1] + mt), rect[3] - mb);
        gdouble vx = x - priv->linemove_x0, vy = y - priv->linemove_y0;
        xy[0] += vx;
        xy[1] += vy;
        xy[2] += vx;
        xy[3] += vy;
    }
    else {
        gint endpoint = priv->moving_endpoint;
        g_assert(endpoint == 0 || endpoint == 1);
        xy[2*endpoint + 0] = x;
        xy[2*endpoint + 1] = y;
        priv->constrain_to_15deg = gwy_vector_layer_get_modifiers(layer) & GDK_SHIFT_MASK;
        enforce_constraints(dataview, priv->constrain_to_15deg, endpoint, xy);
    }
    /* This should not change anything, but it gets rid of occasional small rounding errors moving the point outside
     * the image. */
    xy[0] = fmin(fmax(xy[0], rect[0]), rect[2]);
    xy[1] = fmin(fmax(xy[1], rect[1]), rect[3]);
    xy[2] = fmin(fmax(xy[2], rect[0]), rect[2]);
    xy[3] = fmin(fmax(xy[3], rect[1]), rect[3]);
}

static void
button_pressed(GwyVectorLayer *layer, gdouble x, gdouble y)
{
    gint button = gwy_vector_layer_get_current_button(layer);
    if (button != 1)
        return;

    GwySelection *selection = gwy_vector_layer_get_selection(layer);
    GwyLayerLinePrivate *priv = GWY_LAYER_LINE(layer)->priv;
    GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
    g_return_if_fail(GWY_IS_DATA_VIEW(parent));
    GwyDataView *dataview = GWY_DATA_VIEW(parent);

    /* Do nothing when we are outside. */
    if (!gwy_data_view_coords_widget_clamp(dataview, &x, &y))
        return;

    gdouble xreal, yreal;
    gwy_data_view_coords_widget_to_real(dataview, x, y, &xreal, &yreal);
    priv->constrain_to_15deg = gwy_vector_layer_get_modifiers(layer) & GDK_SHIFT_MASK;

    gint iobj = -1, iln = -1, ipt = near_endpoint(layer, xreal, yreal);
    if (ipt >= 0)
        iobj = ipt/2;
    else {
        iln = near_line(layer, xreal, yreal);
        if (iln >= 0)
            iobj = iln;
    }
    if (just_choose_object(layer, iobj))
        return;

    gdouble xy[OBJECT_SIZE];
    if (iobj >= 0) {
        gwy_vector_layer_set_current_object(layer, iobj);
        gwy_selection_get_object(selection, iobj, xy);
    }

    if (ipt >= 0) {
        /* Moving an endpoint of an existing line. */
        priv->moving_whole_line = FALSE;
        priv->moving_endpoint = ipt % 2;
        xy[2*priv->moving_endpoint + 0] = xreal;
        xy[2*priv->moving_endpoint + 1] = yreal;
    }
    else if (iln >= 0) {
        /* Moving an entire existing line. */
        priv->moving_whole_line = TRUE;
        gwy_assign(priv->linemove_before, xy, OBJECT_SIZE);
        priv->linemove_x0 = xreal;
        priv->linemove_y0 = yreal;
    }
    else {
        /* Starting a new line. */
        priv->moving_whole_line = FALSE;
        iobj = -1;
        xy[2] = xy[0] = xreal;
        xy[3] = xy[1] = yreal;
    }
    if (!priv->moving_whole_line) {
        enforce_constraints(dataview, priv->constrain_to_15deg, priv->moving_endpoint, xy);
        iobj = gwy_vector_layer_update_object(layer, iobj, xy);
    }
    gwy_vector_layer_set_current_object(layer, iobj);

    /* FIXME */
    gwy_data_view_set_named_cursor(dataview, "crosshair");
}

static void
pointer_moved(GwyVectorLayer *layer, gdouble x, gdouble y)
{
    if (!gwy_vector_layer_get_editable(layer))
        return;

    GwySelection *selection = gwy_vector_layer_get_selection(layer);
    if (!selection)
        return;

    //GwyLayerLinePrivate *priv = GWY_LAYER_LINE(layer)->priv;
    GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
    g_return_if_fail(GWY_IS_DATA_VIEW(parent));
    GwyDataView *dataview = GWY_DATA_VIEW(parent);

    gwy_data_view_coords_widget_clamp(dataview, &x, &y);
    gdouble xreal, yreal, xy[OBJECT_SIZE];
    gwy_data_view_coords_widget_to_real(dataview, x, y, &xreal, &yreal);

    if (!gwy_vector_layer_get_current_button(layer)) {
        set_cursor_according_to_proximity(layer, xreal, yreal);
        return;
    }

    gint iobj = gwy_vector_layer_get_current_object(layer);
    if (iobj < 0)
        return;

    update_line_coordinates(layer, iobj, xreal, yreal, xy);
    gwy_vector_layer_update_object(layer, iobj, xy);
}

static void
button_released(GwyVectorLayer *layer, gdouble x, gdouble y)
{
    gint button = gwy_vector_layer_get_current_button(layer);
    gint iobj = gwy_vector_layer_get_current_object(layer);
    if (button != 1 || iobj < 0)
        return;

    GwySelection *selection = gwy_vector_layer_get_selection(layer);
    GwyLayerLinePrivate *priv = GWY_LAYER_LINE(layer)->priv;
    GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
    g_return_if_fail(GWY_IS_DATA_VIEW(parent));
    GwyDataView *dataview = GWY_DATA_VIEW(parent);

    gdouble xreal, yreal;
    gwy_data_view_coords_widget_to_real(dataview, x, y, &xreal, &yreal);
    gdouble xy[OBJECT_SIZE];
    update_line_coordinates(layer, iobj, xreal, yreal, xy);

    if (!priv->moving_whole_line) {
        gint endpoint = priv->moving_endpoint;
        gdouble xother, yother;
        gwy_data_view_coords_real_to_widget(dataview, xy[endpoint + 0], xy[endpoint + 1], &xother, &yother);
        /* FIXME: This is kind of logically wrong. We should discard the selection based on *image* pixels, not screen
         * pixels. */
        if (fabs(x - xother) < 0.1 || fabs(y - yother) < 0.1) {
            gwy_selection_delete_object(selection, iobj);
            iobj = -1;
        }
    }
    if (iobj >= 0)
        gwy_vector_layer_update_object(layer, iobj, xy);

    gwy_vector_layer_set_current_object(layer, -1);
    set_cursor_according_to_proximity(layer, xreal, yreal);
    gwy_selection_finished(selection);
}

/* TODO: Later */
#if 0
static gboolean
key_pressed(GwyVectorLayer *layer, GdkEventKey *event)
{
    gboolean large_step = (event->state & (GDK_CONTROL_MASK | GDK_MOD1_MASK));
    gint which = ((event->state & GDK_SHIFT_MASK) ? 2 : 0);
    GwyDataView *dataview;
    guint keyval = event->keyval;
    gint chosen = priv->chosen, xcurr, ycurr, xnew, ynew, move_distance;
    gdouble xy[4];

    if (chosen < 0
        || chosen >= gwy_selection_get_n_objects(priv->selection))
        return FALSE;

    if (keyval != GDK_KEY_Left && keyval != GDK_KEY_Right
        && keyval != GDK_KEY_Up && keyval != GDK_KEY_Down)
        return FALSE;

    dataview = GWY_DATA_VIEW(GWY_DATA_VIEW_LAYER(layer)->parent);
    g_return_val_if_fail(dataview, FALSE);

    gwy_selection_get_object(priv->selection, chosen, xy);
    gwy_data_view_coords_real_to_widget(dataview, xy[which], xy[which+1], &xcurr, &ycurr);
    xnew = xcurr;
    ynew = ycurr;
    move_distance = (large_step ? 16 : 1);
    if (keyval == GDK_KEY_Left)
        xnew -= move_distance;
    else if (keyval == GDK_KEY_Right)
        xnew += move_distance;
    else if (keyval == GDK_KEY_Up)
        ynew -= move_distance;
    else if (keyval == GDK_KEY_Down)
        ynew += move_distance;
    gwy_data_view_coords_widget_clamp(dataview, &xnew, &ynew);

    if (xnew != xcurr || ynew != ycurr) {
        gwy_data_view_coords_widget_to_real(dataview, xnew, ynew, xy+which, xy+which+1);
        gwy_selection_set_object(priv->selection, chosen, xy);
    }

    return TRUE;
}
#endif

/**
 * gwy_layer_line_new:
 *
 * Creates a new line vector layer.
 *
 * Returns: A newly created line vector layer.
 **/
GwyVectorLayer*
gwy_layer_line_new(void)
{
    return (GwyVectorLayer*)g_object_new(GWY_TYPE_LAYER_LINE, NULL);
}

/**
 * gwy_layer_line_set_numbered:
 * @layer: A line vector layer.
 * @numbered: %TRUE to show line numbers; %FALSE to not show line numbers.
 *
 * Sets whether a line vector layer has visibly numbered lines.
 **/
void
gwy_layer_line_set_numbered(GwyLayerLine *layer,
                            gboolean numbered)
{
    g_return_if_fail(GWY_IS_LAYER_LINE(layer));

    GwyLayerLinePrivate *priv = layer->priv;
    if (!numbered == !priv->numbered)
        return;

    priv->numbered = !!numbered;
    g_object_notify_by_pspec(G_OBJECT(layer), properties[PROP_NUMBERED]);
    gwy_data_view_layer_updated(GWY_DATA_VIEW_LAYER(layer));
}

/**
 * gwy_layer_line_get_numbered:
 * @layer: A line vector layer.
 *
 * Reports whether a line vector layer has visibly numbered lines.
 *
 * Returns: %TRUE if line numbers are shown; %FALSE if line numbers are not shown.
 **/
gboolean
gwy_layer_line_get_numbered(GwyLayerLine *layer)
{
    g_return_val_if_fail(GWY_IS_LAYER_LINE(layer), FALSE);
    return layer->priv->numbered;
}

/**
 * gwy_layer_line_set_center_tick:
 * @layer: A line vector layer.
 * @center_tick: %TRUE to draw a tick in the centre; %FALSE to not draw it.
 *
 * Sets whether a tick is drawn in line centres in a line vector layer.
 **/
void
gwy_layer_line_set_center_tick(GwyLayerLine *layer,
                               gboolean center_tick)
{
    g_return_if_fail(GWY_IS_LAYER_LINE(layer));

    GwyLayerLinePrivate *priv = layer->priv;
    if (!center_tick == !priv->center_tick)
        return;

    priv->center_tick = !!center_tick;
    g_object_notify_by_pspec(G_OBJECT(layer), properties[PROP_CENTER_TICK]);
    gwy_data_view_layer_updated(GWY_DATA_VIEW_LAYER(layer));
}

/**
 * gwy_layer_line_get_center_tick:
 * @layer: A line vector layer.
 *
 * Reports whether a tick is drawn in line centres in a line vector layer.
 *
 * Returns: %TRUE if a tick is drawn in the centre; %FALSE if it is not drawn.
 **/
gboolean
gwy_layer_line_get_center_tick(GwyLayerLine *layer)
{
    g_return_val_if_fail(GWY_IS_LAYER_LINE(layer), FALSE);
    return layer->priv->center_tick;
}

/**
 * gwy_layer_line_set_thickness:
 * @layer: A line vector layer.
 * @thickness: Marker size, denoting line thickness.
 *
 * Sets the size of markers denoting line thickness.
 **/
void
gwy_layer_line_set_thickness(GwyLayerLine *layer, gdouble thickness)
{
    g_return_if_fail(GWY_IS_LAYER_LINE(layer));
    g_return_if_fail(thickness >= 0.0);

    GwyLayerLinePrivate *priv = layer->priv;
    if (thickness == priv->thickness)
        return;

    priv->thickness = thickness;
    g_object_notify_by_pspec(G_OBJECT(layer), properties[PROP_THICKNESS]);
    gwy_data_view_layer_updated(GWY_DATA_VIEW_LAYER(layer));
}

/**
 * gwy_layer_line_get_thickness:
 * @layer: A line vector layer.
 *
 * Gets the size of markers denoting line thickness.
 *
 * Returns: Line thickness.
 **/
gdouble
gwy_layer_line_get_thickness(GwyLayerLine *layer)
{
    g_return_val_if_fail(GWY_IS_LAYER_LINE(layer), 0.0);
    return layer->priv->thickness;
}

static gint
near_line(GwyVectorLayer *layer, gdouble xreal, gdouble yreal)
{
    gdouble d2min;
    gint i = gwy_vector_layer_find_nearest(layer, gwy_math_find_nearest_line, 1, xreal, yreal, &d2min);
    return (d2min > PROXIMITY_DISTANCE*PROXIMITY_DISTANCE) ? -1 : i;
}

static gint
near_endpoint(GwyVectorLayer *layer, gdouble xreal, gdouble yreal)
{
    gdouble d2min;
    gint i = gwy_vector_layer_find_nearest(layer, gwy_math_find_nearest_point, 2, xreal, yreal, &d2min);
    return (d2min > PROXIMITY_DISTANCE*PROXIMITY_DISTANCE) ? -1 : i;
}

static void
enforce_constraints(GwyDataView *dataview, gboolean multiple_of_15deg, gint endpoint,
                    gdouble *xy)
{
    if (multiple_of_15deg) {
        gint ifixed = 2*(1 - endpoint), imoving = 2*endpoint;
        gdouble x0scr, y0scr, x1scr, y1scr;
        gwy_data_view_coords_real_to_widget(dataview, xy[ifixed+0], xy[ifixed+1], &x0scr, &y0scr);
        gwy_data_view_coords_real_to_widget(dataview, xy[imoving+0], xy[imoving+1], &x1scr, &y1scr);
        gdouble scrdx = x1scr - x0scr, scrdy = y1scr - y0scr;
        gdouble scrlen = sqrt(scrdx*scrdx + scrdy*scrdy);
        gdouble phi = atan2(scrdy, scrdx);
        phi = GWY_ROUND(phi/(G_PI/12.0)) * G_PI/12.0;
        x1scr = x0scr + scrlen*cos(phi);
        y1scr = y0scr + scrlen*sin(phi);
        // FIXME: Not working as we need.
        // gwy_data_view_coords_widget_cut_line(dataview, &x0scr, &y0scr, &x1scr, &y1scr);
        gwy_data_view_coords_widget_to_real(dataview, x1scr, y1scr, xy + imoving+0, xy + imoving+1);
    }
}

/**
 * SECTION:layer-line
 * @title: GwyLayerLine
 * @short_description: Data view layer for arbitrary line selections
 *
 * #GwyLayerLine allows selection of lines oriented in arbitrary directions. It uses #GwySelectionLine selection type.
 **/

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