]> git.tdb.fi Git - ext/subsurface.git/blob - profile.c
Use round line noins and caps
[ext/subsurface.git] / profile.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <stdarg.h>
4 #include <string.h>
5 #include <time.h>
6
7 #include "dive.h"
8 #include "display.h"
9 #include "divelist.h"
10
11 int selected_dive = 0;
12
13 /*
14  * Cairo scaling really is horribly horribly mis-designed.
15  *
16  * Which is sad, because I really like Cairo otherwise. But
17  * the fact that the line width is scaled with the same scale
18  * as the coordinate system is a f*&%ing disaster. So we
19  * can't use it, and instead have this butt-ugly wrapper thing..
20  */
21 struct graphics_context {
22         cairo_t *cr;
23         double maxx, maxy;
24         double leftx, rightx;
25         double topy, bottomy;
26 };
27
28 /* Plot info with smoothing and one-, two- and three-minute minimums and maximums */
29 struct plot_info {
30         int nr;
31         int maxtime;
32         struct plot_data {
33                 int sec;
34                 int val;
35                 int smoothed;
36                 struct plot_data *min[3];
37                 struct plot_data *max[3];
38                 int avg[3];
39         } entry[];
40 };
41 #define plot_info_size(nr) (sizeof(struct plot_info) + (nr)*sizeof(struct plot_data))
42
43 /* Scale to 0,0 -> maxx,maxy */
44 #define SCALEX(gc,x)  (((x)-gc->leftx)/(gc->rightx-gc->leftx)*gc->maxx)
45 #define SCALEY(gc,y)  (((y)-gc->topy)/(gc->bottomy-gc->topy)*gc->maxy)
46 #define SCALE(gc,x,y) SCALEX(gc,x),SCALEY(gc,y)
47
48 static void move_to(struct graphics_context *gc, double x, double y)
49 {
50         cairo_move_to(gc->cr, SCALE(gc, x, y));
51 }
52
53 static void line_to(struct graphics_context *gc, double x, double y)
54 {
55         cairo_line_to(gc->cr, SCALE(gc, x, y));
56 }
57
58 #define ROUND_UP(x,y) ((((x)+(y)-1)/(y))*(y))
59
60 /*
61  * When showing dive profiles, we scale things to the
62  * current dive. However, we don't scale past less than
63  * 30 minutes or 90 ft, just so that small dives show
64  * up as such.
65  */
66 static int round_seconds_up(int seconds)
67 {
68         return MAX(30*60, ROUND_UP(seconds, 60*10));
69 }
70
71 static int round_depth_up(depth_t depth)
72 {
73         unsigned mm = depth.mm;
74         /* Minimum 30m */
75         return MAX(30000, ROUND_UP(mm+3000, 10000));
76 }
77
78 typedef struct {
79         int size;
80         double r,g,b;
81         enum {CENTER,LEFT} halign;
82         enum {MIDDLE,TOP,BOTTOM} valign;
83 } text_render_options_t;
84
85 static void plot_text(struct graphics_context *gc, const text_render_options_t *tro,
86                       double x, double y, const char *fmt, ...)
87 {
88         cairo_t *cr = gc->cr;
89         cairo_text_extents_t extents;
90         double dx, dy;
91         char buffer[80];
92         va_list args;
93
94         va_start(args, fmt);
95         vsnprintf(buffer, sizeof(buffer), fmt, args);
96         va_end(args);
97
98         cairo_set_font_size(cr, tro->size);
99         cairo_text_extents(cr, buffer, &extents);
100         dx = 0;
101         switch (tro->halign) {
102         case CENTER:
103                 dx = -(extents.width/2 + extents.x_bearing);
104                 break;
105         case LEFT:
106                 dx = 0;
107                 break;
108         }
109         switch (tro->valign) {
110         case TOP:
111                 dy = extents.height * 1.2;
112                 break;
113         case BOTTOM:
114                 dy = -extents.height * 0.8;
115                 break;
116         case MIDDLE:
117                 dy = 0;
118                 break;
119         }
120
121         move_to(gc, x, y);
122         cairo_rel_move_to(cr, dx, dy);
123
124         cairo_text_path(cr, buffer);
125         cairo_set_source_rgb(cr, 0, 0, 0);
126         cairo_stroke(cr);
127
128         move_to(gc, x, y);
129         cairo_rel_move_to(cr, dx, dy);
130
131         cairo_set_source_rgb(cr, tro->r, tro->g, tro->b);
132         cairo_show_text(cr, buffer);
133 }
134
135 static void render_depth_sample(struct graphics_context *gc, struct plot_data *entry, const text_render_options_t *tro)
136 {
137         int sec = entry->sec;
138         depth_t depth = { entry->val };
139         const char *fmt;
140         double d;
141
142         switch (output_units.length) {
143         case METERS:
144                 d = depth.mm / 1000.0;
145                 fmt = "%.1f";
146                 break;
147         case FEET:
148                 d = to_feet(depth);
149                 fmt = "%.0f";
150                 break;
151         }
152         plot_text(gc, tro, sec, depth.mm, fmt, d);
153 }
154
155 static void plot_text_samples(struct graphics_context *gc, struct plot_info *pi)
156 {
157         static const text_render_options_t deep = {14, 1.0, 0.2, 0.2, CENTER, TOP};
158         static const text_render_options_t shallow = {14, 1.0, 0.2, 0.2, CENTER, BOTTOM};
159         int i;
160
161         for (i = 0; i < pi->nr; i++) {
162                 struct plot_data *entry = pi->entry + i;
163
164                 if (entry->val < 2000)
165                         continue;
166
167                 if (entry == entry->max[2])
168                         render_depth_sample(gc, entry, &deep);
169
170                 if (entry == entry->min[2])
171                         render_depth_sample(gc, entry, &shallow);
172         }
173 }
174
175 static void plot_depth_text(struct dive *dive, struct graphics_context *gc, struct plot_info *pi)
176 {
177         int maxtime, maxdepth;
178
179         /* Get plot scaling limits */
180         maxtime = round_seconds_up(dive->duration.seconds);
181         maxdepth = round_depth_up(dive->maxdepth);
182
183         gc->leftx = 0; gc->rightx = maxtime;
184         gc->topy = 0; gc->bottomy = maxdepth;
185
186         plot_text_samples(gc, pi);
187 }
188
189 static void plot_smoothed_profile(struct graphics_context *gc, struct plot_info *pi)
190 {
191         int i;
192         struct plot_data *entry = pi->entry;
193
194         cairo_set_source_rgba(gc->cr, 1, 0.2, 0.2, 0.20);
195         move_to(gc, entry->sec, entry->smoothed);
196         for (i = 1; i < pi->nr; i++) {
197                 entry++;
198                 line_to(gc, entry->sec, entry->smoothed);
199         }
200         cairo_stroke(gc->cr);
201 }
202
203 static void plot_minmax_profile_minute(struct graphics_context *gc, struct plot_info *pi,
204                                 int index, double a)
205 {
206         int i;
207         struct plot_data *entry = pi->entry;
208
209         cairo_set_source_rgba(gc->cr, 1, 0.2, 1, a);
210         move_to(gc, entry->sec, entry->min[index]->val);
211         for (i = 1; i < pi->nr; i++) {
212                 entry++;
213                 line_to(gc, entry->sec, entry->min[index]->val);
214         }
215         for (i = 1; i < pi->nr; i++) {
216                 line_to(gc, entry->sec, entry->max[index]->val);
217                 entry--;
218         }
219         cairo_close_path(gc->cr);
220         cairo_fill(gc->cr);
221 }
222
223 static void plot_minmax_profile(struct graphics_context *gc, struct plot_info *pi)
224 {
225         plot_minmax_profile_minute(gc, pi, 2, 0.1);
226         plot_minmax_profile_minute(gc, pi, 1, 0.1);
227         plot_minmax_profile_minute(gc, pi, 0, 0.1);
228 }
229
230 static void plot_depth_profile(struct dive *dive, struct graphics_context *gc, struct plot_info *pi)
231 {
232         int i;
233         cairo_t *cr = gc->cr;
234         int begins, sec, depth;
235         struct plot_data *entry;
236         int maxtime, maxdepth, marker;
237
238         /* Get plot scaling limits */
239         maxtime = round_seconds_up(dive->duration.seconds);
240         maxdepth = round_depth_up(dive->maxdepth);
241
242         /* Time markers: every 5 min */
243         gc->leftx = 0; gc->rightx = maxtime;
244         gc->topy = 0; gc->bottomy = 1.0;
245         for (i = 5*60; i < maxtime; i += 5*60) {
246                 move_to(gc, i, 0);
247                 line_to(gc, i, 1);
248         }
249
250         /* Depth markers: every 30 ft or 10 m*/
251         gc->leftx = 0; gc->rightx = 1.0;
252         gc->topy = 0; gc->bottomy = maxdepth;
253         switch (output_units.length) {
254         case METERS: marker = 10000; break;
255         case FEET: marker = 9144; break;        /* 30 ft */
256         }
257
258         cairo_set_source_rgba(cr, 1, 1, 1, 0.5);
259         for (i = marker; i < maxdepth; i += marker) {
260                 move_to(gc, 0, i);
261                 line_to(gc, 1, i);
262         }
263         cairo_stroke(cr);
264
265         /* Show mean depth */
266         cairo_set_source_rgba(cr, 1, 0.2, 0.2, 0.40);
267         move_to(gc, 0, dive->meandepth.mm);
268         line_to(gc, 1, dive->meandepth.mm);
269         cairo_stroke(cr);
270
271         gc->leftx = 0; gc->rightx = maxtime;
272
273         plot_smoothed_profile(gc, pi);
274         plot_minmax_profile(gc, pi);
275
276         entry = pi->entry;
277         cairo_set_source_rgba(cr, 1, 0.2, 0.2, 0.80);
278         begins = entry->sec;
279         move_to(gc, entry->sec, entry->val);
280         for (i = 1; i < pi->nr; i++) {
281                 entry++;
282                 sec = entry->sec;
283                 if (sec <= maxtime) {
284                         depth = entry->val;
285                         line_to(gc, sec, depth);
286                 }
287         }
288         gc->topy = 0; gc->bottomy = 1.0;
289         line_to(gc, MIN(sec,maxtime), 0);
290         line_to(gc, begins, 0);
291         cairo_close_path(cr);
292         cairo_set_source_rgba(cr, 1, 0.2, 0.2, 0.20);
293         cairo_fill_preserve(cr);
294         cairo_set_source_rgba(cr, 1, 0.2, 0.2, 0.80);
295         cairo_stroke(cr);
296 }
297
298 static int setup_temperature_limits(struct dive *dive, struct graphics_context *gc)
299 {
300         int i;
301         int maxtime, mintemp, maxtemp;
302
303         /* Get plot scaling limits */
304         maxtime = round_seconds_up(dive->duration.seconds);
305         mintemp = INT_MAX;
306         maxtemp = 0;
307         for (i = 0; i < dive->samples; i++) {
308                 struct sample *sample = dive->sample+i;
309                 int mkelvin = sample->temperature.mkelvin;
310                 if (!mkelvin)
311                         continue;
312                 if (mkelvin > maxtemp)
313                         maxtemp = mkelvin;
314                 if (mkelvin < mintemp)
315                         mintemp = mkelvin;
316         }
317
318         gc->leftx = 0; gc->rightx = maxtime;
319         /* Show temperatures in roughly the lower third */
320         gc->topy = maxtemp + (maxtemp - mintemp)*2;
321         gc->bottomy = mintemp - (maxtemp - mintemp)/2;
322
323         return maxtemp > mintemp;
324 }
325
326 static void plot_temperature_text(struct dive *dive, struct graphics_context *gc)
327 {
328         int i;
329         static const text_render_options_t tro = {12, 0.2, 0.2, 1.0, LEFT, TOP};
330
331         int last = 0;
332
333         if (!setup_temperature_limits(dive, gc))
334                 return;
335
336         for (i = 0; i < dive->samples; i++) {
337                 const char *unit;
338                 struct sample *sample = dive->sample+i;
339                 int mkelvin = sample->temperature.mkelvin;
340                 int sec, deg;
341                 if (!mkelvin)
342                         continue;
343                 sec = sample->time.seconds;
344                 if (sec < last)
345                         continue;
346                 last = sec + 300;
347                 if (output_units.temperature == FAHRENHEIT) {
348                         deg = to_F(sample->temperature);
349                         unit = "F";
350                 } else {
351                         deg = to_C(sample->temperature);
352                         unit = "C";
353                 }
354                 plot_text(gc, &tro, sec, mkelvin, "%d %s", deg, unit);
355         }
356 }
357
358 static void plot_temperature_profile(struct dive *dive, struct graphics_context *gc)
359 {
360         int i;
361         cairo_t *cr = gc->cr;
362         int last = 0;
363
364         if (!setup_temperature_limits(dive, gc))
365                 return;
366
367         cairo_set_source_rgba(cr, 0.2, 0.2, 1.0, 0.8);
368         for (i = 0; i < dive->samples; i++) {
369                 struct sample *sample = dive->sample+i;
370                 int mkelvin = sample->temperature.mkelvin;
371                 if (!mkelvin) {
372                         if (!last)
373                                 continue;
374                         mkelvin = last;
375                 }
376                 if (last)
377                         line_to(gc, sample->time.seconds, mkelvin);
378                 else
379                         move_to(gc, sample->time.seconds, mkelvin);
380                 last = mkelvin;
381         }
382         cairo_stroke(cr);
383 }
384
385 /* gets both the actual start and end pressure as well as the scaling factors */
386 static int get_cylinder_pressure_range(struct dive *dive, struct graphics_context *gc,
387         pressure_t *startp, pressure_t *endp)
388 {
389         int i;
390         int min, max;
391
392         gc->leftx = 0; gc->rightx = round_seconds_up(dive->duration.seconds);
393
394         max = 0;
395         min = 5000000;
396         if (startp)
397                 startp->mbar = endp->mbar = 0;
398
399         for (i = 0; i < dive->samples; i++) {
400                 int mbar;
401                 struct sample *sample = dive->sample + i;
402
403                 /* FIXME! We only track cylinder 0 right now */
404                 if (sample->cylinderindex)
405                         continue;
406                 mbar = sample->cylinderpressure.mbar;
407                 if (!mbar)
408                         continue;
409                 if (mbar < min)
410                         min = mbar;
411                 if (mbar > max)
412                         max = mbar;
413         }
414         if (startp)
415                 startp->mbar = max;
416         if (endp)
417                 endp->mbar = min;
418         if (!max)
419                 return 0;
420         gc->topy = 0; gc->bottomy = max * 1.5;
421         return 1;
422 }
423
424 static void plot_cylinder_pressure(struct dive *dive, struct graphics_context *gc)
425 {
426         int i, sec = -1;
427
428         if (!get_cylinder_pressure_range(dive, gc, NULL, NULL))
429                 return;
430
431         cairo_set_source_rgba(gc->cr, 0.2, 1.0, 0.2, 0.80);
432
433         move_to(gc, 0, dive->cylinder[0].start.mbar);
434         for (i = 1; i < dive->samples; i++) {
435                 int mbar;
436                 struct sample *sample = dive->sample + i;
437
438                 mbar = sample->cylinderpressure.mbar;
439                 if (!mbar)
440                         continue;
441                 sec = sample->time.seconds;
442                 if (sec <= dive->duration.seconds)
443                         line_to(gc, sec, mbar);
444         }
445         /*
446          * We may have "surface time" events, in which case we don't go
447          * back to dive duration
448          */
449         if (sec < dive->duration.seconds)
450                 line_to(gc, dive->duration.seconds, dive->cylinder[0].end.mbar);
451         cairo_stroke(gc->cr);
452 }
453
454 /*
455  * Return air usage (in liters).
456  */
457 static double calculate_airuse(struct dive *dive)
458 {
459         double airuse = 0;
460         int i;
461
462         for (i = 0; i < MAX_CYLINDERS; i++) {
463                 cylinder_t *cyl = dive->cylinder + i;
464                 int size = cyl->type.size.mliter;
465                 double kilo_atm;
466
467                 if (!size)
468                         continue;
469
470                 kilo_atm = (cyl->start.mbar - cyl->end.mbar) / 1013250.0;
471
472                 /* Liters of air at 1 atm == milliliters at 1k atm*/
473                 airuse += kilo_atm * size;
474         }
475         return airuse;
476 }
477
478 static void plot_info(struct dive *dive, struct graphics_context *gc)
479 {
480         text_render_options_t tro = {10, 0.2, 1.0, 0.2, LEFT, TOP};
481         const double liters_per_cuft = 28.317;
482         const char *unit, *desc;
483         double airuse;
484
485         airuse = calculate_airuse(dive);
486         if (!airuse)
487                 return;
488
489         /* I really need to start addign some unit setting thing */
490         switch (output_units.volume) {
491         case LITER:
492                 unit = "l";
493                 break;
494         case CUFT:
495                 unit = "cuft";
496                 airuse /= liters_per_cuft;
497                 break;
498         }
499         plot_text(gc, &tro, 0.8, 0.8, "vol: %4.2f %s", airuse, unit);
500         if (dive->duration.seconds) {
501                 double pressure = 1 + (dive->meandepth.mm / 10000.0);
502                 double sac = airuse / pressure * 60 / dive->duration.seconds;
503                 plot_text(gc, &tro, 0.8, 0.85, "SAC: %4.2f %s/min", sac, unit);
504         }
505         desc = dive->cylinder[0].type.description;
506         if (desc || dive->cylinder[0].gasmix.o2.permille) {
507                 int o2 = dive->cylinder[0].gasmix.o2.permille / 10;
508                 if (!desc)
509                         desc = "";
510                 if (!o2)
511                         o2 = 21;
512                 plot_text(gc, &tro, 0.8, 0.9, "%s (%d%%)", desc, o2);
513         }
514 }
515
516 static void plot_cylinder_pressure_text(struct dive *dive, struct graphics_context *gc)
517 {
518         pressure_t startp, endp;
519
520         if (get_cylinder_pressure_range(dive, gc, &startp, &endp)) {
521                 int start, end;
522                 const char *unit = "bar";
523
524                 switch (output_units.pressure) {
525                 case PASCAL:
526                         start = startp.mbar * 100;
527                         end = startp.mbar * 100;
528                         unit = "pascal";
529                         break;
530                 case BAR:
531                         start = (startp.mbar + 500) / 1000;
532                         end = (endp.mbar + 500) / 1000;
533                         unit = "bar";
534                         break;
535                 case PSI:
536                         start = to_PSI(startp);
537                         end = to_PSI(endp);
538                         unit = "psi";
539                         break;
540                 }
541
542                 text_render_options_t tro = {10, 0.2, 1.0, 0.2, LEFT, TOP};
543                 plot_text(gc, &tro, 0, startp.mbar, "%d %s", start, unit);
544                 plot_text(gc, &tro, dive->duration.seconds, endp.mbar,
545                           "%d %s", end, unit);
546         }
547 }
548
549 static void analyze_plot_info_minmax_minute(struct plot_data *entry, struct plot_data *first, struct plot_data *last, int index)
550 {
551         struct plot_data *p = entry;
552         int time = entry->sec;
553         int seconds = 90*(index+1);
554         struct plot_data *min, *max;
555         int avg, nr;
556
557         /* Go back 'seconds' in time */
558         while (p > first) {
559                 if (p[-1].sec < time - seconds)
560                         break;
561                 p--;
562         }
563
564         /* Then go forward until we hit an entry past the time */
565         min = max = p;
566         avg = p->val;
567         nr = 1;
568         while (++p < last) {
569                 int val = p->val;
570                 if (p->sec > time + seconds)
571                         break;
572                 avg += val;
573                 nr ++;
574                 if (val < min->val)
575                         min = p;
576                 if (val > max->val)
577                         max = p;
578         }
579         entry->min[index] = min;
580         entry->max[index] = max;
581         entry->avg[index] = (avg + nr/2) / nr;
582 }
583
584 static void analyze_plot_info_minmax(struct plot_data *entry, struct plot_data *first, struct plot_data *last)
585 {
586         analyze_plot_info_minmax_minute(entry, first, last, 0);
587         analyze_plot_info_minmax_minute(entry, first, last, 1);
588         analyze_plot_info_minmax_minute(entry, first, last, 2);
589 }
590
591 static struct plot_info *analyze_plot_info(struct plot_info *pi)
592 {
593         int i;
594         int nr = pi->nr;
595
596         /* Smoothing function: 5-point triangular smooth */
597         for (i = 2; i < nr-2; i++) {
598                 struct plot_data *entry = pi->entry+i;
599                 int val;
600
601                 val = entry[-2].val + 2*entry[-1].val + 3*entry[0].val + 2*entry[1].val + entry[2].val;
602                 entry->smoothed = (val+4) / 9;
603         }
604
605         /* One-, two- and three-minute minmax data */
606         for (i = 0; i < nr; i++) {
607                 struct plot_data *entry = pi->entry +i;
608                 analyze_plot_info_minmax(entry, pi->entry, pi->entry+nr);
609         }
610         
611         return pi;
612 }
613
614 /*
615  * Create a plot-info with smoothing and ranged min/max
616  *
617  * This also makes sure that we have extra empty events on both
618  * sides, so that you can do end-points without having to worry
619  * about it.
620  */
621 static struct plot_info *depth_plot_info(struct dive *dive)
622 {
623         int i, nr = dive->samples + 4, sec;
624         size_t alloc_size = plot_info_size(nr);
625         struct plot_info *pi;
626
627         pi = malloc(alloc_size);
628         if (!pi)
629                 return pi;
630         memset(pi, 0, alloc_size);
631         pi->nr = nr;
632         sec = 0;
633         for (i = 0; i < dive->samples; i++) {
634                 struct sample *sample = dive->sample+i;
635                 struct plot_data *entry = pi->entry + i + 2;
636
637                 sec = entry->sec = sample->time.seconds;
638                 entry->val = sample->depth.mm;
639         }
640         /* Fill in the last two entries with empty values but valid times */
641         i = dive->samples + 2;
642         pi->entry[i].sec = sec + 20;
643         pi->entry[i+1].sec = sec + 40;
644
645         return analyze_plot_info(pi);
646 }
647
648 static void plot(struct graphics_context *gc, int w, int h, struct dive *dive)
649 {
650         double topx, topy;
651         struct plot_info *pi = depth_plot_info(dive);
652
653         topx = w / 20.0;
654         topy = h / 20.0;
655         cairo_translate(gc->cr, topx, topy);
656         cairo_set_line_width(gc->cr, 2);
657         cairo_set_line_cap(gc->cr, CAIRO_LINE_CAP_ROUND);
658         cairo_set_line_join(gc->cr, CAIRO_LINE_JOIN_ROUND);
659
660         /*
661          * We can use "cairo_translate()" because that doesn't
662          * scale line width etc. But the actual scaling we need
663          * do set up ourselves..
664          *
665          * Snif. What a pity.
666          */
667         gc->maxx = (w - 2*topx);
668         gc->maxy = (h - 2*topy);
669
670         /* Temperature profile */
671         plot_temperature_profile(dive, gc);
672
673         /* Cylinder pressure plot */
674         plot_cylinder_pressure(dive, gc);
675
676         /* Depth profile */
677         plot_depth_profile(dive, gc, pi);
678
679         /* Text on top of all graphs.. */
680         plot_temperature_text(dive, gc);
681         plot_depth_text(dive, gc, pi);
682         plot_cylinder_pressure_text(dive, gc);
683
684         /* And info box in the lower right corner.. */
685         gc->leftx = 0; gc->rightx = 1.0;
686         gc->topy = 0; gc->bottomy = 1.0;
687         plot_info(dive, gc);
688
689         /* Bounding box last */
690         cairo_set_source_rgb(gc->cr, 1, 1, 1);
691         move_to(gc, 0, 0);
692         line_to(gc, 0, 1);
693         line_to(gc, 1, 1);
694         line_to(gc, 1, 0);
695         cairo_close_path(gc->cr);
696         cairo_stroke(gc->cr);
697
698 }
699
700 static gboolean expose_event(GtkWidget *widget, GdkEventExpose *event, gpointer data)
701 {
702         struct dive *dive = current_dive;
703         struct graphics_context gc;
704         int w,h;
705
706         w = widget->allocation.width;
707         h = widget->allocation.height;
708
709         gc.cr = gdk_cairo_create(widget->window);
710         cairo_set_source_rgb(gc.cr, 0, 0, 0);
711         cairo_paint(gc.cr);
712
713         if (dive)
714                 plot(&gc, w, h, dive);
715
716         cairo_destroy(gc.cr);
717
718         return FALSE;
719 }
720
721 GtkWidget *dive_profile_widget(void)
722 {
723         GtkWidget *da;
724
725         da = gtk_drawing_area_new();
726         gtk_widget_set_size_request(da, 350, 250);
727         g_signal_connect(da, "expose_event", G_CALLBACK(expose_event), NULL);
728
729         return da;
730 }