1 /*
2  * Copyright (C) 2009 - 2011 Vivien Malerba <malerba@gnome-db.org>
3  * Copyright (C) 2010 David King <davidk@openismus.com>
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18  */
19 
20 #include <gtk/gtk.h>
21 #include <libgda/libgda.h>
22 #include "browser-canvas.h"
23 #include "browser-canvas-priv.h"
24 #include "browser-canvas-table.h"
25 #include "browser-canvas-column.h"
26 #include <glib/gi18n-lib.h>
27 #include <string.h>
28 
29 static void browser_canvas_table_class_init (BrowserCanvasTableClass *class);
30 static void browser_canvas_table_init       (BrowserCanvasTable *drag);
31 static void browser_canvas_table_dispose    (GObject *object);
32 static void browser_canvas_table_finalize   (GObject *object);
33 
34 static void browser_canvas_table_set_property (GObject *object,
35 					       guint param_id,
36 					       const GValue *value,
37 					       GParamSpec *pspec);
38 static void browser_canvas_table_get_property (GObject *object,
39 					       guint param_id,
40 					       GValue *value,
41 					       GParamSpec *pspec);
42 
43 static void browser_canvas_table_drag_data_get (BrowserCanvasItem *citem, GdkDragContext *drag_context,
44 						GtkSelectionData *data, guint info, guint time);
45 static void browser_canvas_table_set_selected (BrowserCanvasItem *citem, gboolean selected);
46 
47 static xmlNodePtr browser_canvas_table_serialize (BrowserCanvasItem *citem);
48 
49 enum
50 {
51 	PROP_0,
52 	PROP_META_STRUCT,
53 	PROP_TABLE,
54 	PROP_MENU_FUNC
55 };
56 
57 struct _BrowserCanvasTablePrivate
58 {
59 	GdaMetaStruct      *mstruct;
60 	GdaMetaTable       *table;
61 
62 	/* UI building information */
63         GSList             *column_items; /* list of GooCanvasItem for the columns */
64 	GSList             *other_items; /* list of GooCanvasItem for other purposes */
65 	gdouble            *column_ypos; /* array for each column's Y position in this canvas group */
66 	GtkWidget          *(*popup_menu_func) (BrowserCanvasTable *ce);
67 
68 	GooCanvasItem      *selection_mark;
69 };
70 
71 /* get a pointer to the parents to be able to call their destructor */
72 static GObjectClass *table_parent_class = NULL;
73 
74 GType
75 browser_canvas_table_get_type (void)
76 {
77 	static GType type = 0;
78 
79         if (G_UNLIKELY (type == 0)) {
80 		static const GTypeInfo info = {
81 			sizeof (BrowserCanvasTableClass),
82 			(GBaseInitFunc) NULL,
83 			(GBaseFinalizeFunc) NULL,
84 			(GClassInitFunc) browser_canvas_table_class_init,
85 			NULL,
86 			NULL,
87 			sizeof (BrowserCanvasTable),
88 			0,
89 			(GInstanceInitFunc) browser_canvas_table_init,
90 			0
91 		};
92 
93 		type = g_type_register_static (TYPE_BROWSER_CANVAS_ITEM, "BrowserCanvasTable", &info, 0);
94 	}
95 
96 	return type;
97 }
98 
99 
100 static void
101 browser_canvas_table_class_init (BrowserCanvasTableClass *class)
102 {
103 	GObjectClass *object_class = G_OBJECT_CLASS (class);
104 	BrowserCanvasItemClass *iclass = BROWSER_CANVAS_ITEM_CLASS (class);
105 
106 	table_parent_class = g_type_class_peek_parent (class);
107 	iclass->drag_data_get = browser_canvas_table_drag_data_get;
108 	iclass->set_selected = browser_canvas_table_set_selected;
109 	iclass->serialize = browser_canvas_table_serialize;
110 
111 	object_class->dispose = browser_canvas_table_dispose;
112 	object_class->finalize = browser_canvas_table_finalize;
113 
114 	/* Properties */
115 	object_class->set_property = browser_canvas_table_set_property;
116 	object_class->get_property = browser_canvas_table_get_property;
117 
118 	g_object_class_install_property
119                 (object_class, PROP_META_STRUCT,
120                  g_param_spec_object ("meta-struct", NULL, NULL,
121 				      GDA_TYPE_META_STRUCT,
122 				      (G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY)));
123 	g_object_class_install_property
124                 (object_class, PROP_TABLE,
125                  g_param_spec_pointer ("table", NULL, NULL,
126 				       (G_PARAM_READABLE | G_PARAM_WRITABLE)));
127 	g_object_class_install_property
128 		(object_class, PROP_MENU_FUNC,
129                  g_param_spec_pointer ("popup_menu_func", "Popup menu function",
130 				       "Function to create a popup menu on each BrowserCanvasTable",
131 				       G_PARAM_WRITABLE));
132 }
133 
134 static gboolean button_press_event_cb (BrowserCanvasTable *ce, GooCanvasItem *target_item, GdkEventButton *event,
135 				       gpointer unused_data);
136 
137 static void
138 browser_canvas_table_init (BrowserCanvasTable *table)
139 {
140 	table->priv = g_new0 (BrowserCanvasTablePrivate, 1);
141 	table->priv->mstruct = NULL;
142 	table->priv->table = NULL;
143 	table->priv->column_ypos = NULL;
144 	table->priv->popup_menu_func = NULL;
145 
146 	table->priv->selection_mark = NULL;
147 
148 	g_signal_connect (G_OBJECT (table), "button-press-event",
149 			  G_CALLBACK (button_press_event_cb), NULL);
150 }
151 
152 static void clean_items (BrowserCanvasTable *ce);
153 static void create_items (BrowserCanvasTable *ce);
154 
155 static void
156 browser_canvas_table_dispose (GObject *object)
157 {
158 	BrowserCanvasTable *ce;
159 
160 	g_return_if_fail (IS_BROWSER_CANVAS_TABLE (object));
161 
162 	ce = BROWSER_CANVAS_TABLE (object);
163 
164 	/* REM: let the GooCanvas library destroy the items itself */
165 	ce->priv->table = NULL;
166 	if (ce->priv->mstruct) {
167 		g_object_unref (ce->priv->mstruct);
168 		ce->priv->mstruct = NULL;
169 	}
170 
171 	/* for the parent class */
172 	table_parent_class->dispose (object);
173 }
174 
175 
176 static void
177 browser_canvas_table_finalize (GObject *object)
178 {
179 	BrowserCanvasTable *ce;
180 	g_return_if_fail (object != NULL);
181 	g_return_if_fail (IS_BROWSER_CANVAS_TABLE (object));
182 
183 	ce = BROWSER_CANVAS_TABLE (object);
184 	if (ce->priv) {
185 		g_slist_free (ce->priv->column_items);
186 		g_slist_free (ce->priv->other_items);
187 		if (ce->priv->column_ypos)
188 			g_free (ce->priv->column_ypos);
189 
190 		g_free (ce->priv);
191 		ce->priv = NULL;
192 	}
193 
194 	/* for the parent class */
195 	table_parent_class->finalize (object);
196 }
197 
198 static void
199 browser_canvas_table_set_property (GObject *object,
200 				   guint param_id,
201 				   const GValue *value,
202 				   GParamSpec *pspec)
203 {
204 	BrowserCanvasTable *ce = NULL;
205 
206 	ce = BROWSER_CANVAS_TABLE (object);
207 
208 	switch (param_id) {
209 	case PROP_META_STRUCT:
210 		ce->priv->mstruct = g_value_dup_object (value);
211 		break;
212 	case PROP_TABLE: {
213 		GdaMetaTable *table;
214 		table = g_value_get_pointer (value);
215 		if (table && (table == ce->priv->table))
216 			return;
217 
218 		if (ce->priv->table) {
219 			ce->priv->table = NULL;
220 			clean_items (ce);
221 		}
222 
223 		if (table) {
224 			ce->priv->table = (GdaMetaTable*) table;
225 			create_items (ce);
226 		}
227 		break;
228 	}
229 	case PROP_MENU_FUNC:
230 		ce->priv->popup_menu_func = (GtkWidget *(*) (BrowserCanvasTable *ce)) g_value_get_pointer (value);
231 		break;
232 	default:
233 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
234 		break;
235 	}
236 }
237 
238 static void
239 browser_canvas_table_get_property (GObject *object,
240 				   guint param_id,
241 				   GValue *value,
242 				   GParamSpec *pspec)
243 {
244 	BrowserCanvasTable *ce = NULL;
245 
246 	ce = BROWSER_CANVAS_TABLE (object);
247 
248 	switch (param_id) {
249 	case PROP_META_STRUCT:
250 		g_value_set_object (value, ce->priv->mstruct);
251 		break;
252 	case PROP_TABLE:
253 		g_value_set_pointer (value, ce->priv->table);
254 		break;
255 	default:
256 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
257 		break;
258 	}
259 }
260 
261 /*
262  * destroy any existing GooCanvasItem obejcts
263  */
264 static void
265 clean_items (BrowserCanvasTable *ce)
266 {
267 	GSList *list;
268 	/* destroy all the items in the group */
269 	while (ce->priv->column_items)
270 		g_object_unref (G_OBJECT (ce->priv->column_items->data));
271 
272 	for (list = ce->priv->other_items; list; list = list->next)
273 		g_object_unref (G_OBJECT (list->data));
274 	g_slist_free (ce->priv->other_items);
275 	ce->priv->other_items = NULL;
276 
277 	/* free the columns positions */
278 	if (ce->priv->column_ypos) {
279 		g_free (ce->priv->column_ypos);
280 		ce->priv->column_ypos = NULL;
281 	}
282 }
283 
284 /*
285  * create new GooCanvasItem objects
286  */
287 static void
288 create_items (BrowserCanvasTable *ce)
289 {
290 	GooCanvasItem *item, *frame, *title;
291         gdouble y, ysep;
292 #define HEADER_Y_PAD 3.
293 #define Y_PAD 0.
294 #define X_PAD 3.
295 #define RADIUS_X 5.
296 #define RADIUS_Y 5.
297 #define MIN_HEIGHT 70.
298 #define SELECTION_SIZE 4.
299         GooCanvasBounds border_bounds;
300         GooCanvasBounds bounds;
301 	const gchar *cstr;
302 	gchar *tmpstr = NULL;
303 	GSList *columns, *list;
304 	gint column_nb;
305 	gdouble column_width;
306 
307 	clean_items (ce);
308 	g_assert (ce->priv->table);
309 
310         /* title */
311 	cstr = GDA_META_DB_OBJECT (ce->priv->table)->obj_short_name;
312 	if (cstr)
313 		tmpstr = g_markup_printf_escaped ("<b>%s</b>", cstr);
314 	else
315 		tmpstr = g_strdup_printf ("<b>%s</b>", _("No name"));
316 
317 	y = RADIUS_Y;
318         title = goo_canvas_text_new  (GOO_CANVAS_ITEM (ce), tmpstr,
319 				      RADIUS_X + X_PAD, y,
320 				      -1, GOO_CANVAS_ANCHOR_NORTH_WEST,
321 				      "font", "Sans 11",
322 				      "use-markup", TRUE, NULL);
323 
324 	g_free (tmpstr);
325         goo_canvas_item_get_bounds (title, &bounds);
326         border_bounds = bounds;
327         border_bounds.x1 = 0.;
328         border_bounds.y1 = 0.;
329         y += bounds.y2 - bounds.y1 + HEADER_Y_PAD;
330 
331 	/* separator's placeholder */
332         ysep = y;
333         y += HEADER_Y_PAD;
334 
335 	/* columns' vertical position */
336 	columns = ce->priv->table->columns;
337 	ce->priv->column_ypos = g_new0 (gdouble, g_slist_length (columns) + 1);
338 
339 	/* columns */
340 	for (column_nb = 0, list = columns; list; list = list->next, column_nb++) {
341 		ce->priv->column_ypos [column_nb] = y;
342 		item = browser_canvas_column_new (GOO_CANVAS_ITEM (ce),
343 						  ce->priv->mstruct,
344 						  GDA_META_TABLE_COLUMN (list->data),
345 						  X_PAD, ce->priv->column_ypos [column_nb], NULL);
346 		ce->priv->column_items = g_slist_append (ce->priv->column_items, item);
347 		goo_canvas_item_get_bounds (item, &bounds);
348 		border_bounds.x1 = MIN (border_bounds.x1, bounds.x1);
349                 border_bounds.x2 = MAX (border_bounds.x2, bounds.x2);
350                 border_bounds.y1 = MIN (border_bounds.y1, bounds.y1);
351                 border_bounds.y2 = MAX (border_bounds.y2, bounds.y2);
352 
353                 y += bounds.y2 - bounds.y1 + Y_PAD;
354 	}
355 	if (!columns && (border_bounds.y2 - border_bounds.y1 < MIN_HEIGHT))
356 		border_bounds.y2 += MIN_HEIGHT - (border_bounds.y2 - border_bounds.y1);
357 
358 	/* border */
359 	column_width = border_bounds.x2 - border_bounds.x1;
360         border_bounds.y2 += RADIUS_Y;
361         border_bounds.x2 += RADIUS_X;
362         frame = goo_canvas_rect_new (GOO_CANVAS_ITEM (ce), border_bounds.x1, border_bounds.y1,
363 				     border_bounds.x2, border_bounds.y2,
364 				     "radius-x", RADIUS_X,
365 				     "radius-y", RADIUS_Y,
366 				     "fill-color", "#f8f8f8",
367 				     NULL);
368 	ce->priv->other_items = g_slist_prepend (ce->priv->other_items, frame);
369 
370 	ce->priv->selection_mark = goo_canvas_rect_new (GOO_CANVAS_ITEM (ce), border_bounds.x1 - SELECTION_SIZE,
371 							border_bounds.y1 - SELECTION_SIZE,
372 							border_bounds.x2 + 2 * SELECTION_SIZE,
373 							border_bounds.y2 + 2 * SELECTION_SIZE,
374 							"radius-x", RADIUS_X,
375 							"radius-y", RADIUS_Y,
376 							"fill-color", "#11d155",//"#ffea08",
377 							"stroke-color", "#11d155",//"#ffea08",
378 							NULL);
379 	g_object_set (G_OBJECT (ce->priv->selection_mark), "visibility", GOO_CANVAS_ITEM_HIDDEN, NULL);
380 
381 	/* title's background */
382 	gchar *cpath;
383 	cpath = g_strdup_printf ("M %d %d H %d V %d H %d Z",
384 				 (gint) border_bounds.x1, (gint) border_bounds.y1,
385 				 (gint) border_bounds.x2, (gint) ysep,
386 				 (gint) border_bounds.x1);
387 	item = goo_canvas_rect_new (GOO_CANVAS_ITEM (ce), border_bounds.x1, border_bounds.y1,
388 				    border_bounds.x2, ysep + RADIUS_X,
389 				    "clip_path", cpath,
390 				    "radius-x", RADIUS_X,
391 				    "radius-y", RADIUS_Y,
392 				    "fill-color", "#aaaaff",
393 				    NULL);
394 	g_free (cpath);
395 	goo_canvas_item_lower (item, NULL);
396 
397 	/* separator */
398         item = goo_canvas_polyline_new_line (GOO_CANVAS_ITEM (ce), border_bounds.x1, ysep, border_bounds.x2, ysep,
399 					     "close-path", FALSE,
400 					     "line-width", .7, NULL);
401 	ce->priv->other_items = g_slist_prepend (ce->priv->other_items, item);
402 
403 	goo_canvas_item_lower (frame, NULL);
404 	goo_canvas_item_lower (ce->priv->selection_mark, NULL);
405 
406 	/* setting the columns' background width to be the same for all */
407 	for (list = ce->priv->column_items; list; list = list->next)
408 		g_object_set (G_OBJECT (list->data), "width", column_width, NULL);
409 }
410 
411 static gboolean
412 button_press_event_cb (BrowserCanvasTable *ce, G_GNUC_UNUSED GooCanvasItem  *target_item,
413 		       GdkEventButton *event,
414 		       G_GNUC_UNUSED gpointer data)
415 {
416 	if ((event->button == 3) && ce->priv->popup_menu_func) {
417 		GtkWidget *menu;
418 		menu = ce->priv->popup_menu_func (ce);
419 		gtk_menu_popup (GTK_MENU (menu), NULL, NULL,
420 				NULL, NULL, ((GdkEventButton *)event)->button,
421 				((GdkEventButton *)event)->time);
422 		return TRUE;
423 	}
424 
425 	return FALSE;
426 }
427 
428 /**
429  * browser_canvas_table_get_column_item
430  * @ce: a #BrowserCanvasTable object
431  * @column: a #GdaMetaTableColumn object
432  *
433  * Get the #BrowserCanvasColumn object representing @column
434  * in @ce.
435  *
436  * Returns: the corresponding #BrowserCanvasColumn
437  */
438 BrowserCanvasColumn *
439 browser_canvas_table_get_column_item (BrowserCanvasTable *ce, GdaMetaTableColumn *column)
440 {
441 	gint pos;
442 
443 	g_return_val_if_fail (ce && IS_BROWSER_CANVAS_TABLE (ce), NULL);
444 	g_return_val_if_fail (ce->priv, NULL);
445 	g_return_val_if_fail (ce->priv->table, NULL);
446 
447 	pos = g_slist_index (ce->priv->table->columns, column);
448 	g_return_val_if_fail (pos >= 0, NULL);
449 
450 	return g_slist_nth_data (ce->priv->column_items, pos);
451 }
452 
453 
454 /**
455  * browser_canvas_table_get_column_ypos
456  * @ce: a #BrowserCanvasTable object
457  * @column: a #GdaMetaTableColumn object
458  *
459  * Get the Y position of the middle of the #BrowserCanvasColumn object representing @column
460  * in @ce, in @ce's coordinates.
461  *
462  * Returns: the Y coordinate.
463  */
464 gdouble
465 browser_canvas_table_get_column_ypos (BrowserCanvasTable *ce, GdaMetaTableColumn *column)
466 {
467 	gint pos;
468 
469 	g_return_val_if_fail (ce && IS_BROWSER_CANVAS_TABLE (ce), 0.);
470 	g_return_val_if_fail (ce->priv, 0.);
471 	g_return_val_if_fail (ce->priv->table, 0.);
472 	g_return_val_if_fail (ce->priv->column_ypos, 0.);
473 
474 	pos = g_slist_index (ce->priv->table->columns, column);
475 	g_return_val_if_fail (pos >= 0, 0.);
476 	return (0.75 * ce->priv->column_ypos[pos+1] + 0.25 * ce->priv->column_ypos[pos]);
477 }
478 
479 
480 /**
481  * browser_canvas_table_new
482  * @parent: the parent item, or NULL.
483  * @table: a #GdaMetaTable to display
484  * @x: the x coordinate
485  * @y: the y coordinate
486  * @...: optional pairs of property names and values, and a terminating NULL.
487  *
488  * Creates a new canvas item to display the @table table
489  *
490  * Returns: a new #GooCanvasItem object
491  */
492 GooCanvasItem *
493 browser_canvas_table_new (GooCanvasItem *parent, GdaMetaStruct *mstruct, GdaMetaTable *table,
494 			 gdouble x, gdouble y, ...)
495 {
496 	GooCanvasItem *item;
497 	const char *first_property;
498 	va_list var_args;
499 
500 	g_return_val_if_fail (GDA_IS_META_STRUCT (mstruct), NULL);
501 
502 	item = g_object_new (TYPE_BROWSER_CANVAS_TABLE, "meta-struct", mstruct,
503 			     "allow-move", TRUE,
504 			     "allow-select", TRUE, NULL);
505 
506 	if (parent) {
507 		goo_canvas_item_add_child (parent, item, -1);
508 		g_object_unref (item);
509 	}
510 
511 	g_object_set (item, "table", table, NULL);
512 
513 	va_start (var_args, y);
514 	first_property = va_arg (var_args, char*);
515 	if (first_property)
516 		g_object_set_valist ((GObject*) item, first_property, var_args);
517 	va_end (var_args);
518 
519 	goo_canvas_item_translate (item, x, y);
520 
521 	return item;
522 }
523 
524 static void
525 browser_canvas_table_drag_data_get (BrowserCanvasItem *citem, G_GNUC_UNUSED GdkDragContext *drag_context,
526 				    GtkSelectionData *data, G_GNUC_UNUSED guint info,
527 				    G_GNUC_UNUSED guint time)
528 {
529 	BrowserCanvasTable *ctable;
530 
531 	ctable = BROWSER_CANVAS_TABLE (citem);
532 	if (!ctable->priv->table)
533 		return;
534 
535 	GdaMetaDbObject *dbo;
536 	gchar *str, *tmp1, *tmp2, *tmp3;
537 
538 	dbo = GDA_META_DB_OBJECT (ctable->priv->table);
539 	tmp1 = gda_rfc1738_encode (dbo->obj_schema);
540 	tmp2 = gda_rfc1738_encode (dbo->obj_name);
541 	tmp3 = gda_rfc1738_encode (dbo->obj_short_name);
542 	str = g_strdup_printf ("OBJ_TYPE=table;OBJ_SCHEMA=%s;OBJ_NAME=%s;OBJ_SHORT_NAME=%s", tmp1, tmp2, tmp3);
543 	g_free (tmp1);
544 	g_free (tmp2);
545 	g_free (tmp3);
546 	gtk_selection_data_set (data, gtk_selection_data_get_target (data), 8, (guchar*) str, strlen (str));
547 	g_free (str);
548 }
549 
550 static void
551 browser_canvas_table_set_selected (BrowserCanvasItem *citem, gboolean selected)
552 {
553 	g_object_set (G_OBJECT (BROWSER_CANVAS_TABLE (citem)->priv->selection_mark),
554 		      "visibility", selected ? GOO_CANVAS_ITEM_VISIBLE : GOO_CANVAS_ITEM_HIDDEN, NULL);
555 }
556 
557 static xmlNodePtr
558 browser_canvas_table_serialize (BrowserCanvasItem *citem)
559 {
560 	BrowserCanvasTable *ctable;
561 
562 	ctable = BROWSER_CANVAS_TABLE (citem);
563 	if (!ctable->priv->table)
564 		return NULL;
565 
566 	GdaMetaDbObject *dbo;
567 	xmlNodePtr node;
568 	GooCanvasBounds bounds;
569 	gchar *str;
570 
571 	dbo = GDA_META_DB_OBJECT (ctable->priv->table);
572 	node = xmlNewNode (NULL, BAD_CAST "table");
573 	xmlSetProp (node, BAD_CAST "schema", BAD_CAST (dbo->obj_schema));
574 	xmlSetProp (node, BAD_CAST "name", BAD_CAST (dbo->obj_name));
575 	goo_canvas_item_get_bounds (GOO_CANVAS_ITEM (citem), &bounds);
576 	str = g_strdup_printf ("%.1f", bounds.x1);
577 	xmlSetProp (node, BAD_CAST "x", BAD_CAST str);
578 	g_free (str);
579 	str = g_strdup_printf ("%.1f", bounds.y1);
580 	xmlSetProp (node, BAD_CAST "y", BAD_CAST str);
581 	g_free (str);
582 
583 	return node;
584 }
585 
586 /**
587  * browser_canvas_table_get_anchor_bounds
588  *
589  * Get the bounds to be used to compute anchors, ie. without the selection mark or any other
590  * artefact not part of the table's rectangle.
591  */
592 void
593 browser_canvas_table_get_anchor_bounds (BrowserCanvasTable *ce, GooCanvasBounds *bounds)
594 {
595 	g_return_if_fail (IS_BROWSER_CANVAS_TABLE (ce));
596 	g_return_if_fail (bounds);
597 
598 	goo_canvas_item_get_bounds (GOO_CANVAS_ITEM (ce), bounds);
599 	bounds->x1 += SELECTION_SIZE;
600 	bounds->y1 += SELECTION_SIZE;
601 	bounds->x2 -= SELECTION_SIZE;
602 	bounds->y2 -= SELECTION_SIZE;
603 }
604