shumate/shumate-path-layer.c
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /* | ||
| 2 | * Copyright (C) 2008-2009 Pierre-Luc Beaudoin <pierre-luc@pierlux.com> | ||
| 3 | * Copyright (C) 2011-2013 Jiri Techet <techet@gmail.com> | ||
| 4 | * Copyright (C) 2019 Marcus Lundblad <ml@update.uu.se> | ||
| 5 | * | ||
| 6 | * This library is free software; you can redistribute it and/or | ||
| 7 | * modify it under the terms of the GNU Lesser General Public | ||
| 8 | * License as published by the Free Software Foundation; either | ||
| 9 | * version 2.1 of the License, or (at your option) any later version. | ||
| 10 | * | ||
| 11 | * This library is distributed in the hope that it will be useful, | ||
| 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
| 14 | * Lesser General Public License for more details. | ||
| 15 | * | ||
| 16 | * You should have received a copy of the GNU Lesser General Public | ||
| 17 | * License along with this library; if not, write to the Free Software | ||
| 18 | * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA | ||
| 19 | */ | ||
| 20 | |||
| 21 | /** | ||
| 22 | * ShumatePathLayer: | ||
| 23 | * | ||
| 24 | * A layer displaying line path between inserted [iface@Location] objects | ||
| 25 | * | ||
| 26 | * This layer shows a connection between inserted objects implementing the | ||
| 27 | * [iface@Location] interface. This means that both [class@Marker] | ||
| 28 | * objects and [class@Coordinate] objects can be inserted into the layer. | ||
| 29 | * Of course, custom objects implementing the [iface@Location] interface | ||
| 30 | * can be used as well. | ||
| 31 | */ | ||
| 32 | |||
| 33 | #include "shumate-path-layer.h" | ||
| 34 | |||
| 35 | #include "shumate-enum-types.h" | ||
| 36 | |||
| 37 | #include <gdk/gdk.h> | ||
| 38 | #include <gtk/gtk.h> | ||
| 39 | #include <glib.h> | ||
| 40 | |||
| 41 | enum | ||
| 42 | { | ||
| 43 | PROP_CLOSED_PATH = 1, | ||
| 44 | PROP_STROKE_WIDTH, | ||
| 45 | PROP_STROKE_COLOR, | ||
| 46 | PROP_FILL, | ||
| 47 | PROP_FILL_COLOR, | ||
| 48 | PROP_STROKE, | ||
| 49 | PROP_OUTLINE_WIDTH, | ||
| 50 | PROP_OUTLINE_COLOR, | ||
| 51 | N_PROPERTIES | ||
| 52 | }; | ||
| 53 | |||
| 54 | static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, }; | ||
| 55 | |||
| 56 | static GdkRGBA DEFAULT_FILL_COLOR = { 0.8, 0.0, 0.0, 0.67 }; | ||
| 57 | static GdkRGBA DEFAULT_STROKE_COLOR = { 0.64, 0.0, 0.0, 1.0 }; | ||
| 58 | static GdkRGBA DEFAULT_OUTLINE_COLOR = { 1.0, 0.8, 0.8, 1.0 }; | ||
| 59 | |||
| 60 | struct _ShumatePathLayer | ||
| 61 | { | ||
| 62 | ShumateLayer parent_instance; | ||
| 63 | |||
| 64 | gboolean closed_path; | ||
| 65 | GdkRGBA *stroke_color; | ||
| 66 | gboolean fill; | ||
| 67 | GdkRGBA *fill_color; | ||
| 68 | gboolean stroke; | ||
| 69 | double stroke_width; | ||
| 70 | GdkRGBA *outline_color; | ||
| 71 | double outline_width; | ||
| 72 | GArray *dashes; /* float */ | ||
| 73 | |||
| 74 | GList *nodes; /* ShumateLocation */ | ||
| 75 | }; | ||
| 76 | |||
| 77 |
4/6shumate_path_layer_class_intern_init:
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
shumate_path_layer_get_type:
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 7 taken 5 times.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 7 not taken.
|
11 | G_DEFINE_TYPE (ShumatePathLayer, shumate_path_layer, SHUMATE_TYPE_LAYER); |
| 78 | |||
| 79 | static void | ||
| 80 | ✗ | on_viewport_changed (ShumatePathLayer *self, | |
| 81 | GParamSpec *pspec, | ||
| 82 | ShumateViewport *view) | ||
| 83 | { | ||
| 84 | ✗ | g_assert (SHUMATE_IS_PATH_LAYER (self)); | |
| 85 | |||
| 86 | ✗ | gtk_widget_queue_draw (GTK_WIDGET (self)); | |
| 87 | ✗ | } | |
| 88 | |||
| 89 | |||
| 90 | static void | ||
| 91 | ✗ | shumate_path_layer_get_property (GObject *object, | |
| 92 | guint property_id, | ||
| 93 | G_GNUC_UNUSED GValue *value, | ||
| 94 | GParamSpec *pspec) | ||
| 95 | { | ||
| 96 | ✗ | ShumatePathLayer *self = SHUMATE_PATH_LAYER (object); | |
| 97 | |||
| 98 | ✗ | switch (property_id) | |
| 99 | { | ||
| 100 | ✗ | case PROP_CLOSED_PATH: | |
| 101 | ✗ | g_value_set_boolean (value, self->closed_path); | |
| 102 | ✗ | break; | |
| 103 | |||
| 104 | ✗ | case PROP_FILL: | |
| 105 | ✗ | g_value_set_boolean (value, self->fill); | |
| 106 | ✗ | break; | |
| 107 | |||
| 108 | ✗ | case PROP_STROKE: | |
| 109 | ✗ | g_value_set_boolean (value, self->stroke); | |
| 110 | ✗ | break; | |
| 111 | |||
| 112 | ✗ | case PROP_FILL_COLOR: | |
| 113 | ✗ | g_value_set_boxed (value, self->fill_color); | |
| 114 | ✗ | break; | |
| 115 | |||
| 116 | ✗ | case PROP_STROKE_COLOR: | |
| 117 | ✗ | g_value_set_boxed (value, self->stroke_color); | |
| 118 | ✗ | break; | |
| 119 | |||
| 120 | ✗ | case PROP_STROKE_WIDTH: | |
| 121 | ✗ | g_value_set_double (value, self->stroke_width); | |
| 122 | ✗ | break; | |
| 123 | |||
| 124 | ✗ | case PROP_OUTLINE_COLOR: | |
| 125 | ✗ | g_value_set_boxed (value, self->outline_color); | |
| 126 | ✗ | break; | |
| 127 | |||
| 128 | ✗ | case PROP_OUTLINE_WIDTH: | |
| 129 | ✗ | g_value_set_double (value, self->outline_width); | |
| 130 | ✗ | break; | |
| 131 | |||
| 132 | ✗ | default: | |
| 133 | ✗ | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); | |
| 134 | } | ||
| 135 | ✗ | } | |
| 136 | |||
| 137 | |||
| 138 | static void | ||
| 139 | ✗ | shumate_path_layer_set_property (GObject *object, | |
| 140 | guint property_id, | ||
| 141 | const GValue *value, | ||
| 142 | GParamSpec *pspec) | ||
| 143 | { | ||
| 144 | ✗ | switch (property_id) | |
| 145 | { | ||
| 146 | ✗ | case PROP_CLOSED_PATH: | |
| 147 | ✗ | shumate_path_layer_set_closed (SHUMATE_PATH_LAYER (object), | |
| 148 | g_value_get_boolean (value)); | ||
| 149 | ✗ | break; | |
| 150 | |||
| 151 | ✗ | case PROP_FILL: | |
| 152 | ✗ | shumate_path_layer_set_fill (SHUMATE_PATH_LAYER (object), | |
| 153 | g_value_get_boolean (value)); | ||
| 154 | ✗ | break; | |
| 155 | |||
| 156 | ✗ | case PROP_STROKE: | |
| 157 | ✗ | shumate_path_layer_set_stroke (SHUMATE_PATH_LAYER (object), | |
| 158 | g_value_get_boolean (value)); | ||
| 159 | ✗ | break; | |
| 160 | |||
| 161 | ✗ | case PROP_FILL_COLOR: | |
| 162 | ✗ | shumate_path_layer_set_fill_color (SHUMATE_PATH_LAYER (object), | |
| 163 | ✗ | g_value_get_boxed (value)); | |
| 164 | ✗ | break; | |
| 165 | |||
| 166 | ✗ | case PROP_STROKE_COLOR: | |
| 167 | ✗ | shumate_path_layer_set_stroke_color (SHUMATE_PATH_LAYER (object), | |
| 168 | ✗ | g_value_get_boxed (value)); | |
| 169 | ✗ | break; | |
| 170 | |||
| 171 | ✗ | case PROP_STROKE_WIDTH: | |
| 172 | ✗ | shumate_path_layer_set_stroke_width (SHUMATE_PATH_LAYER (object), | |
| 173 | ✗ | g_value_get_double (value)); | |
| 174 | ✗ | break; | |
| 175 | |||
| 176 | ✗ | case PROP_OUTLINE_COLOR: | |
| 177 | ✗ | shumate_path_layer_set_outline_color (SHUMATE_PATH_LAYER (object), | |
| 178 | ✗ | g_value_get_boxed (value)); | |
| 179 | ✗ | break; | |
| 180 | |||
| 181 | ✗ | case PROP_OUTLINE_WIDTH: | |
| 182 | ✗ | shumate_path_layer_set_outline_width (SHUMATE_PATH_LAYER (object), | |
| 183 | ✗ | g_value_get_double (value)); | |
| 184 | ✗ | break; | |
| 185 | |||
| 186 | ✗ | default: | |
| 187 | ✗ | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); | |
| 188 | } | ||
| 189 | ✗ | } | |
| 190 | |||
| 191 | |||
| 192 | static void | ||
| 193 | 4 | shumate_path_layer_dispose (GObject *object) | |
| 194 | { | ||
| 195 | 4 | ShumatePathLayer *self = SHUMATE_PATH_LAYER (object); | |
| 196 | 4 | ShumateViewport *viewport = shumate_layer_get_viewport (SHUMATE_LAYER (self)); | |
| 197 | |||
| 198 | 4 | g_signal_handlers_disconnect_by_data (viewport, self); | |
| 199 | |||
| 200 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 4 times.
|
4 | if (self->nodes) |
| 201 | ✗ | shumate_path_layer_remove_all (SHUMATE_PATH_LAYER (object)); | |
| 202 | |||
| 203 | 4 | G_OBJECT_CLASS (shumate_path_layer_parent_class)->dispose (object); | |
| 204 | 4 | } | |
| 205 | |||
| 206 | static void | ||
| 207 | 6 | shumate_path_layer_constructed (GObject *object) | |
| 208 | { | ||
| 209 | 6 | ShumatePathLayer *self = SHUMATE_PATH_LAYER (object); | |
| 210 | 6 | ShumateViewport *viewport; | |
| 211 | |||
| 212 | 6 | G_OBJECT_CLASS (shumate_path_layer_parent_class)->constructed (object); | |
| 213 | |||
| 214 | 6 | viewport = shumate_layer_get_viewport (SHUMATE_LAYER (self)); | |
| 215 | 6 | g_signal_connect_swapped (viewport, "notify", G_CALLBACK (on_viewport_changed), self); | |
| 216 | 6 | } | |
| 217 | |||
| 218 | |||
| 219 | static void | ||
| 220 | 4 | shumate_path_layer_finalize (GObject *object) | |
| 221 | { | ||
| 222 | 4 | ShumatePathLayer *self = SHUMATE_PATH_LAYER (object); | |
| 223 | |||
| 224 |
1/2✓ Branch 2 → 3 taken 4 times.
✗ Branch 2 → 4 not taken.
|
4 | g_clear_pointer (&self->stroke_color, gdk_rgba_free); |
| 225 |
1/2✓ Branch 4 → 5 taken 4 times.
✗ Branch 4 → 6 not taken.
|
4 | g_clear_pointer (&self->outline_color, gdk_rgba_free); |
| 226 |
1/2✓ Branch 6 → 7 taken 4 times.
✗ Branch 6 → 8 not taken.
|
4 | g_clear_pointer (&self->fill_color, gdk_rgba_free); |
| 227 |
1/2✓ Branch 8 → 9 taken 4 times.
✗ Branch 8 → 10 not taken.
|
4 | g_clear_pointer (&self->dashes, g_array_unref); |
| 228 | |||
| 229 | 4 | G_OBJECT_CLASS (shumate_path_layer_parent_class)->finalize (object); | |
| 230 | 4 | } | |
| 231 | |||
| 232 | static void | ||
| 233 | ✗ | shumate_path_layer_snapshot (GtkWidget *widget, | |
| 234 | GtkSnapshot *snapshot) | ||
| 235 | { | ||
| 236 | ✗ | ShumatePathLayer *self = (ShumatePathLayer *)widget; | |
| 237 | ✗ | ShumateViewport *viewport; | |
| 238 | ✗ | g_autoptr(GskPathBuilder) builder = NULL; | |
| 239 | ✗ | g_autoptr(GskPath) path = NULL; | |
| 240 | ✗ | int width, height; | |
| 241 | ✗ | gboolean first = TRUE; | |
| 242 | ✗ | GList *elem; | |
| 243 | |||
| 244 | ✗ | width = gtk_widget_get_width (widget); | |
| 245 | ✗ | height = gtk_widget_get_height (widget); | |
| 246 | ✗ | viewport = shumate_layer_get_viewport (SHUMATE_LAYER (self)); | |
| 247 | |||
| 248 | ✗ | if (!gtk_widget_get_visible (widget) || width <= 0 || height <= 0) | |
| 249 | return; | ||
| 250 | |||
| 251 | ✗ | builder = gsk_path_builder_new (); | |
| 252 | ✗ | for (elem = self->nodes; elem != NULL; elem = elem->next) | |
| 253 | { | ||
| 254 | ✗ | ShumateLocation *location = SHUMATE_LOCATION (elem->data); | |
| 255 | ✗ | double x, y, lat, lon; | |
| 256 | |||
| 257 | ✗ | lat = shumate_location_get_latitude (location); | |
| 258 | ✗ | lon = shumate_location_get_longitude (location); | |
| 259 | ✗ | shumate_viewport_location_to_widget_coords (viewport, widget, lat, lon, &x, &y); | |
| 260 | |||
| 261 | ✗ | if (first) | |
| 262 | { | ||
| 263 | ✗ | gsk_path_builder_move_to (builder, x, y); | |
| 264 | ✗ | first = FALSE; | |
| 265 | } | ||
| 266 | else | ||
| 267 | { | ||
| 268 | ✗ | gsk_path_builder_line_to (builder, x, y); | |
| 269 | } | ||
| 270 | } | ||
| 271 | |||
| 272 | ✗ | if (first) | |
| 273 | ✗ | return; | |
| 274 | |||
| 275 | ✗ | if (self->closed_path) | |
| 276 | ✗ | gsk_path_builder_close (builder); | |
| 277 | |||
| 278 | ✗ | path = gsk_path_builder_free_to_path (g_steal_pointer (&builder)); | |
| 279 | |||
| 280 | ✗ | if (self->fill) | |
| 281 | ✗ | gtk_snapshot_append_fill (snapshot, path, | |
| 282 | GSK_FILL_RULE_WINDING, | ||
| 283 | ✗ | self->fill_color); | |
| 284 | |||
| 285 | ✗ | if (self->stroke) | |
| 286 | { | ||
| 287 | /* width of the backgroud-colored part of the stroke, | ||
| 288 | * will be reduced by the outline, when that is set (non-zero) | ||
| 289 | */ | ||
| 290 | ✗ | double inner_width = self->stroke_width - 2 * self->outline_width; | |
| 291 | |||
| 292 | ✗ | if (self->outline_width > 0) | |
| 293 | { | ||
| 294 | ✗ | g_autoptr(GskStroke) stroke = gsk_stroke_new (self->stroke_width); | |
| 295 | |||
| 296 | ✗ | gsk_stroke_set_line_join (stroke, GSK_LINE_JOIN_BEVEL); | |
| 297 | ✗ | gsk_stroke_set_dash (stroke, | |
| 298 | ✗ | (const float *) self->dashes->data, | |
| 299 | ✗ | self->dashes->len); | |
| 300 | |||
| 301 | ✗ | gtk_snapshot_append_stroke (snapshot, path, stroke, | |
| 302 | ✗ | self->outline_color); | |
| 303 | } | ||
| 304 | |||
| 305 | ✗ | if (inner_width > 0) | |
| 306 | { | ||
| 307 | ✗ | g_autoptr(GskStroke) stroke = gsk_stroke_new (inner_width); | |
| 308 | |||
| 309 | ✗ | gsk_stroke_set_line_join (stroke, GSK_LINE_JOIN_BEVEL); | |
| 310 | ✗ | gsk_stroke_set_dash (stroke, | |
| 311 | ✗ | (const float *) self->dashes->data, | |
| 312 | ✗ | self->dashes->len); | |
| 313 | |||
| 314 | ✗ | gtk_snapshot_append_stroke (snapshot, path, stroke, | |
| 315 | ✗ | self->stroke_color); | |
| 316 | } | ||
| 317 | } | ||
| 318 | } | ||
| 319 | |||
| 320 | static char * | ||
| 321 | ✗ | shumate_path_layer_get_debug_text (ShumateLayer *layer) | |
| 322 | { | ||
| 323 | ✗ | ShumatePathLayer *self = SHUMATE_PATH_LAYER (layer); | |
| 324 | ✗ | return g_strdup_printf ("%d nodes", g_list_length (self->nodes)); | |
| 325 | } | ||
| 326 | |||
| 327 | static void | ||
| 328 | 2 | shumate_path_layer_class_init (ShumatePathLayerClass *klass) | |
| 329 | { | ||
| 330 | 2 | GObjectClass *object_class = G_OBJECT_CLASS (klass); | |
| 331 | 2 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); | |
| 332 | 2 | ShumateLayerClass *layer_class = SHUMATE_LAYER_CLASS (klass); | |
| 333 | |||
| 334 | 2 | object_class->finalize = shumate_path_layer_finalize; | |
| 335 | 2 | object_class->dispose = shumate_path_layer_dispose; | |
| 336 | 2 | object_class->constructed = shumate_path_layer_constructed; | |
| 337 | 2 | object_class->get_property = shumate_path_layer_get_property; | |
| 338 | 2 | object_class->set_property = shumate_path_layer_set_property; | |
| 339 | |||
| 340 | 2 | widget_class->snapshot = shumate_path_layer_snapshot; | |
| 341 | |||
| 342 | 2 | layer_class->get_debug_text = shumate_path_layer_get_debug_text; | |
| 343 | |||
| 344 | /** | ||
| 345 | * ShumatePathLayer:closed: | ||
| 346 | * | ||
| 347 | * The shape is a closed path | ||
| 348 | */ | ||
| 349 | 4 | obj_properties[PROP_CLOSED_PATH] = | |
| 350 | 2 | g_param_spec_boolean ("closed", NULL, NULL, | |
| 351 | FALSE, | ||
| 352 | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); | ||
| 353 | |||
| 354 | /** | ||
| 355 | * ShumatePathLayer:fill: | ||
| 356 | * | ||
| 357 | * The shape should be filled | ||
| 358 | */ | ||
| 359 | 4 | obj_properties[PROP_FILL] = | |
| 360 | 2 | g_param_spec_boolean ("fill", NULL, NULL, | |
| 361 | FALSE, | ||
| 362 | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); | ||
| 363 | |||
| 364 | /** | ||
| 365 | * ShumatePathLayer:stroke: | ||
| 366 | * | ||
| 367 | * The shape should be stroked | ||
| 368 | */ | ||
| 369 | 4 | obj_properties[PROP_STROKE] = | |
| 370 | 2 | g_param_spec_boolean ("stroke", NULL, NULL, | |
| 371 | TRUE, | ||
| 372 | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); | ||
| 373 | |||
| 374 | /** | ||
| 375 | * ShumatePathLayer:stroke-color: | ||
| 376 | * | ||
| 377 | * The path's stroke color | ||
| 378 | */ | ||
| 379 | 4 | obj_properties[PROP_STROKE_COLOR] = | |
| 380 | 2 | g_param_spec_boxed ("stroke-color", NULL, NULL, | |
| 381 | GDK_TYPE_RGBA, | ||
| 382 | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); | ||
| 383 | |||
| 384 | /** | ||
| 385 | * ShumatePathLayer:fill-color: | ||
| 386 | * | ||
| 387 | * The path's fill color | ||
| 388 | */ | ||
| 389 | 4 | obj_properties[PROP_FILL_COLOR] = | |
| 390 | 2 | g_param_spec_boxed ("fill-color", NULL, NULL, | |
| 391 | GDK_TYPE_RGBA, | ||
| 392 | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); | ||
| 393 | |||
| 394 | /** | ||
| 395 | * ShumatePathLayer:stroke-width: | ||
| 396 | * | ||
| 397 | * The path's stroke width (in pixels) | ||
| 398 | */ | ||
| 399 | 4 | obj_properties[PROP_STROKE_WIDTH] = | |
| 400 | 2 | g_param_spec_double ("stroke-width", NULL, NULL, | |
| 401 | 0, | ||
| 402 | 100.0, | ||
| 403 | 2.0, | ||
| 404 | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); | ||
| 405 | |||
| 406 | /** | ||
| 407 | * ShumatePathLayer:outline-color: | ||
| 408 | * | ||
| 409 | * The path's outline color | ||
| 410 | */ | ||
| 411 | 4 | obj_properties[PROP_OUTLINE_COLOR] = | |
| 412 | 2 | g_param_spec_boxed ("outline-color", NULL, NULL, | |
| 413 | GDK_TYPE_RGBA, | ||
| 414 | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); | ||
| 415 | |||
| 416 | /** | ||
| 417 | * ShumatePathLayer:outline-width: | ||
| 418 | * | ||
| 419 | * The path's outline width (in pixels) | ||
| 420 | */ | ||
| 421 | 4 | obj_properties[PROP_OUTLINE_WIDTH] = | |
| 422 | 2 | g_param_spec_double ("outline-width", NULL, NULL, | |
| 423 | 0, | ||
| 424 | 50.0, | ||
| 425 | 0.0, | ||
| 426 | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); | ||
| 427 | |||
| 428 | 2 | g_object_class_install_properties (object_class, | |
| 429 | N_PROPERTIES, | ||
| 430 | obj_properties); | ||
| 431 | 2 | } | |
| 432 | |||
| 433 | static void | ||
| 434 | 6 | shumate_path_layer_init (ShumatePathLayer *self) | |
| 435 | { | ||
| 436 | 6 | self->fill = FALSE; | |
| 437 | 6 | self->stroke = TRUE; | |
| 438 | 6 | self->stroke_width = 2.0; | |
| 439 | 6 | self->outline_width = 0.0; | |
| 440 | 6 | self->nodes = NULL; | |
| 441 | 6 | self->dashes = g_array_new (FALSE, TRUE, sizeof(float)); | |
| 442 | |||
| 443 | 6 | self->fill_color = gdk_rgba_copy (&DEFAULT_FILL_COLOR); | |
| 444 | 6 | self->stroke_color = gdk_rgba_copy (&DEFAULT_STROKE_COLOR); | |
| 445 | 6 | self->outline_color = gdk_rgba_copy (&DEFAULT_OUTLINE_COLOR); | |
| 446 | 6 | } | |
| 447 | |||
| 448 | /** | ||
| 449 | * shumate_path_layer_new: | ||
| 450 | * @viewport: the [class@Viewport] | ||
| 451 | * | ||
| 452 | * Creates a new instance of [class@PathLayer]. | ||
| 453 | * | ||
| 454 | * Returns: a new instance of [class@PathLayer]. | ||
| 455 | */ | ||
| 456 | ShumatePathLayer * | ||
| 457 | 6 | shumate_path_layer_new (ShumateViewport *viewport) | |
| 458 | { | ||
| 459 | 6 | return g_object_new (SHUMATE_TYPE_PATH_LAYER, | |
| 460 | "viewport", viewport, | ||
| 461 | NULL); | ||
| 462 | } | ||
| 463 | |||
| 464 | static void | ||
| 465 | ✗ | position_notify (ShumateLocation *location, | |
| 466 | GParamSpec *pspec, | ||
| 467 | ShumatePathLayer *self) | ||
| 468 | { | ||
| 469 | ✗ | gtk_widget_queue_draw (GTK_WIDGET (self)); | |
| 470 | ✗ | } | |
| 471 | |||
| 472 | static void | ||
| 473 | ✗ | add_node (ShumatePathLayer *self, | |
| 474 | ShumateLocation *location, | ||
| 475 | gboolean prepend, | ||
| 476 | guint position) | ||
| 477 | { | ||
| 478 | ✗ | g_signal_connect (G_OBJECT (location), "notify::latitude", G_CALLBACK (position_notify), self); | |
| 479 | |||
| 480 | ✗ | if (prepend) | |
| 481 | ✗ | self->nodes = g_list_prepend (self->nodes, g_object_ref_sink (location)); | |
| 482 | else | ||
| 483 | ✗ | self->nodes = g_list_insert (self->nodes, g_object_ref_sink (location), position); | |
| 484 | |||
| 485 | ✗ | gtk_widget_queue_draw (GTK_WIDGET (self)); | |
| 486 | ✗ | } | |
| 487 | |||
| 488 | |||
| 489 | /** | ||
| 490 | * shumate_path_layer_add_node: | ||
| 491 | * @self: a [class@PathLayer] | ||
| 492 | * @location: a [iface@Location] | ||
| 493 | * | ||
| 494 | * Adds a [iface@Location] object to the layer. | ||
| 495 | * The node is prepended to the list. | ||
| 496 | */ | ||
| 497 | void | ||
| 498 | ✗ | shumate_path_layer_add_node (ShumatePathLayer *self, | |
| 499 | ShumateLocation *location) | ||
| 500 | { | ||
| 501 | ✗ | g_return_if_fail (SHUMATE_IS_PATH_LAYER (self)); | |
| 502 | ✗ | g_return_if_fail (SHUMATE_IS_LOCATION (location)); | |
| 503 | |||
| 504 | ✗ | add_node (self, location, TRUE, 0); | |
| 505 | } | ||
| 506 | |||
| 507 | |||
| 508 | /** | ||
| 509 | * shumate_path_layer_remove_all: | ||
| 510 | * @self: a [class@PathLayer] | ||
| 511 | * | ||
| 512 | * Removes all [iface@Location] objects from the layer. | ||
| 513 | */ | ||
| 514 | void | ||
| 515 | ✗ | shumate_path_layer_remove_all (ShumatePathLayer *self) | |
| 516 | { | ||
| 517 | ✗ | GList *elem; | |
| 518 | |||
| 519 | ✗ | g_return_if_fail (SHUMATE_IS_PATH_LAYER (self)); | |
| 520 | |||
| 521 | ✗ | for (elem = self->nodes; elem != NULL; elem = elem->next) | |
| 522 | { | ||
| 523 | ✗ | GObject *node = G_OBJECT (elem->data); | |
| 524 | |||
| 525 | ✗ | g_signal_handlers_disconnect_by_func (node, | |
| 526 | G_CALLBACK (position_notify), self); | ||
| 527 | |||
| 528 | ✗ | g_object_unref (node); | |
| 529 | } | ||
| 530 | |||
| 531 | ✗ | g_clear_pointer (&self->nodes, g_list_free); | |
| 532 | ✗ | gtk_widget_queue_draw (GTK_WIDGET (self)); | |
| 533 | } | ||
| 534 | |||
| 535 | |||
| 536 | /** | ||
| 537 | * shumate_path_layer_get_nodes: | ||
| 538 | * @self: a [class@PathLayer] | ||
| 539 | * | ||
| 540 | * Gets a copy of the list of all [iface@Location] objects inserted into the layer. You should | ||
| 541 | * free the list but not its contents. | ||
| 542 | * | ||
| 543 | * Returns: (transfer container) (element-type ShumateLocation): the list | ||
| 544 | */ | ||
| 545 | GList * | ||
| 546 | ✗ | shumate_path_layer_get_nodes (ShumatePathLayer *self) | |
| 547 | { | ||
| 548 | ✗ | GList *lst; | |
| 549 | |||
| 550 | ✗ | g_return_val_if_fail (SHUMATE_IS_PATH_LAYER (self), NULL); | |
| 551 | |||
| 552 | ✗ | lst = g_list_copy (self->nodes); | |
| 553 | ✗ | return g_list_reverse (lst); | |
| 554 | } | ||
| 555 | |||
| 556 | /** | ||
| 557 | * shumate_path_layer_remove_node: | ||
| 558 | * @self: a [class@PathLayer] | ||
| 559 | * @location: a [iface@Location] | ||
| 560 | * | ||
| 561 | * Removes the [iface@Location] object from the layer. | ||
| 562 | */ | ||
| 563 | void | ||
| 564 | ✗ | shumate_path_layer_remove_node (ShumatePathLayer *self, | |
| 565 | ShumateLocation *location) | ||
| 566 | { | ||
| 567 | ✗ | g_return_if_fail (SHUMATE_IS_PATH_LAYER (self)); | |
| 568 | ✗ | g_return_if_fail (SHUMATE_IS_LOCATION (location)); | |
| 569 | |||
| 570 | ✗ | g_signal_handlers_disconnect_by_func (G_OBJECT (location), G_CALLBACK (position_notify), self); | |
| 571 | |||
| 572 | ✗ | self->nodes = g_list_remove (self->nodes, location); | |
| 573 | ✗ | g_object_unref (location); | |
| 574 | ✗ | gtk_widget_queue_draw (GTK_WIDGET (self)); | |
| 575 | } | ||
| 576 | |||
| 577 | /** | ||
| 578 | * shumate_path_layer_insert_node: | ||
| 579 | * @self: a [class@PathLayer] | ||
| 580 | * @location: a [iface@Location] | ||
| 581 | * @position: position in the list where the [iface@Location] object should be inserted | ||
| 582 | * | ||
| 583 | * Inserts a [iface@Location] object to the specified position. | ||
| 584 | */ | ||
| 585 | void | ||
| 586 | ✗ | shumate_path_layer_insert_node (ShumatePathLayer *self, | |
| 587 | ShumateLocation *location, | ||
| 588 | guint position) | ||
| 589 | { | ||
| 590 | ✗ | g_return_if_fail (SHUMATE_IS_PATH_LAYER (self)); | |
| 591 | ✗ | g_return_if_fail (SHUMATE_IS_LOCATION (location)); | |
| 592 | |||
| 593 | ✗ | add_node (self, location, FALSE, position); | |
| 594 | } | ||
| 595 | |||
| 596 | /** | ||
| 597 | * shumate_path_layer_set_fill_color: | ||
| 598 | * @self: a [class@PathLayer] | ||
| 599 | * @color: (nullable): The path's fill color or %NULL to reset to the | ||
| 600 | * default color. The color parameter is copied. | ||
| 601 | * | ||
| 602 | * Set the path's fill color. | ||
| 603 | */ | ||
| 604 | void | ||
| 605 | ✗ | shumate_path_layer_set_fill_color (ShumatePathLayer *self, | |
| 606 | const GdkRGBA *color) | ||
| 607 | { | ||
| 608 | ✗ | g_return_if_fail (SHUMATE_IS_PATH_LAYER (self)); | |
| 609 | |||
| 610 | ✗ | if (self->fill_color != NULL) | |
| 611 | ✗ | gdk_rgba_free (self->fill_color); | |
| 612 | |||
| 613 | ✗ | if (color == NULL) | |
| 614 | ✗ | color = &DEFAULT_FILL_COLOR; | |
| 615 | |||
| 616 | ✗ | self->fill_color = gdk_rgba_copy (color); | |
| 617 | ✗ | g_object_notify_by_pspec (G_OBJECT (self), obj_properties[PROP_FILL_COLOR]); | |
| 618 | |||
| 619 | ✗ | gtk_widget_queue_draw (GTK_WIDGET (self)); | |
| 620 | } | ||
| 621 | |||
| 622 | |||
| 623 | /** | ||
| 624 | * shumate_path_layer_get_fill_color: | ||
| 625 | * @self: a [class@PathLayer] | ||
| 626 | * | ||
| 627 | * Gets the path's fill color. | ||
| 628 | * | ||
| 629 | * Returns: the path's fill color. | ||
| 630 | */ | ||
| 631 | GdkRGBA * | ||
| 632 | ✗ | shumate_path_layer_get_fill_color (ShumatePathLayer *self) | |
| 633 | { | ||
| 634 | ✗ | g_return_val_if_fail (SHUMATE_IS_PATH_LAYER (self), NULL); | |
| 635 | |||
| 636 | ✗ | return self->fill_color; | |
| 637 | } | ||
| 638 | |||
| 639 | |||
| 640 | /** | ||
| 641 | * shumate_path_layer_set_stroke_color: | ||
| 642 | * @self: a [class@PathLayer] | ||
| 643 | * @color: (nullable): The path's stroke color or %NULL to reset to the | ||
| 644 | * default color. The color parameter is copied. | ||
| 645 | * | ||
| 646 | * Set the path's stroke color. | ||
| 647 | */ | ||
| 648 | void | ||
| 649 | ✗ | shumate_path_layer_set_stroke_color (ShumatePathLayer *self, | |
| 650 | const GdkRGBA *color) | ||
| 651 | { | ||
| 652 | ✗ | g_return_if_fail (SHUMATE_IS_PATH_LAYER (self)); | |
| 653 | |||
| 654 | ✗ | if (self->stroke_color != NULL) | |
| 655 | ✗ | gdk_rgba_free (self->stroke_color); | |
| 656 | |||
| 657 | ✗ | if (color == NULL) | |
| 658 | ✗ | color = &DEFAULT_STROKE_COLOR; | |
| 659 | |||
| 660 | ✗ | self->stroke_color = gdk_rgba_copy (color); | |
| 661 | ✗ | g_object_notify_by_pspec (G_OBJECT (self), obj_properties[PROP_STROKE_COLOR]); | |
| 662 | |||
| 663 | ✗ | gtk_widget_queue_draw (GTK_WIDGET (self)); | |
| 664 | } | ||
| 665 | |||
| 666 | |||
| 667 | /** | ||
| 668 | * shumate_path_layer_get_stroke_color: | ||
| 669 | * @self: a [class@PathLayer] | ||
| 670 | * | ||
| 671 | * Gets the path's stroke color. | ||
| 672 | * | ||
| 673 | * Returns: the path's stroke color. | ||
| 674 | */ | ||
| 675 | GdkRGBA * | ||
| 676 | ✗ | shumate_path_layer_get_stroke_color (ShumatePathLayer *self) | |
| 677 | { | ||
| 678 | ✗ | g_return_val_if_fail (SHUMATE_IS_PATH_LAYER (self), NULL); | |
| 679 | |||
| 680 | ✗ | return self->stroke_color; | |
| 681 | } | ||
| 682 | |||
| 683 | /** | ||
| 684 | * shumate_path_layer_set_outline_color: | ||
| 685 | * @self: a [class@PathLayer] | ||
| 686 | * @color: (nullable): The path's outline color or %NULL to reset to the | ||
| 687 | * default color. The color parameter is copied. | ||
| 688 | * | ||
| 689 | * Set the path's outline color. | ||
| 690 | */ | ||
| 691 | void | ||
| 692 | ✗ | shumate_path_layer_set_outline_color (ShumatePathLayer *self, | |
| 693 | const GdkRGBA *color) | ||
| 694 | { | ||
| 695 | ✗ | g_return_if_fail (SHUMATE_IS_PATH_LAYER (self)); | |
| 696 | |||
| 697 | ✗ | if (self->outline_color != NULL) | |
| 698 | ✗ | gdk_rgba_free (self->outline_color); | |
| 699 | |||
| 700 | ✗ | if (color == NULL) | |
| 701 | ✗ | color = &DEFAULT_OUTLINE_COLOR; | |
| 702 | |||
| 703 | ✗ | self->outline_color = gdk_rgba_copy (color); | |
| 704 | ✗ | g_object_notify_by_pspec (G_OBJECT (self), obj_properties[PROP_OUTLINE_COLOR]); | |
| 705 | |||
| 706 | ✗ | gtk_widget_queue_draw (GTK_WIDGET (self)); | |
| 707 | } | ||
| 708 | |||
| 709 | /** | ||
| 710 | * shumate_path_layer_get_outline_color: | ||
| 711 | * @self: a [class@PathLayer] | ||
| 712 | * | ||
| 713 | * Gets the path's outline color. | ||
| 714 | * | ||
| 715 | * Returns: the path's outline color. | ||
| 716 | */ | ||
| 717 | GdkRGBA * | ||
| 718 | ✗ | shumate_path_layer_get_outline_color (ShumatePathLayer *self) | |
| 719 | { | ||
| 720 | ✗ | g_return_val_if_fail (SHUMATE_IS_PATH_LAYER (self), NULL); | |
| 721 | |||
| 722 | ✗ | return self->outline_color; | |
| 723 | } | ||
| 724 | |||
| 725 | /** | ||
| 726 | * shumate_path_layer_set_stroke: | ||
| 727 | * @self: a [class@PathLayer] | ||
| 728 | * @value: if the path is stroked | ||
| 729 | * | ||
| 730 | * Sets the path to be stroked | ||
| 731 | */ | ||
| 732 | void | ||
| 733 | ✗ | shumate_path_layer_set_stroke (ShumatePathLayer *self, | |
| 734 | gboolean value) | ||
| 735 | { | ||
| 736 | ✗ | g_return_if_fail (SHUMATE_IS_PATH_LAYER (self)); | |
| 737 | |||
| 738 | ✗ | self->stroke = value; | |
| 739 | ✗ | g_object_notify_by_pspec (G_OBJECT (self), obj_properties[PROP_STROKE]); | |
| 740 | |||
| 741 | ✗ | gtk_widget_queue_draw (GTK_WIDGET (self)); | |
| 742 | } | ||
| 743 | |||
| 744 | |||
| 745 | /** | ||
| 746 | * shumate_path_layer_get_stroke: | ||
| 747 | * @self: a [class@PathLayer] | ||
| 748 | * | ||
| 749 | * Checks whether the path is stroked. | ||
| 750 | * | ||
| 751 | * Returns: %TRUE if the path is stroked, %FALSE otherwise. | ||
| 752 | */ | ||
| 753 | gboolean | ||
| 754 | ✗ | shumate_path_layer_get_stroke (ShumatePathLayer *self) | |
| 755 | { | ||
| 756 | ✗ | g_return_val_if_fail (SHUMATE_IS_PATH_LAYER (self), FALSE); | |
| 757 | |||
| 758 | ✗ | return self->stroke; | |
| 759 | } | ||
| 760 | |||
| 761 | |||
| 762 | /** | ||
| 763 | * shumate_path_layer_set_fill: | ||
| 764 | * @self: a [class@PathLayer] | ||
| 765 | * @value: if the path is filled | ||
| 766 | * | ||
| 767 | * Sets the path to be filled | ||
| 768 | */ | ||
| 769 | void | ||
| 770 | ✗ | shumate_path_layer_set_fill (ShumatePathLayer *self, | |
| 771 | gboolean value) | ||
| 772 | { | ||
| 773 | ✗ | g_return_if_fail (SHUMATE_IS_PATH_LAYER (self)); | |
| 774 | |||
| 775 | ✗ | self->fill = value; | |
| 776 | ✗ | g_object_notify_by_pspec (G_OBJECT (self), obj_properties[PROP_FILL]); | |
| 777 | |||
| 778 | ✗ | gtk_widget_queue_draw (GTK_WIDGET (self)); | |
| 779 | } | ||
| 780 | |||
| 781 | |||
| 782 | /** | ||
| 783 | * shumate_path_layer_get_fill: | ||
| 784 | * @self: a [class@PathLayer] | ||
| 785 | * | ||
| 786 | * Checks whether the path is filled. | ||
| 787 | * | ||
| 788 | * Returns: %TRUE if the path is filled, %FALSE otherwise. | ||
| 789 | */ | ||
| 790 | gboolean | ||
| 791 | ✗ | shumate_path_layer_get_fill (ShumatePathLayer *self) | |
| 792 | { | ||
| 793 | ✗ | g_return_val_if_fail (SHUMATE_IS_PATH_LAYER (self), FALSE); | |
| 794 | |||
| 795 | ✗ | return self->fill; | |
| 796 | } | ||
| 797 | |||
| 798 | |||
| 799 | /** | ||
| 800 | * shumate_path_layer_set_stroke_width: | ||
| 801 | * @self: a [class@PathLayer] | ||
| 802 | * @value: the width of the stroke (in pixels) | ||
| 803 | * | ||
| 804 | * Sets the width of the stroke | ||
| 805 | */ | ||
| 806 | void | ||
| 807 | ✗ | shumate_path_layer_set_stroke_width (ShumatePathLayer *self, | |
| 808 | double value) | ||
| 809 | { | ||
| 810 | ✗ | g_return_if_fail (SHUMATE_IS_PATH_LAYER (self)); | |
| 811 | |||
| 812 | ✗ | self->stroke_width = value; | |
| 813 | ✗ | g_object_notify_by_pspec (G_OBJECT (self), obj_properties[PROP_STROKE_WIDTH]); | |
| 814 | |||
| 815 | ✗ | gtk_widget_queue_draw (GTK_WIDGET (self)); | |
| 816 | } | ||
| 817 | |||
| 818 | |||
| 819 | /** | ||
| 820 | * shumate_path_layer_get_stroke_width: | ||
| 821 | * @self: a [class@PathLayer] | ||
| 822 | * | ||
| 823 | * Gets the width of the stroke. | ||
| 824 | * | ||
| 825 | * Returns: the width of the stroke | ||
| 826 | */ | ||
| 827 | double | ||
| 828 | ✗ | shumate_path_layer_get_stroke_width (ShumatePathLayer *self) | |
| 829 | { | ||
| 830 | ✗ | g_return_val_if_fail (SHUMATE_IS_PATH_LAYER (self), 0); | |
| 831 | |||
| 832 | ✗ | return self->stroke_width; | |
| 833 | } | ||
| 834 | |||
| 835 | /** | ||
| 836 | * shumate_path_layer_set_outline_width: | ||
| 837 | * @self: a [class@PathLayer] | ||
| 838 | * @value: the width of the outline (in pixels) | ||
| 839 | * | ||
| 840 | * Sets the width of the outline | ||
| 841 | */ | ||
| 842 | void | ||
| 843 | ✗ | shumate_path_layer_set_outline_width (ShumatePathLayer *self, | |
| 844 | double value) | ||
| 845 | { | ||
| 846 | ✗ | g_return_if_fail (SHUMATE_IS_PATH_LAYER (self)); | |
| 847 | |||
| 848 | ✗ | self->outline_width = value; | |
| 849 | ✗ | g_object_notify_by_pspec (G_OBJECT (self), obj_properties[PROP_OUTLINE_WIDTH]); | |
| 850 | |||
| 851 | ✗ | gtk_widget_queue_draw (GTK_WIDGET (self)); | |
| 852 | } | ||
| 853 | |||
| 854 | |||
| 855 | /** | ||
| 856 | * shumate_path_layer_get_outline_width: | ||
| 857 | * @self: a [class@PathLayer] | ||
| 858 | * | ||
| 859 | * Gets the width of the outline. | ||
| 860 | * | ||
| 861 | * Returns: the width of the outline | ||
| 862 | */ | ||
| 863 | double | ||
| 864 | ✗ | shumate_path_layer_get_outline_width (ShumatePathLayer *self) | |
| 865 | { | ||
| 866 | ✗ | g_return_val_if_fail (SHUMATE_IS_PATH_LAYER (self), 0); | |
| 867 | |||
| 868 | ✗ | return self->outline_width; | |
| 869 | } | ||
| 870 | |||
| 871 | /** | ||
| 872 | * shumate_path_layer_set_closed: | ||
| 873 | * @self: a [class@PathLayer] | ||
| 874 | * @value: %TRUE to make the path closed | ||
| 875 | * | ||
| 876 | * Makes the path closed. | ||
| 877 | */ | ||
| 878 | void | ||
| 879 | ✗ | shumate_path_layer_set_closed (ShumatePathLayer *self, | |
| 880 | gboolean value) | ||
| 881 | { | ||
| 882 | ✗ | g_return_if_fail (SHUMATE_IS_PATH_LAYER (self)); | |
| 883 | |||
| 884 | ✗ | self->closed_path = value; | |
| 885 | ✗ | g_object_notify_by_pspec (G_OBJECT (self), obj_properties[PROP_CLOSED_PATH]); | |
| 886 | |||
| 887 | ✗ | gtk_widget_queue_draw (GTK_WIDGET (self)); | |
| 888 | } | ||
| 889 | |||
| 890 | |||
| 891 | /** | ||
| 892 | * shumate_path_layer_get_closed: | ||
| 893 | * @self: a [class@PathLayer] | ||
| 894 | * | ||
| 895 | * Gets information whether the path is closed. | ||
| 896 | * | ||
| 897 | * Returns: %TRUE when the path is closed, %FALSE otherwise | ||
| 898 | */ | ||
| 899 | gboolean | ||
| 900 | ✗ | shumate_path_layer_get_closed (ShumatePathLayer *self) | |
| 901 | { | ||
| 902 | ✗ | g_return_val_if_fail (SHUMATE_IS_PATH_LAYER (self), FALSE); | |
| 903 | |||
| 904 | ✗ | return self->closed_path; | |
| 905 | } | ||
| 906 | |||
| 907 | |||
| 908 | /** | ||
| 909 | * shumate_path_layer_set_dash: | ||
| 910 | * @self: a [class@PathLayer] | ||
| 911 | * @dash_pattern: (element-type guint) (nullable): list of integer values | ||
| 912 | * representing lengths of dashes/spaces | ||
| 913 | * | ||
| 914 | * Sets dashed line pattern in a way similar to gsk_stroke_set_dash(). This | ||
| 915 | * method supports only integer values for segment lengths. The values have to be | ||
| 916 | * passed inside the data pointer of the list (using the %GUINT_TO_POINTER conversion) | ||
| 917 | * | ||
| 918 | * Pass %NULL to use solid line. | ||
| 919 | */ | ||
| 920 | void | ||
| 921 | ✗ | shumate_path_layer_set_dash (ShumatePathLayer *self, | |
| 922 | GList *dash_pattern) | ||
| 923 | { | ||
| 924 | ✗ | GList *iter = NULL; | |
| 925 | |||
| 926 | ✗ | g_return_if_fail (SHUMATE_IS_PATH_LAYER (self)); | |
| 927 | |||
| 928 | ✗ | g_array_set_size (self->dashes, 0); | |
| 929 | ✗ | if (dash_pattern == NULL) | |
| 930 | return; | ||
| 931 | |||
| 932 | ✗ | for (iter = dash_pattern; iter != NULL; iter = iter->next) | |
| 933 | { | ||
| 934 | ✗ | float val = (float) GPOINTER_TO_UINT (iter->data); | |
| 935 | ✗ | g_array_append_val (self->dashes, val); | |
| 936 | } | ||
| 937 | } | ||
| 938 | |||
| 939 | |||
| 940 | /** | ||
| 941 | * shumate_path_layer_get_dash: | ||
| 942 | * @self: a [class@PathLayer] | ||
| 943 | * | ||
| 944 | * Returns the list of dash segment lengths. | ||
| 945 | * | ||
| 946 | * Returns: (transfer full) (element-type guint) (nullable): the list | ||
| 947 | */ | ||
| 948 | GList * | ||
| 949 | ✗ | shumate_path_layer_get_dash (ShumatePathLayer *self) | |
| 950 | { | ||
| 951 | ✗ | GList *list = NULL; | |
| 952 | ✗ | guint i; | |
| 953 | |||
| 954 | ✗ | g_return_val_if_fail (SHUMATE_IS_PATH_LAYER (self), NULL); | |
| 955 | |||
| 956 | ✗ | for (i = 0; i < self->dashes->len; i++) | |
| 957 | ✗ | list = g_list_append (list, GUINT_TO_POINTER ((guint) g_array_index (self->dashes, float, i))); | |
| 958 | |||
| 959 | return list; | ||
| 960 | } | ||
| 961 |