GIRAFFE Pipeline Reference Manual

gilinedata.c
1/*
2 * This file is part of the GIRAFFE Pipeline
3 * Copyright (C) 2002-2019 European Southern Observatory
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (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 St, Fifth Floor, Boston, MA 02110-1301 USA
18 */
19
20#ifdef HAVE_CONFIG_H
21# include <config.h>
22#endif
23
24#include <string.h>
25
26#include <cxtypes.h>
27#include <cxmemory.h>
28#include <cxmessages.h>
29#include <cxmap.h>
30#include <cxstrutils.h>
31
32#include <cpl_error.h>
33#include <cpl_image.h>
34
35#include "gialias.h"
36#include "gierror.h"
37#include "giutils.h"
38#include "gilinedata.h"
39
40
41
42/*
43 * @defgroup gilinedata Line Data Storage
44 *
45 * TBD
46 */
47
50struct GiLineData {
51
52 const cxchar* model;
53
54 cxint nfibers;
55 cxint nlines;
56 cxint* ignore;
57
58 cxdouble* wavelength;
59
60 cpl_image* status;
61
62 cx_map* values;
63
64};
65
66
67inline static cxbool
68_giraffe_linedata_compare(cxcptr s, cxcptr t)
69{
70
71 return strcmp(s, t) < 0 ? TRUE : FALSE;
72
73}
74
75
76inline static cxptr
77_giraffe_linedata_get_data(cx_map* map, const cxchar* name)
78{
79
80
81 cpl_image* data = cx_map_get(map, name);
82
83 if (data == NULL) {
84 return NULL;
85 }
86
87 return cpl_image_get_data(data);
88
89}
90
91
92inline static void
93_giraffe_linedata_clear(GiLineData* self)
94{
95
96 self->nfibers = 0;
97 self->nlines = 0;
98
99 if (self->model) {
100 cx_free((cxptr)self->model);
101 self->model = NULL;
102 }
103
104 if (self->ignore) {
105 cx_free(self->ignore);
106 self->ignore = NULL;
107 }
108
109 if (self->wavelength) {
110 cx_free(self->wavelength);
111 self->wavelength = NULL;
112 }
113
114 if (self->status) {
115 cpl_image_delete(self->status);
116 self->status = NULL;
117 }
118
119 if (self->values) {
120 cx_map_clear(self->values);
121 }
122
123 cx_assert(cx_map_empty(self->values));
124
125 return;
126}
127
128
129inline static cxint
130_giraffe_linedata_assign(GiLineData* self, cx_map* map, const cxchar* name,
131 const cpl_image* values)
132{
133
134 cx_map_iterator position = cx_map_find(map, name);
135
136
137 if (cpl_image_get_size_x(values) != self->nfibers) {
138 return 1;
139 }
140
141 if (cpl_image_get_size_y(values) != self->nlines) {
142 return 2;
143 }
144
145 if (position == cx_map_end(map)) {
146 cx_map_insert(map, cx_strdup(name), values);
147 }
148 else {
149
150 cpl_image* previous = cx_map_assign(map, position, values);
151
152 if (previous != NULL) {
153 cpl_image_delete(previous);
154 previous = NULL;
155 }
156
157 }
158
159 return 0;
160
161}
162
163
164inline static cxint
165_giraffe_linedata_set(GiLineData* self, cx_map* map, const cxchar* name,
166 cxint i, cxint j, cxdouble value)
167{
168
169 cxdouble* data = NULL;
170
171 cx_map_const_iterator position = cx_map_find(map, name);
172
173
174 if (position == cx_map_end(map)) {
175
176 cpl_image* buffer = cpl_image_new(self->nfibers, self->nlines,
177 CPL_TYPE_DOUBLE);
178 cx_map_insert(map, cx_strdup(name), buffer);
179 data = cpl_image_get_data(buffer);
180
181 }
182 else {
183
184 data = cpl_image_get_data(cx_map_get_value(map, position));
185
186 }
187
188 data[self->nfibers * j + i] = value;
189
190 return 0;
191
192}
193
194
195inline static cxint
196_giraffe_linedata_get(const GiLineData* self, const cx_map* map,
197 const cxchar* name, cxint i, cxint j, cxdouble* value)
198{
199
200 cxdouble* data = NULL;
201
202 cx_map_const_iterator position = cx_map_find(map, name);
203
204 if (position == cx_map_end(map)) {
205 return 1;
206 }
207
208 data = cpl_image_get_data(cx_map_get_value(map, position));
209 *value = data[self->nfibers * j + i];
210
211 return 0;
212
213}
214
215
216GiLineData*
217giraffe_linedata_new(void)
218{
219
220 GiLineData* self = cx_calloc(1, sizeof *self);
221
222 self->nfibers = 0;
223 self->nlines = 0;
224
225 self->model = NULL;
226 self->ignore = NULL;
227
228 self->wavelength = NULL;
229 self->status = NULL;
230
231 self->values = cx_map_new(_giraffe_linedata_compare, cx_free,
232 (cx_free_func)cpl_image_delete);
233 cx_assert(cx_map_empty(self->values));
234
235 return self;
236
237}
238
239
240GiLineData*
241giraffe_linedata_create(const cpl_table* lines, const cpl_table* fibers,
242 const cxchar* model)
243{
244 cxint i;
245
246 GiLineData* self = NULL;
247
248
249 if (lines == NULL) {
250 return NULL;
251 }
252
253 if (!cpl_table_has_column(lines, "WLEN")) {
254 return NULL;
255 }
256
257 if (fibers == NULL) {
258 return NULL;
259 }
260
261 if (model == NULL) {
262 return NULL;
263 }
264
265 self = cx_malloc(sizeof(GiLineData));
266 cx_assert(self);
267
268 self->nfibers = cpl_table_get_nrow(fibers);
269 self->nlines = cpl_table_get_nrow(lines);
270
271 self->model = cx_strdup(model);
272 self->ignore = cx_calloc(self->nlines, sizeof(cxint));
273
274 self->wavelength = cx_calloc(self->nlines, sizeof(cxdouble));
275
276 for (i = 0; i < self->nlines; i++) {
277 self->wavelength[i] = cpl_table_get(lines, "WLEN", i, NULL);
278 }
279
280 /* Lazy buffer creation! */
281 self->status = NULL;
282
283 self->values = cx_map_new(_giraffe_linedata_compare, cx_free,
284 (cx_free_func)cpl_image_delete);
285 cx_assert(cx_map_empty(self->values));
286
287 return self;
288
289}
290
291
292void
293giraffe_linedata_delete(GiLineData* self)
294{
295
296 if (self) {
297 _giraffe_linedata_clear(self);
298
299 if (self->values != NULL) {
300 cx_map_delete(self->values);
301 }
302
303 cx_free(self);
304 }
305
306 return;
307
308}
309
310
311cxint
312giraffe_linedata_reset(GiLineData* self, const cpl_table* lines,
313 const cpl_table* fibers, const cxchar* model)
314{
315
316 cxint i;
317
318
319 cx_assert(self != NULL);
320
321 if (lines == NULL) {
322 return 1;
323 }
324
325 if (!cpl_table_has_column(lines, "WLEN")) {
326 return 1;
327 }
328
329 if (fibers == NULL) {
330 return 1;
331 }
332
333 if (model == NULL) {
334 return 1;
335 }
336
337
338 self->nfibers = cpl_table_get_nrow(fibers);
339 self->nlines = cpl_table_get_nrow(lines);
340
341 if (self->model != NULL) {
342 cx_free((cxchar*)self->model);
343 }
344 self->model = cx_strdup(model);
345
346 if (self->ignore != NULL) {
347 cx_free(self->ignore);
348 }
349 self->ignore = cx_calloc(self->nlines, sizeof(cxint));
350
351 self->wavelength = cx_realloc(self->wavelength,
352 self->nlines * sizeof(cxdouble));
353
354 for (i = 0; i < self->nlines; i++) {
355
356 self->wavelength[i] = cpl_table_get(lines, "WLEN", i,
357 NULL);
358
359 }
360
361 if (self->status != NULL) {
362 cpl_image_delete(self->status);
363 self->status = NULL;
364 }
365
366 if (!cx_map_empty(self->values)) {
367 cx_map_clear(self->values);
368 }
369
370 return 0;
371
372}
373
374
375const cxchar*
376giraffe_linedata_model(const GiLineData* self)
377{
378
379 cx_assert(self != NULL);
380
381 return self->model;
382
383}
384
385
386cxsize
387giraffe_linedata_lines(const GiLineData* self)
388{
389
390 cx_assert(self != NULL);
391
392 return self->nlines;
393
394}
395
396
397cxsize
398giraffe_linedata_fibers(const GiLineData* self)
399{
400
401 cx_assert(self != NULL);
402
403 return self->nfibers;
404
405}
406
407
408cxbool
409giraffe_linedata_contains(GiLineData* self, const cxchar* name)
410{
411
412 cx_map_const_iterator position;
413
414
415 cx_assert(self != NULL);
416
417 if (name == NULL) {
418 return FALSE;
419 }
420
421 position = cx_map_find(self->values, name);
422
423 if (position == cx_map_end(self->values)) {
424 return FALSE;
425 }
426
427 return TRUE;
428
429}
430
431
432cxsize
433giraffe_linedata_rejected(const GiLineData* self)
434{
435
436 cxint i;
437 cxint* status;
438
439 cxsize count = 0;
440
441
442 cx_assert(self != NULL);
443
444 if (self->status != NULL) {
445
446 status = cpl_image_get_data(self->status);
447
448 for (i = 0; i < self->nfibers * self->nlines; i++) {
449 if (status[i] > 0) {
450 ++count;
451 }
452 }
453
454 }
455
456 return count;
457
458}
459
460
461cxsize
462giraffe_linedata_accepted(const GiLineData* self)
463{
464
465 cxsize count = 0;
466
467
468 cx_assert(self != NULL);
469
470 count = self->nfibers * self->nlines;
471
472 return count - giraffe_linedata_rejected(self);
473
474}
475
476
477cpl_image*
478giraffe_linedata_status(const GiLineData* self)
479{
480
481 cx_assert(self != NULL);
482
483 if (self->status == NULL) {
484 return cpl_image_new(self->nfibers, self->nlines, CPL_TYPE_INT);
485 }
486
487 return cpl_image_duplicate(self->status);
488
489}
490
491
492cxint
493giraffe_linedata_set_status(GiLineData* self, cxint fiber, cxint line,
494 cxint status)
495{
496
497 cxint* data = NULL;
498
499
500 cx_assert(self != NULL);
501
502 if (fiber >= self->nfibers) {
503 return 1;
504 }
505
506 if (line >= self->nlines) {
507 return 1;
508 }
509
510 if (self->status == NULL) {
511 self->status = cpl_image_new(self->nfibers, self->nlines,
512 CPL_TYPE_INT);
513 if (self->status == NULL) {
514 return -1;
515 }
516 }
517
518 data = cpl_image_get_data(self->status);
519
520 data[self->nfibers * line + fiber] = status;
521
522 if (status != 0) {
523 self->ignore[line] += 1;
524 }
525
526 return 0;
527
528}
529
530
531cxint
532giraffe_linedata_get_status(const GiLineData* self, cxint fiber, cxint line)
533{
534
535 cxint* data = NULL;
536
537
538 cx_assert(self != NULL);
539
540 if (fiber >= self->nfibers) {
541 return 1;
542 }
543
544 if (line >= self->nlines) {
545 return 1;
546 }
547
548 if (self->status == NULL) {
549 return 0;
550 }
551
552 data = cpl_image_get_data(self->status);
553
554 return data[self->nfibers * line + fiber];
555
556}
557
558
559cxint
560giraffe_linedata_set_wavelength(GiLineData* self, cxint line, cxdouble lambda)
561{
562
563 cx_assert(self != NULL);
564
565 if (line < 0 || line >= self->nlines) {
566 return 1;
567 }
568
569 self->wavelength[line] = lambda;
570
571 return 0;
572
573}
574
575
576cxdouble
577giraffe_linedata_get_wavelength(const GiLineData* self, cxint line)
578{
579
580 const cxchar* const fctid = "giraffe_linedata_get_wavelength";
581
582
583 cx_assert(self != NULL);
584
585 if (line < 0 || line >= self->nlines) {
586 cpl_error_set(fctid, CPL_ERROR_ILLEGAL_INPUT);
587 return 0.;
588 }
589
590 return self->wavelength[line];
591
592}
593
594
595cxint
596giraffe_linedata_set(GiLineData* self, const cxchar* name, cxint fiber,
597 cxint line, cxdouble value)
598{
599
600 cxint status = 0;
601
602 cx_assert(self != NULL);
603
604 if (name == NULL) {
605 return 1;
606 }
607
608 if (fiber >= self->nfibers) {
609 return 1;
610 }
611
612 if (line >= self->nlines) {
613 return 1;
614 }
615
616 status = _giraffe_linedata_set(self, self->values, name, fiber, line,
617 value);
618
619 if (status != 0) {
620 return 1;
621 }
622
623 return 0;
624
625}
626
627
628cxdouble
629giraffe_linedata_get(const GiLineData* self, const cxchar* name, cxint fiber,
630 cxint line)
631{
632
633 const cxchar* const fctid = "giraffe_linedata_get";
634
635 cxint status = 0;
636
637 cxdouble value = 0.;
638
639
640 cx_assert(self != NULL);
641
642 if (name == NULL) {
643 return 1;
644 }
645
646 if (fiber >= self->nfibers) {
647 return 1;
648 }
649
650 if (line >= self->nlines) {
651 return 1;
652 }
653
654 status = _giraffe_linedata_get(self, self->values, name, fiber, line,
655 &value);
656
657 if (status != 0) {
658 cpl_error_set(fctid, CPL_ERROR_DATA_NOT_FOUND);
659 return 0.;
660 }
661
662 return value;
663
664}
665
666
667cxint
668giraffe_linedata_set_data(GiLineData* self, const cxchar* name,
669 const cpl_image* data)
670{
671
672 cxint status = 0;
673
674
675 cx_assert(self != NULL);
676
677 if (name == NULL) {
678 return 1;
679 }
680
681 if (data == NULL) {
682 return 1;
683 }
684
685 status = _giraffe_linedata_assign(self, self->values, name, data);
686
687 if (status != 0) {
688 return 1;
689 }
690
691 return 0;
692
693}
694
695
696const cpl_image*
697giraffe_linedata_get_data(const GiLineData* self, const cxchar* name)
698{
699
700 cx_assert(self != NULL);
701
702 if (name == NULL) {
703 return NULL;
704 }
705
706 return cx_map_get(self->values, name);
707
708}
709
710
711cxint
712giraffe_linedata_load(GiLineData* self, const cxchar* filename)
713{
714
715 cxsize extension = 1;
716
717 cpl_table* lines = NULL;
718
719 cpl_propertylist* p = NULL;
720
721
722 if (self == NULL || filename == NULL) {
723 return -1;
724 }
725
726 _giraffe_linedata_clear(self);
727
728
729 giraffe_error_push();
730
731 p = cpl_propertylist_load(filename, 0);
732
733 if (p == NULL) {
734 return 1;
735 }
736
737 if (cpl_propertylist_has(p, GIALIAS_WSOL_LMNAME) == 0) {
738 return 1;
739 }
740 else {
741
742 self->model = cx_strdup(cpl_propertylist_get_string(p,
743 GIALIAS_WSOL_LMNAME));
744
745 }
746
747 if (cpl_error_get_code() != CPL_ERROR_NONE) {
748
749 if (p != NULL) {
750 cpl_propertylist_delete(p);
751 p = NULL;
752 }
753
754 return 1;
755
756 }
757
758 giraffe_error_pop();
759
760 cpl_propertylist_delete(p);
761 p = NULL;
762
763
764 /*
765 * Load line wavelength and line status flags
766 */
767
768 lines = cpl_table_load(filename, extension, 0);
769
770 if (lines == NULL) {
771 _giraffe_linedata_clear(self);
772 return 2;
773 }
774
775 if (cpl_table_has_column(lines, "WLEN") == FALSE) {
776 _giraffe_linedata_clear(self);
777 return 2;
778 }
779 else {
780
781 const cxdouble* lambda = cpl_table_get_data_double(lines, "WLEN");
782
783 self->nlines = cpl_table_get_nrow(lines);
784
785 self->ignore = cx_calloc(self->nlines, sizeof(cxint));
786 self->wavelength = cx_malloc(self->nlines * sizeof(cxdouble));
787
788 memcpy(self->wavelength, lambda, self->nlines * sizeof(cxdouble));
789 }
790
791 ++extension;
792
793 self->status = cpl_image_load(filename, CPL_TYPE_INT, 0, extension);
794
795 if (self->status == NULL) {
796 _giraffe_linedata_clear(self);
797 return 2;
798 }
799
800 self->nfibers = cpl_image_get_size_x(self->status);
801
802 ++extension;
803
804
805 /*
806 * Load the data buffers from the following extensions.
807 * The extension labels are used as names to as the names
808 * of the line parameters.
809 */
810
811 p = cpl_propertylist_load(filename, extension);
812
813 // FIXME: The condition extension < 22 is needed because of a problem
814 // in cpl_propertylist_load() of CPL 4.0. It must be removed if the
815 // patched version is available on Paranal/DFO machines.
816
817 while ((p != NULL) && (extension < 22)) {
818
819 const cxchar* name = cpl_propertylist_get_string(p, GIALIAS_EXTNAME);
820
821 if (name == NULL) {
822 cpl_propertylist_delete(p);
823 p = NULL;
824
825 _giraffe_linedata_clear(self);
826
827 return 3;
828 }
829 else {
830
831 cpl_image* buffer = cpl_image_load(filename, CPL_TYPE_DOUBLE,
832 0, extension);
833
834 if ((cpl_image_get_size_x(buffer) != self->nfibers) ||
835 (cpl_image_get_size_y(buffer) != self->nlines)) {
836
837 cpl_image_delete(buffer);
838 buffer = NULL;
839
840 cpl_propertylist_delete(p);
841 p = NULL;
842
843 _giraffe_linedata_clear(self);
844
845 return 3;
846
847 }
848
849 cx_map_insert(self->values, cx_strdup(name), buffer);
850
851 }
852
853 ++extension;
854
855 cpl_propertylist_delete(p);
856 p = cpl_propertylist_load(filename, extension);
857
858 }
859
860 cpl_propertylist_delete(p);
861 p = NULL;
862
863 return 0;
864
865}
866
867
868cxint
869giraffe_linedata_save(GiLineData* self, const cpl_propertylist* properties,
870 const cxchar* filename)
871{
872
873 cxint status = 0;
874
875 cpl_propertylist* p = NULL;
876
877
878 if (self == NULL || properties == NULL || filename == NULL) {
879 return -1;
880 }
881
882 p = cpl_propertylist_duplicate(properties);
883
884 status = giraffe_linedata_writer(self, p, filename, NULL);
885
886 cpl_propertylist_delete(p);
887 p = NULL;
888
889 return status;
890
891}
892
893
894cxint
895giraffe_linedata_writer(const GiLineData* self, cpl_propertylist* properties,
896 const cxchar* filename, cxcptr data)
897{
898
899 const cxchar* const fctid = "giraffe_linedata_writer";
900
901 cx_map_const_iterator position;
902
903 cpl_propertylist* p = NULL;
904
905 cpl_table* lines = NULL;
906
907
908 /* Unused */
909 (void) data;
910
911 if (self == NULL || properties == NULL || filename == NULL) {
912 return -1;
913 }
914
915 lines = cpl_table_new(self->nlines);
916
917 if (lines == NULL) {
918 return 1;
919 }
920
921 giraffe_error_push();
922
923 cpl_table_new_column(lines, "WLEN", CPL_TYPE_DOUBLE);
924 cpl_table_copy_data_double(lines, "WLEN", self->wavelength);
925
926 if (cpl_error_get_code() != CPL_ERROR_NONE) {
927 cpl_table_delete(lines);
928 lines = NULL;
929
930 return 1;
931 }
932
933 giraffe_error_pop();
934
935
936 cpl_propertylist_erase(properties, "BSCALE");
937 cpl_propertylist_erase(properties, "BZERO");
938 cpl_propertylist_erase(properties, "BUNIT");
939
940 /* FIXME: Workaround for CPL deficiency. World coordinate
941 * keywords are not removed from table headers.
942 */
943
944 cpl_propertylist_erase_regexp(properties, "^CRPIX[0-9]$", 0);
945 cpl_propertylist_erase_regexp(properties, "^CRVAL[0-9]$", 0);
946 cpl_propertylist_erase_regexp(properties, "^CDELT[0-9]$", 0);
947 cpl_propertylist_erase_regexp(properties, "^CTYPE[0-9]$", 0);
948
949 cpl_propertylist_erase_regexp(properties, "^DATA(MIN|MAX)", 0);
950
951 cpl_propertylist_erase(properties, "EXTNAME");
952
953
954 cpl_propertylist_update_string(properties, GIALIAS_WSOL_LMNAME,
955 self->model);
956 cpl_propertylist_set_comment(properties, GIALIAS_WSOL_LMNAME,
957 "Line profile model");
958
959 p = cpl_propertylist_new();
960 cpl_propertylist_append_string(p, GIALIAS_EXTNAME, "LINES");
961 cpl_propertylist_set_comment(p, GIALIAS_EXTNAME, "FITS Extension name");
962
963 giraffe_error_push();
964
965 cpl_table_save(lines, properties, p, filename, CPL_IO_CREATE);
966
967 if (cpl_error_get_code() != CPL_ERROR_NONE) {
968
969 cpl_propertylist_delete(p);
970 p = NULL;
971
972 cpl_table_delete(lines);
973 lines = NULL;
974
975 return 2;
976 }
977
978 cpl_table_delete(lines);
979 lines = NULL;
980
981 giraffe_error_pop();
982
983 cpl_propertylist_set_string(p, GIALIAS_EXTNAME, "LINE_FLAGS");
984
985 giraffe_error_push();
986
987 if (self->status == NULL) {
988
989 cpl_image* status = cpl_image_new(self->nfibers, self->nlines,
990 CPL_TYPE_INT);
991
992 cpl_image_save(status, filename, CPL_BPP_16_SIGNED, p,
993 CPL_IO_EXTEND);
994 cpl_image_delete(status);
995 status = NULL;
996
997 }
998 else {
999 cpl_image_save(self->status, filename, CPL_BPP_16_SIGNED, p,
1000 CPL_IO_EXTEND);
1001 }
1002
1003 if (cpl_error_get_code() != CPL_ERROR_NONE) {
1004 cpl_propertylist_delete(p);
1005 p = NULL;
1006
1007 return 2;
1008 }
1009
1010 position = cx_map_begin(self->values);
1011 while (position != cx_map_end(self->values)) {
1012
1013 cxint format = 0;
1014
1015 const cpl_image* ldata = cx_map_get_value(self->values, position);
1016
1017
1018 switch (cpl_image_get_type(ldata)) {
1019 case CPL_TYPE_INT:
1020 format = CPL_BPP_32_SIGNED;
1021 break;
1022
1023 case CPL_TYPE_FLOAT:
1024 format = CPL_BPP_IEEE_FLOAT;
1025 break;
1026
1027 case CPL_TYPE_DOUBLE:
1028 format = CPL_BPP_IEEE_FLOAT;
1029 break;
1030
1031 default:
1032 cpl_propertylist_delete(p);
1033 p = NULL;
1034
1035 cpl_error_set(fctid, CPL_ERROR_TYPE_MISMATCH);
1036 return 2;
1037
1038 break;
1039 }
1040
1041 cpl_propertylist_set_string(p, GIALIAS_EXTNAME,
1042 cx_map_get_key(self->values, position));
1043
1044 cpl_image_save(ldata, filename, format, p, CPL_IO_EXTEND);
1045
1046 if (cpl_error_get_code() != CPL_ERROR_NONE) {
1047 cpl_propertylist_delete(p);
1048 p = NULL;
1049
1050 return 2;
1051 }
1052
1053 position = cx_map_next(self->values, position);
1054
1055 }
1056
1057 giraffe_error_pop();
1058
1059 cpl_propertylist_delete(p);
1060 p = NULL;
1061
1062 return 0;
1063
1064}

This file is part of the GIRAFFE Pipeline Reference Manual 2.16.11.
Documentation copyright © 2002-2006 European Southern Observatory.
Generated on Wed Apr 17 2024 20:34:24 by doxygen 1.9.6 written by Dimitri van Heesch, © 1997-2004